feat(table-of-contents): scroll-tracking hook + wire docs TOC to it#645
Merged
mattrothenberg merged 7 commits intoJul 20, 2026
Conversation
…ng hook The TableOfContents component is presentational, so consumers had to wire up their own scroll tracking. Add a shared hook that derives the active section from scroll position via an IntersectionObserver (useScrollspy) and pins a section on click / hash deep-link, resuming scrollspy once the smooth scroll settles. It highlights the topmost section in view and never force-jumps to the last item at the page bottom.
Replace the docs TOC's bespoke IntersectionObserver (and its force-jump-to-last-item clamp) with the shared useTableOfContentsActiveId hook, so the active section tracks scroll position and short trailing sections stay reachable. Mount the island client:only since the hook creates an IntersectionObserver during render, which is unavailable during Astro SSR.
Rework the scroll-tracking hook so it is SSR-safe and simpler to consume: - Create the IntersectionObserver only inside effects (never during render), so consumers can server-render again — docs islands revert from client:only back to client:load. - API takes section `ids` (plus `offset`, `root`, `trackHash`) instead of resolved Element targets, and handles hash deep-links internally. - Replace the scrollend-based pin/resume with a debounced scroll listener, fixing a permanently-stuck active state on browsers without scrollend (Safari < 18.2). - Collapse the useIntersectionObserver/useScrollspy layers into the hook and rename the return type to UseTableOfContentsActiveIdResult. - Test the real observer logic via a mock IntersectionObserver instead of mocking the scrollspy layer wholesale. - TableOfContents.Group: attach onClick to the group label link instead of the wrapping <li>, so clicks on nested items no longer bubble into the group handler and override the child's selection. - Document the hook on the TableOfContents docs page with a live custom-scroll-container demo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collaborator
|
/bonk please review and approve |
commit: |
Contributor
Docs PreviewCommit: |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
Review posted on PR #645. Summary:
|
Contributor
- Use \0 as the ids cache-key separator (unambiguous for any valid id). - Document why Group drops onClick when no href is provided. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mattrothenberg
approved these changes
Jul 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.














































































Summary
The
TableOfContentscomponent is presentational, so consumers had to wire up their own scroll tracking. This adds a shareduseTableOfContentsActiveIdhook to@cloudflare/kumoand rewires the docs "On this page" TOC to use it.The hook derives the active section from scroll position via an
IntersectionObserver(useScrollspy) and pins a section on click / hash deep-link, resuming scrollspy once the smooth scroll settles.selectSectionkeeps a clicked / deep-linked section active even when it's too short to reach the activation line.This mirrors the scroll-tracking fix landed for the dashboard TOC (stratus
!40583, DESENG-1024), extracting the shared hook so the behavior isn't re-implemented per consumer.Changes
@cloudflare/kumo(minor): newuseTableOfContentsActiveIdhook (+ internaluseScrollspyprimitive, types, unit tests), exported from the package root.@cloudflare/kumo-docs-astro(patch): the docsTableOfContentsnow consumes the shared hook instead of its own bespokeIntersectionObserver(which force-jumped to the last item at the page bottom). The island is mountedclient:onlybecause the hook builds anIntersectionObserverduring render, which isn't available during Astro SSR.Testing
pnpm --filter @cloudflare/kumo test— 25 passing (incl. 5 new hook tests)pnpm --filter @cloudflare/kumo typecheck+ docsastro check+pnpm lint— cleanpnpm --filter @cloudflare/kumo buildand docsastro build— succeed (71 pages)Manually verified on the docs dev server: the active item tracks scroll, no last-item jump, and click /
#hashpins the section.Reviews
bonk has reviewed the change
automated review not possible because: opened via CLI by an AI agent; bonk + human review requested on the PR
Tests
Tests included/updated
Automated tests not possible - manual testing has been completed as follows:
Additional testing not necessary because: