Skip to content

Fix invalid HTML nesting in ActionList.Heading#8066

Draft
janmaarten-a11y wants to merge 1 commit into
mainfrom
janmaarten-a11y/actionlist-heading-span-fix
Draft

Fix invalid HTML nesting in ActionList.Heading#8066
janmaarten-a11y wants to merge 1 commit into
mainfrom
janmaarten-a11y/actionlist-heading-span-fix

Conversation

@janmaarten-a11y

Copy link
Copy Markdown
Contributor

Background

ActionList.Heading renders the section heading for an ActionList (e.g. an h1h6 that labels the list). Until now it wrapped that heading in a VisuallyHidden component:

<VisuallyHidden isVisible={!visuallyHidden}>
  <HeadingComponent as={as} ...>{children}</HeadingComponent>
</VisuallyHidden>

VisuallyHidden always renders a <span> around its children — it only toggles a CSS class to hide/show. That means every ActionList.Heading ended up as a heading nested inside a <span>, regardless of whether visuallyHidden was set.

That nesting is invalid HTML: a heading (<h1><h6>) is flow content, and a <span> may only contain phrasing content. So we were emitting a <span><h2>…</h2></span> structure that browsers technically have to error-correct. ActionList is used very widely (including indirectly by NavList), so this affected a lot of pages.

This is the same problem — and the same fix — we just landed for NavList.Heading in #8031. This PR brings ActionList.Heading in line.

What changed

Instead of wrapping the heading in a VisuallyHidden span, we apply the visually-hidden CSS class directly to the heading element using clsx:

<HeadingComponent
  as={as}
  className={clsx(className, classes.ActionListHeader, {
    [visuallyHiddenClasses.InternalVisuallyHidden]: visuallyHidden,
  })}
  ...
>
  {children}
</HeadingComponent>

The wrapping <span> is gone, so the heading is now a direct child of the list container — valid HTML.

One detail worth calling out: unlike NavList.Heading, the ActionListHeader class (which provides the heading's margins and the data-list-variant-based spacing) must stay on the heading in all states. So the visually-hidden class is added on top of ActionListHeader only when visuallyHidden is true — they are not mutually exclusive here.

Changed

  • ActionList.Heading no longer wraps its heading in a VisuallyHidden <span>; the visually-hidden styles are applied directly to the heading element.

Removed

  • The internal <span> wrapper around the ActionList.Heading element.

Why this is safe

ActionList is high-traffic, so we checked the things that could plausibly depend on the old structure:

  • Accessible labelling is unaffected. List.tsx and Group.tsx derive aria-labelledby from the heading slot's React props (id ?? headingId), not from the DOM. Removing the wrapper span doesn't change the id wiring.
  • Slot rendering is unaffected. List.tsx renders {slots.heading} directly; only the wrapper element disappears, the heading itself is still emitted.
  • Visible and hidden styling are preserved. ActionListHeader (margins) stays on the heading in every state. When visuallyHidden is set, InternalVisuallyHidden is added (absolute position, 1px clip), matching the previous behaviour.
  • ActionList.GroupHeading is not affected. It uses its own local heading component and only imports the type from Heading.tsx.
  • SSR compatible. This is a pure className change — no new hooks or DOM APIs.
  • No snapshot or e2e test depended on the wrapper span.

Testing & Reviewing

  • Added unit tests in ActionList/Heading.test.tsx covering: the heading is no longer wrapped in a <span>, visuallyHidden applies InternalVisuallyHidden to the heading, and the default (visible) case keeps only ActionListHeader.
  • Ran the ActionList and NavList unit suites together (NavList consumes ActionList.Heading indirectly): all passing.
  • Type-check, full build, and lint/format all pass.

This branch is rebased on top of #8031 (NavList.Heading), since the two changes are closely related.

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Storybook)
  • Changes are SSR compatible
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge
  • (GitHub staff only) Integration tests pass at github/github-ui

Apply the visually-hidden styles directly to the heading element instead
of wrapping it in a VisuallyHidden span. A heading is not valid phrasing
content inside a span, so the wrapper produced invalid HTML for every
ActionList.Heading instance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Jun 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0c52807

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Jun 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Action required

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. If this doesn't work, you can also use the original workflow here. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

To publish a canary release for integration testing, apply the Canary Release label to this PR.

@primer-integration

primer-integration Bot commented Jun 26, 2026

Copy link
Copy Markdown

Integration test results from github/github-ui PR:

Failed  CI   Failed
Running  VRT   Running
Waiting  Projects   Waiting

CI check runs linting, type checking, and unit tests. Check the workflow logs for specific failures.

Need help? If you believe this failure is unrelated to your changes, please reach out to the Primer team for assistance.

@janmaarten-a11y

Copy link
Copy Markdown
Contributor Author

Note for reviewers: the github-ui integration test failures are unrelated to this PR

The auto-generated integration test (github/github-ui#24809) shows failing Check - Types 1/2/3 (and roll-ups). None of these are caused by this change — they're a @primer/react version-skew issue around SelectPanel, not ActionList.Heading.

What's actually failing: every error references cssAnchorPositioningSettings on SelectPanelProps (in github-ui's picker packages — ItemPicker, IssueTypePicker, ProjectPicker, BranchPicker, LabelPicker, ReposSelector, etc.). There is zero reference to ActionList.Heading, Heading.tsx, or the heading markup anywhere in the failing jobs. The "ActionList" strings in the logs are FilteredActionListProps inside SelectPanel type signatures and data-component="ActionList.Item" in rendered DOM — not this component.

Root cause — version skew:

This PR's only diff vs main is ActionList/Heading.tsx — it cannot change SelectPanel's type surface. Any primer canary integration PR right now exhibits the same failures.

What resolves the integration test (all github-ui-side, after release):

  1. Cut @primer/react 38.30.0 (release tracking: Release tracking #8033) — publishes Allow CSS anchor positioning in SelectPanel settings #8026, Add NavList.Heading slot for accessible section-nav headings #8031, and this PR.
  2. github-ui bumps its @primer/react pin to 38.30.0 → clears TS2344/TS2353.
  3. github-ui updates picker tests/call sites to pass cssAnchorPositioningSettings and stops leaking it to the DOM in the LabelPicker path → clears TS2741 and the console.error.

Safe to ignore for this PR. The unit tests covering this change (ActionList + NavList) pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Canary Release Apply this label when you want CI to create a canary release of the current PR component: ActionList integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm react

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant