@workday/canvas-kit-docs 14.3.8 → 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.
Files changed (27) 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/stackblitzFiles/packageJSONFile.js +5 -5
  4. package/dist/es6/lib/stackblitzFiles/packageJSONFile.ts +5 -5
  5. package/dist/es6/mdx/accessibility/examples/Popups/InlinePopupNoPortal.d.ts +2 -0
  6. package/dist/es6/mdx/accessibility/examples/Popups/InlinePopupNoPortal.d.ts.map +1 -0
  7. package/dist/es6/mdx/accessibility/examples/Popups/InlinePopupNoPortal.js +62 -0
  8. package/dist/es6/mdx/accessibility/examples/Popups/InlinePortalPopup.d.ts +7 -0
  9. package/dist/es6/mdx/accessibility/examples/Popups/InlinePortalPopup.d.ts.map +1 -0
  10. package/dist/es6/mdx/accessibility/examples/Popups/InlinePortalPopup.js +63 -0
  11. package/dist/es6/mdx/accessibility/examples/Popups/PopupAriaOwns.d.ts +7 -0
  12. package/dist/es6/mdx/accessibility/examples/Popups/PopupAriaOwns.d.ts.map +1 -0
  13. package/dist/es6/mdx/accessibility/examples/Popups/PopupAriaOwns.js +46 -0
  14. package/dist/mdx/accessibility/InlinePortals.mdx +20 -0
  15. package/dist/mdx/accessibility/Popups.mdx +71 -0
  16. package/dist/mdx/react/dialog/Dialog.mdx +51 -20
  17. package/dist/mdx/react/modal/Modal.mdx +87 -9
  18. package/dist/mdx/react/modal/examples/FormModal.tsx +26 -1
  19. package/dist/mdx/react/modal/examples/ReturnFocus.tsx +137 -39
  20. package/dist/mdx/react/popup/Popup.mdx +55 -28
  21. package/dist/mdx/react/popup/examples/Basic.tsx +20 -3
  22. package/dist/mdx/react/popup/examples/FocusRedirect.tsx +24 -9
  23. package/dist/mdx/react/popup/examples/InitialFocus.tsx +113 -9
  24. package/dist/mdx/react/popup/examples/InlinePopup.tsx +125 -0
  25. package/dist/mdx/react/popup/examples/MultiplePopups.tsx +34 -22
  26. package/lib/ExampleCodeBlock.tsx +12 -3
  27. package/package.json +6 -6
@@ -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>
@@ -1,60 +1,158 @@
1
1
  import React from 'react';
2
2
  import {Modal, useModalModel} from '@workday/canvas-kit-react/modal';
3
3
  import {DeleteButton} from '@workday/canvas-kit-react/button';
4
- import {FormField} from '@workday/canvas-kit-react/form-field';
5
4
  import {Flex, Box} from '@workday/canvas-kit-react/layout';
6
- import {Select} from '@workday/canvas-kit-react/select';
5
+ import {Heading, Text} from '@workday/canvas-kit-react/text';
6
+ import {Tooltip} from '@workday/canvas-kit-react/tooltip';
7
+ import {trashIcon} from '@workday/canvas-system-icons-web';
8
+ import {useUniqueId} from '@workday/canvas-kit-react/common';
9
+ import {createStyles} from '@workday/canvas-kit-styling';
10
+ import {system} from '@workday/canvas-tokens-web';
11
+
12
+ const INITIAL_FILES = ['Resume.docx', 'Cover_Letter.docx', 'References.docx'];
13
+
14
+ const actionStyles = createStyles({
15
+ gap: system.space.x4,
16
+ padding: system.space.x2,
17
+ });
18
+
19
+ const headingStyles = createStyles({
20
+ marginY: system.space.zero,
21
+ });
22
+
23
+ const emptyStateStyles = createStyles({
24
+ maxWidth: '28rem',
25
+ outline: 'none',
26
+ });
27
+
28
+ const listStyles = createStyles({
29
+ flexDirection: 'column',
30
+ gap: system.space.x4,
31
+ marginY: system.space.zero,
32
+ padding: system.space.zero,
33
+ listStyle: 'none',
34
+ maxWidth: '28rem',
35
+ });
36
+
37
+ const rowStyles = createStyles({
38
+ alignItems: 'center',
39
+ justifyContent: 'space-between',
40
+ gap: system.space.x4,
41
+ width: '100%',
42
+ });
43
+
44
+ function fileNameId(name: string) {
45
+ return `return-focus-file-${name.replace(/[^a-zA-Z0-9]/g, '_')}`;
46
+ }
47
+
48
+ /** Index of a delete button to focus after removing `deletedIndex`, or empty list. */
49
+ function nextListFocusAfterDelete(deletedIndex: number, lengthBeforeDelete: number) {
50
+ if (lengthBeforeDelete <= 1) {
51
+ return 'empty' as const;
52
+ }
53
+ return deletedIndex < lengthBeforeDelete - 1 ? deletedIndex : deletedIndex - 1;
54
+ }
7
55
 
8
56
  export default () => {
9
- const ref = React.useRef(null);
10
- const [value, setValue] = React.useState('');
57
+ const [items, setItems] = React.useState<string[]>(() => [...INITIAL_FILES]);
58
+ const [confirmingFileName, setConfirmingFileName] = React.useState<string | null>(null);
59
+ const bodyTextId = useUniqueId();
60
+
61
+ const returnFocusRef = React.useRef<HTMLButtonElement | null>(null);
62
+ const cancelButtonRef = React.useRef<HTMLButtonElement>(null);
63
+ const deleteButtonRefs = React.useRef<(HTMLButtonElement | null)[]>([]);
64
+ const emptyStateRef = React.useRef<HTMLDivElement>(null);
65
+ const pendingDeleteIndexRef = React.useRef<number | null>(null);
66
+ const postDeleteFocusRef = React.useRef<number | 'empty' | null>(null);
67
+
11
68
  const model = useModalModel({
12
- returnFocusRef: ref,
69
+ returnFocusRef,
70
+ initialFocusRef: cancelButtonRef,
13
71
  });
14
72
 
15
- const handleDelete = () => {
16
- console.log('Deleted item');
73
+ React.useEffect(() => {
74
+ if (model.state.visibility === 'hidden') {
75
+ setConfirmingFileName(null);
76
+ pendingDeleteIndexRef.current = null;
77
+ }
78
+ }, [model.state.visibility]);
79
+
80
+ React.useLayoutEffect(() => {
81
+ if (postDeleteFocusRef.current === null) {
82
+ return;
83
+ }
84
+ if (postDeleteFocusRef.current === 'empty') {
85
+ emptyStateRef.current?.focus();
86
+ } else {
87
+ deleteButtonRefs.current[postDeleteFocusRef.current]?.focus();
88
+ }
89
+ postDeleteFocusRef.current = null;
90
+ }, [items]);
91
+
92
+ const openDeleteModal = (index: number) => {
93
+ pendingDeleteIndexRef.current = index;
94
+ setConfirmingFileName(items[index]);
95
+ returnFocusRef.current = deleteButtonRefs.current[index];
96
+ model.events.show();
97
+ };
98
+
99
+ const handleConfirmDelete = () => {
100
+ const idx = pendingDeleteIndexRef.current;
101
+ if (idx === null) {
102
+ return;
103
+ }
104
+ postDeleteFocusRef.current = nextListFocusAfterDelete(idx, items.length);
105
+ pendingDeleteIndexRef.current = null;
106
+ setItems(prev => prev.filter((_, i) => i !== idx));
17
107
  };
18
108
 
19
109
  return (
20
110
  <Modal model={model}>
21
- <Select items={['', 'Delete', 'Two']}>
22
- <FormField>
23
- <FormField.Label>Choose an option</FormField.Label>
24
- <FormField.Input
25
- as={Select.Input}
26
- ref={ref}
27
- onChange={e => {
28
- const option = e.currentTarget.value;
29
- if (option === 'Delete') {
30
- model.events.show();
31
- setValue('');
32
- } else {
33
- setValue(e.currentTarget.value);
34
- }
35
- }}
36
- />
37
- <Select.Popper>
38
- <Select.Card>
39
- <Select.List>{item => <Select.Item>{item}</Select.Item>}</Select.List>
40
- </Select.Card>
41
- </Select.Popper>
42
- </FormField>
43
- </Select>
111
+ <Heading as="h4" size="small" cs={headingStyles}>
112
+ Uploaded Files
113
+ </Heading>
114
+ <Box>
115
+ {items.length > 0 ? (
116
+ <Flex as="ul" cs={listStyles}>
117
+ {items.map((name, index) => (
118
+ <Flex as="li" key={name} cs={rowStyles}>
119
+ <Text as="span" id={fileNameId(name)}>
120
+ {name}
121
+ </Text>
122
+ <Tooltip title="Delete">
123
+ <DeleteButton
124
+ aria-describedby={fileNameId(name)}
125
+ icon={trashIcon}
126
+ ref={el => {
127
+ deleteButtonRefs.current[index] = el;
128
+ }}
129
+ onClick={() => openDeleteModal(index)}
130
+ />
131
+ </Tooltip>
132
+ </Flex>
133
+ ))}
134
+ </Flex>
135
+ ) : (
136
+ <Box ref={emptyStateRef} tabIndex={-1} cs={emptyStateStyles}>
137
+ <Text>No files remaining.</Text>
138
+ </Box>
139
+ )}
140
+ </Box>
44
141
  <Modal.Overlay>
45
- <Modal.Card>
46
- <Modal.CloseIcon aria-label="Close" />
47
- <Modal.Heading>Delete Item</Modal.Heading>
142
+ <Modal.Card aria-describedby={bodyTextId}>
143
+ <Modal.Heading>Delete file?</Modal.Heading>
48
144
  <Modal.Body>
49
- <Box as="p" marginY="zero">
50
- Are you sure you want to delete the item?
51
- </Box>
145
+ <Text id={bodyTextId}>
146
+ {confirmingFileName
147
+ ? `Are you sure you want to delete ${confirmingFileName}?`
148
+ : 'Are you sure you want to delete this file?'}
149
+ </Text>
52
150
  </Modal.Body>
53
- <Flex gap="s" padding="xxs">
54
- <Modal.CloseButton as={DeleteButton} onClick={handleDelete}>
151
+ <Flex cs={actionStyles}>
152
+ <Modal.CloseButton ref={cancelButtonRef}>Cancel</Modal.CloseButton>
153
+ <Modal.CloseButton as={DeleteButton} onClick={handleConfirmDelete}>
55
154
  Delete
56
155
  </Modal.CloseButton>
57
- <Modal.CloseButton>Cancel</Modal.CloseButton>
58
156
  </Flex>
59
157
  </Modal.Card>
60
158
  </Modal.Overlay>
@@ -61,52 +61,56 @@ popup behaviors. For accessibility, these behaviors should be included most of t
61
61
  ### Initial Focus
62
62
 
63
63
  If you want focus to move to a specific element when the popup is opened, set the `initialFocusRef`
64
- of the model. Check with accessibility before doing this. The following example sets the focus on
65
- the "OK" button with an `aria-describedby` pointing to the model's `id` state so screen readers
66
- properly announce the message of the popup when focus is changed to the button. By default, focus
67
- will be placed on the first focusable element when the popup is opened.
64
+ of the model. This is useful for popups that don't have a Close icon button near the top right of
65
+ the popup. In general, we recommend setting focus to the first interactive component inside the
66
+ popup that is the least destructive action.
68
67
 
69
68
  <ExampleCodeBlock code={InitialFocus} />
70
69
 
70
+ > **Accessibility Note**: When initial focus lands on a control **below** the title (such as the OK
71
+ > button in the example above), assign a unique `id` to supplementary text and pass
72
+ > `aria-describedby` on `Popup.Card`. This augments the included `aria-labelledby` reference to
73
+ > `Popup.Heading` so screen readers can announce both the heading and any supplementary text
74
+ > automatically. When initial focus is on the heading itself, add `tabIndex={-1}` to `Popup.Heading`
75
+ > so the title can receive programmatic focus. Choose where focus goes based on your product and
76
+ > accessibility requirements.
77
+
71
78
  ### Focus Redirect
72
79
 
73
80
  Focus management is important to accessibility of popup contents. The following example shows
74
81
  `useFocusRedirect` being used to manage focus in and out of a Popup. This is very useful for
75
- Dialog-style popups. Since `Popup.Popper` renders contents to the bottom of the document body,
76
- `aria-owns` is used for screen readers that support it. This effectively treats a Popup like it
77
- exists in between the buttons while it is opened. Screen readers will navigate the content as if the
78
- content was not portalled to the bottom of the document body. Focus redirection tries to treat the
79
- Popup as if it were inline to the document. Tabbing out of the Popup will close the Popup and move
80
- focus to the next appropriate element.
81
-
82
- > **Note**: Safari does not support `aria-owns`. This means that the contents of the Popup will
83
- > appears out of order to Safari + VoiceOver users. We render popups at the bottom of the
84
- > document.body to ensure proper rendering. You could use `portal=false` on the `Popper` component,
85
- > but that would risk incorrect rendering in all browsers.
82
+ non-modal popups. Focus redirection tries to treat the Popup as if it were inline to the document.
83
+ Tabbing out of the Popup will close the Popup and move focus to an adjacent focusable element.
86
84
 
87
85
  <ExampleCodeBlock code={FocusRedirect} />
88
86
 
87
+ > **Accessibility Note**: The `useFocusRedirect` hook **will not** have any effect on the reading
88
+ > order of a screen reader. Screen reader users may get confused or disoriented when popups are
89
+ > portalled to the bottom of the document body. In this example, we're testing the use of
90
+ > `aria-owns` on a sibling `<div>` element pointing to the `Popup.Card` component. This remaps the
91
+ > hierarchy of the accessibility tree (in supported browsers) to address the reading order problem.
92
+ > For more information, see
93
+ > [Guides > Accessibility > Inline Popups](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-inline-popups--docs).
94
+
89
95
  ### Focus Trapping
90
96
 
91
97
  Focus trapping is similar to the [Focus Redirect](#focus-redirect) example, but will trap focus
92
- inside the popup instead of redirecting focus, it will be trapped inside the Popup. This is most
93
- useful for modal dialogs where the modal must be interacted with before normal interaction can
94
- continue.
95
-
96
- > **Note**: Using focus trapping outside a Modal context can give users a different experience
97
- > depending on how they interact with your application. Focus trapping will not prevent mouse users
98
- > from breaking out of a focus trap, nor will it prevent screen reader users from using virtual
99
- > cursors from breaking out. Modals should use additional techniques to truely "trap" focus into the
100
- > Popup to provide a consistent experience for all users.
98
+ inside the popup instead of redirecting focus to adjacent focusable elements. This is necessary for
99
+ modal dialogs where users must focus on the contents of the dialog before proceeding.
101
100
 
102
101
  <ExampleCodeBlock code={FocusTrap} />
103
102
 
103
+ > **Accessibility Note**: Focus trapping will not prevent mouse users from breaking out of a focus
104
+ > trap, nor will it prevent screen reader users from using virtual reading cursors from breaking
105
+ > out. Consider using [Modal](/components/popups/modal/) instead when you need to focus users'
106
+ > attention on a specific task inside of a popup..
107
+
104
108
  ### Multiple Popups
105
109
 
106
- If you need multiple Popups within the same component, you can create multiple models and pass a
107
- unique model to each Popup. Below is an example of 2 different popups within the same component.
108
- Since each Popup gets its own model, each Popup behaves independently. The same technique can be
109
- used for nested Popups.
110
+ You can render more than one `Popup` in the same view by giving each its own model. This example
111
+ pairs `Popup` with `useDialogModel` and `useModalModel` so you can compare **focus redirection**
112
+ (Tab / Shift + Tab can move focus out of the first popup) and **focus trapping** (focus stays inside
113
+ the second popup until it closes). Opening one does not close the other.
110
114
 
111
115
  <ExampleCodeBlock code={MultiplePopups} />
112
116
 
@@ -123,6 +127,11 @@ preserved.
123
127
 
124
128
  <ExampleCodeBlock code={NestedPopups} />
125
129
 
130
+ > **Accessibility Note**: In this example, observe how users can traverse both opened popups using
131
+ > the keyboard. This is likely to be a confusing experience for users and may necessitate focus
132
+ > trapping inside each popup with careful consideration for setting initial focus and returning
133
+ > focus.
134
+
126
135
  ### Custom Target
127
136
 
128
137
  It is common to have a custom target for your popup. Use the `as` prop to use your custom component.
@@ -138,6 +147,12 @@ requires a `label` prop.
138
147
 
139
148
  <ExampleCodeBlock code={CustomTarget} />
140
149
 
150
+ > **Accessibility Note**: Custom targets must be keyboard focusable, otherwise users will not be
151
+ > able to access the popup. Bear in mind that click handlers only work with the keyboard when
152
+ > applied to HTML `<button>` elements and it is **strongly recommended** to base your custom target
153
+ > on a `<button>` element. Otherwise, you will be required to build in your own custom keyboard
154
+ > event handlers for invoking the popup.
155
+
141
156
  ### Full Screen API
142
157
 
143
158
  By default, popups are created as children of the `document.body` element, but the `PopupStack`
@@ -175,6 +190,18 @@ The Popup component automatically handles right-to-left rendering.
175
190
 
176
191
  <ExampleCodeBlock code={RTL} />
177
192
 
193
+ ## Accessibility
194
+
195
+ Popup content is usually portaled to the bottom of the `document.body`, which can affect **reading
196
+ order for screen readers** and **keyboard focus order**. For more information about Popup
197
+ accessibility, check out our documentation at
198
+ [Guides > Accessibility > Inline Popups](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-inline-popups--docs).
199
+
200
+ - For non-modal dialogs with `aria-owns` built-in to improve reading order for screen readers (that
201
+ support it), check out the [**Dialog**](/components/popups/dialog/) component.
202
+ - For modal dialogs with built-in overlays and focus traps, check out the
203
+ [**Modal**](/components/popups/modal/) component.
204
+
178
205
  ## Component API
179
206
 
180
207
  <>
@@ -6,8 +6,24 @@ import {
6
6
  useCloseOnOutsideClick,
7
7
  useInitialFocus,
8
8
  useReturnFocus,
9
+ useFocusRedirect,
9
10
  } from '@workday/canvas-kit-react/popup';
10
11
  import {Box, Flex} from '@workday/canvas-kit-react/layout';
12
+ import {createStyles, px2rem} from '@workday/canvas-kit-styling';
13
+ import {system} from '@workday/canvas-tokens-web';
14
+
15
+ const cardStyles = createStyles({
16
+ width: px2rem(400),
17
+ });
18
+
19
+ const bodyStyles = createStyles({
20
+ marginY: system.space.zero,
21
+ });
22
+
23
+ const flexStyles = createStyles({
24
+ gap: system.space.x4,
25
+ padding: system.space.x2,
26
+ });
11
27
 
12
28
  export default () => {
13
29
  const model = usePopupModel();
@@ -16,6 +32,7 @@ export default () => {
16
32
  useCloseOnEscape(model);
17
33
  useInitialFocus(model);
18
34
  useReturnFocus(model);
35
+ useFocusRedirect(model);
19
36
 
20
37
  const handleDelete = () => {
21
38
  console.log('Delete Item');
@@ -25,15 +42,15 @@ export default () => {
25
42
  <Popup model={model}>
26
43
  <Popup.Target as={DeleteButton}>Delete Item</Popup.Target>
27
44
  <Popup.Popper placement="top">
28
- <Popup.Card width={400}>
45
+ <Popup.Card cs={cardStyles}>
29
46
  <Popup.CloseIcon aria-label="Close" />
30
47
  <Popup.Heading>Delete Item</Popup.Heading>
31
48
  <Popup.Body>
32
- <Box as="p" marginY="zero">
49
+ <Box as="p" cs={bodyStyles}>
33
50
  Are you sure you'd like to delete the item titled 'My Item'?
34
51
  </Box>
35
52
  </Popup.Body>
36
- <Flex gap="s" padding="xxs">
53
+ <Flex cs={flexStyles}>
37
54
  <Popup.CloseButton as={DeleteButton} onClick={handleDelete}>
38
55
  Delete
39
56
  </Popup.CloseButton>
@@ -11,6 +11,22 @@ import {
11
11
  usePopupModel,
12
12
  } from '@workday/canvas-kit-react/popup';
13
13
  import {Box, Flex} from '@workday/canvas-kit-react/layout';
14
+ import {createStyles, px2rem} from '@workday/canvas-kit-styling';
15
+ import {system} from '@workday/canvas-tokens-web';
16
+ import {useUniqueId} from '@workday/canvas-kit-react/common';
17
+
18
+ const cardStyles = createStyles({
19
+ width: px2rem(400),
20
+ });
21
+
22
+ const bodyStyles = createStyles({
23
+ marginY: system.space.zero,
24
+ });
25
+
26
+ const flexStyles = createStyles({
27
+ gap: system.space.x4,
28
+ padding: system.space.x2,
29
+ });
14
30
 
15
31
  export default () => {
16
32
  const model = usePopupModel();
@@ -25,34 +41,33 @@ export default () => {
25
41
  console.log('Delete Item');
26
42
  };
27
43
 
28
- const popupId = 'popup-test-id';
44
+ const popupId = useUniqueId();
29
45
  const visible = model.state.visibility !== 'hidden';
30
46
  React.useLayoutEffect(() => {
31
47
  if (visible && model.state.stackRef.current) {
32
48
  model.state.stackRef.current.setAttribute('id', popupId);
33
49
  }
34
- }, [model.state.stackRef, visible]);
50
+ }, [model.state.stackRef, visible, popupId]);
35
51
 
36
52
  return (
37
53
  <Popup model={model}>
38
- <Flex gap="s">
54
+ <Flex cs={flexStyles}>
39
55
  <Popup.Target as={DeleteButton}>Delete Item</Popup.Target>
40
- <div aria-owns={popupId} style={{position: 'absolute'}} />
56
+ <div aria-owns={popupId} style={{position: 'absolute'}}></div>
41
57
  <Popup.Popper>
42
- <Popup.Card width={400}>
58
+ <Popup.Card cs={cardStyles}>
43
59
  <Popup.CloseIcon aria-label="Close" />
44
60
  <Popup.Heading>Delete Item</Popup.Heading>
45
61
  <Popup.Body>
46
- <Box as="p" marginY="zero">
62
+ <Box as="p" cs={bodyStyles}>
47
63
  Are you sure you'd like to delete the item titled 'My Item'?
48
64
  </Box>
49
65
  </Popup.Body>
50
- <Flex gap="s" padding="xxs">
66
+ <Flex cs={flexStyles}>
51
67
  <Popup.CloseButton as={DeleteButton} onClick={handleDelete}>
52
68
  Delete
53
69
  </Popup.CloseButton>
54
- {/* Disabled elements should not be focusable and focus should move to the next focusable element */}
55
- <Popup.CloseButton disabled>Cancel</Popup.CloseButton>
70
+ <Popup.CloseButton>Cancel</Popup.CloseButton>
56
71
  </Flex>
57
72
  </Popup.Card>
58
73
  </Popup.Popper>
@@ -7,10 +7,37 @@ import {
7
7
  usePopupModel,
8
8
  useInitialFocus,
9
9
  useReturnFocus,
10
+ useFocusRedirect,
10
11
  } from '@workday/canvas-kit-react/popup';
11
- import {Box, Flex} from '@workday/canvas-kit-react/layout';
12
+ import {Flex} from '@workday/canvas-kit-react/layout';
13
+ import {FormField} from '@workday/canvas-kit-react/form-field';
14
+ import {PrimaryButton} from '@workday/canvas-kit-react/button';
15
+ import {TextInput} from '@workday/canvas-kit-react/text-input';
16
+ import {Text} from '@workday/canvas-kit-react/text';
17
+ import {createStyles, px2rem} from '@workday/canvas-kit-styling';
18
+ import {system} from '@workday/canvas-tokens-web';
19
+ import {useUniqueId} from '@workday/canvas-kit-react/common';
12
20
 
13
- export default () => {
21
+ const cardStyles = createStyles({
22
+ width: px2rem(400),
23
+ });
24
+
25
+ const bodyStyles = createStyles({
26
+ marginY: system.space.zero,
27
+ });
28
+
29
+ const flexStyles = createStyles({
30
+ gap: system.space.x4,
31
+ padding: system.space.x2,
32
+ });
33
+
34
+ const columnStyles = createStyles({
35
+ gap: system.space.x4,
36
+ alignItems: 'flex-start',
37
+ });
38
+
39
+ const InitialFocusOnButton = () => {
40
+ const messageId = useUniqueId();
14
41
  const initialFocusRef = React.useRef(null);
15
42
  const model = usePopupModel({
16
43
  initialFocusRef,
@@ -20,21 +47,21 @@ export default () => {
20
47
  useCloseOnEscape(model);
21
48
  useInitialFocus(model);
22
49
  useReturnFocus(model);
50
+ useFocusRedirect(model);
23
51
 
24
52
  return (
25
53
  <Popup model={model}>
26
- <Popup.Target>Send Message</Popup.Target>
54
+ <Popup.Target>Initial focus: OK button</Popup.Target>
27
55
  <Popup.Popper placement={'bottom'}>
28
- <Popup.Card width={400}>
29
- <Popup.CloseIcon aria-label="Close" />
56
+ <Popup.Card cs={cardStyles} aria-describedby={messageId}>
30
57
  <Popup.Heading>Confirmation</Popup.Heading>
31
58
  <Popup.Body>
32
- <Box as="p" marginY="zero" id="popup-message">
59
+ <Text cs={bodyStyles} id={messageId}>
33
60
  Your message has been sent!
34
- </Box>
61
+ </Text>
35
62
  </Popup.Body>
36
- <Flex gap="s" padding="xxs">
37
- <Popup.CloseButton ref={initialFocusRef} aria-describedby="popup-message">
63
+ <Flex cs={flexStyles}>
64
+ <Popup.CloseButton as={PrimaryButton} ref={initialFocusRef}>
38
65
  OK
39
66
  </Popup.CloseButton>
40
67
  </Flex>
@@ -43,3 +70,80 @@ export default () => {
43
70
  </Popup>
44
71
  );
45
72
  };
73
+
74
+ const InitialFocusOnTextInput = () => {
75
+ const descriptionId = useUniqueId();
76
+ const initialFocusRef = React.useRef<HTMLInputElement>(null);
77
+ const model = usePopupModel({
78
+ initialFocusRef,
79
+ });
80
+
81
+ useCloseOnOutsideClick(model);
82
+ useCloseOnEscape(model);
83
+ useInitialFocus(model);
84
+ useReturnFocus(model);
85
+ useFocusRedirect(model);
86
+
87
+ return (
88
+ <Popup model={model}>
89
+ <Popup.Target>Initial focus: text input</Popup.Target>
90
+ <Popup.Popper placement={'bottom'}>
91
+ <Popup.Card cs={cardStyles} aria-describedby={descriptionId}>
92
+ <Popup.Heading>Quick reply</Popup.Heading>
93
+ <Popup.Body>
94
+ <FormField>
95
+ <FormField.Label>Message</FormField.Label>
96
+ <FormField.Input as={TextInput} ref={initialFocusRef} />
97
+ </FormField>
98
+ </Popup.Body>
99
+ <Flex cs={flexStyles}>
100
+ <Popup.CloseButton as={PrimaryButton}>Send</Popup.CloseButton>
101
+ <Popup.CloseButton>Cancel</Popup.CloseButton>
102
+ </Flex>
103
+ </Popup.Card>
104
+ </Popup.Popper>
105
+ </Popup>
106
+ );
107
+ };
108
+
109
+ const InitialFocusOnHeading = () => {
110
+ const headingFocusRef = React.useRef<HTMLHeadingElement>(null);
111
+ const model = usePopupModel({
112
+ initialFocusRef: headingFocusRef,
113
+ });
114
+
115
+ useCloseOnOutsideClick(model);
116
+ useCloseOnEscape(model);
117
+ useInitialFocus(model);
118
+ useReturnFocus(model);
119
+ useFocusRedirect(model);
120
+
121
+ return (
122
+ <Popup model={model}>
123
+ <Popup.Target>Initial focus: heading</Popup.Target>
124
+ <Popup.Popper placement={'bottom'}>
125
+ <Popup.Card cs={cardStyles}>
126
+ <Popup.Heading ref={headingFocusRef} tabIndex={-1}>
127
+ Important notice
128
+ </Popup.Heading>
129
+ <Popup.Body>
130
+ <Text cs={bodyStyles}>Review the summary below before continuing.</Text>
131
+ </Popup.Body>
132
+ <Flex cs={flexStyles}>
133
+ <Popup.CloseButton as={PrimaryButton}>Continue</Popup.CloseButton>
134
+ </Flex>
135
+ </Popup.Card>
136
+ </Popup.Popper>
137
+ </Popup>
138
+ );
139
+ };
140
+
141
+ export default () => {
142
+ return (
143
+ <Flex cs={columnStyles}>
144
+ <InitialFocusOnButton />
145
+ <InitialFocusOnTextInput />
146
+ <InitialFocusOnHeading />
147
+ </Flex>
148
+ );
149
+ };