Skip to content

Add trigger ARIA state for detached AnchoredOverlay/SelectPanel anchors#8097

Open
joshfarrant wants to merge 5 commits into
mainfrom
detached-anchor-haspopup
Open

Add trigger ARIA state for detached AnchoredOverlay/SelectPanel anchors#8097
joshfarrant wants to merge 5 commits into
mainfrom
detached-anchor-haspopup

Conversation

@joshfarrant

@joshfarrant joshfarrant commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

In detached-anchor mode (renderAnchor={null}, where the consumer owns and renders the trigger element), AnchoredOverlay never applied the aria-haspopup/aria-expanded attributes it normally sets in the renderAnchor path. Screen reader users had no indication that the trigger opens a popup, nor whether it's currently expanded.

Fixes the accessibility gap described in github/primer#6776.

What changed

  • AnchoredOverlay — imperatively reflects aria-haspopup and aria-expanded onto the detached anchor node, mirroring the existing anchor-name effect that already writes to that consumer-owned element. The effect:
    • reads anchorRef.current (attached by commit time) so the collapsed trigger is labelled on mount — not just after the overlay is first opened;
    • guards against clobbering a consumer-supplied aria-haspopup;
    • reflects aria-expanded from open, and cleans up on close/unmount.
  • New anchorHasPopup prop'true' | 'dialog' | 'menu' | 'listbox' | 'tree' | 'grid', default 'true' (non-breaking; 'true' is spec-equivalent to 'menu'). Applied in both the renderAnchor and detached paths so the popup role can be described accurately.
  • SelectPanel passes anchorHasPopup="dialog" to match its role="dialog" popup (previously it hardcoded the spec-equivalent-of-menu default, which mis-described the dialog).

Before / after

Detached trigger (collapsed) Detached trigger (open)
Before no aria-haspopup, no aria-expanded no ARIA state
After aria-haspopup="…" aria-expanded="false" aria-expanded="true"
SelectPanel aria-haspopup="dialog" aria-expanded="true"

Testing

Added coverage on AnchoredOverlay (collapsed state, open/close toggle, consumer-override, renderAnchor path, cleanup) and SelectPanel (aria-haspopup="dialog" on both default and detached anchors)

In detached-anchor mode (renderAnchor={null}) the consumer owns the trigger
element, so AnchoredOverlay never rendered the aria-haspopup/aria-expanded it
applies in the renderAnchor path. Screen reader users had no indication the
trigger opened a popup, nor its expanded state. (github/primer#6776)

- AnchoredOverlay: imperatively reflect aria-haspopup/aria-expanded onto the
  detached anchor node, mirroring the existing anchor-name effect. The effect
  reads anchorRef.current (available after commit) so the collapsed trigger is
  labelled on mount, guards against clobbering a consumer-supplied
  aria-haspopup, and cleans up on close/unmount.
- Add an anchorHasPopup prop ('true' | 'dialog' | 'menu' | 'listbox' | 'tree'
  | 'grid', default 'true') so the popup role can be described accurately;
  applied in both the renderAnchor and detached paths.
- SelectPanel passes anchorHasPopup="dialog" to match its role="dialog" popup.

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

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 04c6a90

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 Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves accessibility for AnchoredOverlay and consumers (notably SelectPanel) by ensuring trigger elements in detached-anchor mode receive appropriate popup semantics (aria-haspopup) and state (aria-expanded), matching the behavior of the renderAnchor path.

Changes:

  • Added anchorHasPopup prop to AnchoredOverlay and applied it to the rendered-anchor path and detached-anchor path.
  • Added an effect to imperatively reflect aria-haspopup / aria-expanded onto a detached anchor element.
  • Updated SelectPanel to use anchorHasPopup="dialog" and added/updated tests to cover both rendered and detached anchors.
Show a summary per file
File Description
packages/react/src/SelectPanel/SelectPanel.tsx Passes anchorHasPopup="dialog" to accurately describe SelectPanel’s dialog overlay.
packages/react/src/SelectPanel/SelectPanel.test.tsx Updates and adds coverage for aria-haspopup="dialog" and aria-expanded on default + detached anchors.
packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx Introduces anchorHasPopup and adds detached-anchor ARIA reflection via an effect.
packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx Adds tests for detached-anchor ARIA behavior and the new anchorHasPopup prop.
packages/react/src/AnchoredOverlay/AnchoredOverlay.docs.json Documents the new anchorHasPopup prop.
.changeset/anchored-overlay-detached-anchor-aria.md Adds a patch changeset for the accessibility fix + SelectPanel update.

Review details

  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment thread packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx Outdated
Comment thread packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx Outdated
- Derive AnchorHasPopup from React.AriaAttributes['aria-haspopup'] (excluding
  boolean/'false') instead of hand-maintaining a parallel union, and export it
  so consumers and tests stay in sync.
- Split the detached-anchor ARIA into two effects: aria-haspopup (stable, no
  longer keyed on `open`, so it isn't removed/re-added on every toggle) and
  aria-expanded (reflects `open`).
- Reference the exported AnchorHasPopup type in tests instead of duplicating
  the union.

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

Copy link
Copy Markdown
Contributor Author

Addressed both review comments in 491679e:

  • aria-haspopup and aria-expanded are now managed in separate effects, so the stable aria-haspopup is no longer removed/re-added on every open/close.
  • AnchorHasPopup is now derived from React.AriaAttributes['aria-haspopup'] (excluding boolean/'false') and exported; the tests reference it instead of duplicating the union.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment thread packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx Outdated
Comment thread packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx Outdated
Add anchorRef to the dependency arrays of the detached aria-haspopup and
aria-expanded effects and drop the exhaustive-deps suppressions. anchorRef is
stable in the common case (so no extra re-runs), but including it keeps the
effects honest and ensures they re-run if a consumer swaps the anchorRef prop
to a new RefObject.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@joshfarrant joshfarrant requested a review from Copilot July 1, 2026 09:46
@joshfarrant

Copy link
Copy Markdown
Contributor Author

Addressed in 98484d9: added anchorRef to the dependency arrays of both detached-anchor ARIA effects and removed the exhaustive-deps suppressions. anchorRef is stable in the common case, so no extra re-runs, but the effects now honestly track a swapped anchorRef prop.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Low

@github-actions github-actions Bot temporarily deployed to storybook-preview-8097 July 1, 2026 09:58 Inactive
@joshfarrant joshfarrant marked this pull request as ready for review July 1, 2026 13:11
@joshfarrant joshfarrant requested a review from a team as a code owner July 1, 2026 13:11
@joshfarrant joshfarrant requested a review from francinelucca July 1, 2026 13:11
@joshfarrant joshfarrant marked this pull request as draft July 1, 2026 14:57
…rs, revert SelectPanel dialog

The github-ui integration test surfaced two regressions:

1. axe aria-allowed-attr violations: consumers using detached mode with the
   anchorRef pointing at a non-interactive element (e.g. a wrapper <div>) had
   aria-haspopup/aria-expanded imperatively written onto them, which is invalid
   ARIA. Guard the writes to only apply when the anchor element's role supports
   these attributes (native interactive elements or an appropriate explicit role).

2. SelectPanel changed aria-haspopup from 'true' to 'dialog' for ALL anchors,
   breaking downstream expectations. Revert to the existing 'true' default to
   keep the fix scoped to the detached-anchor gap #6776 describes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The integration test's remaining failure was DatePicker's 'Single Input
Anchor', which uses a plain <input> (role textbox) as the detached anchor.
role=textbox doesn't support aria-expanded, so writing it is still an axe
aria-allowed-attr violation.

Restrict the imperative write to genuinely button-like triggers: native
<button>/<a href>/<summary>, or an explicit role that supports both
aria-haspopup and aria-expanded (button, combobox, menuitem, link, tab, etc.).
Plain inputs and divs are skipped; a consumer wanting the semantics on an input
should give it role=combobox. Added tests for the input (skipped) and
role=combobox (written) cases.

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

Copy link
Copy Markdown

Integration test results from github/github-ui PR:

Passed  CI   Passed
Passed  VRT   Passed
Passed  Projects   Passed

All checks passed!

@francinelucca francinelucca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me implementation-wise, and satisfies https://github.com/github/primer/issues/6776. I kind of feel like if the consumer is choosing to use a custom anchor then they should also be responsible/empowered for handling the accessibility and attributes of it however they see fit 🤔

Added some more thoughts here https://github.com/github/primer/issues/6776#issuecomment-4861619362

@joshfarrant joshfarrant marked this pull request as ready for review July 2, 2026 08:44
"required": false,
"description": "Indicates the type of popup opened by the anchor. Defaults to `'true'`, which is equivalent to `'menu'` in ARIA.",
"defaultValue": "'true'"
},

@siddharthkp siddharthkp Jul 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have thoughts on the API, but it's a a good to have for the bug fix we are doing here and I don't want to block the bug fix.

Can you split this PR into 2:

  1. the first PR is the bug fix that copies current renderAnchor behavior (value: true)
  2. second PR lets you customize the value of aria-haspopup, so that we can discuss it there.

Thanks!

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

Labels

accessibility integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm ui-quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants