Last active
July 31, 2024 06:35
-
-
Save michojekunle/c0df1d7d05b0017bed52319bd987e834 to your computer and use it in GitHub Desktop.
PHONE INPUT
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
| import { CheckIcon, ChevronsUpDown } from "lucide-react"; | |
| import * as React from "react"; | |
| import * as RPNInput from "react-phone-number-input"; | |
| import flags from "react-phone-number-input/flags"; | |
| import { Button } from "@/components/ui/button"; | |
| import { | |
| Command, | |
| CommandEmpty, | |
| CommandGroup, | |
| CommandInput, | |
| CommandItem, | |
| CommandList, | |
| } from "@/components/ui/command"; | |
| import { Input, InputProps } from "@/components/ui/input"; | |
| import { | |
| Popover, | |
| PopoverContent, | |
| PopoverTrigger, | |
| } from "@/components/ui/popover-dialog"; | |
| import { cn } from "@/lib/utils"; | |
| type PhoneInputProps = Omit< | |
| React.InputHTMLAttributes<HTMLInputElement>, | |
| "onChange" | "value" | |
| > & | |
| Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & { | |
| onChange?: (value: RPNInput.Value) => void; | |
| }; | |
| const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> = | |
| React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>( | |
| ({ className, onChange, ...props }, ref) => { | |
| return ( | |
| <RPNInput.default | |
| ref={ref} | |
| className={cn("flex", className)} | |
| flagComponent={FlagComponent} | |
| countrySelectComponent={CountrySelect} | |
| inputComponent={InputComponent} | |
| /** | |
| * Handles the onChange event. | |
| * | |
| * react-phone-number-input might trigger the onChange event as undefined | |
| * when a valid phone number is not entered. To prevent this, | |
| * the value is coerced to an empty string. | |
| * | |
| * @param {E164Number | undefined}} value - The entered value | |
| */ | |
| // @ts-ignore | |
| onChange={(value) => onChange?.(value || "")} | |
| {...props} | |
| /> | |
| ); | |
| }, | |
| ); | |
| PhoneInput.displayName = "PhoneInput"; | |
| const InputComponent = React.forwardRef<HTMLInputElement, InputProps>( | |
| ({ className, ...props }, ref) => ( | |
| <Input | |
| className={cn("rounded-s-none rounded-e-lg", className)} | |
| {...props} | |
| ref={ref} | |
| /> | |
| ), | |
| ); | |
| InputComponent.displayName = "InputComponent"; | |
| type CountrySelectOption = { label: string; value: RPNInput.Country }; | |
| type CountrySelectProps = { | |
| disabled?: boolean; | |
| value: RPNInput.Country; | |
| onChange: (value: RPNInput.Country) => void; | |
| options: CountrySelectOption[]; | |
| }; | |
| const CountrySelect = ({ | |
| disabled, | |
| value, | |
| onChange, | |
| options, | |
| }: CountrySelectProps) => { | |
| const [searchTerm, setSearchTerm] = React.useState(""); | |
| const filteredOptions = React.useMemo(() => { | |
| return options.filter((option) => | |
| option.label.toLowerCase().startsWith(searchTerm.toLowerCase()), | |
| ); | |
| }, [options, searchTerm]); | |
| const handleSelect = React.useCallback( | |
| (country: RPNInput.Country) => { | |
| onChange(country); | |
| }, | |
| [onChange], | |
| ); | |
| return ( | |
| <Popover> | |
| <PopoverTrigger asChild> | |
| <Button | |
| type="button" | |
| variant={"outline"} | |
| className={cn("flex gap-1 rounded-e-none rounded-s-lg px-3")} | |
| disabled={disabled} | |
| > | |
| <FlagComponent country={value} countryName={value} /> | |
| <ChevronsUpDown | |
| className={cn( | |
| "h-4 w-4 opacity-50 -mr-2", | |
| disabled ? "hidden" : "opacity-100", | |
| )} | |
| /> | |
| </Button> | |
| </PopoverTrigger> | |
| <PopoverContent className="p-0 w-[300px]"> | |
| <Command> | |
| <CommandList> | |
| <CommandInput | |
| placeholder="Search country..." | |
| value={searchTerm} | |
| onValueChange={setSearchTerm} | |
| /> | |
| <CommandEmpty>No country found.</CommandEmpty> | |
| <CommandGroup> | |
| {filteredOptions | |
| .filter((x) => x.value) | |
| .map((option) => ( | |
| <CommandItem | |
| className="gap-2" | |
| key={option.value} | |
| onSelect={() => handleSelect(option.value)} | |
| > | |
| <FlagComponent | |
| country={option.value} | |
| countryName={option.label} | |
| /> | |
| <span className="text-sm flex-1">{option.label}</span> | |
| {option.value && ( | |
| <span className="text-sm text-foreground/50"> | |
| {`+${RPNInput.getCountryCallingCode(option.value)}`} | |
| </span> | |
| )} | |
| <CheckIcon | |
| className={cn( | |
| "ml-auto h-4 w-4", | |
| option.value === value ? "opacity-100" : "opacity-0", | |
| )} | |
| /> | |
| </CommandItem> | |
| ))} | |
| </CommandGroup> | |
| </CommandList> | |
| </Command> | |
| </PopoverContent> | |
| </Popover> | |
| ); | |
| }; | |
| // const CountrySelect = ({ disabled, value, onChange, options }: CountrySelectProps) => { | |
| // const handleSelect = React.useCallback( | |
| // (country: RPNInput.Country) => { | |
| // onChange(country); | |
| // }, | |
| // [onChange], | |
| // ); | |
| // return ( | |
| // <Popover> | |
| // <PopoverTrigger asChild> | |
| // <Button | |
| // type="button" | |
| // variant={"outline"} | |
| // className={cn("flex gap-1 rounded-e-none rounded-s-lg px-3")} | |
| // disabled={disabled} | |
| // > | |
| // <FlagComponent country={value} countryName={value} /> | |
| // <ChevronsUpDown className={cn("h-4 w-4 opacity-50 -mr-2", disabled ? "hidden" : "opacity-100")} /> | |
| // </Button> | |
| // </PopoverTrigger> | |
| // <PopoverContent className="p-0 w-[300px]"> | |
| // <Command> | |
| // <CommandList> | |
| // <CommandInput placeholder="Search country..." /> | |
| // <CommandEmpty>No country found.</CommandEmpty> | |
| // <CommandGroup> | |
| // {options | |
| // .filter((x) => x.value) | |
| // .map((option) => ( | |
| // <CommandItem | |
| // className="gap-2" | |
| // key={option.value} | |
| // onSelect={() => handleSelect(option.value)} | |
| // > | |
| // <FlagComponent country={option.value} countryName={option.label} /> | |
| // <span className="text-sm flex-1">{option.label}</span> | |
| // {option.value && ( | |
| // <span className="text-sm text-foreground/50"> | |
| // {`+${RPNInput.getCountryCallingCode(option.value)}`} | |
| // </span> | |
| // )} | |
| // <CheckIcon | |
| // className={cn("ml-auto h-4 w-4", option.value === value ? "opacity-100" : "opacity-0")} | |
| // /> | |
| // </CommandItem> | |
| // ))} | |
| // </CommandGroup> | |
| // </CommandList> | |
| // </Command> | |
| // </PopoverContent> | |
| // </Popover> | |
| // ); | |
| // }; | |
| const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => { | |
| const Flag = flags[country]; | |
| return ( | |
| <span className="flex h-4 w-6 overflow-hidden rounded-sm bg-foreground/20"> | |
| {Flag && <Flag title={countryName} />} | |
| </span> | |
| ); | |
| }; | |
| FlagComponent.displayName = "FlagComponent"; | |
| export { PhoneInput }; |
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
| "use client"; | |
| import * as React from "react"; | |
| import * as PopoverPrimitive from "@radix-ui/react-popover"; | |
| import { cn } from "@/lib/utils"; | |
| const Popover = PopoverPrimitive.Root; | |
| const PopoverTrigger = PopoverPrimitive.Trigger; | |
| const PopoverContent = React.forwardRef< | |
| React.ElementRef<typeof PopoverPrimitive.Content>, | |
| React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> | |
| >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( | |
| <PopoverPrimitive.Content | |
| ref={ref} | |
| align={align} | |
| sideOffset={sideOffset} | |
| className={cn( | |
| "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | |
| className, | |
| )} | |
| {...props} | |
| /> | |
| )); | |
| PopoverContent.displayName = PopoverPrimitive.Content.displayName; | |
| export { Popover, PopoverTrigger, PopoverContent }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment