feat: OTP/PIN input component - #2218
Draft
rkaraivanov wants to merge 7 commits into
Draft
Conversation
- Refactored the pin input component to use event delegation for better performance and maintainability. - Added edge case handling for focus events to ensure the correct input is focused and selected. - Updated the storybook stories to reflect the changes in the component's API and behavior. - Updated tests to cover the new event handling logic and edge cases.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new PIN/OTP input web component to the Ignite UI Web Components library, including theming, Storybook documentation, validation, and a comprehensive test suite. This extends the set of form-associated input controls while aligning with the project’s theming and validation-container patterns.
Changes:
- Introduces
IgcPinInputComponentwith form association, validation, keyboard handling, paste support, and completion events. - Adds theme scaffolding (base + light/dark overrides + shared theme files) and exports the component from the package entrypoint.
- Adds Storybook stories and new test utilities (paste + focusout simulation) and updates existing specs to use them.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| stories/pin-input.stories.ts | Adds Storybook stories for the new PIN/OTP input component. |
| src/index.ts | Exports the new IgcPinInputComponent and its event map types. |
| src/components/pin-input/validators.ts | Adds required-field validator for the PIN input. |
| src/components/pin-input/themes/themes.ts | Registers light/dark theme style bundles for the component. |
| src/components/pin-input/themes/pin-input.base.scss | Defines the base styling for the PIN input (layout, parts, states). |
| src/components/pin-input/themes/light/pin-input.shared.scss | Light shared theme entry (placeholder for shared light overrides). |
| src/components/pin-input/themes/light/pin-input.bootstrap.scss | Light Bootstrap overrides for the component. |
| src/components/pin-input/themes/light/pin-input.fluent.scss | Light Fluent overrides for the component. |
| src/components/pin-input/themes/light/pin-input.indigo.scss | Light Indigo overrides for the component. |
| src/components/pin-input/themes/light/pin-input.material.scss | Light Material overrides for the component. |
| src/components/pin-input/themes/dark/pin-input.bootstrap.scss | Dark Bootstrap overrides for the component. |
| src/components/pin-input/themes/dark/pin-input.fluent.scss | Dark Fluent overrides for the component. |
| src/components/pin-input/themes/dark/pin-input.indigo.scss | Dark Indigo overrides for the component. |
| src/components/pin-input/themes/dark/pin-input.material.scss | Dark Material overrides for the component. |
| src/components/pin-input/themes/shared/pin-input.bootstrap.scss | Shared Bootstrap sizing tokens for the component. |
| src/components/pin-input/themes/shared/pin-input.fluent.scss | Shared Fluent sizing tokens for the component. |
| src/components/pin-input/themes/shared/pin-input.indigo.scss | Shared Indigo sizing tokens for the component. |
| src/components/pin-input/themes/shared/pin-input.material.scss | Shared Material sizing tokens for the component. |
| src/components/pin-input/pin-input.ts | Implements the new igc-pin-input component (rendering, behavior, events, form integration). |
| src/components/pin-input/pin-input.spec.ts | Adds tests for a11y, keyboard behavior, events, paste, grouping, and form association. |
| src/components/common/utils.spec.ts | Adds reusable test helpers for focusout and paste event simulation. |
| src/components/common/controllers/key-bindings.ts | Exposes Backspace and Delete key constants used by the new component tests/logic. |
| src/components/common/controllers/focus-ring.spec.ts | Refactors to use the new simulateFocusOut helper. |
| src/components/carousel/carousel.spec.ts | Refactors to use the new simulateFocusOut helper. |
| src/components/carousel/carousel-indicator-container.spec.ts | Refactors to use the new simulateFocusOut helper. |
Suppressed comments (2)
stories/pin-input.stories.ts:187
- Storybook controls for
value/separatorare defined, but the Masked story doesn't bind them, so controls won't affect the rendered component in this story.
<igc-pin-input
.length=${length}
.mode=${mode}
?mask=${mask}
label=${ifDefined(label)}
stories/pin-input.stories.ts:216
- Storybook controls for
value/separatorare defined, but the Alphanumeric story doesn't bind them, so controls won't affect the rendered component in this story.
<igc-pin-input
.length=${length}
.mode=${mode}
?mask=${mask}
label=${ifDefined(label)}
|
|
||
| private _handlePaste(event: ClipboardEvent): void { | ||
| const index = this._getCellIndex(event); | ||
| const text = event.clipboardData?.getData('text'); |
Comment on lines
+139
to
+150
| public set length(value: number) { | ||
| if (this._groups.length > 0) return; | ||
| const clamped = clamp(value, MIN_LENGTH, MAX_LENGTH); | ||
| if (clamped === this._length) return; | ||
|
|
||
| this._cells = Array.from( | ||
| { length: clamped }, | ||
| (_, i) => this._cells[i] ?? '' | ||
| ); | ||
| this._length = clamped; | ||
| this._syncFormValue(); | ||
| } |
Comment on lines
+195
to
+209
| public set groups(value: number[]) { | ||
| this._groups = value; | ||
| if (!value.length) return; | ||
| const clamped = clamp( | ||
| value.reduce((a, b) => a + b, 0), | ||
| MIN_LENGTH, | ||
| MAX_LENGTH | ||
| ); | ||
| this._cells = Array.from( | ||
| { length: clamped }, | ||
| (_, i) => this._cells[i] ?? '' | ||
| ); | ||
| this._length = clamped; | ||
| this._syncFormValue(); | ||
| } |
| display: block; | ||
| position: relative; | ||
|
|
||
| --_cell-size: #{rem(48px)}; |
Comment on lines
+441
to
+443
| const clipboardData = new DataTransfer(); | ||
| clipboardData.setData('text/plain', pastedText); | ||
|
|
| }); | ||
|
|
||
| describe('Validation container slots', () => { | ||
| it('', async () => { |
Comment on lines
+142
to
+164
| render: ({ | ||
| length, | ||
| mode, | ||
| mask, | ||
| label, | ||
| placeholder, | ||
| required, | ||
| disabled, | ||
| invalid, | ||
| name, | ||
| }) => html` | ||
| <igc-pin-input | ||
| .length=${length} | ||
| .mode=${mode} | ||
| ?mask=${mask} | ||
| label=${ifDefined(label)} | ||
| placeholder=${ifDefined(placeholder)} | ||
| ?required=${required} | ||
| ?disabled=${disabled} | ||
| ?invalid=${invalid} | ||
| name=${ifDefined(name)} | ||
| ></igc-pin-input> | ||
| `, |
Comment on lines
+340
to
+349
| if (filtered && filtered !== prev) { | ||
| const value = this._cellsValue; | ||
| this._emitInputEvent(value); | ||
|
|
||
| if (index < this._length - 1) { | ||
| this._focusCell(index + 1); | ||
| } | ||
|
|
||
| this._emitCompleteIfFull(value); | ||
| } |
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type of Change
Checklist