@workday/canvas-kit-docs 14.3.9 → 14.3.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.
Files changed (28) hide show
  1. package/dist/es6/lib/ExampleCodeBlock.d.ts.map +1 -1
  2. package/dist/es6/lib/ExampleCodeBlock.js +12 -3
  3. package/dist/es6/lib/docs.js +15500 -5241
  4. package/dist/es6/lib/stackblitzFiles/packageJSONFile.js +5 -5
  5. package/dist/es6/lib/stackblitzFiles/packageJSONFile.ts +5 -5
  6. package/dist/es6/mdx/accessibility/examples/Popups/InlinePopupNoPortal.d.ts +2 -0
  7. package/dist/es6/mdx/accessibility/examples/Popups/InlinePopupNoPortal.d.ts.map +1 -0
  8. package/dist/es6/mdx/accessibility/examples/Popups/InlinePopupNoPortal.js +62 -0
  9. package/dist/es6/mdx/accessibility/examples/Popups/InlinePortalPopup.d.ts +7 -0
  10. package/dist/es6/mdx/accessibility/examples/Popups/InlinePortalPopup.d.ts.map +1 -0
  11. package/dist/es6/mdx/accessibility/examples/Popups/InlinePortalPopup.js +63 -0
  12. package/dist/es6/mdx/accessibility/examples/Popups/PopupAriaOwns.d.ts +7 -0
  13. package/dist/es6/mdx/accessibility/examples/Popups/PopupAriaOwns.d.ts.map +1 -0
  14. package/dist/es6/mdx/accessibility/examples/Popups/PopupAriaOwns.js +46 -0
  15. package/dist/mdx/accessibility/InlinePortals.mdx +20 -0
  16. package/dist/mdx/accessibility/Popups.mdx +71 -0
  17. package/dist/mdx/react/dialog/Dialog.mdx +51 -20
  18. package/dist/mdx/react/modal/Modal.mdx +87 -9
  19. package/dist/mdx/react/modal/examples/FormModal.tsx +26 -1
  20. package/dist/mdx/react/modal/examples/ReturnFocus.tsx +137 -39
  21. package/dist/mdx/react/popup/Popup.mdx +55 -28
  22. package/dist/mdx/react/popup/examples/Basic.tsx +20 -3
  23. package/dist/mdx/react/popup/examples/FocusRedirect.tsx +24 -9
  24. package/dist/mdx/react/popup/examples/InitialFocus.tsx +113 -9
  25. package/dist/mdx/react/popup/examples/InlinePopup.tsx +125 -0
  26. package/dist/mdx/react/popup/examples/MultiplePopups.tsx +34 -22
  27. package/lib/ExampleCodeBlock.tsx +12 -3
  28. package/package.json +6 -6
@@ -18,11 +18,11 @@ export const packageJSONFile = `{
18
18
  "@emotion/react": "11.11.4",
19
19
  "@types/react": "18.2.60",
20
20
  "@types/react-dom": "18.2.19",
21
- "@workday/canvas-kit-labs-react": "14.3.9",
22
- "@workday/canvas-kit-preview-react": "14.3.9",
23
- "@workday/canvas-kit-react": "14.3.9",
24
- "@workday/canvas-kit-react-fonts": "^14.3.9",
25
- "@workday/canvas-kit-styling": "14.3.9",
21
+ "@workday/canvas-kit-labs-react": "14.3.11",
22
+ "@workday/canvas-kit-preview-react": "14.3.11",
23
+ "@workday/canvas-kit-react": "14.3.11",
24
+ "@workday/canvas-kit-react-fonts": "^14.3.11",
25
+ "@workday/canvas-kit-styling": "14.3.11",
26
26
  "@workday/canvas-system-icons-web": "3.0.36",
27
27
  "@workday/canvas-tokens-web": "3.1.2"
28
28
  },
@@ -18,11 +18,11 @@ export const packageJSONFile = `{
18
18
  "@emotion/react": "11.11.4",
19
19
  "@types/react": "18.2.60",
20
20
  "@types/react-dom": "18.2.19",
21
- "@workday/canvas-kit-labs-react": "14.3.9",
22
- "@workday/canvas-kit-preview-react": "14.3.9",
23
- "@workday/canvas-kit-react": "14.3.9",
24
- "@workday/canvas-kit-react-fonts": "^14.3.9",
25
- "@workday/canvas-kit-styling": "14.3.9",
21
+ "@workday/canvas-kit-labs-react": "14.3.11",
22
+ "@workday/canvas-kit-preview-react": "14.3.11",
23
+ "@workday/canvas-kit-react": "14.3.11",
24
+ "@workday/canvas-kit-react-fonts": "^14.3.11",
25
+ "@workday/canvas-kit-styling": "14.3.11",
26
26
  "@workday/canvas-system-icons-web": "3.0.36",
27
27
  "@workday/canvas-tokens-web": "3.1.2"
28
28
  },
@@ -0,0 +1,2 @@
1
+ export declare const InlinePopupNoPortal: () => import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=InlinePopupNoPortal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InlinePopupNoPortal.d.ts","sourceRoot":"","sources":["../../../../../../mdx/accessibility/examples/Popups/InlinePopupNoPortal.tsx"],"names":[],"mappings":"AA0GA,eAAO,MAAM,mBAAmB,+CAsB/B,CAAC"}
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRef } from 'react';
3
+ import { DeleteButton } from '@workday/canvas-kit-react/button';
4
+ import { Popup, usePopupModel, useCloseOnEscape, useCloseOnOutsideClick, useInitialFocus, useReturnFocus, } from '@workday/canvas-kit-react/popup';
5
+ import { Box, Flex } from '@workday/canvas-kit-react/layout';
6
+ import { Heading } from '@workday/canvas-kit-react/text';
7
+ import { createStyles, px2rem } from '@workday/canvas-kit-styling';
8
+ import { system } from '@workday/canvas-tokens-web';
9
+ import { useUniqueId } from '@workday/canvas-kit-react/common';
10
+ const headingStyles = createStyles({
11
+ marginTop: system.space.zero,
12
+ });
13
+ const cardStyles = createStyles({
14
+ width: px2rem(320),
15
+ });
16
+ const flexStyles = createStyles({
17
+ gap: system.space.x4,
18
+ padding: system.space.x2,
19
+ });
20
+ const bodyStyles = createStyles({
21
+ marginY: system.space.zero,
22
+ });
23
+ const clipContainerStyles = createStyles({
24
+ padding: system.space.x4,
25
+ border: `${px2rem(2)} dashed ${system.color.border.info.default}`,
26
+ height: px2rem(200),
27
+ position: 'relative',
28
+ overflow: 'clip',
29
+ });
30
+ const visibleContainerStyles = createStyles({
31
+ padding: system.space.x4,
32
+ border: `${px2rem(2)} dashed ${system.color.border.info.default}`,
33
+ height: px2rem(200),
34
+ position: 'relative',
35
+ overflow: 'visible',
36
+ });
37
+ const scrollContainerStyles = createStyles({
38
+ padding: system.space.x4,
39
+ border: `${px2rem(2)} dashed ${system.color.border.info.default}`,
40
+ height: px2rem(200),
41
+ position: 'relative',
42
+ overflow: 'scroll',
43
+ });
44
+ const comparisonLayoutStyles = createStyles({
45
+ display: 'grid',
46
+ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
47
+ gap: system.space.x6,
48
+ marginBottom: system.space.x4,
49
+ });
50
+ function SingleInlinePopup({ overflowLabel, containerStyles, }) {
51
+ const messageId = useUniqueId();
52
+ const initialFocusRef = useRef(null);
53
+ const model = usePopupModel({ initialFocusRef });
54
+ useCloseOnOutsideClick(model);
55
+ useCloseOnEscape(model);
56
+ useInitialFocus(model);
57
+ useReturnFocus(model);
58
+ return (_jsxs(Box, { cs: containerStyles, children: [_jsx(Heading, { size: "small", as: "h4", cs: headingStyles, children: overflowLabel }), _jsxs(Popup, { model: model, children: [_jsx(Popup.Target, { as: DeleteButton, children: "Delete Item" }), _jsx(Popup.Popper, { placement: "top", portal: false, children: _jsxs(Popup.Card, { cs: cardStyles, "aria-describedby": messageId, children: [_jsx(Popup.Heading, { children: "Delete Item" }), _jsx(Popup.Body, { children: _jsx(Box, { as: "p", id: messageId, cs: bodyStyles, children: "Are you sure you'd like to delete the item titled 'My Item'?" }) }), _jsxs(Flex, { cs: flexStyles, children: [_jsx(Popup.CloseButton, { ref: initialFocusRef, children: "Cancel" }), _jsx(Popup.CloseButton, { as: DeleteButton, children: "Delete" })] })] }) })] })] }));
59
+ }
60
+ export const InlinePopupNoPortal = () => {
61
+ return (_jsxs(_Fragment, { children: [_jsxs(Flex, { cs: comparisonLayoutStyles, children: [_jsx(SingleInlinePopup, { overflowLabel: "overflow: visible", containerStyles: visibleContainerStyles }), _jsx(SingleInlinePopup, { overflowLabel: "overflow: clip", containerStyles: clipContainerStyles }), _jsx(SingleInlinePopup, { overflowLabel: "overflow: scroll", containerStyles: scrollContainerStyles })] }), _jsxs("p", { children: ["With ", _jsx("code", { children: "overflow: visible" }), ", the popup can extend past the dashed border. With", ' ', _jsx("code", { children: "overflow: scroll" }), " (or ", _jsx("code", { children: "hidden" }), " / ", _jsx("code", { children: "clip" }), "), the popup is constrained and any overflow is clipped. The container uses ", _jsx("code", { children: "position: relative" }), ' ', "so it establishes a containing block for the positioned popup."] })] }));
62
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Portals popup content into a sentinel div after the trigger (via PopupStack.pushStackContext)
3
+ * so DOM reading order matches page context. Uses a two-phase open so pushStackContext runs
4
+ * before Popper mounts and registers with the stack.
5
+ */
6
+ export declare const InlinePortalPopup: () => import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=InlinePortalPopup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InlinePortalPopup.d.ts","sourceRoot":"","sources":["../../../../../../mdx/accessibility/examples/Popups/InlinePortalPopup.tsx"],"names":[],"mappings":"AAkCA;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,+CAkE7B,CAAC"}
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { DeleteButton } from '@workday/canvas-kit-react/button';
4
+ import { Popup, usePopupModel, useCloseOnEscape, useCloseOnOutsideClick, useReturnFocus, } from '@workday/canvas-kit-react/popup';
5
+ import { PopupStack } from '@workday/canvas-kit-popup-stack';
6
+ import { Box, Flex } from '@workday/canvas-kit-react/layout';
7
+ import { createStyles, px2rem } from '@workday/canvas-kit-styling';
8
+ import { system } from '@workday/canvas-tokens-web';
9
+ import { changeFocus, useUniqueId } from '@workday/canvas-kit-react/common';
10
+ const cardStyles = createStyles({
11
+ width: px2rem(320),
12
+ });
13
+ const flexStyles = createStyles({
14
+ gap: system.space.x4,
15
+ padding: system.space.x2,
16
+ });
17
+ const layoutStyles = createStyles({
18
+ gap: system.space.x4,
19
+ alignItems: 'flex-start',
20
+ flexDirection: 'column',
21
+ });
22
+ const bodyStyles = createStyles({
23
+ marginY: system.space.zero,
24
+ });
25
+ /**
26
+ * Portals popup content into a sentinel div after the trigger (via PopupStack.pushStackContext)
27
+ * so DOM reading order matches page context. Uses a two-phase open so pushStackContext runs
28
+ * before Popper mounts and registers with the stack.
29
+ */
30
+ export const InlinePortalPopup = () => {
31
+ const messageId = useUniqueId();
32
+ const sentinelRef = React.useRef(null);
33
+ const initialFocusRef = React.useRef(null);
34
+ const model = usePopupModel({ initialFocusRef });
35
+ const visible = model.state.visibility !== 'hidden';
36
+ const [portalReady, setPortalReady] = React.useState(false);
37
+ useCloseOnOutsideClick(model);
38
+ useCloseOnEscape(model);
39
+ useReturnFocus(model);
40
+ // Defer initial focus until Popper content is mounted. useInitialFocus runs when visible while
41
+ // stackRef can still point at an empty sentinel (second open) and throws.
42
+ React.useEffect(() => {
43
+ if (!visible || !portalReady)
44
+ return;
45
+ const el = initialFocusRef.current;
46
+ if (!el)
47
+ return;
48
+ requestAnimationFrame(() => {
49
+ changeFocus(el);
50
+ });
51
+ }, [visible, portalReady]);
52
+ React.useLayoutEffect(() => {
53
+ if (visible && sentinelRef.current && !portalReady) {
54
+ PopupStack.pushStackContext(sentinelRef.current);
55
+ setPortalReady(true);
56
+ }
57
+ if (!visible && portalReady) {
58
+ PopupStack.popStackContext(sentinelRef.current);
59
+ setPortalReady(false);
60
+ }
61
+ }, [visible, portalReady]);
62
+ return (_jsxs(Flex, { cs: layoutStyles, children: [_jsx(Flex, { children: _jsxs(Popup, { model: model, children: [_jsx(Popup.Target, { as: DeleteButton, children: "Delete Item" }), _jsx("div", { ref: sentinelRef }), visible && portalReady ? (_jsx(Popup.Popper, { placement: "top", children: _jsxs(Popup.Card, { cs: cardStyles, "aria-describedby": messageId, children: [_jsx(Popup.Heading, { children: "Delete Item" }), _jsx(Popup.Body, { children: _jsx(Box, { as: "p", id: messageId, cs: bodyStyles, children: "Are you sure you'd like to delete the item titled 'My Item'?" }) }), _jsxs(Flex, { cs: flexStyles, children: [_jsx(Popup.CloseButton, { ref: initialFocusRef, children: "Cancel" }), _jsx(Popup.CloseButton, { as: DeleteButton, children: "Delete" })] })] }) })) : null] }) }), _jsx("p", { children: "This content should come after the popup in the reading order. When someone uses a screen reader or moves through the page with tabbing, they will read or reach this content only after the popup content is shown. This helps keep the page easy to follow and makes sure that the popup is announced before any information that comes next." })] }));
63
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Default portal to body; sibling div with aria-owns references the portaled stack container
3
+ * so screen readers that support aria-owns can present content in logical order.
4
+ * useInitialFocus announces the popup in screen readers; useFocusRedirect manages Tab in/out.
5
+ */
6
+ export declare const PopupAriaOwns: () => import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=PopupAriaOwns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PopupAriaOwns.d.ts","sourceRoot":"","sources":["../../../../../../mdx/accessibility/examples/Popups/PopupAriaOwns.tsx"],"names":[],"mappings":"AAmCA;;;;GAIG;AACH,eAAO,MAAM,aAAa,+CAkDzB,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { DeleteButton } from '@workday/canvas-kit-react/button';
4
+ import { Popup, useCloseOnEscape, useCloseOnOutsideClick, useInitialFocus, useReturnFocus, useFocusRedirect, usePopupModel, } from '@workday/canvas-kit-react/popup';
5
+ import { Box, Flex } from '@workday/canvas-kit-react/layout';
6
+ import { createStyles, px2rem } from '@workday/canvas-kit-styling';
7
+ import { system } from '@workday/canvas-tokens-web';
8
+ import { useUniqueId } from '@workday/canvas-kit-react/common';
9
+ const cardStyles = createStyles({
10
+ width: px2rem(400),
11
+ });
12
+ const bodyStyles = createStyles({
13
+ marginY: system.space.zero,
14
+ });
15
+ const flexStyles = createStyles({
16
+ gap: system.space.x4,
17
+ padding: system.space.x2,
18
+ });
19
+ const layoutStyles = createStyles({
20
+ gap: system.space.x4,
21
+ alignItems: 'flex-start',
22
+ flexDirection: 'column',
23
+ });
24
+ /**
25
+ * Default portal to body; sibling div with aria-owns references the portaled stack container
26
+ * so screen readers that support aria-owns can present content in logical order.
27
+ * useInitialFocus announces the popup in screen readers; useFocusRedirect manages Tab in/out.
28
+ */
29
+ export const PopupAriaOwns = () => {
30
+ const initialFocusRef = React.useRef(null);
31
+ const model = usePopupModel({ initialFocusRef });
32
+ useCloseOnOutsideClick(model);
33
+ useCloseOnEscape(model);
34
+ useInitialFocus(model);
35
+ useReturnFocus(model);
36
+ useFocusRedirect(model);
37
+ const messageId = useUniqueId();
38
+ const popupId = useUniqueId();
39
+ const visible = model.state.visibility !== 'hidden';
40
+ React.useLayoutEffect(() => {
41
+ if (visible && model.state.stackRef.current) {
42
+ model.state.stackRef.current.setAttribute('id', popupId);
43
+ }
44
+ }, [model.state.stackRef, visible, popupId]);
45
+ return (_jsxs(Flex, { cs: layoutStyles, children: [_jsx(Flex, { children: _jsxs(Popup, { model: model, children: [_jsx(Popup.Target, { as: DeleteButton, children: "Delete Item" }), _jsx("div", { "aria-owns": popupId, style: { position: 'absolute' } }), _jsx(Popup.Popper, { children: _jsxs(Popup.Card, { cs: cardStyles, "aria-describedby": messageId, children: [_jsx(Popup.CloseIcon, { "aria-label": "Close" }), _jsx(Popup.Heading, { children: "Delete Item" }), _jsx(Popup.Body, { children: _jsx(Box, { as: "p", id: messageId, cs: bodyStyles, children: "Are you sure you'd like to delete the item titled 'My Item'?" }) }), _jsxs(Flex, { cs: flexStyles, children: [_jsx(Popup.CloseButton, { as: DeleteButton, children: "Delete" }), _jsx(Popup.CloseButton, { ref: initialFocusRef, children: "Cancel" })] })] }) })] }) }), _jsx("p", { children: "This content should come after the popup in the reading order. When someone uses a screen reader or moves through the page with tabbing, they will read or reach this content only after the popup content is shown. This helps keep the page easy to follow and makes sure that the popup is announced before any information that comes next." })] }));
46
+ };
@@ -0,0 +1,20 @@
1
+ import {ExampleCodeBlock} from '@workday/canvas-kit-docs';
2
+ import InlinePortalPopup from './examples/Popups/InlinePortalPopup';
3
+
4
+
5
+ ## Inline portal with `PopupStack`
6
+
7
+ This example builds on the patterns described in
8
+ [Guides > Accessibility > Inline Popups](?path=/docs/guides-accessibility-inline-popups--docs).
9
+ It does **not** use a focus trap. For modal dialogs with overlay and focus trap, use the
10
+ [**Modal**](?path=/docs/components-popups-modal--docs) component instead.
11
+
12
+ Keep using a portal (default stacking and positioning) but mount the portal **into a sentinel
13
+ element** placed right after the trigger. Call `PopupStack.pushStackContext(sentinelElement)` while
14
+ the popup is open so new stack items append to that sentinel instead of `body`. **Tradeoff:** You
15
+ still get **ancestor overflow** clipping—the portaled content is a descendant of the sentinel, not
16
+ `document.body`. You must also handle **`PopupStack` context** (push/pop on open/close), which is
17
+ more moving parts than `portal={false}` alone. Use **`useInitialFocus`** so opening the popup is
18
+ announced when focus enters the dialog.
19
+
20
+ <ExampleCodeBlock code={InlinePortalPopup} />
@@ -0,0 +1,71 @@
1
+ import {ExampleCodeBlock} from '@workday/canvas-kit-docs';
2
+ import InlinePopupNoPortal from './examples/Popups/InlinePopupNoPortal';
3
+ import PopupAriaOwns from './examples/Popups/PopupAriaOwns';
4
+
5
+
6
+ # How screen readers read Popups
7
+
8
+ A **screen reader** is software that reads the page out loud and lets people navigate with the
9
+ keyboard (and sometimes a braille display). It does not “see” the layout the way sighted users do.
10
+ It walks through the page in a sequence that usually matches the **order of elements in the
11
+ DOM**—roughly, the order nodes appear in the HTML tree.
12
+
13
+ That matters for popups: if the popup’s markup is **far away** from the control that opened it in
14
+ the DOM, the screen reader may read a lot of other page content **before** it reaches the popup. The
15
+ user might not realize the popup is there, or they might hear unrelated content mixed in with the
16
+ popup experience.
17
+
18
+ **Moving keyboard focus** into the popup when it opens helps people continue interacting, but it
19
+ does **not** change that underlying reading sequence. So focus management and reading order are
20
+ related problems; you often need to address both.
21
+
22
+ **None of these examples use focus traps.** For modal dialogs with an overlay and focus trap, use
23
+ the [**Modal**](?path=/docs/components-popups-modal--docs) component instead.
24
+
25
+ **`useInitialFocus`:** When the popup opens, each example moves focus into the popup (often to a
26
+ Close control or another safe first stop). That matters because **many screen readers only announce
27
+ new content when focus moves**. If focus stays on the trigger, the user may get **no cue** that a
28
+ popup appeared. When choosing not to use `useInitialFocus`, consider the following:
29
+
30
+ - Use `aria-expanded={true | false}` on `Popup.Target` so assistive tech can report whether the
31
+ popup is open or closed.
32
+ - Use `aria-haspopup="dialog"` on `Popup.Target` as a hint that the control opens a dialog.
33
+ **Caveat:** some older screen readers do not understand the `"dialog"` value. They may treat it
34
+ like a generic popup and **announce “menu”** even when you built a dialog. For that reason, we
35
+ **strongly recommend** testing with your supported browsers and screen reader combinations during
36
+ development.
37
+
38
+ ## 1. Inline popup with `portal={false}`
39
+
40
+ Set `portal={false}` on `Popup.Popper` so the popup renders in the DOM next to its target. Reading
41
+ order follows document order. Use **`useInitialFocus`**, **`useReturnFocus`**, and the usual close
42
+ hooks. **Tradeoff:** the popup is constrained by ancestor `overflow` and positioning context.
43
+
44
+ <ExampleCodeBlock code={InlinePopupNoPortal} />
45
+
46
+ For the same reading-order goal using a **portaled** popup mounted into a sentinel next to the
47
+ trigger (with `PopupStack.pushStackContext`), see
48
+ [**Testing > Inline Portals**](?path=/docs/guides-accessibility-testing-inline-portals--docs).
49
+
50
+ ## 2. Reading order with `aria-owns`
51
+
52
+ You can keep the default portal (content at the bottom of `body`) and still try to **re-parent** the
53
+ popup in the **accessibility tree**: add a sibling element after `Popup.Target` and set
54
+ **`aria-owns`** to the id of the portaled `Popup.Card`. Some assistive technologies will then treat
55
+ that card as “owned” by the trigger for browsing and announcements.
56
+
57
+ **Tradeoffs:**
58
+
59
+ - **Support for `aria-owns` varies.** Do not assume every combination of browser and screen reader
60
+ will honor it the same way.
61
+ - **Tab order still follows the real DOM.** `aria-owns` does not move focus targets. You may still
62
+ need helpers like **`useFocusRedirect`** so keyboard users can reach the popup predictably.
63
+ - Combine with **`useInitialFocus`** so opening the popup still moves focus and gives a clear
64
+ announcement where supported.
65
+
66
+ The Canvas Kit [**Dialog**](?path=/docs/components-popups-dialog--docs) builds this pattern in.
67
+
68
+ Another `aria-owns` example:
69
+ [Advanced Tables > Table With Filterable Column Headers](?path=/docs/guides-accessibility-examples-advanced-tables--docs#filterable-column-headers).
70
+
71
+ <ExampleCodeBlock code={PopupAriaOwns} />
@@ -17,34 +17,65 @@ yarn add @workday/canvas-kit-react
17
17
 
18
18
  ### Basic Example
19
19
 
20
- [Modal](/components/popups/modal/) and Dialog are very similar: most of the examples from Modal can
21
- be adapted to Dialog by changing `Modal` to `Dialog` and replacing `Modal.Overlay` with
22
- `Dialog.Popper`.
23
-
24
- Unlike Modal, Dialog does _not_ render the rest of the page inert while it is active. Dialog should
25
- be used in situations where the task does not require immediate attention such as in the example
26
- below.
20
+ Unlike Modal, Dialog **does not** render the rest of the page inert while it is active. Dialog
21
+ should be used in situations where the task does not require immediate attention.
27
22
 
28
23
  <ExampleCodeBlock code={Basic} />
29
24
 
30
25
  ### Focus Redirect
31
26
 
32
- Since Modal requires immediate attention, it will trap the keyboard focus inside the Modal until an
33
- action is taken. Dialog manages focus differently, however, since it does not require immediate
34
- attention.
35
-
36
- The following example shows how Dialog manages focus in and out of the component.
27
+ Dialog **does not** trap keyboard focus like the Modal component does. Instead, it allows focus to
28
+ move freely in and out of the dialog, supporting more flexible navigation. The following example
29
+ shows how Dialog manages focus in and out of the component.
37
30
 
38
31
  <ExampleCodeBlock code={Focus} />
39
32
 
40
- Instead of trapping focus within the Dialog, it is effectively treated as an inline element next to
41
- its triggering `Dialog.Target` button. Tabbing out of the Dialog will close the popup and move focus
42
- to the next button.
43
-
44
- Dialog also adds an `aria-owns` to a `<div>` element which is rendered as a sibling of the
45
- `Dialog.Target` button. The `aria-owns` references the `Dialog.Card` and allows screen readers which
46
- [support aria-owns](/components/popups/popup/#focus-redirect) to navigate the Dialog as if it
47
- weren't portalled to the bottom of the document body.
33
+ > **Accessibility Note**: Focus redirect **will not** have any effect on the reading order of a
34
+ > screen reader.
35
+
36
+ ## Accessibility
37
+
38
+ `Dialog` composes the popup stack with `useInitialFocus`, `useReturnFocus`, `useCloseOnEscape`,
39
+ `useCloseOnOutsideClick`, and `useFocusRedirect`. The card container includes an ARIA
40
+ **`role="dialog"`** that is **non-modal**: the rest of the page stays available. The card also
41
+ includes an **`aria-labelledby`** attribute referencing the `id` on `Dialog.Heading`, so the dialog
42
+ has an accessible name that matches the visible heading.
43
+
44
+ The Dialog component includes a `<div>` element (sibling to the `Dialog.Target`) with `aria-owns`
45
+ pointing to the `Dialog.Card`. This remaps the hierarchy of the accessibility tree to improve
46
+ sequential reading order in supported browsers. For more information, see
47
+ [Guides > Accessibility > Inline Popups](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-inline-popups--docs).
48
+
49
+ [Dialog Pattern | APG | WAI | W3C](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
50
+
51
+ - Prefer **`Dialog.Heading`** so the dialog is properly labelled; avoid leaving a dialog without an
52
+ accessible name.
53
+ - Ensure icon-only controls such as **`Dialog.CloseIcon`** include an accessible name. Prefer the
54
+ `Tooltip` component to provide a visible label, or a translated `aria-label` string is acceptable.
55
+
56
+ ### Navigation
57
+
58
+ - **Enter** / **Space**: Open the dialog (standard button behavior on the trigger). When it opens,
59
+ focus moves to the **first focusable element** inside the dialog in DOM order—often the close
60
+ control—or to the element referenced by **`initialFocusRef`** on the dialog model when set.
61
+ - **Tab** / **Shift + Tab**: Move through focusable elements inside the dialog; leaving the first or
62
+ last focusable element **closes** the dialog and moves focus to the next or previous focusable
63
+ element on the page (non-modal focus redirect behavior).
64
+ - **Escape**: Closes the dialog and returns focus to the `Dialog.Target` (or configured return
65
+ target).
66
+
67
+ ### Screen Reader Experience
68
+
69
+ - **When the dialog opens:** Screen readers should announce the name and role of the first focused
70
+ control (often the close button), the dialog's name (`Dialog.Heading`) and role.
71
+ - **Reading order:** The dialog contents should be read in the same order as it appears on screen
72
+ for browsers and screen readers that support `aria-owns`. Results vary, so always test with your
73
+ supported browsers and screen reader combinations.
74
+ - **Expanded or collapsed state:** The `Dialog.Target` does not include an expanded or collapsed
75
+ state by default, but it can be added if the interaction design isn't using an initial focus for
76
+ the Dialog. See
77
+ [Guides > Accessibility > Inline Popups](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-inline-popups--docs)
78
+ for more information.
48
79
 
49
80
  ## Component API
50
81
 
@@ -13,9 +13,13 @@ import FormModal from './examples/FormModal';
13
13
 
14
14
  A Modal component is a type of Dialog that renders a translucent overlay that prevents user
15
15
  interaction with the rest of the page. A Modal will render the rest of the page inert until the
16
- Modal is dismissed. A Modal should be used when the user needs to presented with important
16
+ Modal is dismissed. A Modal should be used when the user needs to be presented with important
17
17
  information that must be interacted with before continuing interaction with the rest of the page.
18
18
 
19
+ For tasks that do not require blocking the rest of the page, consider the non-modal
20
+ [**Dialog**](https://workday.github.io/canvas-kit/?path=/docs/components-popups-dialog--docs)
21
+ component instead.
22
+
19
23
  ## Installation
20
24
 
21
25
  ```sh
@@ -34,7 +38,7 @@ dialog.
34
38
  ### Without Close Icon
35
39
 
36
40
  If you wish to remove the close icon button, you can simply omit the `Modal.CloseButton`
37
- subcomponent. If you have a modal dialog that requires the user to accept instead of dismiss though
41
+ subcomponent. If you have a modal dialog that requires the user to accept instead of dismiss through
38
42
  an escape key or clicking outside the modal, you must create a new `PopupModel` without those
39
43
  behaviors and hand that model to the Modal dialog component.
40
44
 
@@ -52,18 +56,29 @@ element receives focus when the modal opens.
52
56
 
53
57
  <ExampleCodeBlock code={CustomFocus} />
54
58
 
59
+ > **Accessibility Note**: When initial focus lands on a control **below** the heading (for example,
60
+ > a text field instead of the close button), give supplementary copy a unique `id` and pass
61
+ > **`aria-describedby`** on **`Modal.Card`** so screen readers can announce both the dialog name and
62
+ > that text. For more examples of custom focus techniques, see
63
+ > [Popup > Initial Focus](https://workday.github.io/canvas-kit/?path=/docs/components-popups-popup--docs#initial-focus).
64
+
55
65
  ### Return Focus
56
66
 
57
- By default, the Modal will return focus to the `Modal.Target` element, but it is possible the Modal
58
- was triggered by an element that won't exist when the modal is closed. An example might be a Modal
59
- that was opened from an Menu item and the act of opening the Modal also closes the Menu, meaning the
60
- Menu item can no longer receive focus. The also probably means the `Modal.Target` component might
61
- not suite your needs. The `Modal.Target` adds both a `ref` and an `onClick`. If you provide a
62
- `returnFocusRef`, you only need to worry about the `onClick`. If you're using a menu, you might need
63
- to use a different callback. Calling `model.events.show()` will show the Modal.
67
+ By default, the Modal will return focus to the `Modal.Target` element. When you open the modal with
68
+ `model.events.show()` (without `Modal.Target`), set **`returnFocusRef`** on the model to the element
69
+ that should receive focus when the modal closes—for example the button that opened it. That covers
70
+ cancel, Escape, and the close icon: focus returns to the control the user activated.
71
+
72
+ If confirming an action **removes** that control from the document (such as deleting the row that
73
+ held the delete button), `returnFocusRef` alone cannot land on a **new** target. The example below
74
+ uses **`useLayoutEffect`** after the list updates to move focus to another row’s delete control, or
75
+ to empty-state text when no files remain.
64
76
 
65
77
  <ExampleCodeBlock code={ReturnFocus} />
66
78
 
79
+ > **Accessibility Note**: After an item is deleted, focus is returned to the next item in the list
80
+ > or to the empty state text when no items remain.
81
+
67
82
  ### Custom Target
68
83
 
69
84
  It is common to have a custom target for your modal. Use the `as` prop to use your custom component.
@@ -79,6 +94,12 @@ requires a `label` prop.
79
94
 
80
95
  <ExampleCodeBlock code={CustomTarget} />
81
96
 
97
+ > **Accessibility Note**: Custom targets must be keyboard focusable, otherwise users will not be
98
+ > able to access the modal. Bear in mind that click handlers only work with the keyboard when
99
+ > applied to HTML `<button>` elements and it is **strongly recommended** to base your custom target
100
+ > on a `<button>` element. Otherwise, you will be required to build in your own custom keyboard
101
+ > event handlers for invoking the modal.
102
+
82
103
  ### Body Content Overflow
83
104
 
84
105
  The Modal automatically handles overflowing content inside the `Modal.Body` element. If contents are
@@ -87,6 +108,11 @@ need to restrict the height of your browser to observe the overflow.
87
108
 
88
109
  <ExampleCodeBlock code={BodyOverflow} />
89
110
 
111
+ > **Accessibility Note**: When body content overflows, ensure users can scroll that region **using
112
+ > only the keyboard**. Mouse users can drag scrollbars, but keyboard users need another path. In
113
+ > this example, **`tabIndex={0}`** is set on **`Modal.Body`** so the scrollable area can receive
114
+ > focus; once focused, **arrow keys** move the viewport within the overflowing content.
115
+
90
116
  ### Full overlay scrolling
91
117
 
92
118
  If content is large, scrolling the entire overlay container is an option. Use the
@@ -105,6 +131,58 @@ hoisted to allow for form validation and allow you to control when the modal clo
105
131
 
106
132
  <ExampleCodeBlock code={FormModal} />
107
133
 
134
+ ## Accessibility
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
155
+ [Guides > Accessibility > Inline Popups](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-inline-popups--docs).
156
+
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.
185
+
108
186
  ## Component API
109
187
 
110
188
  <SymbolDoc name="Modal" fileName="/react/" />
@@ -5,7 +5,17 @@ import {PrimaryButton} from '@workday/canvas-kit-react/button';
5
5
  import {Flex} from '@workday/canvas-kit-react/layout';
6
6
  import {FormField} from '@workday/canvas-kit-react/form-field';
7
7
  import {TextInput} from '@workday/canvas-kit-react/text-input';
8
+ import {Select} from '@workday/canvas-kit-react/select';
8
9
  import {plusIcon} from '@workday/canvas-system-icons-web';
10
+ import {createStyles} from '@workday/canvas-kit-styling';
11
+ import {system} from '@workday/canvas-tokens-web';
12
+
13
+ const FAVORITE_COLOR_OPTIONS = ['Blue', 'Yellow'];
14
+
15
+ const flexStyles = createStyles({
16
+ gap: system.space.x4,
17
+ padding: system.space.x2,
18
+ });
9
19
 
10
20
  export default () => {
11
21
  const model = useModalModel();
@@ -18,6 +28,8 @@ export default () => {
18
28
  console.log('form data', {
19
29
  first: (event.currentTarget.elements.namedItem('first') as HTMLInputElement).value,
20
30
  last: (event.currentTarget.elements.namedItem('last') as HTMLInputElement).value,
31
+ favoriteColor: (event.currentTarget.elements.namedItem('favoriteColor') as HTMLInputElement)
32
+ .value,
21
33
  });
22
34
 
23
35
  // if it looks good, submit to the server and close the modal
@@ -40,8 +52,21 @@ export default () => {
40
52
  <FormField.Label>Last Name</FormField.Label>
41
53
  <FormField.Input as={TextInput} name="last" />
42
54
  </FormField>
55
+ <FormField>
56
+ <FormField.Label>Favorite Color</FormField.Label>
57
+ <FormField.Field>
58
+ <Select items={FAVORITE_COLOR_OPTIONS}>
59
+ <FormField.Input as={Select.Input} name="favoriteColor" />
60
+ <Select.Popper>
61
+ <Select.Card>
62
+ <Select.List>{item => <Select.Item>{item}</Select.Item>}</Select.List>
63
+ </Select.Card>
64
+ </Select.Popper>
65
+ </Select>
66
+ </FormField.Field>
67
+ </FormField>
43
68
  </Modal.Body>
44
- <Flex gap="s" padding="xxs">
69
+ <Flex cs={flexStyles}>
45
70
  <Modal.CloseButton>Cancel</Modal.CloseButton>
46
71
  <PrimaryButton type="submit">Submit</PrimaryButton>
47
72
  </Flex>