"use client"; import { useEffect } from "react"; import { viewPaddingsBridge } from "@/lib/view-paddings"; let activeSheetCount = 0; let hasFocusedKeyboardInput = false; let bodyHadDropdownClass = false; let initialBodyOverflow = ""; let initialHtmlOverflow = ""; let initialAppShellOverflow = ""; let initialAppShellTouchAction = ""; let lockedAppShell: HTMLElement | null = null; export function useSheetScrollLock(isOpen: boolean) { useEffect(() => { if (!isOpen) return; if (activeSheetCount === 0) { bodyHadDropdownClass = document.body.classList.contains("dropdown-open"); initialBodyOverflow = document.body.style.overflow; initialHtmlOverflow = document.documentElement.style.overflow; lockedAppShell = document.querySelector(".app-shell"); initialAppShellOverflow = lockedAppShell?.style.overflowY ?? ""; initialAppShellTouchAction = lockedAppShell?.style.touchAction ?? ""; } activeSheetCount += 1; document.body.classList.add("dropdown-open"); document.body.classList.add("question-sheet-open"); document.body.style.overflow = "hidden"; document.documentElement.style.overflow = "hidden"; if (lockedAppShell) { lockedAppShell.style.overflowY = "hidden"; lockedAppShell.style.touchAction = "none"; } return () => { activeSheetCount = Math.max(0, activeSheetCount - 1); if (activeSheetCount === 0) { document.body.style.overflow = initialBodyOverflow; document.documentElement.style.overflow = initialHtmlOverflow; if (lockedAppShell) { lockedAppShell.style.overflowY = initialAppShellOverflow; lockedAppShell.style.touchAction = initialAppShellTouchAction; lockedAppShell = null; } if (!bodyHadDropdownClass) { document.body.classList.remove("dropdown-open"); } document.body.classList.remove("question-sheet-open"); } }; }, [isOpen]); } const NON_KEYBOARD_INPUT_TYPES = new Set([ "button", "checkbox", "color", "file", "hidden", "image", "radio", "range", "reset", "submit", ]); function isKeyboardInputTarget(target: EventTarget | null): boolean { if (target instanceof HTMLTextAreaElement) { return !target.disabled && !target.readOnly; } if (target instanceof HTMLInputElement) { const type = (target.getAttribute("type") ?? "text").toLowerCase(); return ( !NON_KEYBOARD_INPUT_TYPES.has(type) && !target.disabled && !target.readOnly ); } if (target instanceof HTMLElement) { return target.isContentEditable; } return false; } function syncQuestionInputOpenClass() { if (hasFocusedKeyboardInput) { document.body.classList.add("question-keyboard-open"); } else { document.body.classList.remove("question-keyboard-open"); } } /** * Moves the active question up (same animation the dropdown sheet uses) while * a keyboard input is focused, so the mobile keyboard cannot cover it. */ export function useQuestionInputFocusSync() { useEffect(() => { const handleFocusIn = (event: FocusEvent) => { if (!isKeyboardInputTarget(event.target)) return; hasFocusedKeyboardInput = true; syncQuestionInputOpenClass(); }; const handleFocusOut = (event: FocusEvent) => { if (!isKeyboardInputTarget(event.target)) return; window.setTimeout(() => { hasFocusedKeyboardInput = isKeyboardInputTarget(document.activeElement); syncQuestionInputOpenClass(); }, 0); }; const unsubscribeConfig = viewPaddingsBridge.subscribeConfig((config) => { if (config.keyboardHeight > 0) { hasFocusedKeyboardInput = isKeyboardInputTarget(document.activeElement); } else { // Android Back can hide Flutter's keyboard without dispatching blur. hasFocusedKeyboardInput = false; } syncQuestionInputOpenClass(); }); const initialViewportHeight = window.visualViewport?.height ?? 0; const handleViewportResize = () => { const viewportHeight = window.visualViewport?.height ?? 0; if ( initialViewportHeight > 0 && viewportHeight >= initialViewportHeight - 40 ) { hasFocusedKeyboardInput = false; syncQuestionInputOpenClass(); } }; document.addEventListener("focusin", handleFocusIn); document.addEventListener("focusout", handleFocusOut); window.visualViewport?.addEventListener("resize", handleViewportResize); return () => { document.removeEventListener("focusin", handleFocusIn); document.removeEventListener("focusout", handleFocusOut); window.visualViewport?.removeEventListener( "resize", handleViewportResize, ); unsubscribeConfig(); hasFocusedKeyboardInput = false; document.body.classList.remove("question-keyboard-open"); }; }, []); }