@workday/canvas-kit-docs 14.3.9 → 14.3.10
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/ExampleCodeBlock.d.ts.map +1 -1
- package/dist/es6/lib/ExampleCodeBlock.js +12 -3
- package/dist/es6/lib/stackblitzFiles/packageJSONFile.js +5 -5
- package/dist/es6/lib/stackblitzFiles/packageJSONFile.ts +5 -5
- package/dist/es6/mdx/accessibility/examples/Popups/InlinePopupNoPortal.d.ts +2 -0
- package/dist/es6/mdx/accessibility/examples/Popups/InlinePopupNoPortal.d.ts.map +1 -0
- package/dist/es6/mdx/accessibility/examples/Popups/InlinePopupNoPortal.js +62 -0
- package/dist/es6/mdx/accessibility/examples/Popups/InlinePortalPopup.d.ts +7 -0
- package/dist/es6/mdx/accessibility/examples/Popups/InlinePortalPopup.d.ts.map +1 -0
- package/dist/es6/mdx/accessibility/examples/Popups/InlinePortalPopup.js +63 -0
- package/dist/es6/mdx/accessibility/examples/Popups/PopupAriaOwns.d.ts +7 -0
- package/dist/es6/mdx/accessibility/examples/Popups/PopupAriaOwns.d.ts.map +1 -0
- package/dist/es6/mdx/accessibility/examples/Popups/PopupAriaOwns.js +46 -0
- package/dist/mdx/accessibility/InlinePortals.mdx +20 -0
- package/dist/mdx/accessibility/Popups.mdx +71 -0
- package/dist/mdx/react/dialog/Dialog.mdx +51 -20
- package/dist/mdx/react/modal/Modal.mdx +87 -9
- package/dist/mdx/react/modal/examples/FormModal.tsx +26 -1
- package/dist/mdx/react/modal/examples/ReturnFocus.tsx +137 -39
- package/dist/mdx/react/popup/Popup.mdx +55 -28
- package/dist/mdx/react/popup/examples/Basic.tsx +20 -3
- package/dist/mdx/react/popup/examples/FocusRedirect.tsx +24 -9
- package/dist/mdx/react/popup/examples/InitialFocus.tsx +113 -9
- package/dist/mdx/react/popup/examples/InlinePopup.tsx +125 -0
- package/dist/mdx/react/popup/examples/MultiplePopups.tsx +34 -22
- package/lib/ExampleCodeBlock.tsx +12 -3
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExampleCodeBlock.d.ts","sourceRoot":"","sources":["../../../lib/ExampleCodeBlock.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ExampleCodeBlock.d.ts","sourceRoot":"","sources":["../../../lib/ExampleCodeBlock.tsx"],"names":[],"mappings":"AA6EA,eAAO,MAAM,gBAAgB,aAAY,GAAG,4CAoH3C,CAAC"}
|
|
@@ -3,7 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
4
4
|
import { TertiaryButton } from '@workday/canvas-kit-react/button';
|
|
5
5
|
import { Card } from '@workday/canvas-kit-react/card';
|
|
6
|
-
import {
|
|
6
|
+
import { createStencil, cssVar } from '@workday/canvas-kit-styling';
|
|
7
7
|
import { system } from '@workday/canvas-tokens-web';
|
|
8
8
|
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
|
9
9
|
import { checkCircleIcon, copyIcon } from '@workday/canvas-system-icons-web';
|
|
@@ -21,14 +21,20 @@ import viteEnvFile from '!!raw-loader!./stackblitzFiles/vite-env.d.ts';
|
|
|
21
21
|
import { CanvasProvider, defaultBranding } from '@workday/canvas-kit-react/common';
|
|
22
22
|
const cardStencil = createStencil({
|
|
23
23
|
base: {
|
|
24
|
+
// Lets the block shrink inside flex/grid doc layouts so content width does not force overflow.
|
|
25
|
+
minWidth: 0,
|
|
26
|
+
maxWidth: '100%',
|
|
24
27
|
'[data-part="example-block"]': {
|
|
25
28
|
boxShadow: system.depth[1],
|
|
26
29
|
borderRadius: system.shape.x1,
|
|
27
30
|
position: 'relative',
|
|
31
|
+
minWidth: 0,
|
|
28
32
|
overflow: 'auto', // This allows for the entire ExampleCodeBlock to scroll on smaller viewports
|
|
29
33
|
},
|
|
30
34
|
'[data-part="example-block-container"]': {
|
|
35
|
+
minWidth: 0,
|
|
31
36
|
overflow: 'auto',
|
|
37
|
+
padding: system.space.x1,
|
|
32
38
|
},
|
|
33
39
|
'[data-part="code-block"]': {
|
|
34
40
|
display: 'none',
|
|
@@ -37,8 +43,9 @@ const cardStencil = createStencil({
|
|
|
37
43
|
},
|
|
38
44
|
'[data-part="code-toggle-stackblitz-btn-container"]': {
|
|
39
45
|
position: 'absolute',
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
// Keep inside the padding box; negative inset was expanding scroll overflow by ~1px.
|
|
47
|
+
right: system.space.zero,
|
|
48
|
+
bottom: system.space.zero,
|
|
42
49
|
display: 'flex',
|
|
43
50
|
gap: system.space.x2,
|
|
44
51
|
},
|
|
@@ -119,5 +126,7 @@ export const ExampleCodeBlock = ({ code }) => {
|
|
|
119
126
|
lineHeight: cssVar(system.lineHeight.subtext.large),
|
|
120
127
|
margin: '0',
|
|
121
128
|
padding: `${cssVar(system.space.x8)} ${cssVar(system.space.x10)}`,
|
|
129
|
+
boxSizing: 'border-box',
|
|
130
|
+
maxWidth: '100%',
|
|
122
131
|
}, children: code.__RAW__ }) })), _jsx(Tooltip, { title: copied ? 'Copied!' : 'Copy Source Code', children: _jsx(TertiaryButton, { "aria-label": "Copy Code", size: "large", "data-part": "copy-btn", variant: "inverse", iconPosition: "end", icon: copied ? checkCircleIcon : copyIcon, onClick: onCopy }) })] }) }) })] }));
|
|
123
132
|
};
|
|
@@ -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.
|
|
22
|
-
"@workday/canvas-kit-preview-react": "14.3.
|
|
23
|
-
"@workday/canvas-kit-react": "14.3.
|
|
24
|
-
"@workday/canvas-kit-react-fonts": "^14.3.
|
|
25
|
-
"@workday/canvas-kit-styling": "14.3.
|
|
21
|
+
"@workday/canvas-kit-labs-react": "14.3.10",
|
|
22
|
+
"@workday/canvas-kit-preview-react": "14.3.10",
|
|
23
|
+
"@workday/canvas-kit-react": "14.3.10",
|
|
24
|
+
"@workday/canvas-kit-react-fonts": "^14.3.10",
|
|
25
|
+
"@workday/canvas-kit-styling": "14.3.10",
|
|
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.
|
|
22
|
-
"@workday/canvas-kit-preview-react": "14.3.
|
|
23
|
-
"@workday/canvas-kit-react": "14.3.
|
|
24
|
-
"@workday/canvas-kit-react-fonts": "^14.3.
|
|
25
|
-
"@workday/canvas-kit-styling": "14.3.
|
|
21
|
+
"@workday/canvas-kit-labs-react": "14.3.10",
|
|
22
|
+
"@workday/canvas-kit-preview-react": "14.3.10",
|
|
23
|
+
"@workday/canvas-kit-react": "14.3.10",
|
|
24
|
+
"@workday/canvas-kit-react-fonts": "^14.3.10",
|
|
25
|
+
"@workday/canvas-kit-styling": "14.3.10",
|
|
26
26
|
"@workday/canvas-system-icons-web": "3.0.36",
|
|
27
27
|
"@workday/canvas-tokens-web": "3.1.2"
|
|
28
28
|
},
|
|
@@ -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
|
-
|
|
21
|
-
be
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`Dialog
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
|
58
|
-
|
|
59
|
-
that
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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/" />
|