Skip to content

fix(core): label Dialog from its header title via aria-labelledby#4000

Open
bhamodi wants to merge 1 commit into
facebook:mainfrom
bhamodi:a11y/dialog-default-labelledby
Open

fix(core): label Dialog from its header title via aria-labelledby#4000
bhamodi wants to merge 1 commit into
facebook:mainfrom
bhamodi:a11y/dialog-default-labelledby

Conversation

@bhamodi

@bhamodi bhamodi commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Dialog rendered <dialog aria-modal="true"> with no default accessible-name wiring (Dialog.tsx:547-551 pre-fix). The DialogHeader title -- the visible, auto-focused <Heading level={2} tabIndex={-1}> (DialogHeader.tsx:158-165) -- had no id, and DialogContext carried only {isInline: boolean} (DialogContext.ts:14-17), so there was no channel to connect the two. Any consumer who didn't manually pass aria-label/aria-labelledby shipped an unnamed modal: screen readers announced "dialog" with no indication of what the dialog was for. AlertDialog already solves exactly this with useId -- it generates a title id and passes aria-labelledby down to Dialog (AlertDialog.tsx:137-138, 154-155); this PR gives plain Dialog + DialogHeader the same behavior automatically.

Dialog now generates a title id via useId and publishes it through DialogContext alongside a registerTitle callback. DialogHeader renders the id on its title Heading and registers its presence in a mount effect (with cleanup on unmount). Dialog emits aria-labelledby={titleId} only when (a) a header title has actually registered and (b) the consumer passed neither aria-label nor aria-labelledby -- the consumer always wins, since their values spread after the default. Registration (rather than an unconditional id reference) handles the no-DialogHeader case robustly: an aria-labelledby pointing at a nonexistent id is itself invalid, so no header means no attribute. A dev-time warn-once effect (mirroring the unnamed-dialog warning in usePopover.tsx:393-401) fires when a dialog is open with no resolvable name; a ref mirror of the registration state lets the warning effect see a title registered in the same commit (child effects run before parent effects).

Changes

  • Dialog/DialogContext.ts: add optional titleId and registerTitle to DialogContextValue (existing consumers -- CommandPalette reads only isInline -- are unaffected).
  • Dialog/Dialog.tsx: generate titleId via useId; track header-title registration (state for the attribute, ref for same-commit warning reads); emit aria-labelledby on the <dialog> only when unlabelled-by-consumer and a title exists; add the warn-once unnamed-dialog effect; document the default-labelling behavior in the component JSDoc.
  • Dialog/DialogHeader.tsx: render id={titleId} on the title Heading; register presence with the parent Dialog via effect; update JSDoc.
  • Dialog/Dialog.doc.mjs / Dialog/DialogHeader.doc.mjs: note that the header title labels the dialog via aria-labelledby.
  • Dialog/Dialog.test.tsx: five new tests under accessible name.
  • .changeset/dialog-default-labelledby.md: patch changeset.

Test plan

  • node_modules/.bin/vitest run --root . packages/core/src/Dialog packages/core/src/AlertDialog -- 64 pass (Dialog.test.tsx 35, DialogHeader.test.tsx, useImperativeDialog.test.tsx, AlertDialog.test.tsx; AlertDialog suite untouched and green). Five new tests:
    • dialog is labelled by the DialogHeader title by default (aria-labelledby equals the heading's id; accessible name equals the title)
    • consumer-provided aria-label wins (no aria-labelledby emitted)
    • consumer-provided aria-labelledby wins (external id preserved)
    • dialog without header and without labels emits no dangling aria-labelledby and fires the dev warning exactly once (console.warn spy)
    • header title suppresses the warning
    • The two positive tests (default labelling; warning) were confirmed failing before the fix (2 failed | 28 passed pre-fix); the consumer-wins tests are regression guards.
  • node_modules/.bin/vitest run --root . packages/core -- 4586 pass (203 files; full core suite green, no pre-existing failures).
  • node_modules/.bin/tsc --project packages/core/tsconfig.json --noEmit -- clean.
  • node_modules/.bin/eslint on changed files -- clean; prettier --check on changed ts/tsx/md files -- clean (the repo's lint-staged pre-commit formats staged files, which also normalized two tiny pre-existing formatting drifts in the touched files).

Notes

Found during a broader a11y audit of core components; scoped to one fix per PR. Coexists cleanly with #3984, which touches only Dialog.tsx's style block (animationName); this PR's hunks are in different regions (imports, context wiring, aria attributes, effects) and the animation styles are untouched. AlertDialog is deliberately unchanged -- its explicit useId wiring already produces a named dialog, and since it renders no DialogHeader and passes its own aria-labelledby, the new default never interferes with it.

@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jul 15, 2026 2:52pm

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 15, 2026
@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge labels Jul 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

No new or modified components detected.

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.6KB 0B

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | View full report

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. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant