@workday/canvas-kit-docs 16.0.10 → 16.0.12

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.
@@ -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
 
@@ -53,9 +53,10 @@ build popup UIs that are not already covered by Canvas Kit.
53
53
  The Popup has no pre-defined behaviors built in, therefore the `usePopupModel` must always be used
54
54
  to create a new `model`. This `model` is then used by all behavior hooks to apply additional popup
55
55
  behaviors to the compound component group. The following example creates a typical popup around a
56
- target element and adds `useCloseOnOutsideClick`, `useCloseOnEscape`, `useInitialFocus`, and
57
- `useReturnFocus` behaviors. You can read through the [hooks](#hooks) section to learn about all the
58
- popup behaviors. For accessibility, these behaviors should be included most of the time.
56
+ target element and adds `useCloseOnOutsideClick`, `useCloseOnEscape`, `useInitialFocus`,
57
+ `useReturnFocus`, and `useFocusRedirect` behaviors. You can read through the [hooks](#hooks) section
58
+ to learn about all the popup behaviors. For accessibility, these behaviors should be included most
59
+ of the time.
59
60
 
60
61
  <ExampleCodeBlock code={Basic} />
61
62
 
@@ -193,15 +194,300 @@ The Popup component automatically handles right-to-left rendering.
193
194
 
194
195
  ## Accessibility
195
196
 
196
- Popup content is usually portaled to the bottom of the `document.body`, which can affect **reading
197
- order for screen readers** and **keyboard focus order**. For more information about Popup
198
- accessibility, check out our documentation at
199
- [Guides > Accessibility > Inline Popups](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-inline-popups--docs).
197
+ Ensure users of assistive technology can discover, name, and operate a popup that is typically
198
+ portaled to the end of `document.body`: the popup has an accessible name that matches its visible
199
+ heading, keyboard users can open and dismiss it predictably, and focus and reading order remain
200
+ usable despite portal placement (see
201
+ [Guides > Accessibility > Inline Popups](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-inline-popups--docs)).
202
+ Prefer a semantic component before composing **Popup** directly:
203
+ [**Dialog**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-dialog--docs) for a
204
+ standard non-modal dialog (behaviors and `aria-owns` built in), or
205
+ [**Modal**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-modal--docs) for
206
+ blocking tasks with focus trapping and assistive sibling hiding (see also the W3C
207
+ [Dialog (Modal) Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)). Use **Popup** with
208
+ composed hooks when you need a custom popup stack or behavior set that those components do not
209
+ provide.
210
+
211
+ ### Minimum Accessible Structure
212
+
213
+ The following matches the [Basic Example](#basic-example): hoist **`usePopupModel`**, compose the
214
+ non-modal behavior hooks on that model, place **`Popup.CloseIcon`** before **`Popup.Heading`** so
215
+ default open focus lands on the dismiss control first, and use **`Popup.CloseButton`** for actions
216
+ that should also close the popup.
217
+
218
+ ```tsx
219
+ import {DeleteButton} from '@workday/canvas-kit-react/button';
220
+ import {
221
+ Popup,
222
+ useCloseOnEscape,
223
+ useCloseOnOutsideClick,
224
+ useFocusRedirect,
225
+ useInitialFocus,
226
+ usePopupModel,
227
+ useReturnFocus,
228
+ } from '@workday/canvas-kit-react/popup';
229
+
230
+ const Example = () => {
231
+ const model = usePopupModel();
232
+
233
+ useCloseOnOutsideClick(model);
234
+ useCloseOnEscape(model);
235
+ useInitialFocus(model);
236
+ useReturnFocus(model);
237
+ useFocusRedirect(model);
238
+
239
+ return (
240
+ <Popup model={model}>
241
+ <Popup.Target as={DeleteButton}>Delete Item</Popup.Target>
242
+ <Popup.Popper>
243
+ <Popup.Card>
244
+ <Popup.CloseIcon aria-label="Close" />
245
+ <Popup.Heading>Delete Item</Popup.Heading>
246
+ <Popup.Body>
247
+ <p>Are you sure you'd like to delete the item titled 'My Item'?</p>
248
+ </Popup.Body>
249
+ <Popup.ButtonGroup>
250
+ <Popup.CloseButton>Cancel</Popup.CloseButton>
251
+ <Popup.CloseButton as={DeleteButton}>Delete</Popup.CloseButton>
252
+ </Popup.ButtonGroup>
253
+ </Popup.Card>
254
+ </Popup.Popper>
255
+ </Popup>
256
+ );
257
+ };
258
+ ```
259
+
260
+ Include a dismiss control: **`Popup.CloseButton`** with visible text (for example "Cancel" or
261
+ "Close"), and/or **`Popup.CloseIcon`** when the design uses an icon-only dismiss (requires
262
+ **`aria-label`** or **`Tooltip`**). Pass the same **`model`** instance to **`Popup`** and every
263
+ behavior hook so focus and dismiss wiring share one stack.
264
+
265
+ ### Built-in Behaviors
266
+
267
+ Canvas Kit applies ARIA and DOM wiring automatically via Popup subcomponents when you compose them.
268
+ Behavioral hooks are **not** applied by `usePopupModel` alone—you must call them (as in the Basic
269
+ Example). Once applied, **do not duplicate them** in consuming code.
270
+
271
+ **Popup behaviors** (_compose on the model; recommended for non-modal dialogs_):
272
+
273
+ - `useInitialFocus` — moves focus into the popup when it opens (default: first focusable element in
274
+ DOM order; optional override via `initialFocusRef` on the model)
275
+ - `useReturnFocus` — returns focus to `Popup.Target` (or configured return target) when it closes
276
+ - `useCloseOnEscape` — <kbd>Escape</kbd> closes the popup
277
+ - `useCloseOnOutsideClick` — pointer interaction outside closes the popup
278
+ - `useFocusRedirect` — <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> at the first or last
279
+ focusable element inside the popup closes it and moves focus to the next or previous focusable
280
+ element on the page (non-modal; **not** a focus trap; does **not** change screen reader reading
281
+ order; does **not** provide `aria-owns`)
282
+
283
+ **ARIA and DOM** (_applied by hooks/subcomponents_):
284
+
285
+ - `Popup.Card`: `role="dialog"`, `aria-labelledby` referencing the heading `id` (non-modal by
286
+ default; page content is not hidden with `aria-hidden` unless you compose
287
+ `useAssistiveHideSiblings`)
288
+ - `Popup.Heading`: `id` wired to `Popup.Card`'s `aria-labelledby`
289
+ - `Popup.Popper`: positions and registers the popup with the stack; unlike
290
+ [**Dialog.Popper**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-dialog--docs),
291
+ it does **not** set `aria-owns`
292
+ - `Popup.CloseIcon` / `Popup.CloseButton`: `onClick` that calls `model.events.hide()`
293
+ - `Popup.Target`: `ref` and `onClick` to open and to receive return focus
294
+
295
+ **Implementation note on `aria-owns`:** `useFocusRedirect` does not provide `aria-owns`. When you
296
+ need remapped reading order for portaled content, add it yourself (see **Reading order** in
297
+ Accessibility Requirements) or prefer **Dialog**, which wires `aria-owns` automatically. Support
298
+ varies by browser and screen reader.
299
+
300
+ **Keyboard** (_trigger is `Popup.Target`, default `SecondaryButton`_):
301
+
302
+ - <kbd>Enter</kbd> / <kbd>Space</kbd> on the trigger opens the popup (standard button behavior)
303
+ - On open and close, focus is managed by **`useInitialFocus`** and **`useReturnFocus`** when those
304
+ hooks are composed (application overrides: see **Focus management** in Accessibility Requirements)
305
+ - <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> move focus forward and backward through
306
+ interactive elements inside the popup (standard sequential focus behavior)
307
+ - With **`useFocusRedirect`**, tabbing past the last or before the first focusable element closes
308
+ the popup
309
+ - <kbd>Escape</kbd> closes the popup when **`useCloseOnEscape`** is composed and returns focus per
310
+ `useReturnFocus`
311
+
312
+ **Screen reader expectations** (_when built-in behaviors and recommended hooks are used as
313
+ intended_):
314
+
315
+ - On open, assistive technology should announce the first focused control (often a dismiss control),
316
+ the popup name (`Popup.Heading`), and `dialog` role
317
+ - Background page content remains available to assistive technology unless you compose
318
+ **`useAssistiveHideSiblings`** (prefer
319
+ [**Modal**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-modal--docs) for
320
+ that pattern)
321
+ - Reading order may still follow document order at the end of `body` unless `aria-owns` remapping is
322
+ added and honored; support varies by browser and screen reader
323
+
324
+ ### Accessibility Requirements
325
+
326
+ Required in application code for an accessible Popup. Always hoist **`usePopupModel`** and pass the
327
+ same instance to **`Popup`** and behavior hooks. Rows marked _(conditional)_ apply only when the
328
+ situation matches—otherwise omit.
329
+
330
+ **If no design spec is provided:** compose the Basic Example hooks (`useCloseOnOutsideClick`,
331
+ `useCloseOnEscape`, `useInitialFocus`, `useReturnFocus`, `useFocusRedirect`); use default focus
332
+ behavior; omit **`initialFocusRef`**, **`returnFocusRef`**, **`aria-describedby`**,
333
+ **`aria-expanded`**, **`aria-haspopup`**, **`useFocusTrap`**, and **`useAssistiveHideSiblings`**.
334
+ Prefer **Dialog** or **Modal** when those components already match the product need.
335
+
336
+ **Focus management — defaults and developer prompts:** When **`useInitialFocus`** /
337
+ **`useReturnFocus`** are composed, Canvas Kit handles open and close focus automatically. **State
338
+ the default to the developer first.** Only set **`initialFocusRef`** or **`returnFocusRef`** after
339
+ the developer (or an explicit design spec) chooses a non-default target. **Do not generate focus
340
+ refs by default.**
341
+
342
+ | When | Default behavior | Ask the developer before overriding |
343
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
344
+ | Popup **opens** | **`useInitialFocus`** moves focus to the **first focusable element** in DOM order inside the popup (often **`Popup.CloseIcon`** or **`Popup.CloseButton`**). Omit **`initialFocusRef`**. | _Which element should receive focus when the popup opens?_ (Only when the default first focusable element is wrong for the design.) Attach **`initialFocusRef`** to that element on **`usePopupModel`**. |
345
+ | Popup **closes** | **`useReturnFocus`** moves focus to **`Popup.Target`**. Omit **`returnFocusRef`**. | _Which element should receive focus when the popup closes?_ (Only when return focus should land somewhere other than **`Popup.Target`**.) |
346
+
347
+ If close **removes the trigger from the DOM**, **`returnFocusRef`** alone is not enough—move focus
348
+ after the UI updates (for example with **`useLayoutEffect`**). See
349
+ [Modal > Return Focus](https://workday.github.io/canvas-kit/?path=/docs/components-popups-modal--docs#return-focus).
350
+
351
+ **Custom targets** _(conditional)_: Apply when using a custom **`as`** component on
352
+ **`Popup.Target`**. **`Popup.Target`** adds **`onClick`** and **`ref`**. Custom targets must forward
353
+ both to a **keyboard-focusable** element (prefer a native **`<button>`** or
354
+ **`as={SecondaryButton}`** / another Canvas Kit button). Wrap the component in
355
+ **`React.forwardRef`** when it does not forward refs by default (required if the popup can open
356
+ programmatically before the user clicks the target).
357
+
358
+ **Reading order (`aria-owns`)** _(conditional)_:
359
+
360
+ Popup content is portaled; **`useFocusRedirect`** alone does not fix screen reader reading order.
361
+ When a design needs remapped sequential reading order and you are not using **Dialog**, set an `id`
362
+ on the stack element and point a sibling element's **`aria-owns`** at that `id` (see the
363
+ [Focus Redirect](#focus-redirect) example). Prefer **Dialog** when that pattern is the product
364
+ default—Dialog wires **`aria-owns`** for you.
365
+
366
+ **Modal-like focus trapping** _(conditional)_:
367
+
368
+ Prefer [**Modal**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-modal--docs)
369
+ for blocking tasks. If you must compose trapping on **Popup**, use **`useFocusTrap`** with
370
+ **`useAssistiveHideSiblings`** (and typically **omit** **`useFocusRedirect`**). Focus trapping does
371
+ not stop mouse or virtual-cursor escape by itself.
372
+
373
+ **Open focus below the heading** _(conditional; see supplementary copy row below)_:
374
+
375
+ Button-focus variant (matches [Initial Focus](#initial-focus)): when open focus lands on a primary
376
+ action below the heading, wire **`aria-describedby`** to the supplementary copy. For the form-field
377
+ variant (focus an input), see
378
+ [Dialog](https://workday.github.io/canvas-kit/?path=/docs/components-popups-dialog--docs#accessibility-requirements)
379
+ or
380
+ [Modal](https://workday.github.io/canvas-kit/?path=/docs/components-popups-modal--docs#accessibility-requirements)
381
+ **Open focus below the heading**.
382
+
383
+ ```tsx
384
+ import React from 'react';
385
+
386
+ import {PrimaryButton} from '@workday/canvas-kit-react/button';
387
+ import {useUniqueId} from '@workday/canvas-kit-react/common';
388
+ import {
389
+ Popup,
390
+ useCloseOnEscape,
391
+ useCloseOnOutsideClick,
392
+ useFocusRedirect,
393
+ useInitialFocus,
394
+ usePopupModel,
395
+ useReturnFocus,
396
+ } from '@workday/canvas-kit-react/popup';
397
+
398
+ const Example = () => {
399
+ const messageId = useUniqueId();
400
+ const initialFocusRef = React.useRef(null);
401
+ const model = usePopupModel({initialFocusRef});
402
+
403
+ useCloseOnOutsideClick(model);
404
+ useCloseOnEscape(model);
405
+ useInitialFocus(model);
406
+ useReturnFocus(model);
407
+ useFocusRedirect(model);
408
+
409
+ return (
410
+ <Popup model={model}>
411
+ <Popup.Target>Open</Popup.Target>
412
+ <Popup.Popper>
413
+ <Popup.Card aria-describedby={messageId}>
414
+ <Popup.Heading>Confirmation</Popup.Heading>
415
+ <Popup.Body>
416
+ <p id={messageId}>Your message has been sent!</p>
417
+ </Popup.Body>
418
+ <Popup.ButtonGroup>
419
+ <Popup.CloseButton as={PrimaryButton} ref={initialFocusRef}>
420
+ OK
421
+ </Popup.CloseButton>
422
+ </Popup.ButtonGroup>
423
+ </Popup.Card>
424
+ </Popup.Popper>
425
+ </Popup>
426
+ );
427
+ };
428
+ ```
200
429
 
201
- - For non-modal dialogs with `aria-owns` built-in to improve reading order for screen readers (that
202
- support it), check out the [**Dialog**](/components/popups/dialog/) component.
203
- - For modal dialogs with built-in overlays and focus traps, check out the
204
- [**Modal**](/components/popups/modal/) component.
430
+ When open focus lands on **`Popup.Heading`** itself, add **`tabIndex={-1}`** so the heading can
431
+ receive programmatic focus.
432
+
433
+ | Requirement | How to satisfy |
434
+ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
435
+ | Shared model + behavior hooks | Hoist **`usePopupModel`**, pass **`model={model}`** to **`Popup`**, and compose at least the Basic Example hooks for non-modal dialogs (`useCloseOnOutsideClick`, `useCloseOnEscape`, `useInitialFocus`, `useReturnFocus`, `useFocusRedirect`) unless a design deliberately omits one. |
436
+ | Accessible popup name | Use **`Popup.Heading`** so `aria-labelledby` on `Popup.Card` references a visible title. Do not omit the heading: **`Popup.Card` always sets `aria-labelledby`**, and an `aria-label` fallback is unreliable when that ID does not exist. |
437
+ | Dismiss control | Provide a way to close the popup: **`Popup.CloseButton`** with visible text (no extra **`aria-label`** needed), and/or **`Popup.CloseIcon`** for icon-only dismiss (requires **`Tooltip`** or translated **`aria-label`**). |
438
+ | Keyboard-operable trigger | See **Custom targets** above. |
439
+ | Supplementary copy when overriding open focus _(conditional)_ | When **`initialFocusRef`** places open focus **below** **`Popup.Heading`**, assign a unique `id` to supplementary text and pass **`aria-describedby`** on **`Popup.Card`**. See **Open focus below the heading** above and [Initial Focus](#initial-focus). |
440
+ | Reading order remapping _(conditional)_ | See **Reading order (`aria-owns`)** above, or use **Dialog**. |
441
+ | Focus trapping / hide siblings _(conditional)_ | Prefer **Modal**. If composing on **Popup**, see **Modal-like focus trapping** above. |
442
+ | Open/closed state on the trigger _(conditional)_ | See **Wiring aria-expanded** below. **Default:** omit **`aria-expanded`** and **`aria-haspopup`**. |
443
+
444
+ **Summary for code generation:**
445
+
446
+ - **REQUIRED:** shared `usePopupModel`, non-modal behavior hooks (unless design omits), accessible
447
+ name, dismiss control, keyboard-operable trigger
448
+ - **CONDITIONAL:** **`initialFocusRef`**, **`returnFocusRef`**, **`aria-describedby`**,
449
+ **`tabIndex={-1}`** on heading focus, **`aria-owns`**, **`useFocusTrap`** /
450
+ **`useAssistiveHideSiblings`**, **`aria-expanded`** / **`aria-haspopup`**, **`forwardRef`** on
451
+ custom **`Popup.Target`**
452
+
453
+ **Wiring aria-expanded** _(conditional)_:
454
+
455
+ The **`aria-expanded`** pattern is **uncommon** for dialog-like Popups—omit **`aria-expanded`** and
456
+ **`aria-haspopup`** unless a review deliberately keeps open focus on the trigger (for example
457
+ **`initialFocusRef`** on the trigger per design spec). When required, on **`Popup.Target`** set
458
+ **`aria-expanded={model.state.visibility !== 'hidden'}`** and **`aria-haspopup="dialog"`**. See
459
+ **Focus management** and the open/closed-state row above.
460
+
461
+ ### Anti-Patterns
462
+
463
+ Do **not** generate code that does the following (see **Accessibility Requirements** above for what
464
+ to supply instead):
465
+
466
+ - Manually set `role="dialog"`, `aria-labelledby`, or the heading `id` on **`Popup.Card`** or
467
+ **`Popup.Heading`** — Canvas Kit hooks wire these
468
+ - Call behavior hooks on a **different** model instance than the one passed to **`Popup`**, or omit
469
+ **`model={model}`** after composing hooks outside the container
470
+ - Assume **`usePopupModel`** alone provides focus, escape, outside-click, or redirect behaviors —
471
+ compose the hooks (or use **Dialog** / **Modal**)
472
+ - Omit **`Popup.Popper`**, render **`Popup.Card`** outside it, or add a custom portal/restructure
473
+ instead of **`Popup` → `Popup.Popper` → `Popup.Card`** without using **`usePopupStack`**
474
+ - Use **`open`** / **`onClose`** props on **`Popup`** — Popup has no controlled visibility props;
475
+ use **`usePopupModel`** and **`model.events.show()`** / **`model.events.hide()`**
476
+ - Reach for **Popup** + **`useFocusTrap`** / **`useAssistiveHideSiblings`** when **Modal** already
477
+ matches the product need, or for non-modal UX when **Dialog** already matches
478
+ - Set **`initialFocusRef`** or **`returnFocusRef`** by default — state the default focus behavior
479
+ first and ask the developer before overriding (see **Focus management** in Accessibility
480
+ Requirements)
481
+ - Add **`aria-expanded`** / **`aria-haspopup`** on the default dialog-like Popup path, or bind
482
+ **`aria-expanded`** to a static value (see **Wiring aria-expanded** in Accessibility Requirements)
483
+ - Use a custom **`Popup.Target`** **`as`** component that does not forward **`ref`** to a focusable
484
+ element — use **`React.forwardRef`** or a Canvas Kit button component instead
485
+ - Rely on **`returnFocusRef`** alone when close **removes the trigger from the DOM** (see
486
+ [Modal > Return Focus](https://workday.github.io/canvas-kit/?path=/docs/components-popups-modal--docs#return-focus))
487
+ - Nest multiple **Popup** instances without deliberate initial focus and return-focus planning
488
+ - Assume **`useFocusRedirect`** fixes screen reader reading order, or that **`aria-owns`** remapping
489
+ works in all browser and screen reader combinations — test your supported combinations
490
+ - Expect **`Popup.Popper`** to set **`aria-owns`** like **Dialog.Popper** — it does not
205
491
 
206
492
  ## Component API
207
493