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.
 
 
 
 
 

152 lines
4.8 KiB

"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<HTMLElement>(".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");
};
}, []);
}