@workday/canvas-kit-docs
Version:
Documentation components of Canvas Kit components
72 lines (52 loc) • 4.04 kB
text/mdx
import {ExampleCodeBlock} from '@workday/canvas-kit-docs';
import InlinePopupNoPortal from './examples/Popups/InlinePopupNoPortal';
import PopupAriaOwns from './examples/Popups/PopupAriaOwns';
A **screen reader** is software that reads the page out loud and lets people navigate with the
keyboard (and sometimes a braille display). It does not “see” the layout the way sighted users do.
It walks through the page in a sequence that usually matches the **order of elements in the
DOM**—roughly, the order nodes appear in the HTML tree.
That matters for popups: if the popup’s markup is **far away** from the control that opened it in
the DOM, the screen reader may read a lot of other page content **before** it reaches the popup. The
user might not realize the popup is there, or they might hear unrelated content mixed in with the
popup experience.
**Moving keyboard focus** into the popup when it opens helps people continue interacting, but it
does **not** change that underlying reading sequence. So focus management and reading order are
related problems; you often need to address both.
**None of these examples use focus traps.** For modal dialogs with an overlay and focus trap, use
the [**Modal**](?path=/docs/components-popups-modal--docs) component instead.
**`useInitialFocus`:** When the popup opens, each example moves focus into the popup (often to a
Close control or another safe first stop). That matters because **many screen readers only announce
new content when focus moves**. If focus stays on the trigger, the user may get **no cue** that a
popup appeared. When choosing not to use `useInitialFocus`, consider the following:
- Use `aria-expanded={true | false}` on `Popup.Target` so assistive tech can report whether the
popup is open or closed.
- Use `aria-haspopup="dialog"` on `Popup.Target` as a hint that the control opens a dialog.
**Caveat:** some older screen readers do not understand the `"dialog"` value. They may treat it
like a generic popup and **announce “menu”** even when you built a dialog. For that reason, we
**strongly recommend** testing with your supported browsers and screen reader combinations during
development.
Set `portal={false}` on `Popup.Popper` so the popup renders in the DOM next to its target. Reading
order follows document order. Use **`useInitialFocus`**, **`useReturnFocus`**, and the usual close
hooks. **Tradeoff:** the popup is constrained by ancestor `overflow` and positioning context.
<ExampleCodeBlock code={InlinePopupNoPortal} />
For the same reading-order goal using a **portaled** popup mounted into a sentinel next to the
trigger (with `PopupStack.pushStackContext`), see
[**Testing > Inline Portals**](?path=/docs/guides-accessibility-testing-inline-portals--docs).
## 2. Reading order with `aria-owns`
You can keep the default portal (content at the bottom of `body`) and still try to **re-parent** the
popup in the **accessibility tree**: add a sibling element after `Popup.Target` and set
**`aria-owns`** to the id of the portaled `Popup.Card`. Some assistive technologies will then treat
that card as “owned” by the trigger for browsing and announcements.
**Tradeoffs:**
- **Support for `aria-owns` varies.** Do not assume every combination of browser and screen reader
will honor it the same way.
- **Tab order still follows the real DOM.** `aria-owns` does not move focus targets. You may still
need helpers like **`useFocusRedirect`** so keyboard users can reach the popup predictably.
- Combine with **`useInitialFocus`** so opening the popup still moves focus and gives a clear
announcement where supported.
The Canvas Kit [**Dialog**](?path=/docs/components-popups-dialog--docs) builds this pattern in.
Another `aria-owns` example:
[Advanced Tables > Table With Filterable Column Headers](?path=/docs/guides-accessibility-examples-advanced-tables--docs#filterable-column-headers).
<ExampleCodeBlock code={PopupAriaOwns} />