diff --git a/src/app/globals.css b/src/app/globals.css index bc50f1e..c4cb2a6 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -268,6 +268,15 @@ body.section-overlay-open .app-shell { } } +@keyframes flutter-bottomsheet-exit { + 0% { + transform: translate3d(0, 0, 0); + } + 100% { + transform: translate3d(0, 100%, 0); + } +} + @keyframes flutter-scrim-enter { 0% { opacity: 0; @@ -277,15 +286,24 @@ body.section-overlay-open .app-shell { } } +@keyframes flutter-scrim-exit { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + .flutter-sheet-surface { animation: flutter-bottomsheet-enter 250ms cubic-bezier(0, 0, 0.2, 1) forwards; will-change: transform; } .flutter-sheet-surface-exit { - transform: translate3d(0, 100%, 0) !important; - transition: transform 200ms cubic-bezier(0.4, 0, 1, 1) !important; + animation: flutter-bottomsheet-exit 200ms cubic-bezier(0.4, 0, 1, 1) forwards !important; will-change: transform; + pointer-events: none; } .flutter-scrim { @@ -295,9 +313,10 @@ body.section-overlay-open .app-shell { } .flutter-scrim-exit { - opacity: 0 !important; - transition: opacity 200ms cubic-bezier(0.4, 0, 1, 1) !important; + background-color: rgba(0, 0, 0, 0.54) !important; + animation: flutter-scrim-exit 200ms cubic-bezier(0.4, 0, 1, 1) forwards !important; will-change: opacity; + pointer-events: none; } /* ── Section Overlay Slide-in Panel (Exact Flutter Najm Matching: 350ms in, 200ms out, easeInOut) ── */ diff --git a/src/app/questions-list/[slug]/answer-pace-sheet.tsx b/src/app/questions-list/[slug]/answer-pace-sheet.tsx index 11d8b46..f95e9a2 100644 --- a/src/app/questions-list/[slug]/answer-pace-sheet.tsx +++ b/src/app/questions-list/[slug]/answer-pace-sheet.tsx @@ -55,12 +55,9 @@ export default function AnswerPaceSheet({ setIsOpen(false); }; - if (!isOpen) { - return null; - } - return ( ( diff --git a/src/components/Componentes/currency-sheet.tsx b/src/components/Componentes/currency-sheet.tsx index 73bb1a4..22d00ad 100644 --- a/src/components/Componentes/currency-sheet.tsx +++ b/src/components/Componentes/currency-sheet.tsx @@ -24,29 +24,65 @@ export function CurrencySheet({ title, }: CurrencySheetProps) { const { dictionary: t, locale } = useI18n(); + const [isRendered, setIsRendered] = useState(isOpen); const [isClosing, setIsClosing] = useState(false); + const isClosingRef = useRef(false); const [searchQuery, setSearchQuery] = useState(""); const listRef = useRef(null); const sheetRef = useRef(null); + const isMountedRef = useRef(true); + const prevIsOpenRef = useRef(isOpen); + const timerRef = useRef(null); + + useEffect(() => { + isMountedRef.current = true; + return () => { + isMountedRef.current = false; + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, []); const isRtl = locale === "fa" || locale === "ar" || locale === "ur"; const defaultTitle = isRtl ? "انتخاب ارز" : "Select Currency"; const sheetTitle = title || defaultTitle; const closeSheet = useCallback(() => { + if (isClosingRef.current) return; + isClosingRef.current = true; setIsClosing(true); - window.setTimeout(() => { + + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = window.setTimeout(() => { + if (isMountedRef.current) { + setIsRendered(false); + setIsClosing(false); + isClosingRef.current = false; + setSearchQuery(""); + } onClose(); - setIsClosing(false); - setSearchQuery(""); }, EXIT_ANIMATION_MS); }, [onClose]); - useSheetScrollLock(isOpen && !isClosing, { onBack: closeSheet }); + // When isOpen changes from outside + useEffect(() => { + if (prevIsOpenRef.current !== isOpen) { + prevIsOpenRef.current = isOpen; + if (isOpen) { + if (timerRef.current) clearTimeout(timerRef.current); + isClosingRef.current = false; + setIsClosing(false); + setIsRendered(true); + } else if (!isClosingRef.current && isRendered) { + closeSheet(); + } + } + }, [isOpen, isRendered, closeSheet]); + + useSheetScrollLock(isRendered, { onBack: closeSheet }); // Escape key handler useEffect(() => { - if (!isOpen) return; + if (!isRendered) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { closeSheet(); @@ -54,7 +90,7 @@ export function CurrencySheet({ }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [isOpen, closeSheet]); + }, [isRendered, closeSheet]); const filteredCurrencies = useMemo(() => { const q = searchQuery.toLowerCase().trim(); @@ -87,7 +123,7 @@ export function CurrencySheet({ ? "لم يتم العثور على عملات" : "No currencies found"; - if (!isOpen && !isClosing) return null; + if (!isRendered) return null; return createPortal(
(null); useEffect(() => { isMountedRef.current = true; setMounted(true); return () => { isMountedRef.current = false; + if (timerRef.current) clearTimeout(timerRef.current); }; }, []); - // When isOpen changes from outside, reset isClosing + const closeSheet = useCallback(() => { + if (isClosingRef.current) return; + isClosingRef.current = true; + setIsClosing(true); + + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = window.setTimeout(() => { + if (isMountedRef.current) { + setIsRendered(false); + setIsClosing(false); + isClosingRef.current = false; + } + onClose(); + }, EXIT_ANIMATION_MS); + }, [onClose]); + + // When isOpen changes from outside useEffect(() => { - if (isOpen) { - setIsClosing(false); + if (prevIsOpenRef.current !== isOpen) { + prevIsOpenRef.current = isOpen; + if (isOpen) { + if (timerRef.current) clearTimeout(timerRef.current); + isClosingRef.current = false; + setIsClosing(false); + setIsRendered(true); + } else if (!isClosingRef.current && isRendered) { + closeSheet(); + } } - }, [isOpen]); + }, [isOpen, isRendered, closeSheet]); const resolvedTitle = "Tips"; const resolvedDescription = @@ -53,22 +82,11 @@ export function HelpModal({ "Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance."; const resolvedButtonText = buttonText ?? t["Got it"] ?? "Got it"; - const closeSheet = useCallback(() => { - if (isClosing) return; - setIsClosing(true); - window.setTimeout(() => { - if (isMountedRef.current) { - setIsClosing(false); - } - onClose(); - }, EXIT_ANIMATION_MS); - }, [isClosing, onClose]); - - useHardwareBackHandler(closeSheet, isOpen && !isClosing); + useHardwareBackHandler(closeSheet, isRendered && !isClosing); // Lock body scroll useEffect(() => { - if (!isOpen || !mounted) return; + if (!isRendered || !mounted) return; const previousBodyOverflow = document.body.style.overflow; const previousHtmlOverflow = document.documentElement.style.overflow; document.body.style.overflow = "hidden"; @@ -77,9 +95,9 @@ export function HelpModal({ document.body.style.overflow = previousBodyOverflow; document.documentElement.style.overflow = previousHtmlOverflow; }; - }, [isOpen, mounted]); + }, [isRendered, mounted]); - if (!isOpen || !mounted) return null; + if (!isRendered || !mounted) return null; return createPortal(
, "children" | "title" > & { + isOpen?: boolean; icon?: InformationSheetIcon | null; title?: ReactNode | ((controls: { close: () => void }) => ReactNode); description?: ReactNode; @@ -182,6 +184,7 @@ function resolveIcon(icon: InformationSheetIcon | null | undefined) { } export function InformationSheet({ + isOpen = true, icon, title, description, @@ -200,36 +203,63 @@ export function InformationSheet({ locale === "ur" || locale === "he" || locale === "ks"; - const [isVisible, setIsVisible] = useState(true); + const [mounted, setMounted] = useState(false); + const [isRendered, setIsRendered] = useState(isOpen); const [isClosing, setIsClosing] = useState(false); + const isClosingRef = useRef(false); const isMountedRef = useRef(true); + const prevIsOpenRef = useRef(isOpen); + const timerRef = useRef(null); useEffect(() => { isMountedRef.current = true; + setMounted(true); return () => { isMountedRef.current = false; + if (timerRef.current) clearTimeout(timerRef.current); }; }, []); - const resolvedIcon = resolveIcon(icon); const closeSheet = useCallback(() => { - if (isClosing) { + if (isClosingRef.current) { return; } + isClosingRef.current = true; setIsClosing(true); - window.setTimeout(() => { + + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = window.setTimeout(() => { if (isMountedRef.current) { - setIsVisible(false); + setIsRendered(false); + setIsClosing(false); + isClosingRef.current = false; } onClose?.(); }, EXIT_ANIMATION_MS); - }, [isClosing, onClose]); + }, [onClose]); + + // Synchronize external isOpen prop changes safely without bounce + useEffect(() => { + if (prevIsOpenRef.current !== isOpen) { + prevIsOpenRef.current = isOpen; + if (isOpen) { + if (timerRef.current) clearTimeout(timerRef.current); + isClosingRef.current = false; + setIsClosing(false); + setIsRendered(true); + } else if (!isClosingRef.current && isRendered) { + closeSheet(); + } + } + }, [isOpen, isRendered, closeSheet]); + + const resolvedIcon = resolveIcon(icon); useHardwareBackHandler(() => { closeSheet(); return true; - }, isVisible && !isClosing); + }, isRendered && !isClosing); const controls = { close: closeSheet }; const resolvedTitle = typeof title === "function" ? title(controls) : title; @@ -243,7 +273,7 @@ export function InformationSheet({ ); useEffect(() => { - if (!isVisible) { + if (!isRendered) { return; } @@ -257,13 +287,13 @@ export function InformationSheet({ document.body.style.overflow = previousBodyOverflow; document.documentElement.style.overflow = previousHtmlOverflow; }; - }, [isVisible]); + }, [isRendered]); - if (!isVisible) { + if (!isRendered || !mounted) { return null; } - return ( + const content = (
); + + return createPortal(content, document.body); } export default InformationSheet; diff --git a/src/components/Componentes/question-birthplace.tsx b/src/components/Componentes/question-birthplace.tsx index b700121..26fe3a1 100644 --- a/src/components/Componentes/question-birthplace.tsx +++ b/src/components/Componentes/question-birthplace.tsx @@ -249,7 +249,7 @@ export function QuestionBirthplace({ setIsClosing(false); }, [disabled]); - useSheetScrollLock(isOpen && !isClosing, { onBack: closeSheet }); + useSheetScrollLock(isOpen, { onBack: closeSheet }); // Handle escape key useEffect(() => { diff --git a/src/components/Componentes/question-date-sheet.tsx b/src/components/Componentes/question-date-sheet.tsx index eb84f27..8075f61 100644 --- a/src/components/Componentes/question-date-sheet.tsx +++ b/src/components/Componentes/question-date-sheet.tsx @@ -181,7 +181,7 @@ export function QuestionDateSheet({ window.setTimeout(onClose, EXIT_ANIMATION_MS); }, [onClose]); - useSheetScrollLock(!isClosing, { onBack: closeSheet }); + useSheetScrollLock(true, { onBack: closeSheet }); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { diff --git a/src/components/Componentes/question-phone.tsx b/src/components/Componentes/question-phone.tsx index 86b35b3..a1e7bee 100644 --- a/src/components/Componentes/question-phone.tsx +++ b/src/components/Componentes/question-phone.tsx @@ -586,7 +586,7 @@ export function QuestionPhone({ ); }, [countryList, searchQuery]); - useSheetScrollLock(isOpen && !isClosing, { onBack: closeSheet }); + useSheetScrollLock(isOpen, { onBack: closeSheet }); useEffect(() => { if (!isOpen) return; diff --git a/src/components/Componentes/question-sheet.tsx b/src/components/Componentes/question-sheet.tsx index e51ee0c..3787a08 100644 --- a/src/components/Componentes/question-sheet.tsx +++ b/src/components/Componentes/question-sheet.tsx @@ -81,7 +81,7 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { setIsClosing(false); }, [disabled, selectedList]); - useSheetScrollLock(isOpen && !isClosing, { onBack: closeSheet }); + useSheetScrollLock(isOpen, { onBack: closeSheet }); // Handle escape key useEffect(() => { diff --git a/src/components/Componentes/support-sheet.tsx b/src/components/Componentes/support-sheet.tsx index f87c492..2d3df09 100644 --- a/src/components/Componentes/support-sheet.tsx +++ b/src/components/Componentes/support-sheet.tsx @@ -17,40 +17,58 @@ export type SupportSheetProps = { export function SupportSheet({ isOpen, onClose }: SupportSheetProps) { const { dictionary: t } = useI18n(); const [mounted, setMounted] = useState(false); + const [isRendered, setIsRendered] = useState(isOpen); const [isClosing, setIsClosing] = useState(false); + const isClosingRef = useRef(false); const isMountedRef = useRef(true); + const prevIsOpenRef = useRef(isOpen); + const timerRef = useRef(null); useEffect(() => { isMountedRef.current = true; setMounted(true); return () => { isMountedRef.current = false; + if (timerRef.current) clearTimeout(timerRef.current); }; }, []); - // When isOpen changes from outside, reset isClosing - useEffect(() => { - if (isOpen) { - setIsClosing(false); - } - }, [isOpen]); - const closeSheet = useCallback(() => { - if (isClosing) return; + if (isClosingRef.current) return; + isClosingRef.current = true; setIsClosing(true); - window.setTimeout(() => { + + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = window.setTimeout(() => { if (isMountedRef.current) { + setIsRendered(false); setIsClosing(false); + isClosingRef.current = false; } onClose(); }, EXIT_ANIMATION_MS); - }, [isClosing, onClose]); + }, [onClose]); + + // When isOpen changes from outside + useEffect(() => { + if (prevIsOpenRef.current !== isOpen) { + prevIsOpenRef.current = isOpen; + if (isOpen) { + if (timerRef.current) clearTimeout(timerRef.current); + isClosingRef.current = false; + setIsClosing(false); + setIsRendered(true); + } else if (!isClosingRef.current && isRendered) { + closeSheet(); + } + } + }, [isOpen, isRendered, closeSheet]); - useHardwareBackHandler(closeSheet, isOpen && !isClosing); + useHardwareBackHandler(closeSheet, isRendered && !isClosing); // Lock body scroll useEffect(() => { - if (!isOpen || !mounted) return; + if (!isRendered || !mounted) return; const previousBodyOverflow = document.body.style.overflow; const previousHtmlOverflow = document.documentElement.style.overflow; document.body.style.overflow = "hidden"; @@ -59,7 +77,7 @@ export function SupportSheet({ isOpen, onClose }: SupportSheetProps) { document.body.style.overflow = previousBodyOverflow; document.documentElement.style.overflow = previousHtmlOverflow; }; - }, [isOpen, mounted]); + }, [isRendered, mounted]); const handleSupportContact = () => { if (typeof window !== "undefined" && "HabibApp" in window) { @@ -84,7 +102,7 @@ export function SupportSheet({ isOpen, onClose }: SupportSheetProps) { closeSheet(); }; - if (!isOpen || !mounted) return null; + if (!isRendered || !mounted) return null; return createPortal(
; @@ -50,6 +46,7 @@ export function TestExitSheet({ return ( {/* Question Title */} -

- {q.text} -

+
+

+ {q.text} +

+
{/* Answer Options Stack */} -
+
{options.map((option) => { const isSelected = qSelectedValue === option.value; @@ -422,4 +430,3 @@ export default function TestQuestionsFlow({ ); } -