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