You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

156 lines
5.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"use client";
import { useEffect, useId, useRef, useState } from "react";
import { P1FieldLabel } from "@/components/priority1/feathery-card";
import {
filterPhoneCountries,
findPhoneCountry,
type PhoneCountry,
} from "@/lib/priority1/phone-countries";
interface P1PhoneFieldProps {
country: string;
value: string;
disabled?: boolean;
error?: string;
onCountryChange: (code: string) => void;
onChange: (digits: string) => void;
}
function formatUsPhoneDisplay(digits: string): string {
const d = digits.replace(/\D/g, "").slice(0, 10);
if (d.length <= 3) return d;
if (d.length <= 6) return `${d.slice(0, 3)} ${d.slice(3)}`;
return `${d.slice(0, 3)} ${d.slice(3, 6)} ${d.slice(6)}`;
}
function formatPhoneDisplay(country: PhoneCountry, digits: string): string {
if (country.code === "US" || country.dial === "+1") {
return formatUsPhoneDisplay(digits);
}
return digits.replace(/\D/g, "").slice(0, 15);
}
function maxPhoneDigits(country: PhoneCountry): number {
if (country.code === "US" || (country.dial === "+1" && country.code !== "DO")) {
return 10;
}
return 15;
}
/** 电话 — 可搜索国家列表AZ+ 自动显示区号前缀 */
export function P1PhoneField({
country,
value,
disabled,
error,
onCountryChange,
onChange,
}: P1PhoneFieldProps) {
const listId = useId();
const rootRef = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const selected = findPhoneCountry(country);
const filtered = filterPhoneCountries(search);
useEffect(() => {
if (!open) return;
const onDoc = (e: MouseEvent) => {
if (!rootRef.current?.contains(e.target as Node)) setOpen(false);
};
document.addEventListener("mousedown", onDoc);
return () => document.removeEventListener("mousedown", onDoc);
}, [open]);
const pickCountry = (c: PhoneCountry) => {
onCountryChange(c.code);
setOpen(false);
setSearch("");
};
return (
<div className="space-y-1" ref={rootRef}>
<P1FieldLabel required htmlFor="p1-phone">
</P1FieldLabel>
<div
className={`flex overflow-visible rounded-sm border bg-neutral-100 ${
error ? "border-red-500" : "border-neutral-400"
}`}
>
<div className="relative shrink-0">
<button
type="button"
disabled={disabled}
aria-expanded={open}
aria-controls={listId}
onClick={() => setOpen((v) => !v)}
className="flex h-11 min-w-[7.5rem] items-center gap-1 border-r border-neutral-300 bg-white px-2 text-left text-sm focus:outline-none disabled:opacity-60"
>
<span className="truncate font-medium">{selected.dial}</span>
<span className="text-neutral-400" aria-hidden>
</span>
</button>
{open ? (
<div
id={listId}
className="absolute left-0 top-full z-30 mt-1 w-72 rounded-sm border border-neutral-300 bg-white shadow-lg"
>
<div className="border-b border-neutral-200 p-2">
<input
type="search"
autoFocus
value={search}
placeholder="搜索国家或区号…"
onChange={(e) => setSearch(e.target.value)}
className="h-9 w-full rounded-sm border border-neutral-300 px-2 text-sm focus:border-neutral-600 focus:outline-none"
/>
</div>
<ul className="max-h-56 overflow-y-auto py-1" role="listbox">
{filtered.map((c) => (
<li key={c.code}>
<button
type="button"
role="option"
aria-selected={c.code === selected.code}
onClick={() => pickCountry(c)}
className={`flex w-full items-center justify-between px-3 py-2 text-left text-sm hover:bg-neutral-100 ${
c.code === selected.code ? "bg-neutral-50 font-medium" : ""
}`}
>
<span className="truncate">{c.name}</span>
<span className="ml-2 shrink-0 text-neutral-500">{c.dial}</span>
</button>
</li>
))}
{filtered.length === 0 ? (
<li className="px-3 py-2 text-sm text-neutral-500"></li>
) : null}
</ul>
</div>
) : null}
</div>
<div className="flex min-w-0 flex-1 items-center gap-1 px-2 text-sm text-neutral-600">
<span className="shrink-0 font-medium">{selected.dial}</span>
<input
id="p1-phone"
type="tel"
disabled={disabled}
value={formatPhoneDisplay(selected, value)}
placeholder={selected.code === "US" ? "753 640 7420" : "电话号码"}
onChange={(e) =>
onChange(
e.target.value.replace(/\D/g, "").slice(0, maxPhoneDigits(selected)),
)
}
className="h-11 min-w-0 flex-1 bg-transparent focus:outline-none disabled:opacity-60"
/>
</div>
</div>
{error ? <p className="text-xs text-red-600">{error}</p> : null}
</div>
);
}