Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/warm-taxis-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
1 change: 1 addition & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
combobox: dynamic(() => import('../stories/combobox.mdx')),
input: dynamic(() => import('../stories/input.mdx')),
'input-group': dynamic(() => import('../stories/input-group.mdx')),
'phone-input': dynamic(() => import('../stories/phone-input.mdx')),
item: dynamic(() => import('../stories/item.mdx')),
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
heading: dynamic(() => import('../stories/heading.mdx')),
Expand Down
18 changes: 18 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ import {
Success as OtpComponentSuccess,
} from '../stories/otp.component.stories';
import { meta as otpMeta } from '../stories/otp.stories';
import {
Default as PhoneInputDefault,
Disabled as PhoneInputDisabled,
Invalid as PhoneInputInvalid,
meta as phoneInputMeta,
Prefilled as PhoneInputPrefilled,
Sizes as PhoneInputSizes,
} from '../stories/phone-input.stories';
import {
Alignment as PopoverComponentAlignment,
Default as PopoverComponentDefault,
Expand Down Expand Up @@ -297,6 +305,15 @@ const inputGroupModule: StoryModule = {
Invalid: InputGroupInvalid,
};

const phoneInputModule: StoryModule = {
meta: phoneInputMeta,
Default: PhoneInputDefault,
Sizes: PhoneInputSizes,
Prefilled: PhoneInputPrefilled,
Disabled: PhoneInputDisabled,
Invalid: PhoneInputInvalid,
};

const popoverComponentModule: StoryModule = {
meta: popoverComponentMeta,
Default: PopoverComponentDefault,
Expand Down Expand Up @@ -536,6 +553,7 @@ export const registry: StoryModule[] = [
flowComponentModule,
inputModule,
inputGroupModule,
phoneInputModule,
itemModule,
dialogComponentModule,
headingModule,
Expand Down
72 changes: 72 additions & 0 deletions packages/swingset/src/stories/phone-input.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as PhoneInputStories from './phone-input.stories';

# PhoneInput

The `PhoneInput` combines a searchable country picker and native telephone input into one Mosaic field while exposing a normalized E.164 value.

It composes `InputGroup` for the telephone field and the inline `Combobox` composition for the country search inside its `Popover`.

## Playground

<Preview
name='Default'
storyModule={PhoneInputStories}
/>

## Props

<PropTable
meta={PhoneInputStories.meta}
extra={[
{ name: 'value', type: 'string', default: '—' },
{ name: 'defaultValue', type: 'string', default: "''" },
{ name: 'onValueChange', type: '(value: string) => void', default: '—' },
{ name: 'country', type: 'CountryIso', default: '—' },
{ name: 'defaultCountry', type: 'CountryIso', default: "'us'" },
{ name: 'onCountryChange', type: '(country: CountryIso) => void', default: '—' },
{ name: 'countrySearchPlaceholder', type: 'string', default: "'Search country or code'" },
{ name: 'noResultsMessage', type: 'string', default: "'No countries found'" },
]}
/>

`value` and `defaultValue` use E.164. `onValueChange` reports that normalized value while the visible input formats the national number. Control `country` separately when countries share a calling code.

## Usage

<Usage
component='PhoneInput'
module='@clerk/ui/mosaic/components/phone-input'
props={{ name: 'phoneNumber', placeholder: '202 555 0123' }}
/>

---

## Examples

### Sizes

<Story
name='Sizes'
storyModule={PhoneInputStories}
/>

### Prefilled international number

<Story
name='Prefilled'
storyModule={PhoneInputStories}
/>

### Disabled

<Story
name='Disabled'
storyModule={PhoneInputStories}
/>

### Invalid

<Story
name='Invalid'
storyModule={PhoneInputStories}
/>
106 changes: 106 additions & 0 deletions packages/swingset/src/stories/phone-input.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
'use client';

import { Field } from '@clerk/ui/mosaic/components/field';
import type { PhoneInputProps } from '@clerk/ui/mosaic/components/phone-input';
import { PhoneInput } from '@clerk/ui/mosaic/components/phone-input';

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 './phone-input.stories?raw';

export const meta: StoryMeta = {
group: 'Components',
title: 'PhoneInput',
source: 'packages/ui/src/mosaic/components/phone-input/phone-input.tsx',
styles: {
_variants: {
size: { sm: {}, md: {}, lg: {} },
},
_defaultVariants: {
size: 'md',
},
},
};

const stackStyles = {
display: 'grid',
gap: 8,
width: 320,
} as const;

function knobsAsProps(props: Record<string, unknown>) {
return props as unknown as PhoneInputProps;
}

export function Default(props: Record<string, unknown>) {
return (
<Field.Root style={stackStyles}>
<Field.Label>Phone number</Field.Label>
<PhoneInput
{...knobsAsProps(props)}
name='phoneNumber'
placeholder='202 555 0123'
/>
<Field.Description>We will send a verification code to this number.</Field.Description>
</Field.Root>
);
}

export function Sizes() {
return (
<div style={{ display: 'grid', gap: 12, width: 320 }}>
<PhoneInput
size='sm'
aria-label='Small phone number'
placeholder='Small'
/>
<PhoneInput
size='md'
aria-label='Medium phone number'
placeholder='Medium'
/>
<PhoneInput
size='lg'
aria-label='Large phone number'
placeholder='Large'
/>
</div>
);
}

export function Prefilled() {
return (
<Field.Root style={stackStyles}>
<Field.Label>Phone number</Field.Label>
<PhoneInput defaultValue='+306901234567' />
<Field.Description>Paste an international number to update the detected country.</Field.Description>
</Field.Root>
);
}

export function Disabled() {
return (
<Field.Root
disabled
style={stackStyles}
>
<Field.Label>Phone number</Field.Label>
<PhoneInput defaultValue='+12025550123' />
</Field.Root>
);
}

export function Invalid() {
return (
<Field.Root
invalid
style={stackStyles}
>
<Field.Label>Phone number</Field.Label>
<PhoneInput defaultValue='+1202' />
<Field.Error>Enter a valid phone number.</Field.Error>
</Field.Root>
);
}
3 changes: 3 additions & 0 deletions packages/ui/src/mosaic/components/phone-input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { PhoneInput } from './phone-input';
export type { PhoneInputProps } from './phone-input';
export type { CountryIso } from '../../../elements/PhoneInput/countryCodeData';
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as stylex from '@stylexjs/stylex';

import { colorVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex';

export const styles = stylex.create({
trigger: {
paddingInlineEnd: 0,
paddingInlineStart: space['2.5'],
},
triggerContent: {
alignItems: 'center',
display: 'flex',
gap: space['0.5'],
},
flag: {
fontSize: typeScaleVars['--cl-text-sm-size'],
lineHeight: 1,
},
divider: {
backgroundColor: colorVars['--cl-color-border'],
flexShrink: 0,
height: space['3.5'],
width: '1px',
},
prefix: {
gap: space['2'],
fontVariantNumeric: 'tabular-nums',
paddingInlineEnd: 0,
paddingInlineStart: space['2'],
},
control: {
fontVariantNumeric: 'tabular-nums',
paddingInlineStart: space['2'],
},
popup: {
borderRadius: radiusVars['--cl-radius-lg'],
overflow: 'hidden',
backgroundColor: colorVars['--cl-color-card'],
boxShadow: `0 12px 12px -7px light-dark(oklch(0.2046 0 0 / 12%), transparent),
0 24px 24px -10px light-dark(oklch(0.2046 0 0 / 4%), transparent),
0 0 0 1px light-dark(oklch(0.2046 0 0 / 4%), oklch(1 0 0 / 10%))`,
color: colorVars['--cl-color-card-foreground'],
width: '100%',
},
countrySearch: {
marginInline: space['2'],
flexShrink: 0,
marginBlockEnd: space['1'],
marginBlockStart: space['2'],
},
optionName: {
overflow: 'hidden',
flexGrow: 1,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
minWidth: 0,
},
optionCode: {
color: colorVars['--cl-color-neutral-faded'],
fontVariantNumeric: 'tabular-nums',
},
checkHidden: {
visibility: 'hidden',
},
});
Loading
Loading