mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
142 lines
4.6 KiB
TypeScript
142 lines
4.6 KiB
TypeScript
import * as React from "react";
|
|
|
|
import { E164Number } from "libphonenumber-js";
|
|
import { ChevronDownIcon, PhoneIcon } from "lucide-react";
|
|
import * as RPNInput from "react-phone-number-input";
|
|
import flags from "react-phone-number-input/flags";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
import { Input, InputProps } from "@/components/ui/input";
|
|
|
|
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("shadow-xs flex rounded-md", className)}
|
|
international
|
|
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
|
|
*/
|
|
onChange={(value) => onChange?.((value || "") as E164Number)}
|
|
{...props}
|
|
/>
|
|
);
|
|
},
|
|
);
|
|
PhoneInput.displayName = "PhoneInput";
|
|
|
|
const InputComponent = React.forwardRef<HTMLInputElement, InputProps>(
|
|
({ className, ...props }, ref) => (
|
|
<Input
|
|
data-slot="phone-input"
|
|
className={cn(
|
|
"-ms-px rounded-s-none border-0 bg-transparent py-1.5 shadow-sm ring-1 ring-inset ring-gray-600 focus:ring-2 focus:ring-inset focus:ring-gray-300 focus-visible:z-10",
|
|
className,
|
|
)}
|
|
style={
|
|
{
|
|
backgroundColor: "var(--phone-input-bg)",
|
|
color: "var(--phone-input-color)",
|
|
} as React.CSSProperties
|
|
}
|
|
{...props}
|
|
ref={ref}
|
|
/>
|
|
),
|
|
);
|
|
InputComponent.displayName = "InputComponent";
|
|
|
|
type CountrySelectProps = {
|
|
disabled?: boolean;
|
|
value: RPNInput.Country;
|
|
onChange: (value: RPNInput.Country) => void;
|
|
options: { label: string; value: RPNInput.Country | undefined }[];
|
|
};
|
|
|
|
const CountrySelect = ({
|
|
disabled,
|
|
value,
|
|
onChange,
|
|
options,
|
|
}: CountrySelectProps) => {
|
|
const handleSelect = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
onChange(event.target.value as RPNInput.Country);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="has-aria-invalid:border-destructive/60 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-disabled:pointer-events-none has-disabled:opacity-50 relative inline-flex h-9 items-center self-stretch rounded-s-md border-0 bg-transparent py-2 pe-2 ps-3 text-muted-foreground shadow-sm outline-none ring-1 ring-inset ring-gray-600 transition-[color,box-shadow] focus-within:z-10 focus-within:ring-2 focus-within:ring-inset focus-within:ring-gray-300 hover:text-foreground"
|
|
style={
|
|
{
|
|
backgroundColor: "var(--phone-input-bg)",
|
|
color: "var(--phone-input-color)",
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
<div className="inline-flex items-center gap-1" aria-hidden="true">
|
|
<FlagComponent country={value} countryName={value} aria-hidden="true" />
|
|
<span className="text-muted-foreground/80">
|
|
<ChevronDownIcon size={16} aria-hidden="true" />
|
|
</span>
|
|
</div>
|
|
<select
|
|
disabled={disabled}
|
|
value={value}
|
|
onChange={handleSelect}
|
|
className="absolute inset-0 text-sm opacity-0"
|
|
aria-label="Select country"
|
|
>
|
|
<option key="default" value="">
|
|
Select a country
|
|
</option>
|
|
{options
|
|
.filter((x) => x.value)
|
|
.map((option, i) => (
|
|
<option key={option.value ?? `empty-${i}`} value={option.value}>
|
|
{option.label}{" "}
|
|
{option.value &&
|
|
`+${RPNInput.getCountryCallingCode(option.value)}`}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {
|
|
const Flag = flags[country];
|
|
|
|
return (
|
|
<span className="w-5 overflow-hidden rounded-sm">
|
|
{Flag ? (
|
|
<Flag title={countryName} />
|
|
) : (
|
|
<PhoneIcon size={16} aria-hidden="true" />
|
|
)}
|
|
</span>
|
|
);
|
|
};
|
|
FlagComponent.displayName = "FlagComponent";
|
|
|
|
export { PhoneInput };
|