Skip to content

Commit

Permalink
feat(phone-input): add phone input component
Browse files Browse the repository at this point in the history
  • Loading branch information
omeralpi committed Feb 10, 2024
1 parent 0fae3fd commit b9a503d
Show file tree
Hide file tree
Showing 24 changed files with 916 additions and 0 deletions.
98 changes: 98 additions & 0 deletions apps/www/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,13 @@ export const Index: Record<string, any> = {
component: React.lazy(() => import("@/registry/default/ui/pagination")),
files: ["registry/default/ui/pagination.tsx"],
},
"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 @@ -782,6 +789,48 @@ export const Index: Record<string, any> = {
component: React.lazy(() => import("@/registry/default/example/pagination-demo")),
files: ["registry/default/example/pagination-demo.tsx"],
},
"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 @@ -1399,6 +1448,13 @@ export const Index: Record<string, any> = {
component: React.lazy(() => import("@/registry/new-york/ui/pagination")),
files: ["registry/new-york/ui/pagination.tsx"],
},
"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 @@ -2008,6 +2064,48 @@ export const Index: Record<string, any> = {
component: React.lazy(() => import("@/registry/new-york/example/pagination-demo")),
files: ["registry/new-york/example/pagination-demo.tsx"],
},
"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 @@ -230,6 +230,12 @@ export const docsConfig: DocsConfig = {
items: [],
label: "New",
},
{
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, type PhoneInputValue } from "@/components/ui/phone-input"
```

```tsx
const [value, setValue] = React.useState<PhoneInputValue>()

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 @@ -69,6 +69,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 @@ -249,6 +249,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\nexport type PhoneInputValue = RPNInput.Value\n\ntype PhoneInputProps = React.ComponentProps<typeof RPNInput.default>\n\nconst PhoneInput = ({ className, ...props }: PhoneInputProps) => {\n return (\n <RPNInput.default\n placeholder={\"Enter a phone number\"}\n className={cn(\"flex\", className)}\n flagComponent={FlagComponent}\n inputComponent={InputComponent}\n countrySelectComponent={CountrySelect}\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)\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(\"rounded-e-none rounded-s-lg pl-3 pr-1 flex gap-1\")}\n disabled={disabled}\n >\n <span className=\"flex items-center truncate\">\n <div className=\"bg-foreground/20 rounded-sm flex w-6 h-4\">\n {value && <FlagComponent country={value} countryName={value} />}\n </div>\n </span>\n <ChevronsUpDown className={`h-4 w-4 ${disabled ? \"hidden\" : \"\"}`} />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-[300px] p-0\">\n <Command>\n <CommandList>\n <CommandInput placeholder=\"Search country...\" />\n <CommandEmpty>No country found.</CommandEmpty>\n <CommandGroup>\n {options\n .filter((x) => x.value)\n .map((option) => (\n <CommandItem\n className={\"text-sm gap-2\"}\n key={option.value}\n onSelect={() => handleSelect(option.value)}\n >\n <FlagComponent\n country={option.value}\n countryName={option.label}\n />\n <span>{option.label}</span>\n <span className=\"text-foreground/50\">\n {`+${RPNInput.getCountryCallingCode(option.value)}`}\n </span>\n <Check\n className={`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\n className={\"inline object-contain w-6 h-4 overflow-hidden rounded-sm\"}\n >\n {Flag && <Flag title={countryName} />}\n </span>\n )\n}\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 { CheckIcon, ChevronsUpDownIcon } 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/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\nexport type PhoneInputValue = RPNInput.Value\n\ntype PhoneInputProps = React.ComponentProps<typeof RPNInput.default>\n\nconst PhoneInput = ({ className, ...props }: PhoneInputProps) => {\n return (\n <RPNInput.default\n placeholder={\"Enter a phone number\"}\n className={cn(\"flex\", className)}\n flagComponent={FlagComponent}\n inputComponent={InputComponent}\n countrySelectComponent={CountrySelect}\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)\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(\"rounded-e-none rounded-s-lg pl-3 pr-1 flex gap-1\")}\n disabled={disabled}\n >\n <span className=\"flex items-center truncate\">\n <div className=\"bg-foreground/20 rounded-sm flex w-6 h-4\">\n {value && <FlagComponent country={value} countryName={value} />}\n </div>\n </span>\n <ChevronsUpDownIcon\n className={`h-4 w-4 ${disabled ? \"hidden\" : \"\"}`}\n />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-[300px] p-0\">\n <Command>\n <CommandList>\n <CommandInput placeholder=\"Search country...\" />\n <CommandEmpty>No country found.</CommandEmpty>\n <CommandGroup>\n {options\n .filter((x) => x.value)\n .map((option) => (\n <CommandItem\n className={\"text-sm gap-2\"}\n key={option.value}\n onSelect={() => handleSelect(option.value)}\n >\n <FlagComponent\n country={option.value}\n countryName={option.label}\n />\n <span>{option.label}</span>\n <span className=\"text-foreground/50\">\n {`+${RPNInput.getCountryCallingCode(option.value)}`}\n </span>\n <CheckIcon\n className={`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\n className={\"inline object-contain w-6 h-4 overflow-hidden rounded-sm\"}\n >\n {Flag && <Flag title={countryName} />}\n </span>\n )\n}\n\nexport { PhoneInput }\n"
}
],
"type": "components:ui"
}
12 changes: 12 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,12 @@
import * as React from "react"

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

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

return <PhoneInput value={value} onChange={setValue} defaultCountry="US" />
}
12 changes: 12 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,12 @@
import * as React from "react"

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

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

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

0 comments on commit b9a503d

Please sign in to comment.