-
Notifications
You must be signed in to change notification settings - Fork 469
feat(ui): Add Mosaic Otp component #9580
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
846c95c
feat(ui): add Mosaic Otp component
alexcarpenter 605043c
refactor(ui): rename inputSurface to inputStyles
alexcarpenter b9c3fdc
feat(ui): default Otp length to 6 and drop className/style
alexcarpenter 82c22e5
fix(ui): honor a direct aria-invalid on Otp
alexcarpenter a43672a
Apply suggestion from @alexcarpenter
alexcarpenter 2cf0171
Merge branch 'main' into carp/mosaic-otp-input-compontn
alexcarpenter 347bada
Update otp.styles.ts
alexcarpenter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- |
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| import * as OtpStories from './otp.component.stories'; | ||
|
|
||
| # OTP | ||
|
|
||
| The Mosaic `Otp` is the verification-code field: one styled box per character, built on the headless [OTP primitive](/primitives/otp). Focus advances as the code is typed, `Backspace` walks back, and a pasted code spreads across the boxes. Each box carries the same border, hover, and focus treatment as `Input`. | ||
|
|
||
| ## Playground | ||
|
|
||
| <Preview | ||
| name='Default' | ||
| storyModule={OtpStories} | ||
| /> | ||
|
|
||
| ## Props | ||
|
|
||
| <PropTable | ||
| meta={OtpStories.meta} | ||
| styleProps={false} | ||
| extra={[ | ||
| { name: 'length', type: 'number', default: '6' }, | ||
| { name: 'value', type: 'string', default: '—' }, | ||
| { name: 'defaultValue', type: 'string', default: "''" }, | ||
| { name: 'onValueChange', type: '(value: string) => void', default: '—' }, | ||
| { name: 'onComplete', type: '(value: string) => void', default: '—' }, | ||
| { name: 'pattern', type: "'numeric' | 'alpha' | 'alphanumeric'", default: "'numeric'" }, | ||
| { name: 'mask', type: 'boolean', default: 'false' }, | ||
| { name: 'name', type: 'string', default: '—' }, | ||
| { name: 'disabled', type: 'boolean', default: 'false' }, | ||
| ]} | ||
| /> | ||
|
|
||
| Every prop of the headless [OTP primitive](/primitives/otp) passes through. The boxes are styled through their `.cl-otp` and `.cl-otp-slot` classes, so `Otp` takes no `className` or `style`. | ||
|
|
||
| ## Usage | ||
|
|
||
| `length` defaults to `6`. Give the group an accessible name with `aria-label`, or place it in a `Field.Root` with a `Field.Label`. | ||
|
|
||
| ```tsx | ||
| import { Otp } from '@clerk/ui/mosaic/components/otp'; | ||
|
|
||
| <Otp | ||
| aria-label='Verification code' | ||
| onComplete={code => verify(code)} | ||
| />; | ||
| ``` | ||
|
|
||
| The value is uncontrolled by default. Pass `value` with `onValueChange` to control it. There is no `onChange`: the boxes are separate inputs, so the whole code is reported as one string. | ||
|
|
||
| ```tsx | ||
| const [code, setCode] = useState(''); | ||
|
|
||
| <Otp | ||
| value={code} | ||
| onValueChange={setCode} | ||
| aria-label='Verification code' | ||
| />; | ||
| ``` | ||
|
|
||
| Inside a `Field.Root`, the field's `disabled` and `invalid` flow into the boxes, and the label and messages are associated with the group: | ||
|
|
||
| ```tsx | ||
| <Field.Root invalid={Boolean(error)}> | ||
| <Field.Label>Verification code</Field.Label> | ||
| <Otp name='code' /> | ||
| {error ? <Field.Error>{error}</Field.Error> : <Field.Description>Didn’t receive a code? Resend</Field.Description>} | ||
| </Field.Root> | ||
| ``` | ||
|
|
||
| `status` overrides that: `'error'` marks every box invalid and colours it negative, `'success'` colours a verified code positive. | ||
|
|
||
| --- | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Sizes | ||
|
|
||
| <Story | ||
| name='Sizes' | ||
| storyModule={OtpStories} | ||
| /> | ||
|
|
||
| ### Success | ||
|
|
||
| <Story | ||
| name='Success' | ||
| storyModule={OtpStories} | ||
| /> | ||
|
|
||
| ### Error | ||
|
|
||
| <Story | ||
| name='Error' | ||
| storyModule={OtpStories} | ||
| /> | ||
|
|
||
| ### Disabled | ||
|
|
||
| <Story | ||
| name='Disabled' | ||
| storyModule={OtpStories} | ||
| /> | ||
|
|
||
| ## Parts | ||
|
|
||
| | Part | Stable slot class | Description | | ||
| | ---- | ----------------- | ---------------------------------------------- | | ||
| | Root | `.cl-otp` | The `role="group"` holding the boxes. | | ||
| | Slot | `.cl-otp-slot` | One `input` per character, one box in the row. | | ||
|
|
||
| Both carry `data-size` and `data-status`, plus `data-disabled` when disabled. Each slot additionally carries the primitive's `data-active` (holds focus) and `data-filled` (holds a character). | ||
104 changes: 104 additions & 0 deletions
104
packages/swingset/src/stories/otp.component.stories.tsx
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { Field } from '@clerk/ui/mosaic/components/field'; | ||
| import type { OtpProps } from '@clerk/ui/mosaic/components/otp'; | ||
| import { Otp } from '@clerk/ui/mosaic/components/otp'; | ||
|
|
||
| import type { StoryMeta } from '@/lib/types'; | ||
|
|
||
| // Exposes this file's own source (via the `?raw` webpack rule) so each `<Story>` example | ||
| // renders a code footer with its function's source. See `StoryModule.__source`. | ||
| export { default as __source } from './otp.component.stories?raw'; | ||
|
|
||
| export const meta: StoryMeta = { | ||
| group: 'Components', | ||
| title: 'OTP', | ||
| source: 'packages/ui/src/mosaic/components/otp/otp.tsx', | ||
| styles: { | ||
| _variants: { | ||
| size: { sm: {}, md: {}, lg: {} }, | ||
| status: { neutral: {}, success: {}, error: {} }, | ||
| }, | ||
| _defaultVariants: { | ||
| size: 'md', | ||
| status: 'neutral', | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const stackStyles = { | ||
| display: 'grid', | ||
| gap: 8, | ||
| justifyItems: 'start', | ||
| } as const; | ||
|
|
||
| function knobsAsProps(props: Record<string, unknown>) { | ||
| return props as unknown as OtpProps; | ||
| } | ||
|
|
||
| export function Default(props: Record<string, unknown>) { | ||
| return ( | ||
| <Otp | ||
| {...knobsAsProps(props)} | ||
| aria-label='Verification code' | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export function Sizes() { | ||
| return ( | ||
| <div style={{ display: 'grid', gap: 16, justifyItems: 'start' }}> | ||
| <Otp | ||
| size='sm' | ||
| defaultValue='123' | ||
| aria-label='Small code' | ||
| /> | ||
| <Otp | ||
| size='md' | ||
| defaultValue='123' | ||
| aria-label='Medium code' | ||
| /> | ||
| <Otp | ||
| size='lg' | ||
| defaultValue='123' | ||
| aria-label='Large code' | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export function Success() { | ||
| return ( | ||
| <Field.Root style={stackStyles}> | ||
| <Otp | ||
| status='success' | ||
| defaultValue='123456' | ||
| aria-label='Verification code' | ||
| /> | ||
| <Field.Description>Success</Field.Description> | ||
| </Field.Root> | ||
| ); | ||
| } | ||
|
|
||
| export function Error() { | ||
| return ( | ||
| <Field.Root | ||
| invalid | ||
| style={stackStyles} | ||
| > | ||
| <Otp | ||
| defaultValue='123456' | ||
| aria-label='Verification code' | ||
| /> | ||
| <Field.Error>Incorrect code</Field.Error> | ||
| </Field.Root> | ||
| ); | ||
| } | ||
|
|
||
| export function Disabled() { | ||
| return ( | ||
| <Otp | ||
| disabled | ||
| defaultValue='123' | ||
| aria-label='Verification code' | ||
| /> | ||
| ); | ||
| } |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { Otp } from './otp'; | ||
| export type { OtpProps, OtpSize, OtpStatus } from './otp'; |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.