@workday/canvas-kit-docs 16.0.9 → 16.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,7 +31,7 @@ yarn add @workday/canvas-kit-react
31
31
 
32
32
  <ExampleCodeBlock code={Basic} />
33
33
 
34
- `Menu` will automatically focus on the cursor item (first item by default). The `Menu` uses a menu
34
+ `Menu` will automatically move focus to the first menu item when it opens. The `Menu` uses a menu
35
35
  model which composes a list model and a popup model and sets up accessibility features for you.
36
36
 
37
37
  > **Note:** When content exceeds `60vh`, the menu content is clipped and the menu becomes scrollable.
@@ -99,9 +99,9 @@ indicate this dual role.
99
99
 
100
100
  <ExampleCodeBlock code={Nested} />
101
101
 
102
- > **Accessibility Note**: When a menu item has an attached submenu, the `<Menu.Submenu.TargetItem>`
103
- > includes `aria-haspopup="true"` and `aria-expanded={true | false}` properties. These properties
104
- > will alert screen reader users to the available submenu systems.
102
+ > **Accessibility Note**: Canvas Kit applies `aria-haspopup` and `aria-expanded` on
103
+ > **`Menu.Submenu.TargetItem`** automatically. Do not set these manually see
104
+ > [Accessibility](#accessibility).
105
105
 
106
106
  ### Nested Dynamic Items
107
107
 
@@ -115,45 +115,190 @@ submenus.
115
115
 
116
116
  ## Accessibility
117
117
 
118
- Our Menu component is based on the Menu Button pattern on the ARIA Authoring Practices Guide from
119
- the W3C and relies on the roving tabindex technique for managing focus within the opened menu. This
120
- means that the minimum requirements for screen reader support and keyboard navigation are included
121
- in the component.
122
-
123
- [Menu Button Pattern | APG | WAI | W3C](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/)
124
-
125
- - The `<Menu.Target>` sub-component uses `aria-haspopup="true"` and `aria-expanded={true | false}`
126
- properties. This benefits screen reader users by indicating when a button element has an attached
127
- menu.
128
- - The `<Menu.List>` sub-component uses `role="menu"` and `<Menu.Item>` uses `role="menuitem"` ARIA
129
- roles. These roles allow screen readers to pass through arrow key events to the web application.
130
- - The `<Menu.List>` sub-component includes an `aria-labelledby` ID reference to the `<Menu.Target>`
131
- sub-component. This assigns a label to the menu for context.
132
-
133
- ### Navigation
134
-
135
- - **Enter or Space**: When focused on the menu button, opens the menu and moves focus to the first
136
- menu item. When focused on a menu item, activates the item and closes the menu
137
- - **Escape**: Closes the menu and returns focus to the menu button
138
- - **Up & Down Arrow**: Moves focus up and down the menu items
139
- - **Home & End**: Moves focus to the first or last menu item
140
- - **Right & Left Arrow**: When focused on a menu item with a submenu, opens the submenu and moves
141
- focus to the first item in the submenu or closes the submenu and returns focus to the parent menu
142
- item
143
-
144
- ### Screen Reader Experience
145
-
146
- - The menu button will be announced with its label text followed by the button role, a notification
147
- that it has a popup menu, and the current state of the menu (For example: "Actions, button, menu
148
- popup, collapsed")
149
- - **Opening the Menu:** When the menu button is activated, screen readers will announce the menu
150
- opening, the number of menu items available, and the currently focused item (For example:
151
- "Actions, menu, First Action, menu item, 1 of 4.")
152
- - **Navigating Menu Items:** As focus moves between menu items, screen readers will announce the
153
- item name and its position in the list (For example: "Second Action, menu item, 2 of 4.")
154
- - **Menu Items with Submenus:** When focused on a menu item that has a submenu, screen readers will
155
- announce that it has a submenu and provide the expanded/collapsed state (For example: "More
156
- Actions, menu item, has submenu, collapsed, 3 of 4.")
118
+ `Menu` follows the
119
+ [Menu Button Pattern | APG | WAI | W3C](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/), which
120
+ has two parts with different accessibility jobs.
121
+
122
+ **Menu button** (`Menu.Target`): a focusable control that opens and closes the menu. It exposes
123
+ popup presence and expanded/collapsed state (`aria-haspopup`, `aria-expanded`), and receives focus
124
+ again when the menu is dismissed.
125
+
126
+ **Menu popup** (`Menu.List` and its items): the floating action list. It uses `role="menu"` /
127
+ `role="menuitem"`, is labeled by the button, and manages focus inside the list with **roving
128
+ tabindex** so users can move between items and activate one.
129
+
130
+ Use **Menu** for action lists opened from a control. Prefer
131
+ [**Select**](https://workday.github.io/canvas-kit/?path=/docs/components-inputs-select--docs) or
132
+ [**Combobox**](https://workday.github.io/canvas-kit/?path=/docs/features-combobox--docs)
133
+ when choosing a value from options (`Menu.Option` / `listbox` patterns are composed there—do not use
134
+ `Menu.Option` alone for a standard menu button). Prefer
135
+ [**Modal**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-modal--docs) or
136
+ [**Dialog**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-dialog--docs) for
137
+ task dialogs, not menus.
138
+
139
+ ### Minimum Accessible Structure
140
+
141
+ The following matches the [Basic Example](#basic-example): a keyboard-operable **`Menu.Target`**,
142
+ portaled **`Menu.Popper` → `Menu.Card` → `Menu.List`**, and **`Menu.Item`** children. On open, focus
143
+ moves to the first menu item by default.
144
+
145
+ ```tsx
146
+ import {Menu} from '@workday/canvas-kit-react/menu';
147
+
148
+ <Menu>
149
+ <Menu.Target>Open Menu</Menu.Target>
150
+ <Menu.Popper>
151
+ <Menu.Card>
152
+ <Menu.List>
153
+ <Menu.Item>First Item</Menu.Item>
154
+ <Menu.Item>Second Item</Menu.Item>
155
+ </Menu.List>
156
+ </Menu.Card>
157
+ </Menu.Popper>
158
+ </Menu>;
159
+ ```
160
+
161
+ Provide a clearly named **`Menu.Target`** (visible text, or an icon-only control with
162
+ **`Tooltip`**, or a translated **`aria-label`** if you are not using **`Tooltip`**).
163
+ Use **`aria-disabled`** on items that should stay in the keyboard sequence but
164
+ not activate — do not use the native `disabled` attribute for disabled menu items.
165
+
166
+ ### Built-in Behaviors
167
+
168
+ Canvas Kit applies these automatically via `useMenuModel` (list + popup) and Menu subcomponents.
169
+ **Do not duplicate them** in consuming code.
170
+
171
+ **Popup behaviors** (_composed on the default model_):
172
+
173
+ - `useAlwaysCloseOnOutsideClick` — pointer interaction outside closes the menu
174
+ - `useCloseOnEscape` — <kbd>Escape</kbd> closes the menu
175
+ - `useReturnFocus` (_on `Menu.List`_) — returns focus to **`Menu.Target`** (or configured return
176
+ target) when the menu closes
177
+ - `useFocusRedirect` (_on `Menu.List`_) — <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> from
178
+ inside the menu closes it and moves focus to the next or previous focusable element on the page
179
+ (not a focus trap)
180
+
181
+ **ARIA and DOM** (_applied by hooks/subcomponents_):
182
+
183
+ - **`Menu.Target`**: shared model `id`, `aria-haspopup="true"`,
184
+ `aria-expanded={visibility === 'visible'}`; <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd> also open
185
+ the menu
186
+ - **`Menu.List`**: `role="menu"`, `aria-labelledby` referencing the target `id`,
187
+ `aria-orientation` from the model
188
+ - **`Menu.Item`**: `role="menuitem"`, roving `tabIndex` (`0` on the focused item, `-1` on others);
189
+ in default `mode="single"`, activating an item selects it and closes the menu (and any open parent
190
+ menus)
191
+ - **`Menu.Group`**: `role="group"` with `aria-labelledby` referencing **`Menu.Group.Heading`** (or
192
+ the heading created from the `title` prop)
193
+ - **`Menu.Submenu.TargetItem`**: `role="menuitem"`, `aria-haspopup="true"`, `aria-expanded` for the
194
+ submenu
195
+
196
+ **Implementation note on open focus:** Menu does **not** compose `useInitialFocus`. In default
197
+ `mode="single"`, **`useMenuItemFocus`** moves focus to the first menu item when the menu opens. Do
198
+ not generate **`initialFocusRef`** — it is not wired on Menu.
199
+
200
+ **Implementation note on `mode="multiple"`:** `useMenuModel` supports `mode="multiple"`, which keeps
201
+ the menu open and toggles selection in model state. **`Menu.Item`** uses `role="menuitem"`, which
202
+ does not support **`aria-selected`**, so selected state is not exposed to assistive technology. Do
203
+ not generate **`mode="multiple"`** with **`Menu.Item`** for an accessible multi-select UI—use
204
+ [**Select**](https://workday.github.io/canvas-kit/?path=/docs/components-inputs-select--docs),
205
+ **MultiSelect**, or
206
+ [**Combobox**](https://workday.github.io/canvas-kit/?path=/docs/features-combobox--docs) instead.
207
+
208
+ **Keyboard** (_trigger is `Menu.Target`, default `SecondaryButton`; list uses vertical orientation by
209
+ default_):
210
+
211
+ - <kbd>Enter</kbd> / <kbd>Space</kbd> on the trigger opens the menu (button activation)
212
+ - <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd> on the trigger also opens the menu
213
+ - On open, focus moves to the first menu item by default
214
+ - <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd> moves the roving tabindex between items
215
+ - <kbd>Home</kbd> / <kbd>End</kbd> moves to the first or last item
216
+ - <kbd>Enter</kbd> / <kbd>Space</kbd> on an item activates it and closes the menu (default
217
+ `mode="single"`)
218
+ - <kbd>Escape</kbd> closes the menu and returns focus per `useReturnFocus`
219
+ - <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> closes the menu via `useFocusRedirect`
220
+ - <kbd>ArrowRight</kbd> / <kbd>Enter</kbd> / <kbd>Space</kbd> on **`Menu.Submenu.TargetItem`** opens
221
+ the submenu
222
+ - <kbd>ArrowLeft</kbd> on a submenu item closes it (for LTR languages)
223
+
224
+ **Screen reader expectations** (_when built-in behaviors are used as intended_):
225
+
226
+ - On the trigger: name, button role, menu popup is available, and expanded/collapsed state
227
+ (for example: "Open Menu, button, menu popup, collapsed")
228
+ - On open: menu role (labeled by the trigger), focused item name, menuitem role, and often position
229
+ in set (for example: "Open Menu, menu, First Item, menu item, 1 of 4")
230
+ - While navigating: each focused item’s name and role; group labels when entering a
231
+ **`Menu.Group`**; submenu items announce has-popup / expanded state (for example: "More Actions, menu item, has submenu, collapsed, 3 of 4.")
232
+ - Disabled items with **`aria-disabled`** remain discoverable but not selectable
233
+
234
+ ### Accessibility Requirements
235
+
236
+ Required in application code for an accessible Menu. Hoist **`useMenuModel`** when you need return
237
+ focus overrides or dynamic `items`. Rows marked _(conditional)_ apply only when the situation
238
+ matches—otherwise omit.
239
+
240
+ **If no design spec is provided:** use **`Menu.Target`** + **`Menu.Item`** (not **`Menu.Option`**),
241
+ default `mode="single"`, default open focus on the first menu item, and omit **`returnFocusRef`**,
242
+ **`initialFocusRef`**, and manual ARIA on Target/List/Item.
243
+
244
+ **Focus management — defaults and developer prompts:** Canvas Kit handles open and close focus for
245
+ the default menu button pattern. **State the default to the developer first.** Only set
246
+ **`returnFocusRef`** after the developer (or an explicit design spec) chooses a non-default return
247
+ target. **Do not generate `returnFocusRef` or `initialFocusRef` by default.**
248
+
249
+ | When | Default behavior | Ask the developer before overriding |
250
+ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
251
+ | Menu **opens** | Focus moves to the **first menu item** by default via item focus hooks—not `useInitialFocus`. Omit **`initialFocusRef`**. | _Which item should receive focus when the menu opens?_ Prefer item order / `data-id` registration; do not assume **`initialFocusRef`** works on Menu. |
252
+ | Menu **closes** | **`useReturnFocus`** moves focus to **`Menu.Target`**. Omit **`returnFocusRef`**. | _Which element should receive focus when the menu closes?_ (Only when return focus should land somewhere other than **`Menu.Target`**.) |
253
+
254
+ **Custom targets** _(conditional)_: Apply when using a custom **`as`** component on
255
+ **`Menu.Target`**. **`Menu.Target`** adds **`onClick`**, keyboard openers, and **`ref`**. Custom
256
+ targets must forward **`ref`** and props to a **keyboard-focusable** element (prefer a native
257
+ **`<button>`** or **`as={SecondaryButton}`**). Wrap the component in **`React.forwardRef`** when it
258
+ does not forward refs by default.
259
+
260
+ | Requirement | How to satisfy |
261
+ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
262
+ | Keyboard-operable, named trigger | **`Menu.Target`** with visible text, or icon-only with **`Tooltip`** (default `type="label"` sets `aria-label`) or a translated **`aria-label`** without **`Tooltip`**. See **Custom targets** above. |
263
+ | Menu list composition | **`Menu.Popper` → `Menu.Card` → `Menu.List`** with **`Menu.Item`** children (or dynamic `items` + render prop on **`Menu.List`**). |
264
+ | Disabled items _(conditional)_ | **`aria-disabled`** on **`Menu.Item`** so the item stays in the roving tabindex / screen reader sequence. |
265
+ | Stable item ids _(conditional)_ | **`data-id`** on items when using **`onSelect`**, dynamic lists, or nested menus that need stable selection ids. |
266
+ | Complex item content / icons _(conditional)_ | For static API when children are not plain text, set **`data-text`** on **`Menu.Item`** so typeahead/filtering can resolve the item text. Decorative icons alongside **`Menu.Item.Text`** usually need no extra accessible name. |
267
+ | Groups _(conditional)_ | **`Menu.Group`** with **`title`** or **`Menu.Group.Heading`** so `role="group"` is labeled. Group headers are not keyboard-selectable. |
268
+ | Nested menus _(conditional)_ | **`Menu.Submenu`** with **`Menu.Submenu.TargetItem`** plus **`Popper` / `Card` / `List` / `Item`**. Do not manually set submenu `aria-haspopup` / `aria-expanded`. |
269
+ | Context menu trigger _(conditional)_ | **`Menu.TargetContext`** instead of **`Menu.Target`**. OS/browser support for `contextmenu` / Shift+F10 varies—provide an alternate open path for critical actions when required. |
270
+ | Selectable or multi-select options _(conditional)_ | Do **not** use **`Menu.Option`**, `role="listbox"`, or **`mode="multiple"`** with **`Menu.Item`** for a menu button. Compose via [**Select**](https://workday.github.io/canvas-kit/?path=/docs/components-inputs-select--docs), **MultiSelect**, or [**Combobox**](https://workday.github.io/canvas-kit/?path=/docs/features-combobox--docs). |
271
+
272
+ **Summary for code generation:**
273
+
274
+ - **REQUIRED:** keyboard-operable named **`Menu.Target`**, **`Menu.Popper` → `Menu.Card` →
275
+ `Menu.List`**, **`Menu.Item`** (or dynamic list items)
276
+ - **CONDITIONAL:** **`aria-disabled`**, **`data-id`**, **`data-text`**, groups, submenus,
277
+ **`Menu.TargetContext`**, **`returnFocusRef`**, **`forwardRef`** on custom targets
278
+
279
+ ### Anti-Patterns
280
+
281
+ Do **not** generate code that does the following (see **Accessibility Requirements** above for what
282
+ to supply instead):
283
+
284
+ - Manually set `role="menu"`, `role="menuitem"`, `aria-labelledby`, `aria-orientation`,
285
+ `aria-haspopup`, `aria-expanded`, shared `id`, or roving `tabIndex` on **`Menu.Target`**,
286
+ **`Menu.List`**, **`Menu.Item`**, or **`Menu.Submenu.TargetItem`** — Canvas Kit hooks wire these
287
+ - Omit **`Menu.Popper`**, or render **`Menu.Card` / `Menu.List`** outside the Menu composition
288
+ - Use **`Menu.Option`**, `role="listbox"`, or **`mode="multiple"`** with **`Menu.Item`** for
289
+ selectable or multi-select UIs — use **Select**, **MultiSelect**, or **Combobox** instead (see
290
+ **Implementation note on `mode="multiple"`** in Built-in Behaviors)
291
+ - Set **`initialFocusRef`** — Menu does not compose **`useInitialFocus`**, so this prop has no
292
+ effect on open focus (see **Built-in Behaviors**)
293
+ - Set **`returnFocusRef`** by default — state the default return-to-target behavior first and ask
294
+ before overriding
295
+ - Use native **`disabled`** (or deprecated **`isDisabled`**) instead of **`aria-disabled`** when
296
+ the item should remain discoverable
297
+ - Skip **`data-text`** on static items whose accessible/filter text is not plain string children
298
+ - Use a custom **`Menu.Target`** **`as`** component that does not forward **`ref`** to a focusable
299
+ element — use **`React.forwardRef`** or a Canvas Kit button component instead
300
+ - Treat Menu like a **Modal** / **Dialog** (focus trap, `role="dialog"`, inert page) — Menu is a
301
+ menu button popup with roving tabindex inside **`role="menu"`**
157
302
 
158
303
  ## Component API
159
304
 
@@ -28,7 +28,7 @@ yarn add @workday/canvas-kit-react
28
28
 
29
29
  ## Usage
30
30
 
31
- ### Basic
31
+ ### Basic Example
32
32
 
33
33
  The basic behavior of a modal is to hide all content from all users that is "behind" the modal
34
34
  dialog.
@@ -37,10 +37,10 @@ dialog.
37
37
 
38
38
  ### Without Close Icon
39
39
 
40
- If you wish to remove the close icon button, you can simply omit the `Modal.CloseButton`
41
- subcomponent. If you have a modal dialog that requires the user to accept instead of dismiss through
42
- an escape key or clicking outside the modal, you must create a new `PopupModel` without those
43
- behaviors and hand that model to the Modal dialog component.
40
+ If you wish to remove the close icon button, you can simply omit the `Modal.CloseIcon` subcomponent.
41
+ If you have a modal dialog that requires the user to accept instead of dismiss through an escape key
42
+ or clicking outside the modal, you must create a new `PopupModel` without those behaviors and hand
43
+ that model to the Modal dialog component.
44
44
 
45
45
  <ExampleCodeBlock code={WithoutCloseIcon} />
46
46
 
@@ -133,55 +133,233 @@ hoisted to allow for form validation and allow you to control when the modal clo
133
133
 
134
134
  ## Accessibility
135
135
 
136
- `Modal` uses the default modal model (`useModalModel`), which composes **`useInitialFocus`**,
137
- **`useReturnFocus`**, **`useCloseOnOverlayClick`**, **`useCloseOnEscape`**, **`useFocusTrap`**,
138
- **`useAssistiveHideSiblings`**, and **`useDisableBodyScroll`**.
139
-
140
- **`Modal.Card`** exposes **`role="dialog"`** and **`aria-labelledby`** referencing the `id` on
141
- **`Modal.Heading`**, so the dialog has an accessible name that matches the visible heading. If you
142
- do not use **`Modal.Heading`**, add an **`aria-label`** on **`Modal.Card`** instead.
143
-
144
- **`aria-modal`:** The card sets **`aria-modal="false"`**. When **`aria-modal`** is `true`, some
145
- assistive technologies hide everything outside the dialog—including portaled UI owned by the dialog
146
- (such as a Select menu rendered as a sibling of the modal). Canvas Kit keeps
147
- **`aria-modal="false"`** for a better VoiceOver experience while **`useAssistiveHideSiblings`**
148
- applies **`aria-hidden`** to siblings of the modal stack so background content stays hidden from
149
- assistive technology while the modal is open.
150
-
151
- Unlike [**Dialog**](/components/popups/dialog/), Modal does **not** add the sibling **`aria-owns`**
152
- pattern used to remap reading order for portaled non-modal dialogs. Focus moves into the modal when
153
- it opens, and sibling hiding reduces exposure to content behind the overlay. For portals, reading
154
- order, and related tradeoffs, see
136
+ Ensure users of assistive technology can discover, name, and operate a **modal** dialog: the rest of
137
+ the page is blocked by an overlay, background content is hidden from assistive technology via
138
+ sibling **`aria-hidden`**, keyboard focus is trapped inside the modal, the dialog has an accessible
139
+ name that matches its visible heading, and keyboard users can open and dismiss it predictably.
140
+
141
+ Use **Modal** when the user must complete or acknowledge a task before continuing with the page. For
142
+ non-blocking tasks, use
143
+ [**Dialog**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-dialog--docs)
144
+ instead. Prefer **Modal** for the standard blocking dialog; use
145
+ [**Popup**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-popup--docs) with
146
+ composed hooks when you need a custom popup stack or to omit behaviors (for example Escape or
147
+ overlay dismiss). For portals, reading order, and related tradeoffs, see
155
148
  [Guides > Accessibility > Inline Popups](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-inline-popups--docs).
149
+ See also the
150
+ [Modal Dialog Pattern | APG | WAI | W3C](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/).
151
+
152
+ ### Minimum Accessible Structure
153
+
154
+ The following matches the [Basic Example](#basic-example) layout: **`Modal.CloseIcon`** before
155
+ **`Modal.Heading`** so open focus lands on the dismiss control first; primary actions use
156
+ **`Modal.CloseButton`** (which closes the modal on activate).
157
+
158
+ ```tsx
159
+ import {PrimaryButton} from '@workday/canvas-kit-react/button';
160
+ import {Modal} from '@workday/canvas-kit-react/modal';
161
+
162
+ <Modal>
163
+ <Modal.Target as={PrimaryButton}>Open</Modal.Target>
164
+ <Modal.Overlay>
165
+ <Modal.Card>
166
+ <Modal.CloseIcon aria-label="Close" />
167
+ <Modal.Heading>Title</Modal.Heading>
168
+ <Modal.Body>Content</Modal.Body>
169
+ <Modal.ButtonGroup>
170
+ <Modal.CloseButton>Cancel</Modal.CloseButton>
171
+ <Modal.CloseButton as={PrimaryButton}>Acknowledge</Modal.CloseButton>
172
+ </Modal.ButtonGroup>
173
+ </Modal.Card>
174
+ </Modal.Overlay>
175
+ </Modal>;
176
+ ```
177
+
178
+ Include a dismiss control: **`Modal.CloseButton`** with visible text (for example "Cancel" or
179
+ "Close"), and/or **`Modal.CloseIcon`** when the design uses an icon-only dismiss (requires
180
+ **`aria-label`** or **`Tooltip`**). Use **`Modal.CloseButton`** for actions that should also close
181
+ the modal (for example "Acknowledge"). Compose with **`Modal.Overlay` → `Modal.Card`** (or
182
+ **`Modal.OverflowOverlay`** when the entire overlay should scroll).
183
+
184
+ ### Built-in Behaviors
185
+
186
+ Canvas Kit applies these automatically via `useModalModel` and Modal subcomponents. **Do not
187
+ duplicate them** in consuming code.
188
+
189
+ **Popup behaviors** (_composed on the default model_):
190
+
191
+ - `useInitialFocus` — moves focus into the modal when it opens (default: first focusable element in
192
+ DOM order; optional override via `initialFocusRef` on the model)
193
+ - `useReturnFocus` — returns focus to `Modal.Target` (or configured return target) when it closes
194
+ - `useCloseOnOverlayClick` — pointer interaction on the overlay (outside the dialog) closes the
195
+ modal
196
+ - `useCloseOnEscape` — <kbd>Escape</kbd> closes the modal
197
+ - `useFocusTrap` — <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> cycle focus **inside** the modal
198
+ (keyboard focus does not leave the dialog)
199
+ - `useAssistiveHideSiblings` — applies **`aria-hidden`** to siblings of the modal stack while open
200
+ - `useDisableBodyScroll` — prevents background page scroll while the modal is open
201
+
202
+ **ARIA and DOM** (_applied by hooks/subcomponents_):
203
+
204
+ - `Modal.Card`: `role="dialog"`, `aria-labelledby` referencing the heading `id`, and
205
+ **`aria-modal="false"`**
206
+ - `Modal.Heading`: `id` wired to `Modal.Card`'s `aria-labelledby`; when there is no icon-only close
207
+ button before the heading, `useModalHeading` may temporarily set **`tabindex="0"`** on the heading
208
+ so initial focus still lands near the start of the dialog
209
+ - `Modal.CloseIcon` / `Modal.CloseButton`: `onClick` that calls `model.events.hide()`
210
+ - `Modal.Target`: `ref` and `onClick` to open and to receive return focus
211
+
212
+ **Keyboard** (_trigger is `Modal.Target`, default `SecondaryButton`_):
213
+
214
+ - <kbd>Enter</kbd> / <kbd>Space</kbd> on the trigger opens the modal (standard button behavior)
215
+ - On open and close, focus is managed by **`useInitialFocus`** and **`useReturnFocus`** (application
216
+ overrides: see **Focus management** in Accessibility Requirements)
217
+ - <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> move focus through interactive elements
218
+ **inside** the modal; focus stays trapped within the dialog
219
+ - <kbd>Escape</kbd> closes the modal and returns focus per `useReturnFocus` (unless Escape dismiss
220
+ is omitted via a custom model—see **Accept-only / no Escape dismiss**)
221
+
222
+ **Screen reader expectations** (_when built-in behaviors are used as intended_):
223
+
224
+ - On open, assistive technology should announce the first focused control (often a dismiss control),
225
+ the dialog name (`Modal.Heading`), and `dialog` role
226
+ - Sibling elements of the modal stack receive **`aria-hidden="true"`** while the modal is visible,
227
+ which hides the rest of the page from many assistive technologies
228
+ - Trapping **keyboard** focus does not stop all screen reader virtual-cursor movement outside the
229
+ dialog; treat the trap as the primary keyboard affordance, not a hard boundary—verify behavior in
230
+ your supported browser and screen reader combinations
231
+
232
+ ### Accessibility Requirements
233
+
234
+ Required in application code for an accessible Modal. Hoist **`useModalModel`** when you need to
235
+ configure focus targets, open without **`Modal.Target`**, or control when the modal closes (for
236
+ example form validation). Rows marked _(conditional)_ apply only when the situation
237
+ matches—otherwise omit.
238
+
239
+ **If no design spec is provided:** use default focus behavior; include a dismiss control and
240
+ **`Modal.Heading`**; omit **`initialFocusRef`**, **`returnFocusRef`**, and **`aria-describedby`**.
241
+ Do not remove Escape or overlay dismiss unless the design requires accept-only confirmation.
242
+
243
+ **Focus management — defaults and developer prompts:** Canvas Kit handles open and close focus
244
+ automatically. **State the default to the developer first.** Only set **`initialFocusRef`** or
245
+ **`returnFocusRef`** after the developer (or an explicit design spec) chooses a non-default target.
246
+ **Do not generate focus refs by default.**
247
+
248
+ | When | Default behavior | Ask the developer before overriding |
249
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
250
+ | Modal **opens** | **`useInitialFocus`** moves focus to the **first focusable element** in DOM order inside the modal (often **`Modal.CloseIcon`** or **`Modal.CloseButton`**). Omit **`initialFocusRef`**. | _Which element should receive focus when the modal opens?_ (Only when the default first focusable element is wrong for the design.) Attach **`initialFocusRef`** to that element on **`useModalModel`**. |
251
+ | Modal **closes** | **`useReturnFocus`** moves focus to **`Modal.Target`**. Omit **`returnFocusRef`**. | _Which element should receive focus when the modal closes?_ (Only when return focus should land somewhere other than **`Modal.Target`**.) |
252
+
253
+ If close **removes the trigger from the DOM**, **`returnFocusRef`** alone is not enough—move focus
254
+ after the UI updates (for example with **`useLayoutEffect`**). See [Return Focus](#return-focus).
255
+
256
+ **Custom targets** _(conditional)_: Apply when using a custom **`as`** component on
257
+ **`Modal.Target`**. **`Modal.Target`** adds **`onClick`** and **`ref`**. Custom targets must forward
258
+ both to a **keyboard-focusable** element (prefer a native **`<button>`** or
259
+ **`as={SecondaryButton}`** / another Canvas Kit button). Wrap the component in
260
+ **`React.forwardRef`** when it does not forward refs by default (required if the modal can open
261
+ programmatically before the user clicks the target).
262
+
263
+ | Requirement | How to satisfy |
264
+ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
265
+ | Accessible dialog name | Use **`Modal.Heading`** so `aria-labelledby` on `Modal.Card` references a visible title. Do not omit the heading: **`Modal.Card` always sets `aria-labelledby`**, and an `aria-label` fallback is unreliable when that ID does not exist. |
266
+ | Dismiss control | Provide a way to close the modal: **`Modal.CloseButton`** with visible text (no extra **`aria-label`** needed), and/or **`Modal.CloseIcon`** for icon-only dismiss (requires **`Tooltip`** or translated **`aria-label`**). |
267
+ | Keyboard-operable trigger | See **Custom targets** above. |
268
+ | Supplementary copy when overriding open focus _(conditional)_ | When **`initialFocusRef`** places open focus **below** **`Modal.Heading`**, assign a unique `id` to supplementary text and pass **`aria-describedby`** on **`Modal.Card`**. See **Open focus below the heading** below, [Custom Focus](#custom-focus), and [Popup > Initial Focus](https://workday.github.io/canvas-kit/?path=/docs/components-popups-popup--docs#initial-focus) (button-focus variant). |
269
+ | Keyboard-scrollable overflowing body _(conditional)_ | When **`Modal.Body`** content overflows, set **`tabIndex={0}`** on **`Modal.Body`** so keyboard users can focus the scroll region and use arrow keys. See [Body Content Overflow](#body-content-overflow). |
270
+ | Accept-only / no Escape dismiss _(conditional)_ | Only when the design requires the user to accept (not dismiss via Escape or overlay click): compose a custom **`usePopupModel`** with the modal behaviors you still need, **omitting** **`useCloseOnEscape`** and **`useCloseOnOverlayClick`**. See [Without Close Icon](#without-close-icon). |
271
+
272
+ **Open focus below the heading** _(conditional; see supplementary copy row above)_:
273
+
274
+ When open focus moves past the heading (for example into a form field), wire **`aria-describedby`**
275
+ so assistive technology still announces the supplementary copy. For focusing a primary action
276
+ instead of an input, see
277
+ [Popup > Initial Focus](https://workday.github.io/canvas-kit/?path=/docs/components-popups-popup--docs#initial-focus).
278
+
279
+ ```tsx
280
+ import React from 'react';
281
+
282
+ import {useUniqueId} from '@workday/canvas-kit-react/common';
283
+ import {FormField} from '@workday/canvas-kit-react/form-field';
284
+ import {Modal, useModalModel} from '@workday/canvas-kit-react/modal';
285
+ import {TextInput} from '@workday/canvas-kit-react/text-input';
286
+
287
+ const Example = () => {
288
+ const descriptionId = useUniqueId();
289
+ const inputRef = React.useRef<HTMLInputElement>(null);
290
+ const model = useModalModel({initialFocusRef: inputRef});
291
+
292
+ return (
293
+ <Modal model={model}>
294
+ <Modal.Target>Open</Modal.Target>
295
+ <Modal.Overlay>
296
+ <Modal.Card aria-describedby={descriptionId}>
297
+ <Modal.CloseIcon aria-label="Close" />
298
+ <Modal.Heading>Title</Modal.Heading>
299
+ <Modal.Body>
300
+ <p id={descriptionId}>Enter your email to continue.</p>
301
+ <FormField>
302
+ <FormField.Label>Email</FormField.Label>
303
+ <FormField.Input as={TextInput} ref={inputRef} />
304
+ </FormField>
305
+ </Modal.Body>
306
+ <Modal.CloseButton>Cancel</Modal.CloseButton>
307
+ </Modal.Card>
308
+ </Modal.Overlay>
309
+ </Modal>
310
+ );
311
+ };
312
+ ```
156
313
 
157
- [Modal Dialog Pattern | APG | WAI | W3C](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
158
-
159
- - Prefer **`Modal.Heading`** so the dialog is properly labelled; avoid leaving a dialog without an
160
- accessible name.
161
- - Ensure icon-only controls such as **`Modal.CloseIcon`** include an accessible name. Prefer the
162
- `Tooltip` component to provide a visible label, or a translated `aria-label` string is acceptable.
163
-
164
- ### Navigation
165
-
166
- - **Enter** / **Space**: Open the modal (standard button behavior on the trigger). When it opens,
167
- focus moves to the **first focusable element** inside the modal in DOM order—often the close
168
- control—or to the element referenced by **`initialFocusRef`** on the model when set.
169
- - **Tab** / **Shift + Tab**: Move through focusable elements inside the modal; focus **stays**
170
- within the modal (**focus trap**).
171
- - **Escape**: Closes the modal and returns focus to **`Modal.Target`** (or the configured return
172
- target, such as **`returnFocusRef`**).
173
-
174
- ### Screen Reader Experience
175
-
176
- - **When the modal opens:** Screen readers should announce the first focused control (often the
177
- close button), the dialog's name (**`Modal.Heading`**) and role.
178
- - **Background content:** Sibling elements of the modal stack receive **`aria-hidden="true"`** while
179
- the modal is visible, which hides the rest of the page from many assistive technologies. Mouse
180
- users are blocked by the overlay and inert interaction expectations; always verify behavior in
181
- your supported browser and screen reader combinations.
182
- - **Focus trap limits:** Trapping **keyboard** focus does not stop mouse users from interacting
183
- outside the dialog card, and some screen reader users can move a virtual cursor outside the
184
- trapped region. Treat the trap as the primary keyboard affordance, not a hard security boundary.
314
+ **Summary for code generation:**
315
+
316
+ - **REQUIRED:** accessible name, dismiss control, keyboard-operable trigger,
317
+ **`Modal.Overlay` → `Modal.Card`** composition
318
+ - **CONDITIONAL:** **`initialFocusRef`**, **`returnFocusRef`**, **`aria-describedby`**,
319
+ **`forwardRef`** on custom **`Modal.Target`**, **`tabIndex={0}`** on overflowing **`Modal.Body`**,
320
+ custom model omitting Escape/overlay dismiss, **`Modal.OverflowOverlay`**
321
+
322
+ ### Anti-Patterns
323
+
324
+ Do **not** generate code that does the following (see **Accessibility Requirements** above for what
325
+ to supply instead):
326
+
327
+ - Manually set `role="dialog"`, `aria-labelledby`, or dialog `id` on **`Modal.Card`** or
328
+ **`Modal.Heading`** Canvas Kit hooks wire these
329
+ - Override **`aria-modal`** to **`"true"`** on **`Modal.Card`** — when **`aria-modal`** is `true`,
330
+ some assistive technologies hide everything outside the dialog, including portaled UI owned by the
331
+ modal (such as a Select menu rendered as a sibling). Canvas Kit sets **`aria-modal="false"`** for
332
+ a better VoiceOver experience while **`useAssistiveHideSiblings`** applies **`aria-hidden`** to
333
+ background siblings. Do not change this unless accessibility has approved it. Unlike
334
+ [**Dialog**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-dialog--docs),
335
+ Modal also does **not** use the sibling **`aria-owns`** reading-order pattern
336
+ - Omit **`Modal.Overlay`** (or **`Modal.OverflowOverlay`**), render **`Modal.Card`** outside it, or
337
+ add a custom portal/restructure instead of **`Modal` `Modal.Overlay` `Modal.Card`**
338
+ - Use **`open`** / **`onClose`** props on **`Modal`** — Modal has no controlled visibility props;
339
+ use **`useModalModel`** and **`model.events.show()`** / **`model.events.hide()`**
340
+ - Use **Dialog** when the task must block the rest of the page, or add **`useFocusRedirect`** /
341
+ **`aria-owns`** expecting Modal-like blocking behavior Modal uses a focus trap and sibling
342
+ hiding instead
343
+ - Set **`initialFocusRef`** or **`returnFocusRef`** by default — state the default focus behavior
344
+ first and ask the developer before overriding (see **Focus management** in Accessibility
345
+ Requirements)
346
+ - Add **`aria-expanded`** or **`aria-haspopup`** on **`Modal.Target`** — those attributes apply to
347
+ **non-modal** dialogs (see
348
+ [**Dialog**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-dialog--docs) /
349
+ [**Popup**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-popup--docs)); Modal
350
+ moves focus into the dialog on open and must not use this pattern
351
+ - Use a custom **`Modal.Target`** **`as`** component that does not forward **`ref`** to a focusable
352
+ element — use **`React.forwardRef`** or a Canvas Kit button component instead
353
+ - Rely on **`returnFocusRef`** alone when close **removes the trigger from the DOM** (see
354
+ [Return Focus](#return-focus))
355
+ - Omit Escape and overlay dismiss without an explicit accept-only design requirement, or remove
356
+ **`Modal.CloseIcon`** without providing another dismiss path (see **Accept-only / no Escape
357
+ dismiss**)
358
+ - Leave overflowing **`Modal.Body`** content without a keyboard path to scroll (see
359
+ **Keyboard-scrollable overflowing body**)
360
+ - Nest multiple **`Modal`** instances without deliberate initial focus and return-focus planning
361
+ - Assume the focus trap alone fully hides outside content from every assistive technology — verify
362
+ supported browser and screen reader combinations
185
363
 
186
364
  ## Component API
187
365