dequelabs / dequelabs/cauldron
Drawer: missing role="dialog" and automatic accessible name
Open
- Dominant language
- TypeScript
- Stars
- 127
- Forks
- 31
- Avg merge
- 2d 12h
- Merged PRs (30d)
- 8
Description
The `Drawer` component renders a plain `
` with no `role` attribute and no automatic accessible name. Callers who pass `aria-label` or `aria-labelledby` via props are labeling an anonymous div — screen readers ignore that label because there is no role to attach it to. Screen readers do not announce entry into a dialog region when the drawer opens, and virtual-cursor users have no landmark or structural cue that they are in a bounded modal area.
The component already does the right things for containment: `AriaIsolate` hides the rest of the page from AT while open, and `useFocusTrap` constrains keyboard focus. The gap is role announcement and accessible naming.
## Acceptance criteria
- The `Drawer` root `
` has `role="dialog"` when rendered.
- The `Drawer` root `
- The `Drawer` root `
` has `aria-modal="true"` when `behavior === 'modal'` (the default).
- The `Drawer` automatically receives an accessible name via `aria-labelledby`, following the same pattern used by `Dialog`:
- An auto-generated heading ID (via `useId`) is created by `Drawer` and passed through a `DrawerContext`.
- A `DrawerHeading` sub-component reads the ID from context and applies it as its own `id`, so `aria-labelledby` on the root element always references the visible heading text.
- A dev-mode warning is raised (matching the existing `Dialog` pattern) if the drawer is open but no `DrawerHeading` is present inside it.
- When the `Drawer` opens, focus moves to the heading rather than the drawer container, matching `Dialog`. Focusing the container draws a focus ring around the entire drawer (which looks bad) and is a weaker announcement; focusing the heading fixes both. The `DrawerHeading` takes `tabIndex={-1}` and a ref carried through `DrawerContext` (like `DialogContext.headingRef`), which `Drawer` passes to `useFocusTrap` as its `initialFocusElement`.
- The `Drawer` automatically receives an accessible name via `aria-labelledby`, following the same pattern used by `Dialog`:
- An auto-generated heading ID (via `useId`) is created by `Drawer` and passed through a `DrawerContext`.
- A `DrawerHeading` sub-component reads the ID from context and applies it as its own `id`, so `aria-labelledby` on the root element always references the visible heading text.
- A dev-mode warning is raised (matching the existing `Dialog` pattern) if the drawer is open but no `DrawerHeading` is present inside it.
- When the `Drawer` opens, focus moves to the heading rather than the drawer container, matching `Dialog`. Focusing the container draws a focus ring around the entire drawer (which looks bad) and is a weaker announcement; focusing the heading fixes both. The `DrawerHeading` takes `tabIndex={-1}` and a ref carried through `DrawerContext` (like `DialogContext.headingRef`), which `Drawer` passes to `useFocusTrap` as its `initialFocusElement`.
## References
- WAI-ARIA Authoring Practices — [Dialog (Modal) Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
- `Dialog` component implementation of the same pattern: `packages/react/src/components/Dialog/index.tsx`
- Resolved for `Dialog` in #1259
Contributor guide
Assessment
This issue has not been assessed yet.