Skip to content

fix(core): expose aria-expanded and aria-controls on MobileNav toggle#3721

Open
bhamodi wants to merge 1 commit into
facebook:mainfrom
bhamodi:a11y/mobilenav-toggle-expanded
Open

fix(core): expose aria-expanded and aria-controls on MobileNav toggle#3721
bhamodi wants to merge 1 commit into
facebook:mainfrom
bhamodi:a11y/mobilenav-toggle-expanded

Conversation

@bhamodi

@bhamodi bhamodi commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

MobileNavToggle rendered a Button with an aria-label and onClick, but exposed no aria-expanded and no aria-controls (packages/core/src/MobileNav/MobileNavToggle.tsx:69). Screen-reader users had no way to tell whether the nav drawer was currently open, nor which element the hamburger button controls.

Making aria-controls resolve required a shared id, which did not exist: the AppShell mobile context (useAppShellMobile()) exposed no drawer id, and the MobileNav <dialog> had no id at all. This wires that up end to end:

  • AppShell generates a stable useId() and publishes it on the mobile context as mobileNavId.
  • MobileNav applies that id to its <dialog> (falling back to a locally generated useId() when used standalone outside AppShell).
  • MobileNavToggle sets aria-expanded={isMobileNavOpen} and aria-controls={mobileNavId}.

aria-controls is set unconditionally here (unlike the conditionally-mounted region in #3719): the native <dialog> stays in the DOM whenever the layout is below the mobile breakpoint -- it is mounted via React <Activity mode="hidden"> (or a plain fragment fallback) and simply hidden when closed -- so the referenced id always resolves. Purely additive; no behavior or visual change.

Changes

  • MobileNav/MobileNavToggle.tsx: forward aria-expanded + aria-controls from context onto the toggle button.
  • MobileNav/MobileNav.tsx: apply the shared mobileNavId (with a standalone useId() fallback) as the <dialog> id.
  • AppShell/AppShell.tsx + AppShell/AppShellMobileContext.tsx: generate and publish mobileNavId on the mobile context.
  • AppShell/useAppShellMobile.doc.mjs: document the new mobileNavId return field.
  • MobileNavToggle showcase blocks + SideNav.test.tsx fixtures: supply mobileNavId where the context value is constructed by hand.

Test plan

  • node_modules/.bin/vitest run --root . packages/core/src/MobileNav packages/core/src/AppShell -- 55 pass (4 files), including three new tests in MobileNavToggle.test.tsx (all confirmed failing before the fix):
    • exposes aria-expanded="false" when the drawer is closed
    • exposes aria-expanded="true" after opening the drawer
    • references the nav drawer via aria-controls that resolves to the dialog -- asserts the toggle's aria-controls resolves via document.getElementById to the <dialog> element.
  • node_modules/.bin/vitest run --root . packages/core/src/SideNav packages/core/src/TopNav -- 151 pass (they render MobileNav in drawer mode).
  • node_modules/.bin/vitest run --root . packages/core -- 3848 pass (full core suite, incl. the docs contract test).
  • node_modules/.bin/tsc --project packages/core/tsconfig.json --noEmit -- clean.
  • node_modules/.bin/eslint on changed files -- clean.

Notes

Found during a broader a11y audit of core components; scoped to one fix per PR. Button forwards arbitrary aria-* props to the underlying <button> (the same {...props} spread the existing behavior relies on), so aria-controls reaches the DOM. Standalone MobileNav (outside AppShell) still gets a valid unique <dialog> id via its own useId().

@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

@bhamodi is attempting to deploy a commit to the Meta Open Source Team on Vercel.

A member of the Team first needs to authorize it.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 9, 2026
@bhamodi bhamodi force-pushed the a11y/mobilenav-toggle-expanded branch from 25691b5 to 89c294e Compare July 9, 2026 06:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant