-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(pin-input): add PinInput component
- Loading branch information
Showing
7 changed files
with
991 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const DEFAULT_DISPLAY_VALUE = (v: string) => v; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
126
packages/web-components/src/pin-input/pin-input.style.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.