Files
sendou.ink/app/modules/scroll-lock/scroll-lock.ts

68 lines
1.8 KiB
TypeScript

const RELEASE_DELAY_MS = 24;
const SCROLLBAR_WIDTH_PROPERTY = "--scrollbar-width";
let lockCount = 0;
let restoreStyles: (() => void) | null = null;
let releaseTimeoutId: number | null = null;
export function lockScroll() {
cancelScheduledRelease();
lockCount++;
restoreStyles ??= applyLockStyles();
let released = false;
return () => {
if (released) return;
released = true;
lockCount--;
if (lockCount === 0) {
scheduleRelease();
}
};
}
export function isScrollLocked() {
return restoreStyles !== null;
}
function applyLockStyles() {
const root = document.documentElement;
const body = document.body;
const previous = {
overflow: body.style.overflow,
paddingRight: body.style.paddingRight,
scrollbarWidth: root.style.getPropertyValue(SCROLLBAR_WIDTH_PROPERTY),
};
const scrollbarWidth = window.innerWidth - root.clientWidth;
body.style.overflow = "hidden";
if (scrollbarWidth > 0) {
// Replace scrollbar with padding instead of using scrollbar-gutter stable because of some Chromium quirk
const bodyPadding = Number.parseFloat(getComputedStyle(body).paddingRight);
body.style.paddingRight = `${bodyPadding + scrollbarWidth}px`;
root.style.setProperty(SCROLLBAR_WIDTH_PROPERTY, `${scrollbarWidth}px`);
}
return () => {
body.style.overflow = previous.overflow;
body.style.paddingRight = previous.paddingRight;
root.style.setProperty(SCROLLBAR_WIDTH_PROPERTY, previous.scrollbarWidth);
};
}
function scheduleRelease() {
cancelScheduledRelease();
releaseTimeoutId = window.setTimeout(() => {
releaseTimeoutId = null;
if (lockCount > 0) return;
restoreStyles?.();
restoreStyles = null;
}, RELEASE_DELAY_MS);
}
function cancelScheduledRelease() {
if (releaseTimeoutId === null) return;
window.clearTimeout(releaseTimeoutId);
releaseTimeoutId = null;
}