Skip to content

fix(page): keep the table of contents on the clicked entry and in step with its sections - #122

Merged
sdcorejs merged 2 commits into
mainfrom
claude/page-toc-scroll-spy
Oct 29, 2026
Merged

sdcorejs merged 2 commits into
mainfrom
claude/page-toc-scroll-spy

Conversation

@sdcorejs

Copy link
Copy Markdown
Owner

Summary

Fixes two problems with the current-entry tracking in the sd-page table of contents (toc, used by sd-page-form and sd-page-detail).

1. The highlight flickered when you clicked an entry. scrollToSection() starts a smooth scroll, and every scroll event of that animation recomputed the current entry. Clicking the fourth entry from the first one highlighted 4, then 1, 2, 3 and 4 again. A section too near the end of the page to reach the top also lost its highlight to the last entry once the scroll stopped.

  • While a click scroll runs, the clicked entry stays current. The clicked section is stored as an element, not an index, because the list can change during the scroll.
  • The scroll counts as finished on scrollend, or after 150 ms without a scroll event (SD_PAGE_TOC_SCROLL_IDLE_MS). The timer also ends the click mode when the section was already in place and nothing scrolled. Browsers without scrollend therefore keep a working scroll spy. sd-anchor lacks this fallback and can freeze in that case.
  • When the scroll finishes, the clicked entry stays current as long as its section is in view. If the user scrolled elsewhere in the meantime, the entry follows the scroll position again.

2. The current entry kept a stale index. activeTocIndex was only recomputed on scroll. When sections were added or removed, or a section changed height (a table finished loading, a section collapsed), the old index could mark the wrong entry, or point past the end of the list so that no entry was marked, until the next scroll.

  • The current entry is recomputed right after the list of sections is rebuilt.
  • A ResizeObserver watches the scroll area and every listed section. .c-page-content keeps a fixed height and does not grow with its content, so the observer watches the sections themselves. It is created only for a page that has entries and is disconnected on destroy.

sd-page.md describes the new behaviour, and CHANGELOG.md has a ### Fixed → #### Page entry under ## [Unreleased]. Rolled out to v20, v21 and v22 with npm run sync.

No input, output or export changes.

Verification

Ran locally on Node 24.22.1 (Windows):

  • Specs first. Six new specs in page.component.spec.ts. Four of them failed on the old code:

    • stays on the clicked entry while the scroll passes the sections above it
    • keeps the clicked entry when the scroll stops with its section in view below the top
    • moves with the sections when one above the current one is removed
    • moves when a section above grows without a scroll

    The other two guard against a frozen scroll spy: "follows the scroll again after scrollend" and "… once scrolling stops, in a browser without scrollend".

  • After the fix: page, section and modules/pattern specs passed 159/159 (seed 259933). The page spec alone passed 38/38 again with seed 968605.

  • ESLint and Prettier: clean on the changed .ts files.

  • npm run check:sync: passed, before and after the commit. npm run test:showcase-changelog: 10/10 passed.

  • Real browser. Ran Playwright against the showcase /pattern/settings at 1300×520 with real smooth scrolling:

    • Click "Bảo mật": the highlight goes straight from "Thông tin cửa hàng" to "Bảo mật".
    • Click "Thông báo": the page scrolls to the bottom and "Thông báo" stays current. The old code switched to "Bảo mật".
    • Smooth scroll back to the top by hand: the highlight steps through the entries as before.
  • Not run locally: the full v19 suite with --code-coverage, and tests on v20/v21/v22. CI covers both.

API consistency review

  • No public API changes. The two new constants are module-private.
  • Behaviour is now closer to sd-anchor, which already suppressed the scroll spy during a click scroll, without its dependency on scrollend.
  • Self-review: done. Independent review: pending.

🤖 Generated with Claude Code

…p with its sections

Clicking an entry of the sd-page table of contents scrolls smoothly to the
section, and every scroll event of that animation recomputed the current
entry, so the highlight jumped back and walked through the entries in between.
A section too near the end of the page to reach the top also lost its
highlight to the last entry once the scroll stopped.

- While a click scroll runs, the clicked entry stays current. The scroll
  counts as finished on `scrollend`, or after 150 ms without a scroll event,
  so browsers without `scrollend` do not freeze the scroll spy.
- When it finishes, the clicked entry stays current while its section is in
  view; otherwise the entry follows the scroll position again.
- The current entry is recomputed when the list of sections changes and when
  the scroll area or a listed section is resized (ResizeObserver, created
  only for a page that has entries). Before, the old index stayed and could
  point at the wrong entry, or past the end of the list, until the next
  scroll.

Rolled out from v19 with npm run sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sdcorejs sdcorejs closed this Oct 29, 2026
@sdcorejs
sdcorejs deleted the claude/page-toc-scroll-spy branch October 29, 2026 06:48
@sdcorejs
sdcorejs restored the claude/page-toc-scroll-spy branch October 29, 2026 06:49
@sdcorejs sdcorejs reopened this Oct 29, 2026
…l-spy

# Conflicts:
#	versions/v19/SYNC-STATUS.md
#	versions/v20/SYNC-STATUS.md
#	versions/v21/SYNC-STATUS.md
#	versions/v22/SYNC-STATUS.md
@sdcorejs
sdcorejs merged commit 69e2d24 into main Oct 29, 2026
7 checks passed
@sdcorejs
sdcorejs deleted the claude/page-toc-scroll-spy branch October 29, 2026 07:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants