paperclip/ui/src/lib/pin-document-scroll.ts

31 lines
1.4 KiB
TypeScript

// Registers a capture-phase `scroll` listener on `win` that snaps the
// `documentElement.scrollTop` and `body.scrollTop` back to 0 whenever they
// drift. The desktop app shell expects neither to ever be non-zero — only
// `#main-content` should scroll — but `scrollIntoView`'s ancestor walk can
// reach `<html>` (which Chrome drives via its internal smooth-scroll
// algorithm, bypassing the JS `scrollTop` setter and ignoring CSS
// `overflow` on the root viewport). Snapping back on every scroll tick
// keeps the shell visually pinned.
//
// Returns a cleanup function that removes the listener.
export function pinDocumentScrollToZero(
doc: Document = document,
win: Window = window,
): () => void {
const onScroll = () => {
if (doc.documentElement.scrollTop !== 0) {
doc.documentElement.scrollTop = 0;
}
if (doc.body.scrollTop !== 0) {
doc.body.scrollTop = 0;
}
};
// Intentionally NOT `passive: true` — a passive listener lets the browser
// paint the scrolled frame before this handler runs, producing a one-frame
// flash of the shifted shell. The handler doesn't call `preventDefault()`
// and `scroll` events aren't cancelable, so dropping the passive flag has
// no performance downside and runs the reset synchronously with dispatch.
win.addEventListener("scroll", onScroll, { capture: true });
return () => win.removeEventListener("scroll", onScroll, { capture: true });
}