52 lines
1.8 KiB
TypeScript
52 lines
1.8 KiB
TypeScript
import React from 'react';
|
|||
|
|
|
||
|
|
/**
|
||
|
|
* The document root (`html`, `body`, `#root`) is `overflow: hidden` and must
|
||
|
|
* never scroll — every scrollable area lives in a dedicated container. Chromium
|
||
|
|
* still scrolls hidden-overflow ancestors programmatically, most visibly when
|
||
|
|
* a textarea caret moves out of view (PageUp/PageDown in the prompt box, or a
|
||
|
|
* long prompt being typed) and the browser scrolls it into view. Once that
|
||
|
|
* happens the whole app shifts up, hides the title bar, and nothing the user
|
||
|
|
* does with the wheel or keyboard can scroll it back.
|
||
|
|
*
|
||
|
|
* Snap every root scroll straight back to zero.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const rootScrollTargets = (): HTMLElement[] => {
|
||
|
|
const targets = [document.documentElement, document.body];
|
||
|
|
const appRoot = document.getElementById('root');
|
||
|
|
if (appRoot) targets.push(appRoot);
|
||
|
|
return targets;
|
||
|
|
};
|
||
|
|
|
||
|
|
export const resetRootScroll = (): boolean => {
|
||
|
|
let reset = false;
|
||
|
|
for (const target of rootScrollTargets()) {
|
||
|
|
if (target.scrollTop !== 0) {
|
||
|
|
target.scrollTop = 0;
|
||
|
|
reset = true;
|
||
|
|
}
|
||
|
|
if (target.scrollLeft !== 0) {
|
||
|
|
target.scrollLeft = 0;
|
||
|
|
reset = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return reset;
|
||
|
|
};
|
||
|
|
|
||
|
|
export const isRootScrollTarget = (target: EventTarget | null): boolean =>
|
||
|
|
target === document || rootScrollTargets().some((element) => element === target);
|
||
|
|
|
||
|
|
export const useRootScrollLock = (): void => {
|
||
|
|
React.useEffect(() => {
|
||
|
|
const handleScroll = (event: Event) => {
|
||
|
|
if (isRootScrollTarget(event.target)) resetRootScroll();
|
||
|
|
};
|
||
|
|
// Capture: the root's own scroll events don't bubble to inner listeners,
|
||
|
|
// and scroll events from inner containers are filtered out above.
|
||
|
|
document.addEventListener('scroll', handleScroll, { capture: true, passive: true });
|
||
|
|
resetRootScroll();
|
||
|
|
return () => document.removeEventListener('scroll', handleScroll, { capture: true });
|
||
|
|
}, []);
|
||
|
|
};
|