- {/* Country Selection Dropdown */}
+ {/* Country Selection Trigger */}
) : (
<>
- {/* 1. Country Selection Dropdown */}
+ {/* 1. Country Selection Trigger */}
-
- {isOpen && (
-
- {options.length > 3 || searchQuery ? (
-
-
-
setSearchQuery(e.target.value)}
- placeholder="Search..."
- className="flex-1 bg-transparent text-[14px] font-medium text-[#181818] outline-none placeholder:text-[#98A2B3]"
- />
- {searchQuery ? (
-
- ) : null}
-
- ) : null}
-
-
e.stopPropagation()}
- onTouchMove={(e) => e.stopPropagation()}
- onTouchEnd={(e) => e.stopPropagation()}
- className="flex max-h-[220px] flex-col gap-3.5 overflow-y-auto overscroll-contain pr-1"
- >
- {filteredOptions.length > 0 ? (
- filteredOptions.map((option) => {
- const isSelected = selectedCountry === option;
-
- return (
-
- );
- })
- ) : (
-
- No options found
-
- )}
-
-
- )}
{/* 2. City Text Input */}
>
)}
+
+ {/* Country Selection Bottom Sheet Modal */}
+ {isOpen &&
+ createPortal(
+
event.stopPropagation()}
+ onTouchStart={(event) => event.stopPropagation()}
+ onTouchMove={(event) => event.stopPropagation()}
+ onTouchEnd={(event) => event.stopPropagation()}
+ onClick={(e) => {
+ if (e.target === e.currentTarget) {
+ closeSheet();
+ }
+ }}
+ >
+
e.stopPropagation()}
+ >
+ {/* Drag Handle Notch */}
+
+
+ {/* Header with Title and Close Button */}
+
+
+ {selectCountryPlaceholder}
+
+
+
+
+ {/* Search Bar */}
+
+
+
+
setSearchQuery(e.target.value)}
+ placeholder={searchPlaceholder}
+ className="flex-1 bg-transparent text-[14px] font-medium text-[#181818] outline-none placeholder:text-[#98A2B3]"
+ />
+ {searchQuery ? (
+
+ ) : null}
+
+
+
+ {/* Country Options List */}
+
+ {filteredOptions.length > 0 ? (
+ filteredOptions.map((option) => {
+ const isSelected = selectedCountry === option;
+
+ return (
+
+ );
+ })
+ ) : (
+
+ {noResultsText}
+
+ )}
+
+
+
,
+ document.body,
+ )}
);
}
diff --git a/src/components/Componentes/question-date-sheet.tsx b/src/components/Componentes/question-date-sheet.tsx
new file mode 100644
index 0000000..6b9514e
--- /dev/null
+++ b/src/components/Componentes/question-date-sheet.tsx
@@ -0,0 +1,348 @@
+"use client";
+
+import {
+ type ReactNode,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import { createPortal } from "react-dom";
+import type { QuestionField } from "@/lib/schema-adapter";
+import { useI18n } from "@/translations/provider";
+import { useSheetScrollLock } from "./use-sheet-scroll-lock";
+
+const EXIT_ANIMATION_MS = 300;
+const WHEEL_ITEM_HEIGHT = 48;
+const WHEEL_EDGE_PADDING = 108;
+const SCROLL_SETTLE_MS = 90;
+
+const MONTH_VALUES = [
+ "01",
+ "02",
+ "03",
+ "04",
+ "05",
+ "06",
+ "07",
+ "08",
+ "09",
+ "10",
+ "11",
+ "12",
+];
+
+const MIN_AGE = 18;
+const currentYear = new Date().getFullYear();
+const maxBirthYear = currentYear - MIN_AGE;
+const YEARS = Array.from({ length: 80 }, (_, i) =>
+ (maxBirthYear - i).toString(),
+);
+
+function daysInMonth(year: string, month: string): number {
+ const y = Number.parseInt(year, 10);
+ const m = Number.parseInt(month, 10);
+ if (!y || !m) return 31;
+ return new Date(y, m, 0).getDate();
+}
+
+type DatePart = "day" | "month" | "year";
+
+type QuestionDateSheetProps = {
+ question: QuestionField;
+ value: string;
+ onApply: (formattedDate: string) => void;
+ onClose: () => void;
+};
+
+function WheelColumn({
+ ariaLabel,
+ items,
+ selectedIndex,
+ onSelect,
+}: {
+ ariaLabel: string;
+ items: Array<{ value: string; label: ReactNode }>;
+ selectedIndex: number;
+ onSelect: (index: number) => void;
+}) {
+ const listRef = useRef
(null);
+ const settleTimeoutRef = useRef(null);
+ const initialScrollDoneRef = useRef(false);
+
+ useEffect(() => {
+ const frame = window.requestAnimationFrame(() => {
+ const list = listRef.current;
+ if (!list || initialScrollDoneRef.current) return;
+ initialScrollDoneRef.current = true;
+ list.scrollTop = selectedIndex * WHEEL_ITEM_HEIGHT;
+ });
+ return () => window.cancelAnimationFrame(frame);
+ }, []);
+
+ useEffect(() => {
+ return () => {
+ if (settleTimeoutRef.current !== null) {
+ window.clearTimeout(settleTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ const handleScroll = useCallback(() => {
+ const list = listRef.current;
+ if (!list) return;
+ if (settleTimeoutRef.current !== null) {
+ window.clearTimeout(settleTimeoutRef.current);
+ }
+ settleTimeoutRef.current = window.setTimeout(() => {
+ settleTimeoutRef.current = null;
+ const index = Math.max(
+ 0,
+ Math.min(
+ items.length - 1,
+ Math.round(list.scrollTop / WHEEL_ITEM_HEIGHT),
+ ),
+ );
+ if (index !== selectedIndex) {
+ onSelect(index);
+ }
+ }, SCROLL_SETTLE_MS);
+ }, [items.length, onSelect, selectedIndex]);
+
+ return (
+ event.stopPropagation()}
+ onTouchMove={(event) => event.stopPropagation()}
+ onTouchEnd={(event) => event.stopPropagation()}
+ className="h-[264px] flex-1 snap-y snap-mandatory overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
+ >
+
+ {items.map((item, index) => {
+ const isSelected = index === selectedIndex;
+ return (
+
+
+ {item.label}
+
+
+ );
+ })}
+
+
+ );
+}
+
+export function QuestionDateSheet({
+ question,
+ value,
+ onApply,
+ onClose,
+}: QuestionDateSheetProps) {
+ const { dictionary: t, locale } = useI18n();
+ const [isClosing, setIsClosing] = useState(false);
+
+ const parts = value ? value.split("-") : [];
+ const [selectedYear, setSelectedYear] = useState(parts[0] || YEARS[0]);
+ const [selectedMonth, setSelectedMonth] = useState(
+ parts[1] || MONTH_VALUES[0],
+ );
+ const [selectedDay, setSelectedDay] = useState(parts[2] || "01");
+ const dirtyRef = useRef(false);
+
+ useSheetScrollLock(true);
+
+ const closeSheet = useCallback(() => {
+ setIsClosing(true);
+ window.setTimeout(onClose, EXIT_ANIMATION_MS);
+ }, [onClose]);
+
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ closeSheet();
+ }
+ };
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [closeSheet]);
+
+ const months = useMemo(() => {
+ return MONTH_VALUES.map((val, idx) => {
+ const date = new Date(2000, idx, 15);
+ const label = new Intl.DateTimeFormat(`${locale}-u-ca-gregory`, {
+ month: "long",
+ }).format(date);
+ return { value: val, label };
+ });
+ }, [locale]);
+
+ const days = useMemo(() => {
+ return Array.from({ length: 31 }, (_, i) => {
+ const num = i + 1;
+ const val = num < 10 ? `0${num}` : `${num}`;
+ return {
+ value: val,
+ label: new Intl.NumberFormat(locale).format(num),
+ };
+ });
+ }, [locale]);
+
+ const years = useMemo(() => {
+ return YEARS.map((y) => ({
+ value: y,
+ label: new Intl.NumberFormat(locale, { useGrouping: false }).format(
+ Number.parseInt(y, 10),
+ ),
+ }));
+ }, [locale]);
+
+ const dayIndex = Math.max(
+ 0,
+ days.findIndex((d) => d.value === selectedDay),
+ );
+ const monthIndex = Math.max(
+ 0,
+ months.findIndex((m) => m.value === selectedMonth),
+ );
+ const yearIndex = Math.max(
+ 0,
+ years.findIndex((y) => y.value === selectedYear),
+ );
+
+ // Keep the day valid for the selected month/year (Feb, leap years, ...).
+ useEffect(() => {
+ const maxDay = daysInMonth(selectedYear, selectedMonth);
+ const dayNum = Number.parseInt(selectedDay, 10);
+ if (dayNum > maxDay) {
+ setSelectedDay(String(maxDay).padStart(2, "0"));
+ }
+ }, [selectedYear, selectedMonth, selectedDay]);
+
+ // Apply the picked date immediately while spinning the wheels, so the
+ // field above the sheet always reflects the current selection.
+ useEffect(() => {
+ if (!dirtyRef.current) return;
+ const maxDay = daysInMonth(selectedYear, selectedMonth);
+ const dayNum = Math.min(Number.parseInt(selectedDay, 10), maxDay);
+ const formattedDay = String(dayNum).padStart(2, "0");
+ onApply(
+ `${selectedYear}-${selectedMonth.padStart(2, "0")}-${formattedDay}`,
+ );
+ }, [selectedYear, selectedMonth, selectedDay, onApply]);
+
+ const handleSelect = useCallback((part: DatePart, index: number) => {
+ dirtyRef.current = true;
+ if (part === "day") {
+ setSelectedDay(String(index + 1).padStart(2, "0"));
+ } else if (part === "month") {
+ setSelectedMonth(MONTH_VALUES[index]);
+ } else {
+ setSelectedYear(YEARS[index]);
+ }
+ }, []);
+
+ const dayLabel = t.Day || "Day";
+ const monthLabel = t.Month || "Month";
+ const yearLabel = t.Year || "Year";
+
+ return createPortal(
+ {
+ if (event.key === "Escape") closeSheet();
+ }}
+ onClick={(event) => {
+ if (event.target === event.currentTarget) closeSheet();
+ }}
+ >
+
+
+
+ {question.title}
+
+
+
+
+
+
+ {dayLabel}
+ {monthLabel}
+ {yearLabel}
+
+
+
+
+ handleSelect("day", index)}
+ />
+ handleSelect("month", index)}
+ />
+ handleSelect("year", index)}
+ />
+
+
+
+
+
,
+ document.body,
+ );
+}
+
+export default QuestionDateSheet;
diff --git a/src/components/Componentes/question-date.test.tsx b/src/components/Componentes/question-date.test.tsx
new file mode 100644
index 0000000..9321f95
--- /dev/null
+++ b/src/components/Componentes/question-date.test.tsx
@@ -0,0 +1,60 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import type { QuestionField } from "@/lib/schema-adapter";
+import { QuestionDate } from "./question-date";
+
+let answerValue: string | null = null;
+
+vi.mock("@/translations/provider", () => ({
+ useI18n: () => ({
+ locale: "en",
+ dictionary: { Age: "Age", Day: "Day", Month: "Month", Year: "Year" },
+ }),
+}));
+
+vi.mock("./question-answer-storage", () => ({
+ useQuestionAnswers: () => ({
+ getAnswerValue: () => answerValue,
+ setAnswerValue: vi.fn(),
+ }),
+}));
+
+const question = {
+ id: "personal_identity.date_of_birth",
+ title: "Date of Birth",
+ type: "date",
+ order: 1,
+ required: true,
+ baseRequired: true,
+ isVisible: true,
+ description: "",
+ tooltip: "",
+ extras: { placeHolder: "YYYY-MM-DD", range: [0, 0], options: [] },
+ options: [],
+ ui_config: { isDob: true },
+} as QuestionField;
+
+describe("QuestionDate", () => {
+ beforeEach(() => {
+ answerValue = null;
+ });
+
+ it("hides the derived age until a birth date exists", () => {
+ const { rerender } = render();
+ expect(screen.queryByText("Age")).toBeNull();
+
+ answerValue = "1979-05-15";
+ rerender();
+
+ expect(screen.getByText("Age")).toBeDefined();
+ expect(screen.getByDisplayValue(/^\d+$/)).toBeDefined();
+ });
+
+ it("renders calendar years without thousands separators", () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /select date/i }));
+
+ const yearWheel = screen.getByLabelText("Year");
+ expect(yearWheel.textContent).not.toMatch(/[,٬]/);
+ });
+});
diff --git a/src/components/Componentes/question-date.tsx b/src/components/Componentes/question-date.tsx
index 0bebf71..ca4af7d 100644
--- a/src/components/Componentes/question-date.tsx
+++ b/src/components/Componentes/question-date.tsx
@@ -1,9 +1,10 @@
"use client";
-import { useEffect, useMemo, useState } from "react";
+import { useMemo, useState } from "react";
import type { QuestionField } from "@/lib/schema-adapter";
import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage";
+import QuestionDateSheet from "./question-date-sheet";
import QuestionTitle from "./question-title";
type QuestionDateProps = {
@@ -11,80 +12,35 @@ type QuestionDateProps = {
disabled?: boolean;
};
-const MONTH_VALUES = [
- "01",
- "02",
- "03",
- "04",
- "05",
- "06",
- "07",
- "08",
- "09",
- "10",
- "11",
- "12",
-];
-
-const DAYS = Array.from({ length: 31 }, (_, i) => {
- const num = i + 1;
- const val = num < 10 ? `0${num}` : `${num}`;
- return { value: val, label: `${num}` };
-});
-
const MIN_AGE = 18;
-const currentYear = new Date().getFullYear();
-const maxBirthYear = currentYear - MIN_AGE;
-const YEARS = Array.from({ length: 80 }, (_, i) =>
- (maxBirthYear - i).toString(),
-);
-
-export function QuestionDate({
- question,
- disabled,
-}: QuestionDateProps) {
- const { locale } = useI18n();
+
+function parseDateParts(dateValue: string): {
+ year: string;
+ month: string;
+ day: string;
+} {
+ const parts = dateValue ? dateValue.split("-") : [];
+ return {
+ year: parts[0] || "",
+ month: parts[1] || "",
+ day: parts[2] || "",
+ };
+}
+
+export function QuestionDate({ question, disabled }: QuestionDateProps) {
+ const { dictionary: t, locale } = useI18n();
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const value = getAnswerValue(question);
const dateValue = typeof value === "string" ? value : "";
+ const { year, month, day } = parseDateParts(dateValue);
- const months = useMemo(() => {
- return MONTH_VALUES.map((val, idx) => {
- const date = new Date(2000, idx, 15);
- const label = new Intl.DateTimeFormat(`${locale}-u-ca-gregory`, {
- month: "long",
- }).format(date);
- return { value: val, label };
- });
- }, [locale]);
-
- const [selectedYear, setSelectedYear] = useState(() => {
- const parts = dateValue ? dateValue.split("-") : [];
- return parts[0] || "";
- });
- const [selectedMonth, setSelectedMonth] = useState(() => {
- const parts = dateValue ? dateValue.split("-") : [];
- return parts[1] || "";
- });
- const [selectedDay, setSelectedDay] = useState(() => {
- const parts = dateValue ? dateValue.split("-") : [];
- return parts[2] || "";
- });
-
- useEffect(() => {
- const parts = dateValue ? dateValue.split("-") : [];
- if (parts.length === 3) {
- setSelectedYear(parts[0] || "");
- setSelectedMonth(parts[1] || "");
- setSelectedDay(parts[2] || "");
- }
- }, [dateValue]);
+ const [isOpen, setIsOpen] = useState(false);
const calculatedAge = useMemo(() => {
- if (!selectedYear || !selectedMonth || !selectedDay) return null;
- const y = Number.parseInt(selectedYear, 10);
- const m = Number.parseInt(selectedMonth, 10);
- const d = Number.parseInt(selectedDay, 10);
+ if (!year || !month || !day) return null;
+ const y = Number.parseInt(year, 10);
+ const m = Number.parseInt(month, 10);
+ const d = Number.parseInt(day, 10);
if (
!y ||
@@ -108,37 +64,35 @@ export function QuestionDate({
}
return age >= 0 ? age : null;
- }, [selectedYear, selectedMonth, selectedDay]);
+ }, [year, month, day]);
const isUnder18 = calculatedAge !== null && calculatedAge < MIN_AGE;
- const updateDate = (y: string, m: string, d: string) => {
- setSelectedYear(y);
- setSelectedMonth(m);
- setSelectedDay(d);
-
- if (y && m && d) {
- const formattedMonth = m.padStart(2, "0");
- const formattedDay = d.padStart(2, "0");
- setAnswerValue(
- question, `${y}-${formattedMonth}-${formattedDay}`,
- );
- } else {
- setAnswerValue(question, "");
- }
- };
-
- const handleDayChange = (newDay: string) => {
- updateDate(selectedYear, selectedMonth, newDay);
+ const displayDate = useMemo(() => {
+ if (!year || !month || !day) return "";
+ const date = new Date(
+ Number.parseInt(year, 10),
+ Number.parseInt(month, 10) - 1,
+ Number.parseInt(day, 10),
+ );
+ return new Intl.DateTimeFormat(`${locale}-u-ca-gregory`, {
+ day: "numeric",
+ month: "long",
+ year: "numeric",
+ }).format(date);
+ }, [year, month, day, locale]);
+
+ const openSheet = () => {
+ if (disabled) return;
+ setIsOpen(true);
};
- const handleMonthChange = (newMonth: string) => {
- updateDate(selectedYear, newMonth, selectedDay);
+ const handleApply = (formattedDate: string) => {
+ setAnswerValue(question, formattedDate);
};
- const handleYearChange = (newYear: string) => {
- updateDate(newYear, selectedMonth, selectedDay);
- };
+ const placeholder =
+ t["Select date"] || (locale === "fa" ? "انتخاب تاریخ" : "Select date");
return (
- {/* 3 Select Dropdowns: Day, Month, Year */}
-
- {/* Day / روز */}
-
-
- {/* Month */}
-
-
- {/* Year */}
-
-
-
- {/* Display Calculated Age */}
-
-
- Age
-
-
+
+
- {isUnder18 ? (
-
- {locale === "fa"
- ? "حداقل سن برای ثبتنام ۱۸ سال میباشد."
- : "Minimum age required for registration is 18 years."}
+ >
+ {displayDate || placeholder}
+
+
+
+
+ {calculatedAge !== null ? (
+
+
+ {t.Age || (locale === "fa" ? "سن" : "Age")}
- ) : null}
-
+
+ {isUnder18 ? (
+
+ {locale === "fa"
+ ? "حداقل سن برای ثبتنام ۱۸ سال میباشد."
+ : "Minimum age required for registration is 18 years."}
+
+ ) : null}
+
+ ) : null}
+
+ {isOpen && (
+
setIsOpen(false)}
+ />
+ )}
);
}
diff --git a/src/components/Componentes/question-phone.tsx b/src/components/Componentes/question-phone.tsx
index 9642162..72a0a4a 100644
--- a/src/components/Componentes/question-phone.tsx
+++ b/src/components/Componentes/question-phone.tsx
@@ -1,13 +1,16 @@
"use client";
import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber";
-import { useEffect, useRef, useState, useMemo, useCallback } from "react";
-import type { QuestionField } from "@/lib/schema-adapter";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { createPortal } from "react-dom";
import type { MarriagePhoneFieldValue } from "@/hooks/marriage/types";
+import type { QuestionField } from "@/lib/schema-adapter";
+import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title";
-import { LoadingSkeleton } from "./loading-skeleton";
-import { useI18n } from "@/translations/provider";
+import { useSheetScrollLock } from "./use-sheet-scroll-lock";
+
+const EXIT_ANIMATION_MS = 300;
type QuestionPhoneProps = {
question: QuestionField;
@@ -226,7 +229,7 @@ export function QuestionPhone({
countryCode = "+44",
disabled,
}: QuestionPhoneProps) {
- const { locale } = useI18n();
+ const { dictionary: t, locale } = useI18n();
const { getAnswerValue, setAnswerValue, isLoading } = useQuestionAnswers();
const value = getAnswerValue(question);
const defaultCodeValue = countryCode.trim() || "+44";
@@ -263,29 +266,25 @@ export function QuestionPhone({
const hasFetchedIpRef = useRef(false);
const userInteractedRef = useRef(false);
- const needsIpFetch = useCallback(() => {
- if (typeof window === "undefined") return false;
- if (localStorage.getItem("geoIPPhoneCode")) return false;
- if (localStorage.getItem("hasCheckedGeoIPPhone")) return false;
- return true;
- }, []);
-
- const [isResolvingCode, setIsResolvingCode] = useState(() => {
- if (isLoading) return true;
- const hasSavedValue =
- value &&
- ((isMarriagePhoneFieldValue(value) &&
- (value.countryCode || value.phoneNumber)) ||
- (typeof value === "string" && value.trim().length > 0));
- if (hasSavedValue) return false;
- return needsIpFetch();
- });
-
const [isOpen, setIsOpen] = useState(false);
+ const [isClosing, setIsClosing] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
- const containerRef = useRef(null);
const listRef = useRef(null);
const searchInputRef = useRef(null);
+ const closeSheet = useCallback(() => {
+ setIsClosing(true);
+ window.setTimeout(() => {
+ setIsOpen(false);
+ setIsClosing(false);
+ setSearchQuery("");
+ }, EXIT_ANIMATION_MS);
+ }, []);
+
+ const openSheet = useCallback(() => {
+ if (disabled) return;
+ setIsOpen(true);
+ setIsClosing(false);
+ }, [disabled]);
const normalizedPhoneState = getNormalizedPhoneValue(codeValue, phoneValue);
const showInvalidState =
@@ -333,7 +332,7 @@ export function QuestionPhone({
code: `+${callingCode}`,
flag,
});
- } catch (e) {}
+ } catch {}
}
return list.sort((a, b) => a.name.localeCompare(b.name));
@@ -348,6 +347,13 @@ export function QuestionPhone({
return match ? match.flag : "🏳️";
}, [codeValue, countryList]);
+ const selectedCountry = useMemo(() => {
+ const cleanActiveCode = codeValue.replace(/[^\d]/g, "");
+ return countryList.find(
+ (country) => country.code.replace(/[^\d]/g, "") === cleanActiveCode,
+ );
+ }, [codeValue, countryList]);
+
const filteredCountries = useMemo(() => {
const q = searchQuery.toLowerCase().trim();
if (!q) return countryList;
@@ -357,48 +363,17 @@ export function QuestionPhone({
);
}, [countryList, searchQuery]);
- // Lock page scroll when dropdown is open
- useEffect(() => {
- if (isOpen) {
- document.body.classList.add("dropdown-open");
- document.body.style.overflow = "hidden";
- document.documentElement.style.overflow = "hidden";
- } else {
- document.body.classList.remove("dropdown-open");
- document.body.style.overflow = "";
- document.documentElement.style.overflow = "";
- }
- return () => {
- document.body.classList.remove("dropdown-open");
- document.body.style.overflow = "";
- document.documentElement.style.overflow = "";
- };
- }, [isOpen]);
+ useSheetScrollLock(isOpen);
useEffect(() => {
- if (isOpen) {
- const timer = setTimeout(() => {
- searchInputRef.current?.focus();
- }, 50);
- return () => clearTimeout(timer);
- }
- }, [isOpen]);
+ if (!isOpen) return;
- // Close dropdown on click outside
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (
- containerRef.current &&
- !containerRef.current.contains(event.target as Node)
- ) {
- setIsOpen(false);
- }
- }
- document.addEventListener("mousedown", handleClickOutside);
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === "Escape") closeSheet();
};
- }, []);
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [closeSheet, isOpen]);
// Sync state if external value changes (e.g. backend data loaded)
useEffect(() => {
@@ -497,7 +472,7 @@ export function QuestionPhone({
.then((res) => res.json())
.then((data) => {
clearTimeout(timeoutId);
- if (data && data.country_calling_code) {
+ if (data?.country_calling_code) {
applyCode(data.country_calling_code);
} else {
throw new Error("No calling code in response");
@@ -515,7 +490,7 @@ export function QuestionPhone({
.then((res) => res.json())
.then((data) => {
clearTimeout(secondaryTimeoutId);
- if (data && data.calling_code) {
+ if (data?.calling_code) {
applyCode(data.calling_code);
} else {
applyFallback();
@@ -561,13 +536,13 @@ export function QuestionPhone({
setCodeValue(selectedCode);
setPhoneValue(truncatedPhone);
updateStoredValue(selectedCode, truncatedPhone);
- setIsOpen(false);
- setSearchQuery("");
+ closeSheet();
};
+ const selectCountryTitle = t["Select country"] || question.title;
+
return (
setIsOpen(!isOpen)}
+ onClick={openSheet}
className="flex items-center gap-1.5 h-full px-2 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors cursor-pointer outline-none border-0 text-[15px] font-medium leading-none text-[#181818] tabular-nums"
>
{activeFlag}
{codeValue || defaultCodeValue}
-
+
- {
- userInteractedRef.current = true;
- if (typeof window !== "undefined") {
- localStorage.setItem("geoIPPhoneCode", codeValue);
- localStorage.setItem("hasCheckedGeoIPPhone", "true");
- }
- const nextPhoneValue = sanitizePhoneNumber(
- event.target.value,
- );
- const maxLen = getMaxLengthForCountry(codeValue);
- const truncatedPhone = nextPhoneValue.slice(0, maxLen);
-
- setPhoneValue(truncatedPhone);
- updateStoredValue(codeValue, truncatedPhone);
- }}
- dir="ltr"
- className="h-full w-full border-0 bg-transparent p-0 text-left text-[15px] font-medium leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#98A2B3]"
- />
-
+ {
+ userInteractedRef.current = true;
+ if (typeof window !== "undefined") {
+ localStorage.setItem("geoIPPhoneCode", codeValue);
+ localStorage.setItem("hasCheckedGeoIPPhone", "true");
+ }
+ const nextPhoneValue = sanitizePhoneNumber(event.target.value);
+ const maxLen = getMaxLengthForCountry(codeValue);
+ const truncatedPhone = nextPhoneValue.slice(0, maxLen);
+
+ setPhoneValue(truncatedPhone);
+ updateStoredValue(codeValue, truncatedPhone);
+ }}
+ dir="ltr"
+ className="h-full w-full border-0 bg-transparent p-0 text-left text-[15px] font-medium leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#98A2B3]"
+ />
+
) : null}
- {/* Dropdown Options Panel */}
- {isOpen && (
-
- {/* Search Input Bar */}
-
-
-
setSearchQuery(e.target.value)}
- placeholder={
- locale === "fa"
- ? "جستجوی کشور یا پیششماره..."
- : "Search country or dial code..."
- }
- className="flex-1 bg-transparent text-[14px] font-medium text-[#181818] outline-none placeholder:text-[#98A2B3]"
- />
- {searchQuery ? (
-
- ) : null}
-
-
- {/* Country List */}
+ {/* The phone field moves up while the country sheet enters from below. */}
+ {isOpen &&
+ createPortal(
{
+ if (event.key === "Escape") closeSheet();
+ }}
+ onClick={(event) => {
+ if (event.target === event.currentTarget) closeSheet();
+ }}
>
- {filteredCountries.length > 0 ? (
- filteredCountries.map((c) => {
- return (
-
+