Skip to content

which-heading-do-i-need: nested sections are not heading context; heading-less boundaries are transparent#776

Merged
NullVoxPopuli merged 2 commits into
universal-ember:mainfrom
NullVoxPopuli-ai-agent:fix-heading-level-empty-sections
Jul 13, 2026
Merged

which-heading-do-i-need: nested sections are not heading context; heading-less boundaries are transparent#776
NullVoxPopuli merged 2 commits into
universal-ember:mainfrom
NullVoxPopuli-ai-agent:fix-heading-level-empty-sections

Conversation

@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor

Fixes two ways getSectionHeadingLevel (which-heading-do-i-need) disagreed with its own documented algorithm — "We'll need to check the subtrees between these elements, stopping if we encounter other boundary elements" — surfaced while fixing an axe heading-order failure via universal-ember/kolay#318 (see the review discussion there).

1. Fallback traversal recursed into sectioning content

findHeadingIn's previous-sibling scan skips boundary siblings, but its fallback child-traversal recursed into every child, including nested <section>s. Headings inside a nested/sibling section belong to that section and are not context for ours, but the fallback found them anyway. Consequences:

  • sibling headings behind non-boundary wrappers resolved one level deeper than the first heading, instead of equal;
  • sibling <section>s cascaded one level per section (h3, h4, h5, ...).

The fallback now skips sectioning content (section, article, aside, nav) — consistent with the sibling scan. header / footer / main are still traversed: a <header> in particular typically contains its section's heading, which the existing "wrapped headings" rendering tests cover (a blanket boundary skip fails 4 of them; this split keeps them green).

2. Heading-less boundaries isolated their contents to <h1>

levelOf returned 1 as soon as the upward walk reached the next boundary, even when that boundary had no heading of its own. A heading-less wrapper <section> therefore cut everything inside it off from the document — kolay's api-docs signatures rendered <h1>s mid-page (a 2+ level heading-order jump on consuming docs pages, flagged by axe-core ≥ 4.12). A boundary without a heading has no level to nest under, so it is now transparent: the walk keeps searching outward for context.

<h2>API Reference</h2>
<section>          <!-- no heading of its own -->
  <section>
    <Heading />    <!-- was: h1; now: h3 -->
  </section>
</section>

Verification

  • 2 new unit tests (heading-less sections are transparent; sibling-section headings are not context)
  • full test-app suite: 388 pass / 0 heading failures — including all existing wrapped headings, extraneous sections, shadow-root, and mixed section/aside/article cases (4 <InViewport /> failures in my local run are pre-existing: identical on an unmodified checkout, IntersectionObserver-timing related, untouched by this change)
  • package lint: eslint, tsc, prettier all green
  • downstream check: with this fix packed into kolay#318 and that packed into nvp.ui (floating deps, axe-core 4.12.1), all api-docs signature headings resolve as equal-level siblings one below the heading that precedes the block (e.g. h2 API Reference → h3 Element / h3 Arguments / h3 Blocks), union variants no longer cascade, and nvp.ui's full suite passes 83/83

🤖 Generated with Claude Code

…xt; empty boundaries are transparent

Fixes two ways getSectionHeadingLevel disagreed with its own documented
algorithm ("We'll need to check the subtrees between these elements,
stopping if we encounter other boundary elements"):

1. findHeadingIn's fallback child-traversal recursed into every child,
   including sectioning content. Headings inside a nested/sibling
   <section> belong to that section and are not context for ours, but
   the fallback would find them anyway. This made sibling headings
   resolve one level deeper than the first through non-boundary
   wrappers, and made sibling <section>s cascade one level per section
   (h3, h4, h5, ...) instead of staying at equal levels.

   The fallback now skips sectioning content (section, article, aside,
   nav) like the previous-sibling traversal already did. header,
   footer, and main are still traversed: a <header> in particular
   typically contains its section's heading (covered by the existing
   "wrapped headings" rendering tests).

2. levelOf returned 1 as soon as the walk reached the next boundary,
   even if that boundary had no heading of its own. A heading-less
   wrapper <section> therefore isolated everything inside it, pinning
   headings to <h1> mid-document. Such a boundary has no heading level
   to nest under, so it is now transparent: the walk keeps searching
   outward for context.

Both fixes come out of universal-ember/kolay#318, where api-docs
headings rendered inside a heading-less <section> came out as <h1>
(failing axe's heading-order audit in consuming docs pages), and
union-signature variants cascaded one level per variant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@github-actions

Copy link
Copy Markdown
Contributor
Project Preview URL
Docs https://be23b687.ember-primitives.pages.dev

Logs

NullVoxPopuli-ai-agent pushed a commit to NullVoxPopuli-ai-agent/kolay that referenced this pull request Jul 13, 2026
The behaviors the previous comments described working around are bugs in
which-heading-do-i-need, now fixed in universal-ember/ember-primitives#776
(fallback traversal no longer treats nested sectioning content as heading
context; heading-less boundaries are transparent). The comments now just
state the intent: these headings are siblings within <Load>'s <section>.

With that fix, all signature headings resolve to the same level, one
below the heading that precedes the api-docs block (verified against
nvp.ui with both fixes packed: timeline renders h3 Timeline -> h4
Element / h4 Arguments / h4 Blocks, union variants at equal levels,
83/83 with axe-core 4.12.1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NullVoxPopuli NullVoxPopuli added the bug Something isn't working label Jul 13, 2026
… even pinned to 2 CPUs)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NullVoxPopuli-ai-agent pushed a commit to NullVoxPopuli-ai-agent/kolay that referenced this pull request Jul 13, 2026
The behaviors the previous comments described working around are bugs in
which-heading-do-i-need, now fixed in universal-ember/ember-primitives#776
(fallback traversal no longer treats nested sectioning content as heading
context; heading-less boundaries are transparent). The comments now just
state the intent: these headings are siblings within <Load>'s <section>.

With that fix, all signature headings resolve to the same level, one
below the heading that precedes the api-docs block (verified against
nvp.ui with both fixes packed: timeline renders h3 Timeline -> h4
Element / h4 Arguments / h4 Blocks, union variants at equal levels,
83/83 with axe-core 4.12.1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor Author

Re: the Docs' Tests timeout on the first run (86843900699) — investigated as a possible traversal hang; it isn't one:

  • The job ran exactly one test (ok 1 - Docs: Scroller at 14:30:40), then produced nothing for 14 minutes until the 15-minute job cap killed it, with Chrome still alive at cleanup — a wedged browser, not a slow creep (main's Docs' Tests baseline is ~2.5m).
  • Could not reproduce: the full docs suite passes locally in ~1.5m total, with the all-pages a11y test at 42.4s — and 43.5s when pinned to 2 CPUs (taskset -c 0,1) to match the CI runner class.
  • Termination argument for the changed walk: levelOf only ever moves current strictly upward (parentNode, or shadow-root → host), and the stopAt extension for heading-less boundaries comes from nearestAncestor, i.e. is always a strict ancestor — no revisiting is possible; findHeadingIn is structural recursion over children/previousSibling on a finite tree. The extension also doesn't change asymptotics meaningfully: the pages test went 42s → 43s vs the unmodified baseline locally.

The retriggered run confirms: Docs' Tests passed in 2m15s, and every other job is green.

@NullVoxPopuli NullVoxPopuli merged commit 3d4923f into universal-ember:main Jul 13, 2026
17 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 13, 2026
NullVoxPopuli-ai-agent pushed a commit to NullVoxPopuli-ai-agent/kolay that referenced this pull request Jul 13, 2026
0.60.1 ships which-heading-do-i-need 0.4.1, which contains the
traversal fixes from universal-ember/ember-primitives#776 (fallback
does not treat nested sectioning content as heading context;
heading-less boundaries are transparent). Together with the markup
change in this PR, all api-docs signature headings now resolve to the
same level, one below the heading that precedes the block
(e.g. "## API Reference" -> h3 Element / h3 Arguments / h3 Blocks).

Bumped in the addon package, docs-app, and both test apps that pin it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants