Skip to content
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

feat(phone-input): new phone input component #2694

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
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
98 changes: 98 additions & 0 deletions apps/www/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,13 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"phone-input": {
name: "phone-input",
type: "components:ui",
registryDependencies: ["button","command","input","popover"],
component: React.lazy(() => import("@/registry/default/ui/phone-input")),
files: ["registry/default/ui/phone-input.tsx"],
},
"popover": {
name: "popover",
type: "components:ui",
Expand Down Expand Up @@ -1369,6 +1376,48 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"phone-input-demo": {
name: "phone-input-demo",
type: "components:example",
registryDependencies: ["phone-input"],
component: React.lazy(() => import("@/registry/default/example/phone-input-demo")),
files: ["registry/default/example/phone-input-demo.tsx"],
},
"phone-input-default": {
name: "phone-input-default",
type: "components:example",
registryDependencies: ["phone-input"],
component: React.lazy(() => import("@/registry/default/example/phone-input-default")),
files: ["registry/default/example/phone-input-default.tsx"],
},
"phone-input-international": {
name: "phone-input-international",
type: "components:example",
registryDependencies: ["phone-input"],
component: React.lazy(() => import("@/registry/default/example/phone-input-international")),
files: ["registry/default/example/phone-input-international.tsx"],
},
"phone-input-national": {
name: "phone-input-national",
type: "components:example",
registryDependencies: ["phone-input"],
component: React.lazy(() => import("@/registry/default/example/phone-input-national")),
files: ["registry/default/example/phone-input-national.tsx"],
},
"phone-input-initial": {
name: "phone-input-initial",
type: "components:example",
registryDependencies: ["phone-input"],
component: React.lazy(() => import("@/registry/default/example/phone-input-initial")),
files: ["registry/default/example/phone-input-initial.tsx"],
},
"phone-input-form": {
name: "phone-input-form",
type: "components:example",
registryDependencies: ["phone-input","form"],
component: React.lazy(() => import("@/registry/default/example/phone-input-form")),
files: ["registry/default/example/phone-input-form.tsx"],
},
"popover-demo": {
name: "popover-demo",
type: "components:example",
Expand Down Expand Up @@ -2681,6 +2730,13 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"phone-input": {
name: "phone-input",
type: "components:ui",
registryDependencies: ["button","command","input","popover"],
component: React.lazy(() => import("@/registry/new-york/ui/phone-input")),
files: ["registry/new-york/ui/phone-input.tsx"],
},
"popover": {
name: "popover",
type: "components:ui",
Expand Down Expand Up @@ -3759,6 +3815,48 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"phone-input-demo": {
name: "phone-input-demo",
type: "components:example",
registryDependencies: ["phone-input"],
component: React.lazy(() => import("@/registry/new-york/example/phone-input-demo")),
files: ["registry/new-york/example/phone-input-demo.tsx"],
},
"phone-input-default": {
name: "phone-input-default",
type: "components:example",
registryDependencies: ["phone-input"],
component: React.lazy(() => import("@/registry/new-york/example/phone-input-default")),
files: ["registry/new-york/example/phone-input-default.tsx"],
},
"phone-input-international": {
name: "phone-input-international",
type: "components:example",
registryDependencies: ["phone-input"],
component: React.lazy(() => import("@/registry/new-york/example/phone-input-international")),
files: ["registry/new-york/example/phone-input-international.tsx"],
},
"phone-input-national": {
name: "phone-input-national",
type: "components:example",
registryDependencies: ["phone-input"],
component: React.lazy(() => import("@/registry/new-york/example/phone-input-national")),
files: ["registry/new-york/example/phone-input-national.tsx"],
},
"phone-input-initial": {
name: "phone-input-initial",
type: "components:example",
registryDependencies: ["phone-input"],
component: React.lazy(() => import("@/registry/new-york/example/phone-input-initial")),
files: ["registry/new-york/example/phone-input-initial.tsx"],
},
"phone-input-form": {
name: "phone-input-form",
type: "components:example",
registryDependencies: ["phone-input","form"],
component: React.lazy(() => import("@/registry/new-york/example/phone-input-form")),
files: ["registry/new-york/example/phone-input-form.tsx"],
},
"popover-demo": {
name: "popover-demo",
type: "components:example",
Expand Down
6 changes: 6 additions & 0 deletions apps/www/config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ export const docsConfig: DocsConfig = {
href: "/docs/components/pagination",
items: [],
},
{
title: "Phone Input",
href: "/docs/components/phone-input",
items: [],
label: "New",
},
{
title: "Popover",
href: "/docs/components/popover",
Expand Down
1 change: 1 addition & 0 deletions apps/www/content/docs/components/form.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ See the following links for more examples on how to use the `<Form />` component
- [Checkbox](/docs/components/checkbox#form)
- [Date Picker](/docs/components/date-picker#form)
- [Input](/docs/components/input#form)
- [Phone Input](/docs/components/phone-input#form)
- [Radio Group](/docs/components/radio-group#form)
- [Select](/docs/components/select#form)
- [Switch](/docs/components/switch#form)
Expand Down
101 changes: 101 additions & 0 deletions apps/www/content/docs/components/phone-input.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
title: Phone Input
description: This component provides phone number input based on the selected country.
component: true
links:
doc: https://catamphetamine.gitlab.io/react-phone-number-input
---

<ComponentPreview
name="phone-input-demo"
className="[&_phone-input]:max-w-xs"
/>

## About

The `PhoneInput` component is built on top of [React Phone Number Input](https://catamphetamine.gitlab.io/react-phone-number-input).

## Installation

<Tabs defaultValue="cli">

<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn-ui@latest add phone-input
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="phone-input" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Usage

```tsx
import { PhoneInput } from "@/components/ui/phone-input"
```

```tsx
const [value, setValue] = React.useState("")

return <PhoneInput value={value} onChange={setValue} />
```

## Examples

### Default

<ComponentPreview
name="phone-input-demo"
className="[&_phone-input]:max-w-xs"
/>

### Setting default country

<ComponentPreview
name="phone-input-default"
className="[&_phone-input]:max-w-xs"
/>

### Setting force international format

<ComponentPreview
name="phone-input-international"
className="[&_phone-input]:max-w-xs"
/>

### Setting force national format

<ComponentPreview
name="phone-input-national"
className="[&_phone-input]:max-w-xs"
/>

### Setting initial value format

<ComponentPreview
name="phone-input-initial"
className="[&_phone-input]:max-w-xs"
/>

### Form

<ComponentPreview name="phone-input-form" />
1 change: 1 addition & 0 deletions apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"react-day-picker": "^8.7.1",
"react-dom": "^18.2.0",
"react-hook-form": "^7.44.2",
"react-phone-number-input": "^3.3.9",
"react-resizable-panels": "^0.0.55",
"react-wrap-balancer": "^0.4.1",
"recharts": "^2.6.2",
Expand Down
16 changes: 16 additions & 0 deletions apps/www/public/registry/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,22 @@
],
"type": "components:ui"
},
{
"name": "phone-input",
"dependencies": [
"react-phone-number-input"
],
"registryDependencies": [
"button",
"command",
"input",
"popover"
],
"files": [
"ui/phone-input.tsx"
],
"type": "components:ui"
},
{
"name": "popover",
"dependencies": [
Expand Down
19 changes: 19 additions & 0 deletions apps/www/public/registry/styles/default/phone-input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "phone-input",
"dependencies": [
"react-phone-number-input"
],
"registryDependencies": [
"button",
"command",
"input",
"popover"
],
"files": [
{
"name": "phone-input.tsx",
"content": "import * as React from \"react\"\nimport { Check, ChevronsUpDown } from \"lucide-react\"\nimport * as RPNInput from \"react-phone-number-input\"\nimport flags from \"react-phone-number-input/flags\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/registry/default/ui/command\"\nimport { Input, InputProps } from \"@/registry/default/ui/input\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/default/ui/popover\"\n\ntype PhoneInputProps = Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n \"onChange\" | \"value\"\n> &\n Omit<RPNInput.Props<typeof RPNInput.default>, \"onChange\"> & {\n onChange?: (value: RPNInput.Value) => void\n }\n\nconst PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =\n React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(\n ({ className, onChange, ...props }, ref) => (\n <RPNInput.default\n ref={ref}\n className={cn(\"flex\", className)}\n flagComponent={FlagComponent}\n countrySelectComponent={CountrySelect}\n inputComponent={InputComponent}\n /**\n * Handles the onChange event.\n *\n * react-phone-number-input might trigger the onChange event as undefined\n * when a valid phone number is not entered. To prevent this,\n * the value is coerced to an empty string.\n *\n * @param {E164Number | undefined} value - The entered value\n */\n onChange={(value) => onChange?.(value || \"\")}\n {...props}\n />\n )\n )\nPhoneInput.displayName = \"PhoneInput\"\n\nconst InputComponent = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, ...props }, ref) => (\n <Input\n className={cn(\"rounded-s-none rounded-e-lg\", className)}\n {...props}\n ref={ref}\n />\n )\n)\nInputComponent.displayName = \"InputComponent\"\n\ntype CountrySelectOption = { label: string; value: RPNInput.Country }\n\ntype CountrySelectProps = {\n disabled?: boolean\n value: RPNInput.Country\n onChange: (value: RPNInput.Country) => void\n options: CountrySelectOption[]\n}\n\nconst CountrySelect = ({\n disabled,\n value,\n onChange,\n options,\n}: CountrySelectProps) => {\n const handleSelect = React.useCallback(\n (country: RPNInput.Country) => {\n onChange(country)\n },\n [onChange]\n )\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant={\"outline\"}\n className={cn(\"flex gap-1 rounded-e-none rounded-s-lg px-3\")}\n disabled={disabled}\n >\n <FlagComponent country={value} countryName={value} />\n <ChevronsUpDown\n className={cn(\n \"h-4 w-4 opacity-50 -mr-2\",\n disabled ? \"hidden\" : \"opacity-100\"\n )}\n />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"p-0 w-[300px]\">\n <Command>\n <CommandList>\n <CommandInput placeholder=\"Search country...\" />\n <CommandEmpty>No country found.</CommandEmpty>\n <CommandGroup>\n {options.map((option) => {\n console.log({ option })\n return (\n <CommandItem\n className=\"gap-2\"\n key={option.value || \"ZZ\"}\n onSelect={() => handleSelect(option.value)}\n >\n <FlagComponent\n country={option.value}\n countryName={option.label}\n />\n <span className=\"text-sm flex-1\">{option.label}</span>\n {option.value && (\n <span className=\"text-sm text-foreground/50\">\n {`+${RPNInput.getCountryCallingCode(option.value)}`}\n </span>\n )}\n <Check\n className={cn(\n \"ml-auto h-4 w-4\",\n option.value === value ? \"opacity-100\" : \"opacity-0\"\n )}\n />\n </CommandItem>\n )\n })}\n </CommandGroup>\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n )\n}\n\nconst FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {\n const Flag = flags[country]\n\n return (\n <span className=\"flex h-4 w-6 overflow-hidden rounded-sm bg-foreground/20\">\n {Flag && <Flag title={countryName} />}\n </span>\n )\n}\nFlagComponent.displayName = \"FlagComponent\"\n\nexport { PhoneInput }\n"
}
],
"type": "components:ui"
}
19 changes: 19 additions & 0 deletions apps/www/public/registry/styles/new-york/phone-input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "phone-input",
"dependencies": [
"react-phone-number-input"
],
"registryDependencies": [
"button",
"command",
"input",
"popover"
],
"files": [
{
"name": "phone-input.tsx",
"content": "import * as React from \"react\"\nimport { CaretSortIcon, CheckIcon } from \"@radix-ui/react-icons\"\nimport * as RPNInput from \"react-phone-number-input\"\nimport flags from \"react-phone-number-input/flags\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/new-york/ui/button\"\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/registry/new-york/ui/command\"\nimport { Input, InputProps } from \"@/registry/new-york/ui/input\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york/ui/popover\"\n\ntype PhoneInputProps = Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n \"onChange\" | \"value\"\n> &\n Omit<RPNInput.Props<typeof RPNInput.default>, \"onChange\"> & {\n onChange?: (value: RPNInput.Value) => void\n }\n\nconst PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =\n React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(\n ({ className, onChange, ...props }, ref) => (\n <RPNInput.default\n ref={ref}\n className={cn(\"flex\", className)}\n flagComponent={FlagComponent}\n countrySelectComponent={CountrySelect}\n inputComponent={InputComponent}\n /**\n * Handles the onChange event.\n *\n * react-phone-number-input might trigger the onChange event as undefined\n * when a valid phone number is not entered. To prevent this,\n * the value is coerced to an empty string.\n *\n * @param {E164Number | undefined} value - The entered value\n */\n onChange={(value) => onChange?.(value || \"\")}\n {...props}\n />\n )\n )\nPhoneInput.displayName = \"PhoneInput\"\n\nconst InputComponent = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, ...props }, ref) => (\n <Input\n className={cn(\"rounded-s-none rounded-e-lg\", className)}\n {...props}\n ref={ref}\n />\n )\n)\nInputComponent.displayName = \"InputComponent\"\n\ntype CountrySelectOption = { label: string; value: RPNInput.Country }\n\ntype CountrySelectProps = {\n disabled?: boolean\n value: RPNInput.Country\n onChange: (value: RPNInput.Country) => void\n options: CountrySelectOption[]\n}\n\nconst CountrySelect = ({\n disabled,\n value,\n onChange,\n options,\n}: CountrySelectProps) => {\n const handleSelect = React.useCallback(\n (country: RPNInput.Country) => {\n onChange(country)\n },\n [onChange]\n )\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant={\"outline\"}\n className={cn(\"flex gap-1 rounded-e-none rounded-s-lg px-3\")}\n disabled={disabled}\n >\n <FlagComponent country={value} countryName={value} />\n <CaretSortIcon\n className={cn(\n \"h-4 w-4 opacity-50 -mr-2\",\n disabled ? \"hidden\" : \"opacity-100\"\n )}\n />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"p-0 w-[300px]\">\n <Command>\n <CommandList>\n <CommandInput placeholder=\"Search country...\" />\n <CommandEmpty>No country found.</CommandEmpty>\n <CommandGroup>\n {options.map((option) => (\n <CommandItem\n className=\"gap-2\"\n key={option.value || \"ZZ\"}\n onSelect={() => handleSelect(option.value)}\n >\n <FlagComponent\n country={option.value}\n countryName={option.label}\n />\n <span className=\"text-sm flex-1\">{option.label}</span>\n {option.value && (\n <span className=\"text-sm text-foreground/50\">\n {`+${RPNInput.getCountryCallingCode(option.value)}`}\n </span>\n )}\n <CheckIcon\n className={cn(\n \"ml-auto h-4 w-4\",\n option.value === value ? \"opacity-100\" : \"opacity-0\"\n )}\n />\n </CommandItem>\n ))}\n </CommandGroup>\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n )\n}\n\nconst FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {\n const Flag = flags[country]\n\n return (\n <span className=\"flex h-4 w-6 overflow-hidden rounded-sm bg-foreground/20\">\n {Flag && <Flag title={countryName} />}\n </span>\n )\n}\nFlagComponent.displayName = \"FlagComponent\"\n\nexport { PhoneInput }\n"
}
],
"type": "components:ui"
}
9 changes: 9 additions & 0 deletions apps/www/registry/default/example/phone-input-default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from "react"

import { PhoneInput } from "@/registry/default/ui/phone-input"

export default function PhoneInputDefault() {
const [value, setValue] = React.useState("")

return <PhoneInput value={value} onChange={setValue} defaultCountry="US" />
}
15 changes: 15 additions & 0 deletions apps/www/registry/default/example/phone-input-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from "react"

import { PhoneInput } from "@/registry/default/ui/phone-input"

export default function PhoneInputDemo() {
const [value, setValue] = React.useState("")

return (
<PhoneInput
value={value}
onChange={setValue}
placeholder="Enter a phone nsumber"
/>
)
}
Loading