Add an accessible Table of Contents block - #288
Open
Castellon-ACM wants to merge 6 commits into
Open
Conversation
Registers a dynamic frontblocks/table-of-contents block. Since it usually appears before the headings it needs to link to, its own render_callback can't see them yet (sibling blocks haven't rendered). It instead outputs a small config placeholder, and a the_content filter at priority 30 — after do_blocks() has rendered the whole post — scans the final HTML for every heading, assigns a stable unique anchor (and tabindex, for focus-on-navigate) to any heading lacking one while always preserving author-supplied ids, and replaces each placeholder with the real navigation markup filtered to that instance's configured heading-level range. Supports optional list style (bulleted/numbered/plain), accent color, a collapsible mode (rendered as a native <details>/<summary>, so expand/collapse is keyboard-accessible for free), and a sticky position. Heading discovery is regex-based against the rendered HTML tags themselves, so it works with core Heading blocks, GenerateBlocks headings, or any other block that outputs an <h1>-<h6> tag.
- Clicking an entry scrolls to and moves keyboard/screen-reader focus onto its target heading, not just the viewport — using instant (not smooth) scrolling for visitors with prefers-reduced-motion. - An IntersectionObserver marks whichever section is currently in view via aria-current="location" on its link, updated silently (no aria-live region) so it never triggers a screen-reader announcement on every scroll tick. - Styles for the list/collapsible/sticky variants, a visible focus indicator scoped to the TOC's own focus-target headings (not a blanket [tabindex="-1"] selector, to avoid affecting unrelated elements elsewhere on the site), and a prefers-reduced-motion fallback disabling transitions and smooth scrolling.
Inspector controls for title, list style, heading-level range, collapsible/collapsed-by-default, sticky, and accent color. The edit preview lists core Heading blocks found anywhere in the current post (recursing into innerBlocks) — GenerateBlocks and other third-party heading output isn't a distinct, reliably identifiable block type in the editor's block tree, so it's a best-effort preview only; the published page is unaffected since the frontend discovers every heading tag in the final rendered HTML regardless of its source.
Covers: heading id assignment, preserving author-supplied ids, avoiding collisions between generated and author-supplied ids, disambiguating duplicate heading text, level-range filtering, the empty-range no-render case, collapsible/collapsed-by-default output, list style variants, sticky/accent-color output, heading discovery being independent of the originating block, multiple independent TOC instances in one post, the render_placeholder/injector contract, and skipping headings with no real text content.
Covers click-to-scroll-and-focus, instant scrolling under prefers-reduced-motion, gracefully doing nothing for a link with no matching heading, and the IntersectionObserver-driven active-section highlighting (including that a previous active link loses aria-current when a new section becomes active). Wired into CI.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Contributor
|
Three things:
|
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
Closes #260.
Adds a dynamic
frontblocks/table-of-contentsblock that generates accessible navigation from the headings in the current post.How it works
Since the block usually appears before the headings it needs to link to, it can't discover them from its own
render_callback(sibling blocks haven't rendered yet). Instead,render_callback()outputs a small config placeholder, and athe_contentfilter at priority 30 — afterdo_blocks()has rendered the whole post — scans the final HTML for every heading, assigns stable anchors, and replaces the placeholder with real navigation markup.<h1>-<h6>tags, not tied to any specific block — works with core Heading blocks, GenerateBlocks headings, or anything else that outputs a heading tag.idgets one generated from its text (disambiguated with a-2,-3... suffix on collisions, and never colliding with an existing author-supplied id either); an author-set id (e.g. via core Heading's "HTML anchor" field) is always preserved untouched.tabindex="-1"so clicking a Table of Contents entry moves keyboard/screen-reader focus to the destination, not just the viewport.IntersectionObservermarks the current section's link viaaria-current="location", updated silently (noaria-liveregion) so it never triggers a screen-reader announcement on every scroll tick.prefers-reduced-motion: reduce.<details>/<summary>— free, fully keyboard-accessible expand/collapse with zero custom JS.has_block()).Test plan
TableOfContentsTest.php, 17 tests): id assignment, preserving author ids, avoiding id collisions, duplicate-text disambiguation, level-range filtering, empty-range no-render, collapsible/collapsed-by-default output, list style variants, sticky/accent-color, block-agnostic heading discovery, multiple independent TOC instances, the placeholder/injector contract, and skipping text-less headings.table-of-contents-frontend.test.js, 6 tests): click-to-scroll-and-focus, instant scrolling under reduced motion, gracefully handling a missing target, and the IntersectionObserver active-section logic.composer lint(phpcs) clean.document.activeElement) to the heading, not just scroll;aria-current="location"correctly tracked the visible section while scrolling.<details>/<summary>, toggled open/closed by a plain click with.openverified before/after — no custom JS involved.