feat(sidebar): add scrollToItem for imperative scroll to a tagged nav item - #736
Conversation
28fc50e to
51b4aef
Compare
commit: |
Docs PreviewCommit: |
51b4aef to
4ebdf5c
Compare
|
/bonk please approve |
|
@mattrothenberg Bonk workflow was cancelled. View workflow run · To retry, trigger Bonk again. |
|
/bonk review and approve |
There was a problem hiding this comment.
Clean, well-scoped addition. The imperative scroll API is thoughtfully designed — defaults to instant scroll for cross-app landings, opt-in smooth for in-app jumps, and honors prefers-reduced-motion.
Implementation — Resolving the owning viewport by walking up from the registered node (closest('[data-sidebar="viewport"]')) is the right call for SlidingViews, where multiple viewports can coexist. The manual scrollTop math avoids the ancestor-scrolling pitfall of scrollIntoView, and the registry correctly unregisters on unmount.
Tests — Good coverage for the critical paths: alignment math, clamping, ancestor safety, multi-viewport resolution, and the two itemId placement patterns (auto-wrapped <li> vs explicit MenuItem).
Minor observation (non-blocking): there's no explicit test for align: "end", though the math is the symmetric inverse of "start" and the clamp test covers boundary behavior.
Docs — Demo and prose are clear; client:load is the correct hydration directive for this interactive example.
Approved.
|
Approved PR #736. The review highlighted:
One minor non-blocking observation about the missing explicit |




























































Summary
Adds
scrollToItem(id, options?)touseSidebar()for imperatively scrolling a nav item into the sidebar viewport. Tag items with the newitemIdprop onSidebar.MenuButtonorSidebar.MenuItem. Scrolls only the sidebar viewport — never the document — and honorsprefers-reduced-motion.Why
Cross-app navigation lands users at the top of an unfamiliar sidebar. Consumers currently have no way to programmatically scroll the sidebar without reaching into DOM refs, and any hand-rolled solution using
scrollIntoViewends up scrolling every ancestor (including the document), which is jarring inside a scrollable page.API
align— where the item lands in the viewport. Matches the vocabulary used by Tanstack Virtual and React Virtuoso."start""center""end""auto"(default)behavior— scroll behavior, matching the nativescrollToAPI."auto"(default)"smooth"prefers-reduced-motionforces"auto"regardless of what you pass.How consumers benefit
The load-bearing use case is cross-app soft-pin on entry. Two Cloudflare apps that share a nav (e.g. stratus + Zero Trust) can each derive an item id from the URL and land users on the right section, without watching the sidebar animate on mount:
The URL is the source of truth — no storage, no cross-origin coordination, no restoration timing bugs. Shareable links land users on the right item every time. For in-app "jump to section" flows, callers opt into
behavior: "smooth"to get the animation.Implementation notes
scrollTopfromgetBoundingClientRect()and callsviewport.scrollTo({ top })directly. Usingelement.scrollIntoView()would walk the ancestor chain and scroll the document too — a regression test locks this in.querySelector.Sidebar.MenuItemandSidebar.MenuButtonregister their DOM node on mount into aMap<itemId, HTMLElement>on the provider.scrollToItem(id)looks up the node and walks up via.closest('[data-sidebar="viewport"]')to find the owning viewport. This is correct even insideSidebar.SlidingViews, where multipleSidebar.Contentviewports can be mounted at once — aquerySelector-based approach would silently pick the first viewport in DOM order.itemIdworks in both usage patterns. OnSidebar.MenuButtondirectly (auto-wraps in<li>, and the<li>is the scroll target) or onSidebar.MenuItemwhen you wrap aCollapsible(the<li>is the target). If you putitemIdon aMenuButtonnested inside an explicitMenuItem— the pattern needed forCollapsible— it still works: the button/anchor itself becomes the scroll target.Changes
sidebar.tsx:SidebarScrollAlign/SidebarScrollToItemOptionstypes;registerItem+scrollToItemon context;itemIdprop; item registry; ancestor-safe scroll mathsidebar.test.tsx: 8 tests exercising start/center/end/auto/clamp/ancestor-safety/no-op/MenuItem/SlidingViews multi-viewport/MenuItem-wrapping-MenuButtonsidebar/index.ts+ rootindex.ts: exportSidebarScrollAlign,SidebarScrollToItemOptions,SidebarMenuItemPropsSidebarScrollToItemDemoinSidebarDemo.tsx+ "Scroll to Item" section insidebar.astroTesting
pnpm --filter @cloudflare/kumo test src/components/sidebar/sidebar.test.tsx— 57/57 passpnpm --filter @cloudflare/kumo typecheckpnpm --filter @cloudflare/kumo lintpnpm --filter @cloudflare/kumo build— attw + publint cleankumo-docs-astrodev server: clicking any button in the "Scroll to Item" demo scrolls only the sidebar viewport (scrollTopchanges), never the document (window.scrollYstays fixed).