-
Notifications
You must be signed in to change notification settings - Fork 474
feat(ui): Add phone input #9656
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
22 commits
Select commit
Hold shift + click to select a range
d8d4bf6
feat(ui): add combobox
austincalvelage 061e4de
docs(swingset): mark combobox as work in progress
austincalvelage 1c0714f
test(ui): remove combobox CSS assertions
austincalvelage 30d4ad7
refactor(ui): use input group start in combobox examples
austincalvelage e5cbd7d
refactor(ui): compose combobox triggers with input group slots
austincalvelage 6609771
fix(ui): automatically anchor combobox popup to input group
austincalvelage 4ff2958
docs(swingset): show combobox building blocks in examples
austincalvelage 20a4a88
feat(ui): make combobox selection-aware
austincalvelage 9b6cfe1
docs(swingset): demonstrate combobox selection behavior
austincalvelage 47713f1
refactor(ui): use headless combobox primitive
austincalvelage f2dce79
fix(ui): inherit input group variant in combobox
austincalvelage 1247d4d
docs(swingset): simplify combobox documentation
austincalvelage d2ca84f
`refactor(ui): deduplicate combobox logic`
austincalvelage 0cc0d3e
feat(ui): add phone input
austincalvelage 754c491
fix(ui): compose phone input with input group slots
austincalvelage 5ebfb1e
docs(swingset): mark phone input as work in progress
austincalvelage e4a6c29
test(ui): remove phone input CSS assertions
austincalvelage dcb5eac
refactor(ui): use current input group parts in phone input
austincalvelage 9949fcf
fix(ui): refine phone input layout and popup anchoring
austincalvelage cb3f48d
refactor(ui): reuse phone input refs and selection indicator
austincalvelage 37e6780
fix(ui): prevent phone country codes from wrapping
austincalvelage fab18d2
fix(ui): autofocus country search when phone picker opens
austincalvelage 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
Some comments aren't visible on the classic Files Changed page.
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
| 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,57 @@ | ||
| import * as ComboboxStories from './combobox.stories'; | ||
|
|
||
| # Combobox | ||
|
|
||
| ## Example | ||
|
|
||
| <Story | ||
| name='Default' | ||
| storyModule={ComboboxStories} | ||
| /> | ||
|
|
||
| `value` is the selected option; `inputValue` is the search text. Clearing the input clears the selection. | ||
|
|
||
| ## Inline lists | ||
|
|
||
| Use `List` inside an existing popover. Set `inline` and bind the root's open state to that popover | ||
| so closing resets the search, not the selection. | ||
|
|
||
| ```tsx | ||
| <Combobox.Root inline open={open} onOpenChange={setOpen} value={country} onValueChange={setCountry}> | ||
| <InputGroup.Root> | ||
| <InputGroup.Start> | ||
| <Icon name='search' aria-hidden='true' /> | ||
| </InputGroup.Start> | ||
| <Combobox.Input aria-label='Search countries' /> | ||
| </InputGroup.Root> | ||
| <Combobox.List> | ||
| <Combobox.Collection items={countries} itemToStringLabel={country => country.name}> | ||
| {country => ( | ||
| <Combobox.Option key={country.iso} value={country.iso} label={country.name}> | ||
| {country.name} | ||
| </Combobox.Option> | ||
| )} | ||
| </Combobox.Collection> | ||
| </Combobox.List> | ||
| </Combobox.Root> | ||
| ``` | ||
|
|
||
| ## Scrolling | ||
|
|
||
| <Story | ||
| name='Scrolling' | ||
| storyModule={ComboboxStories} | ||
| /> | ||
|
|
||
| ## Parts | ||
|
|
||
| | Part | Description | | ||
| | --------------------- | --------------------------------------------------------------------------- | | ||
| | `Combobox.Root` | Owns input, selection, open state, and keyboard navigation. | | ||
| | `Combobox.Input` | Search input; renders Mosaic `Input` unless composed through another input. | | ||
| | `Combobox.Trigger` | Opens and closes the option list while keeping focus on the input. | | ||
| | `Combobox.Popup` | Portals, positions, surfaces, and scrolls a floating option list. | | ||
| | `Combobox.List` | Scrollable inline listbox. | | ||
| | `Combobox.Collection` | Filters items and renders each option, with an optional empty state. | | ||
| | `Combobox.Option` | Selectable option with active, selected, and disabled states. | | ||
| | `Combobox.Empty` | Empty result message. | |
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,108 @@ | ||
| 'use client'; | ||
|
|
||
| import { Button } from '@clerk/ui/mosaic/components/button'; | ||
| import { Combobox } from '@clerk/ui/mosaic/components/combobox'; | ||
| import { Field } from '@clerk/ui/mosaic/components/field'; | ||
| import { Icon } from '@clerk/ui/mosaic/components/icon'; | ||
| import { InputGroup } from '@clerk/ui/mosaic/components/input-group'; | ||
|
|
||
| import type { StoryMeta } from '@/lib/types'; | ||
|
|
||
| export { default as __source } from './combobox.stories?raw'; | ||
|
|
||
| export const meta: StoryMeta = { | ||
| group: 'Components', | ||
| status: 'wip', | ||
| title: 'Combobox', | ||
| source: 'packages/ui/src/mosaic/components/combobox/combobox.tsx', | ||
| }; | ||
|
|
||
| export function Default() { | ||
| const options = ['Apple', 'Apricot', 'Banana', 'Blackberry', 'Cherry', 'Fig', 'Grape', 'Pear', 'Plum']; | ||
|
|
||
| return ( | ||
| <Combobox.Root> | ||
| <Field.Root style={{ width: 320 }}> | ||
| <Field.Label>Fruit</Field.Label> | ||
| <InputGroup.Root> | ||
| <Combobox.Input placeholder='Search fruit…' /> | ||
| <InputGroup.End> | ||
| <Combobox.Trigger | ||
| aria-label='Toggle fruit options' | ||
| render={<Button />} | ||
| > | ||
| <Icon | ||
| name='chevron-down' | ||
| size='sm' | ||
| aria-hidden='true' | ||
| /> | ||
| </Combobox.Trigger> | ||
| </InputGroup.End> | ||
| </InputGroup.Root> | ||
| </Field.Root> | ||
| <Combobox.Popup> | ||
| <Combobox.Collection | ||
| items={options} | ||
| itemToStringLabel={option => option} | ||
| empty={<Combobox.Empty>No fruit found</Combobox.Empty>} | ||
| > | ||
| {option => ( | ||
| <Combobox.Option | ||
| key={option} | ||
| value={option.toLowerCase()} | ||
| label={option} | ||
| > | ||
| {option} | ||
| <Combobox.OptionIndicator /> | ||
| </Combobox.Option> | ||
| )} | ||
| </Combobox.Collection> | ||
| </Combobox.Popup> | ||
| </Combobox.Root> | ||
| ); | ||
| } | ||
|
|
||
| export function Scrolling() { | ||
| const options = Array.from({ length: 40 }, (_, index) => `Fruit ${index + 1}`); | ||
|
|
||
| return ( | ||
| <Combobox.Root> | ||
| <Field.Root style={{ width: 320 }}> | ||
| <Field.Label>Fruit</Field.Label> | ||
| <InputGroup.Root> | ||
| <Combobox.Input placeholder='Search fruit…' /> | ||
| <InputGroup.End> | ||
| <Combobox.Trigger | ||
| aria-label='Toggle fruit options' | ||
| render={<Button />} | ||
| > | ||
| <Icon | ||
| name='chevron-down' | ||
| size='sm' | ||
| aria-hidden='true' | ||
| /> | ||
| </Combobox.Trigger> | ||
| </InputGroup.End> | ||
| </InputGroup.Root> | ||
| </Field.Root> | ||
| <Combobox.Popup> | ||
| <Combobox.Collection | ||
| items={options} | ||
| itemToStringLabel={option => option} | ||
| empty={<Combobox.Empty>No fruit found</Combobox.Empty>} | ||
| > | ||
| {option => ( | ||
| <Combobox.Option | ||
| key={option} | ||
| value={option.toLowerCase()} | ||
| label={option} | ||
| > | ||
| {option} | ||
| <Combobox.OptionIndicator /> | ||
| </Combobox.Option> | ||
| )} | ||
| </Combobox.Collection> | ||
| </Combobox.Popup> | ||
| </Combobox.Root> | ||
| ); | ||
| } |
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,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} | ||
| /> |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: clerk/javascript
Length of output: 15683
🏁 Script executed:
Repository: clerk/javascript
Length of output: 35859
🤖 get_repo_knowledge executed:
get_repo_knowledge clerk/javascript /tmp/coderabbit-repo-knowledge/clerk-javascript-ccdea2b6/conventions /tmp/coderabbit-repo-knowledge/clerk-javascript-ccdea2b6/learningsLength of output: 47664
🌐 Web query:
React useLayoutEffect server rendering use client directive client components prerendered official documentation💡 Result:
In React, useLayoutEffect is designed to run synchronously after all DOM mutations but before the browser paints [1][2]. Because server-rendered environments lack a DOM, useLayoutEffect does not execute during server rendering [1][3]. Regarding your specific areas of interest: Client Components and 'use client': The 'use client' directive marks a file as part of a Client Component module, allowing it to use React hooks like useLayoutEffect [4][5]. However, marking a component with 'use client' does not exempt it from server rendering [5]. These components are still rendered to HTML on the server during the initial load, and useLayoutEffect will still not run during that server-side execution [5]. Official Guidance and Patterns: React's official documentation advises that because useLayoutEffect cannot run on the server, relying on it for initial render logic can cause a mismatch between the server-rendered HTML and the client-hydrated UI, often manifesting as visible content "jumping" or flashing [1][6]. To manage this, the documentation suggests the following approaches: 1. Prefer useEffect: If the logic does not require blocking the browser paint, use useEffect instead [1][3]. useEffect also does not run on the server, but it is architecturally designed to handle post-hydration execution without blocking the initial paint [1][2]. 2. Delay Rendering: If the component must use useLayoutEffect and would appear broken before that effect runs, delay rendering the component until the client has hydrated [1][7]. This is commonly achieved by using a state variable (e.g., isMounted) initialized to false, which is then set to true inside a useEffect call [1][8]. The component renders a fallback or nothing until isMounted is true [1][3]. 3. Isomorphic Hooks: Some developers use a custom hook (sometimes called useIsomorphicLayoutEffect) that resolves to useLayoutEffect on the client and useEffect on the server to maintain consistency and avoid warnings, though the underlying need for layout timing must still be reconciled with the fact that the server has no layout information [6][3]. Warnings: Historically, React emitted a warning when useLayoutEffect was used during server rendering [2][3]. While there have been discussions and changes regarding this warning, the core technical limitation remains: useLayoutEffect is a browser-only tool [1][9]. Even if the warning is suppressed, the effect itself will not run until the JavaScript is downloaded and executed on the client [7][2]. Top results: [1], [7], [4], [3], [5]
Citations:
🏁 Script executed:
Repository: clerk/javascript
Length of output: 11349
🏁 Script executed:
Repository: clerk/javascript
Length of output: 6828
🌐 Web query:
site:react.dev/reference/react/useLayoutEffect server rendering warning useLayoutEffect does nothing on the server💡 Result:
The warning "useLayoutEffect does nothing on the server" occurs because useLayoutEffect is designed to run only on the client side after the browser has performed layout calculations [1][2]. During server-side rendering (SSR), there is no browser environment and therefore no layout information available to calculate, making the execution of such effects impossible [1][3]. To resolve this warning, you can use one of the following approaches recommended by the official React documentation [1][4]: 1. Replace useLayoutEffect with useEffect: If the logic does not strictly require blocking the paint (e.g., it does not involve immediate visual adjustments that would cause a visible "jump"), switching to useEffect is the preferred solution [1][5]. useEffect runs after the component has rendered and the browser has painted the screen [1][4]. 2. Use a client-only component: You can mark the component as client-only, which tells React to show a fallback (such as a spinner or loading state) during server rendering and only mount the component after hydration is complete [3][4]. 3. Delay rendering until after hydration: You can introduce a state variable (e.g., isMounted) initialized to false. Use a useEffect hook to set this value to true once the component mounts on the client. Then, use conditional rendering to display your content only when isMounted is true [1][6]. This ensures the component logic relying on layout measurements only executes in the client browser [1][7]. If your use case involves external data synchronization rather than layout measurements, consider using useSyncExternalStore instead, which is designed to support server rendering environments [3][7].
Citations:
Use an isomorphic layout effect in
PopoverPositioner.'use client'preserves the client boundary, but it does not prevent server pre-rendering. TheReact.useLayoutEffecthook is registered during SSR, where its callback cannot run and React may emit a server-rendering warning. UseReact.useEffecton the server andReact.useLayoutEffectin the browser through an isomorphic helper.🤖 Prompt for AI Agents
Source: Coding guidelines