Files
Jyotisha/frontend/src/lib/viewport-scroll-lock.ts
T
jesse-ux 1b508d5e9a
Independent Staging Quality Gate / validate (push) Successful in 11m21s
Independent Staging Quality Gate / publish (push) Successful in 10m46s
fix(web): iPhone 键盘收起后顶栏复位,积分块与盘面芯片同高
2026-09-17 13:13:39 +08:00

86 lines
3.0 KiB
TypeScript

/**
* iOS Safari scrolls `window` to keep a focused input in view. `html, body
* { overflow: hidden }` then blocks the user from dragging back, and reload
* restores that offset. Reset window scroll only after the keyboard is gone.
*/
export const KEYBOARD_OPEN_GAP_PX = 100;
export const KEYBOARD_CLOSED_SLACK_PX = 1;
export function isKeyboardOpen(visualHeight: number, innerHeight: number): boolean {
return visualHeight < innerHeight - KEYBOARD_OPEN_GAP_PX;
}
export function shouldResetWindowScroll(input: {
scrollY: number;
innerHeight: number;
visualViewportHeight: number | null;
}): boolean {
if (input.scrollY <= 0) return false;
if (input.visualViewportHeight == null) return true;
if (isKeyboardOpen(input.visualViewportHeight, input.innerHeight)) return false;
return input.visualViewportHeight >= input.innerHeight - KEYBOARD_CLOSED_SLACK_PX;
}
type ViewportLike = {
height: number;
addEventListener: (type: "resize" | "scroll", listener: () => void) => void;
removeEventListener: (type: "resize" | "scroll", listener: () => void) => void;
};
export type ViewportScrollLockHost = {
matchMedia?: (query: string) => { matches: boolean };
visualViewport?: ViewportLike | null;
innerHeight: number;
scrollY: number;
scrollTo: (x: number, y: number) => void;
history: { scrollRestoration: string };
addEventListener: (type: string, listener: () => void) => void;
removeEventListener: (type: string, listener: () => void) => void;
document: {
addEventListener: (type: string, listener: () => void) => void;
removeEventListener: (type: string, listener: () => void) => void;
};
setTimeout?: (fn: () => void, ms: number) => unknown;
};
export function installViewportScrollLock(host: ViewportScrollLockHost): () => void {
const coarse = host.matchMedia?.("(pointer: coarse)").matches === true;
const viewport = host.visualViewport ?? null;
if (!coarse && !viewport) return () => undefined;
host.history.scrollRestoration = "manual";
host.scrollTo(0, 0);
const resetIfSettled = () => {
if (!shouldResetWindowScroll({
scrollY: host.scrollY,
innerHeight: host.innerHeight,
visualViewportHeight: viewport?.height ?? null,
})) return;
host.scrollTo(0, 0);
};
const onPageShow = () => {
host.scrollTo(0, 0);
};
const onFocusOut = () => {
const later = host.setTimeout ?? ((fn: () => void) => fn());
later(resetIfSettled, 300);
};
viewport?.addEventListener("resize", resetIfSettled);
viewport?.addEventListener("scroll", resetIfSettled);
host.addEventListener("pageshow", onPageShow);
host.addEventListener("orientationchange", resetIfSettled);
host.document.addEventListener("focusout", onFocusOut);
return () => {
viewport?.removeEventListener("resize", resetIfSettled);
viewport?.removeEventListener("scroll", resetIfSettled);
host.removeEventListener("pageshow", onPageShow);
host.removeEventListener("orientationchange", resetIfSettled);
host.document.removeEventListener("focusout", onFocusOut);
};
}