Skip to content

Commit

Permalink
feat(pin-input): add PinInput component
Browse files Browse the repository at this point in the history
  • Loading branch information
mimshins committed Jan 22, 2025
1 parent e2c7360 commit 63b46f6
Show file tree
Hide file tree
Showing 7 changed files with 991 additions and 0 deletions.
71 changes: 71 additions & 0 deletions packages/web-components/src/pin-input/Validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Validator } from "../utils";

export type PinInputState = {
/**
* The value of the input.
*/
readonly value: string;

/**
* Whether the input is required.
*/
readonly required: boolean;

/**
* The number of inputs.
* Defaults to 4.
*/
readonly pins: number;

/**
* The number of each input's length.
* Defaults to 1.
*/
readonly pinLength: number;
};

class PinInputValidator extends Validator<PinInputState> {
private _control?: HTMLInputElement;

protected override computeValidity(state: PinInputState) {
if (!this._control) {
// Lazily create the platform input
this._control = document.createElement("input");
this._control.type = "text";
}

const { pinLength, pins, value, required } = state;

const expectedLength = pins * pinLength;

this._control.value = expectedLength === value.length ? value : "";
this._control.required = required;

return {
validity: this._control.validity,
validationMessage: this._control.validationMessage,
};
}

protected override equals(prev: PinInputState, next: PinInputState) {
return (
prev.value === next.value &&
prev.required === next.required &&
prev.pins === next.pins &&
prev.pinLength === next.pinLength
);
}

protected override copy(state: PinInputState) {
const { value, required, pinLength, pins } = state;

return {
value,
required,
pinLength,
pins,
};
}
}

export default PinInputValidator;
1 change: 1 addition & 0 deletions packages/web-components/src/pin-input/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_DISPLAY_VALUE = (v: string) => v;
13 changes: 13 additions & 0 deletions packages/web-components/src/pin-input/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BaseEvent } from "../utils";

export class CompleteEvent extends BaseEvent<null> {
public static readonly type = "complete";

constructor() {
super(CompleteEvent.type, {
details: null,
composed: true,
bubbles: true,
});
}
}
83 changes: 83 additions & 0 deletions packages/web-components/src/pin-input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { customElement } from "lit/decorators.js";
import { PinInput } from "./pin-input";
import styles from "./pin-input.style";

/**
* @summary The pin-input component.
*
* @tag tapsi-pin-input
*
* @prop {string} [value=""] -
* The current value of the input. It is always a string.
* @prop {string} [name=""] -
* The HTML name to use in form submission.
* @prop {boolean} [disabled=false] -
* Whether or not the element is disabled.
* @prop {boolean} [required=false] -
* Indicates that the user must specify a value for the input before the
* owning form can be submitted and will render an error state when
* `reportValidity()` is invoked when value is empty.
*
* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/required
* @prop {boolean} [readonly=false] -
* Indicates whether or not a user should be able to edit the input's
* value.
*
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#readonly
* @prop {string} [placeholder=""] -
* Defines the text displayed in the input when it has no value.
*
* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/placeholder
* @prop {string} [autocomplete=""] -
* Describes what, if any, type of autocomplete functionality the input
* should provide.
*
* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
* @prop {string} [supporting-text=""] -
* Conveys additional information below the text input, such as how it should
* be used.
* @prop {boolean} [error=false] -
* Gets or sets whether or not the text input is in a visually invalid state.
*
* This error state overrides the error state controlled by
* `reportValidity()`.
* @prop {string} [error-text=""] -
* The error message that replaces supporting text when `error` is true. If
* `errorText` is an empty string, then the supporting text will continue to
* show.
*
* This error message overrides the error message displayed by
* `reportValidity()`.
* @prop {string} [label=""] -
* The label of the input.
* - If the `hideLabel` property is true, the label will be hidden visually
* but still accessible to screen readers.
* - Otherwise, a visible label element will be rendered.
*
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label
* @prop {string} [labelledby=""] -
* Identifies the element (or elements) that labels the input.
*
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby
* @prop {boolean} [hide-label=false] - Whether to hide the label or not.
* @prop {boolean} [masked=false] - Determines whether input values should be masked or not.
* @prop {number} [pins=4] -
* The number of inputs.
* Defaults to 4.
* @prop {number} [pinlength=1] -
* The number of each input's length.
* Defaults to 1.
* @prop {"alphanumeric" | "numeric"} [type="alphanumeric"] -
* Determines which values can be entered.
* Defaults to "alphanumeric".
*/
@customElement("tapsi-pin-input")
export class TapsiPinInput extends PinInput {
public static override readonly styles = [styles];
}

declare global {
interface HTMLElementTagNameMap {
"tapsi-pin-input": TapsiPinInput;
}
}
126 changes: 126 additions & 0 deletions packages/web-components/src/pin-input/pin-input.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { css } from "lit";

const styles = css`
*,
*::before,
*::after {
box-sizing: border-box;
}
:host {
--pin-bg-color: var(--tapsi-color-surface-tertiary);
--pin-border-color: transparent;
--support-color: var(--tapsi-color-content-tertiary);
--input-color: var(--tapsi-color-content-primary);
--input-placeholder-color: var(--tapsi-color-content-tertiary);
--label-color: var(--tapsi-color-content-primary);
display: inline-block;
vertical-align: middle;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.root.disabled {
--pin-bg-color: var(--tapsi-color-surface-disabled);
--pin-border-color: transparent;
--support-color: var(--tapsi-color-content-disabled);
--input-color: var(--tapsi-color-content-disabled);
--label-color: var(--tapsi-color-content-disabled);
--input-placeholder-color: var(--tapsi-color-content-disabled);
cursor: not-allowed;
}
.root.error {
--pin-bg-color: var(--tapsi-color-surface-negative-light);
--pin-border-color: var(--tapsi-color-border-negative);
--support-color: var(--tapsi-color-content-negative);
}
.root:not(.error) .input:focus {
--pin-bg-color: var(--tapsi-color-surface-secondary);
--pin-border-color: var(--tapsi-color-border-inverse-primary);
--support-color: var(--tapsi-color-content-secondary);
}
.root {
font-family: var(--tapsi-font-family);
display: flex;
flex-direction: column;
gap: var(--tapsi-spacing-4);
}
.label {
color: var(--label-color);
font-family: var(--tapsi-font-family);
line-height: var(--tapsi-typography-label-md-height);
font-size: var(--tapsi-typography-label-md-size);
font-weight: var(--tapsi-typography-label-md-weight);
}
.supporting-text {
color: var(--support-color);
font-family: var(--tapsi-font-family);
line-height: var(--tapsi-typography-body-sm-height);
font-size: var(--tapsi-typography-body-sm-size);
font-weight: var(--tapsi-typography-body-sm-weight);
}
.pins {
direction: ltr;
display: flex;
align-items: center;
padding-right: var(--tapsi-spacing-6);
padding-left: var(--tapsi-spacing-6);
gap: var(--tapsi-spacing-5);
}
.input {
border: 0;
outline: none;
width: 3.25rem;
height: 3.25rem;
padding: var(--tapsi-spacing-4);
color: var(--input-color);
background-color: var(--pin-bg-color);
caret-color: var(--tapsi-color-surface-accent);
text-align: center;
box-shadow: 0 0 0 var(--tapsi-stroke-2) var(--pin-border-color);
border-radius: var(--tapsi-radius-3);
font-family: var(--tapsi-font-family);
line-height: var(--tapsi-typography-headline-sm-height);
font-size: var(--tapsi-typography-headline-sm-size);
font-weight: var(--tapsi-typography-headline-sm-weight);
}
.input::placeholder {
color: var(--input-placeholder-color);
}
`;

export default styles;
Loading

0 comments on commit 63b46f6

Please sign in to comment.