|
|
"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;
|
|
|
}
|
|
|
|
|
|
/** 电话 — 可搜索国家列表(A–Z)+ 自动显示区号前缀 */
|
|
|
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>
|
|
|
);
|
|
|
}
|