Skip to content

Replace lock_scroll.js with a Rust implementation using web_sys #1

@max-wells

Description

@max-wells

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:

  • Live in crates/leptos_ui (or a new crates/scroll_lock crate)
  • Use web_sys for all DOM manipulation
  • Expose the same lock() / unlock(delay) / is_locked() API
  • Be callable from Leptos components without JS interop strings
  • Pass the same exclusion list for internal components
  • Remove public/hooks/lock_scroll.js and its <script> tag in shell.rs
  • Update all 9 components to call the Rust API instead of window.ScrollLock

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

  • Run the existing Playwright tests and make sure they all pass after the migration
  • Also run the tests for the 9 affected components (dialog.spec.ts, sheet.spec.ts, drawer.spec.ts, etc.) to confirm scroll lock/unlock still works correctly in each

Missing test coverage to add

The existing test file does not cover the following — please add test cases for:

  • Scrollbar width compensation — when a scrollbar is visible, opening a Dialog/Sheet should not cause the page to shift horizontally. Check that body padding-right equals the scrollbar width while locked, and is restored to its original value after unlock.
  • Unlock delay — verify scroll is still locked during the delay period and fully restored after it elapses
  • Nested scrollable containers — verify inner scrollable divs are also locked and their scroll positions restored correctly

Difficulty

Medium-high — requires web_sys DOM manipulation and familiarity with Leptos component patterns.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions