@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.
- package/dist/es6/lib/stackblitzFiles/packageJSONFile.js +5 -5
- package/dist/es6/lib/stackblitzFiles/packageJSONFile.ts +5 -5
- package/dist/mdx/react/dialog/Dialog.mdx +235 -48
- package/dist/mdx/react/form-field/FormField.mdx +174 -45
- package/dist/mdx/react/menu/Menu.mdx +188 -43
- package/dist/mdx/react/modal/Modal.mdx +230 -52
- package/dist/mdx/react/popup/Popup.mdx +297 -11
- package/dist/mdx/react/text-area/TextArea.mdx +134 -30
- package/dist/mdx/react/text-input/TextInput.mdx +193 -34
- package/package.json +6 -6
|
@@ -31,7 +31,7 @@ yarn add @workday/canvas-kit-react
|
|
|
31
31
|
|
|
32
32
|
<ExampleCodeBlock code={Basic} />
|
|
33
33
|
|
|
34
|
-
`Menu` will automatically focus
|
|
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**:
|
|
103
|
-
>
|
|
104
|
-
>
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
**`
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|