Scroll lock utility

This commit is contained in:
Mats Bosson 2026-03-29 04:41:35 +07:00
parent 7e201f6af6
commit 18f4869b20
3 changed files with 84 additions and 0 deletions

View File

@ -0,0 +1,37 @@
// Global lock counter so nested modals don't prematurely restore scroll
let lockCount = 0;
let originalOverflow = "";
export interface ScrollLock {
lock: () => void;
unlock: () => void;
}
/**
* Prevents body scroll. Uses a reference count so nested overlays
* (e.g. a dialog inside a drawer) don't prematurely restore scrolling.
*/
export function createScrollLock(): ScrollLock {
let locked = false;
return {
lock() {
if (locked) return;
locked = true;
if (lockCount === 0) {
originalOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
}
lockCount++;
},
unlock() {
if (!locked) return;
locked = false;
lockCount = Math.max(0, lockCount - 1);
if (lockCount === 0) {
document.body.style.overflow = originalOverflow;
originalOverflow = "";
}
},
};
}

View File

@ -0,0 +1,2 @@
export { createScrollLock } from "./create-scroll-lock";
export type { ScrollLock } from "./create-scroll-lock";

View File

@ -0,0 +1,45 @@
import { createRoot } from "solid-js";
import { afterEach, describe, expect, it } from "vitest";
import { createScrollLock } from "../../src/utilities/scroll-lock/create-scroll-lock";
describe("createScrollLock", () => {
afterEach(() => {
document.body.style.overflow = "";
document.body.style.paddingRight = "";
});
it("sets overflow hidden on body when locked", () => {
createRoot((dispose) => {
const lock = createScrollLock();
lock.lock();
expect(document.body.style.overflow).toBe("hidden");
lock.unlock();
dispose();
});
});
it("restores overflow on unlock", () => {
document.body.style.overflow = "auto";
createRoot((dispose) => {
const lock = createScrollLock();
lock.lock();
lock.unlock();
expect(document.body.style.overflow).toBe("auto");
dispose();
});
});
it("handles multiple locks — only unlocks when all release", () => {
createRoot((dispose) => {
const lockA = createScrollLock();
const lockB = createScrollLock();
lockA.lock();
lockB.lock();
lockA.unlock();
expect(document.body.style.overflow).toBe("hidden"); // still locked by B
lockB.unlock();
expect(document.body.style.overflow).toBe(""); // now unlocked
dispose();
});
});
});