Context
public/hooks/lock_scroll.js is a vanilla JS scroll lock utility (~250 lines) loaded as a static asset via app/src/shell.rs. It exposes a global window.ScrollLock singleton that Leptos components call via JS interop.
It is used by 9 components: Dialog, Sheet, Drawer, Select, DropdownMenu, Command, ContextMenu, Menubar, MultiSelect.
What the current JS does
window.ScrollLock.lock() // locks scroll on body + all scrollable containers
window.ScrollLock.unlock(delay?) // restores scroll positions (delay for exit animations)
window.ScrollLock.isLocked() // returns bool
The implementation:
- Finds all scrollable elements in the DOM (batched read to avoid reflow)
- Saves scroll positions and original inline styles
- Fixes the body (
position: fixed, top: -scrollY, overflow: hidden)
- Compensates scrollbar width on fixed elements to prevent layout shift
- Excludes internal components from being locked:
ScrollArea, CommandList, SelectContent, MultiSelectContent, DropdownMenuContent, ContextMenuContent
- Supports an optional unlock delay (used by animated exit transitions)
Goal
Rewrite this in Rust using web_sys so the entire codebase is JS-free for this utility.
The new implementation should:
Notes
- The JS file is the reference implementation — behavior must match exactly
- Unlock delay is critical for animated components (Sheet, Drawer use it for exit animations)
- Scrollbar width compensation must be preserved to avoid layout shift on lock
Acceptance Criteria
The project uses Playwright for e2e tests (e2e/tests/).
There is already an existing test file: e2e/tests/hooks/use-lock-body-scroll.spec.ts
Missing test coverage to add
The existing test file does not cover the following — please add test cases for:
Difficulty
Medium-high — requires web_sys DOM manipulation and familiarity with Leptos component patterns.
Context
public/hooks/lock_scroll.jsis a vanilla JS scroll lock utility (~250 lines) loaded as a static asset viaapp/src/shell.rs. It exposes a globalwindow.ScrollLocksingleton that Leptos components call via JS interop.It is used by 9 components:
Dialog,Sheet,Drawer,Select,DropdownMenu,Command,ContextMenu,Menubar,MultiSelect.What the current JS does
The implementation:
position: fixed,top: -scrollY,overflow: hidden)ScrollArea,CommandList,SelectContent,MultiSelectContent,DropdownMenuContent,ContextMenuContentGoal
Rewrite this in Rust using
web_sysso the entire codebase is JS-free for this utility.The new implementation should:
crates/leptos_ui(or a newcrates/scroll_lockcrate)web_sysfor all DOM manipulationlock()/unlock(delay)/is_locked()APIpublic/hooks/lock_scroll.jsand its<script>tag inshell.rswindow.ScrollLockNotes
Acceptance Criteria
The project uses Playwright for e2e tests (
e2e/tests/).There is already an existing test file:
e2e/tests/hooks/use-lock-body-scroll.spec.tsdialog.spec.ts,sheet.spec.ts,drawer.spec.ts, etc.) to confirm scroll lock/unlock still works correctly in eachMissing test coverage to add
The existing test file does not cover the following — please add test cases for:
bodypadding-right equals the scrollbar width while locked, and is restored to its original value after unlock.Difficulty
Medium-high — requires
web_sysDOM manipulation and familiarity with Leptos component patterns.