@workday/canvas-kit-docs 17.0.0-alpha.0605-next.0 → 17.0.0-alpha.0616-next.0

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.
@@ -1491,6 +1491,26 @@ module.exports = {specifications: [
1491
1491
  {
1492
1492
  "type": "it",
1493
1493
  "name": "should not have any axe errors"
1494
+ },
1495
+ {
1496
+ "type": "describe",
1497
+ "name": "when the form is submitted with an empty required select",
1498
+ "children": [
1499
+ {
1500
+ "type": "it",
1501
+ "name": "should focus the first invalid field"
1502
+ }
1503
+ ]
1504
+ },
1505
+ {
1506
+ "type": "describe",
1507
+ "name": "when a later required field is empty",
1508
+ "children": [
1509
+ {
1510
+ "type": "it",
1511
+ "name": "should focus the first invalid field"
1512
+ }
1513
+ ]
1494
1514
  }
1495
1515
  ]
1496
1516
  }
@@ -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": "16.1.1",
22
- "@workday/canvas-kit-preview-react": "16.1.1",
23
- "@workday/canvas-kit-react": "16.1.1",
24
- "@workday/canvas-kit-react-fonts": "^16.1.1",
25
- "@workday/canvas-kit-styling": "16.1.1",
21
+ "@workday/canvas-kit-labs-react": "16.1.3",
22
+ "@workday/canvas-kit-preview-react": "16.1.3",
23
+ "@workday/canvas-kit-react": "16.1.3",
24
+ "@workday/canvas-kit-react-fonts": "^16.1.3",
25
+ "@workday/canvas-kit-styling": "16.1.3",
26
26
  "@workday/canvas-system-icons-web": "^5.0.3",
27
27
  "@workday/canvas-expressive-icons-web": "1.0.1",
28
28
  "@workday/canvas-tokens-web": "4.4.0-beta.11"
@@ -19,11 +19,11 @@ export const packageJSONFile = `{
19
19
  "@emotion/react": "11.11.4",
20
20
  "@types/react": "18.2.60",
21
21
  "@types/react-dom": "18.2.19",
22
- "@workday/canvas-kit-labs-react": "16.1.1",
23
- "@workday/canvas-kit-preview-react": "16.1.1",
24
- "@workday/canvas-kit-react": "16.1.1",
25
- "@workday/canvas-kit-react-fonts": "^16.1.1",
26
- "@workday/canvas-kit-styling": "16.1.1",
22
+ "@workday/canvas-kit-labs-react": "16.1.3",
23
+ "@workday/canvas-kit-preview-react": "16.1.3",
24
+ "@workday/canvas-kit-react": "16.1.3",
25
+ "@workday/canvas-kit-react-fonts": "^16.1.3",
26
+ "@workday/canvas-kit-styling": "16.1.3",
27
27
  "@workday/canvas-system-icons-web": "^5.0.3",
28
28
  "@workday/canvas-expressive-icons-web": "1.0.1",
29
29
  "@workday/canvas-tokens-web": "4.4.0-beta.11"
@@ -86,7 +86,9 @@ Further reading:
86
86
  ```
87
87
 
88
88
  Check out our [Aria Live Region Examples](?path=/docs/guides-accessibility-aria-live-regions--docs)
89
- for more detailed information about this topic.
89
+ for more detailed information about this topic. For labels, error text, `aria-invalid`, and focus
90
+ after a failed submit, see
91
+ [Form Validation](?path=/docs/guides-accessibility-form-validation--docs).
90
92
 
91
93
  Sometimes, things change on a webpage without the whole page reloading. Think of a live sports score
92
94
  that updates every few seconds or a search result that appears as you type. For users of screen
@@ -1,7 +1,39 @@
1
1
  import {ExampleCodeBlock} from '@workday/canvas-kit-docs';
2
+
2
3
  import TextInputWithReactHookForm from './examples/TextInputWithReactHookForm';
3
4
 
4
5
 
5
- ## Uncontrolled Text Inputs Using React Hook Form
6
+ # Form Validation with React Hook Form and Yup
7
+
8
+ Accessible form validation is not a library feature. It is a composition of **labels**, **error text
9
+ tied to the field**, **`aria-invalid`**, and **keyboard focus**. Canvas Kit
10
+ [`FormField`](?path=/docs/components-inputs-form-field--docs#accessibility) owns the first three
11
+ when you use its subcomponents. The application owns when validation runs and where focus goes after
12
+ submit.
13
+
14
+ 1. **Identify every field, including required ones** Set `autoComplete` with a
15
+ [standard token](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill)
16
+ when the field collects a known personal data type (`username`, `current-password`, `email`,
17
+ `name`). That supports
18
+ [WCAG 1.3.5 Identify Input Purpose](https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html).
19
+ 2. **Move focus to the first invalid field after submit** Failed submit must not leave focus on the
20
+ submit button. Keyboard and screen-reader users need to land on the first field that is still
21
+ invalid, in **DOM order**.
22
+ 3. **Choose when to validate** Prefer validating **on submit**, then on **blur / touched** for
23
+ fields the user has already visited (`mode: 'onTouched'` in this example). Validating on every
24
+ keystroke while the field is still incomplete is noisy and can announce `aria-invalid` too early.
25
+ (Relevant criteria:
26
+ [3.3.1 Error Identification](https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html),
27
+ [3.3.3 Error Suggestion](https://www.w3.org/WAI/WCAG22/Understanding/error-suggestion.html).)
28
+ 4. **Do not use a live region for every field error** Field errors belong on the field
29
+ (`aria-invalid` + `aria-describedby`). They are announced when the field **receives focus**. A
30
+ page-level live region that restates every invalid field on submit is easy to overdo. Live
31
+ announcements cannot be replayed, can interrupt, and have uneven support. If you add a form-level
32
+ summary, keep it short, do not duplicate every message, and read
33
+ [ARIA Live Regions](?path=/docs/guides-accessibility-aria-live-regions--docs) first.
34
+
35
+ This example wires `FormField`, `TextInput`, `Select`, and `InputGroup` to
36
+ [React Hook Form](https://www.react-hook-form.com/) with [Yup](https://github.com/jquense/yup). The
37
+ same practices apply if you use Formik, a server round-trip, or hand-rolled state.
6
38
 
7
39
  <ExampleCodeBlock code={TextInputWithReactHookForm} />
@@ -1,13 +1,18 @@
1
1
  import React from 'react';
2
- import {FieldErrorsImpl, useForm} from 'react-hook-form';
3
- import {SchemaOf, object, string} from 'yup';
2
+ import {FieldErrors, Resolver, useForm} from 'react-hook-form';
3
+ import {SchemaOf, ValidationError, object, string} from 'yup';
4
4
 
5
5
  import {PrimaryButton, TertiaryButton} from '@workday/canvas-kit-react/button';
6
- import {useUniqueId} from '@workday/canvas-kit-react/common';
7
- import {FormField} from '@workday/canvas-kit-react/form-field';
6
+ import {ErrorType, useUniqueId} from '@workday/canvas-kit-react/common';
7
+ import {
8
+ FormField,
9
+ useFormFieldInput,
10
+ useFormFieldModel,
11
+ } from '@workday/canvas-kit-react/form-field';
8
12
  import {Flex} from '@workday/canvas-kit-react/layout';
9
13
  import {Select} from '@workday/canvas-kit-react/select';
10
- import {TextInput} from '@workday/canvas-kit-react/text-input';
14
+ import {InputGroup, TextInput} from '@workday/canvas-kit-react/text-input';
15
+ import {Tooltip} from '@workday/canvas-kit-react/tooltip';
11
16
  import {createStyles, px2rem} from '@workday/canvas-kit-styling';
12
17
  import {visibleIcon, visibleStrikethroughIcon} from '@workday/canvas-system-icons-web';
13
18
  import {system} from '@workday/canvas-tokens-web';
@@ -18,31 +23,30 @@ const styles = createStyles({
18
23
  alignItems: 'flex-start',
19
24
  });
20
25
 
21
- type YupValidationResolver = <T extends {}>(
22
- validationSchema: SchemaOf<T>
23
- ) => (data: T) => Promise<{values: T; errors: {}} | {values: {}; errors: FieldErrorsImpl<T>}>;
24
-
25
- const useYupValidationResolver: YupValidationResolver = validationSchema => {
26
- return React.useCallback(
26
+ const useYupValidationResolver = <T extends {}>(validationSchema: SchemaOf<T>): Resolver<T> => {
27
+ return React.useCallback<Resolver<T>>(
27
28
  async data => {
28
29
  try {
29
- const values = await validationSchema.validate(data, {abortEarly: false});
30
- return {values, errors: {}};
31
- } catch (errors) {
32
- return {
33
- values: {},
34
- //@ts-ignore
35
- errors: errors.inner.reduce(
36
- (allErrors, currentError) => ({
37
- ...allErrors,
38
- [currentError.path]: {
39
- type: currentError.type ?? 'validation',
40
- message: currentError.message,
41
- },
42
- }),
43
- {}
44
- ),
45
- };
30
+ await validationSchema.validate(data, {abortEarly: false});
31
+ return {values: data, errors: {}};
32
+ } catch (error) {
33
+ // Yup throws a `ValidationError` for failed validation. Anything else is a real error.
34
+ if (!ValidationError.isError(error)) {
35
+ throw error;
36
+ }
37
+
38
+ const errors = error.inner.reduce<Record<string, {type: string; message: string}>>(
39
+ (allErrors, currentError) => ({
40
+ ...allErrors,
41
+ [currentError.path ?? '']: {
42
+ type: currentError.type ?? 'validation',
43
+ message: currentError.message,
44
+ },
45
+ }),
46
+ {}
47
+ );
48
+
49
+ return {values: {}, errors: errors as FieldErrors<T>};
46
50
  }
47
51
  },
48
52
  [validationSchema]
@@ -74,6 +78,9 @@ const options = [
74
78
  ];
75
79
 
76
80
  export default () => {
81
+ const formRef = React.useRef<HTMLFormElement>(null);
82
+ const [showPassword, setShowPassword] = React.useState(false);
83
+ const [submitAttempt, setSubmitAttempt] = React.useState(0);
77
84
  const {
78
85
  handleSubmit,
79
86
  register,
@@ -84,19 +91,36 @@ export default () => {
84
91
  password: 'foobarbaz',
85
92
  role: '',
86
93
  },
87
- resolver: useYupValidationResolver(validationSchema),
94
+ resolver: useYupValidationResolver<LoginSchema>(validationSchema),
88
95
  mode: 'onTouched',
96
+ shouldFocusError: true,
89
97
  });
90
98
 
91
- const onSubmit = handleSubmit(values => {
92
- setShowPassword(false);
93
- // Send data to server
94
- setTimeout(() => {
95
- alert(JSON.stringify(values, null, 2));
96
- }, 0);
97
- });
99
+ const onSubmit = handleSubmit(
100
+ values => {
101
+ setShowPassword(false);
102
+ // Send data to server
103
+ setTimeout(() => {
104
+ alert(JSON.stringify(values, null, 2));
105
+ }, 0);
106
+ },
107
+ () => {
108
+ // Count failed submits so we can move focus after FormField paints `aria-invalid`.
109
+ setSubmitAttempt(count => count + 1);
110
+ }
111
+ );
112
+
113
+ React.useEffect(() => {
114
+ if (submitAttempt === 0) {
115
+ return;
116
+ }
117
+
118
+ const firstInvalidField = formRef.current?.querySelector<HTMLElement>(
119
+ '[aria-invalid="true"]:not([aria-hidden="true"]):not([tabindex="-1"])'
120
+ );
121
+ firstInvalidField?.focus();
122
+ }, [submitAttempt, errors]);
98
123
 
99
- const [showPassword, setShowPassword] = React.useState(false);
100
124
  const passwordId = useUniqueId();
101
125
  const passwordRef = React.useRef<HTMLInputElement | null>(null);
102
126
  const {ref: passwordCallbackRef, ...passwordRegistration} = register('password');
@@ -104,8 +128,15 @@ export default () => {
104
128
  passwordCallbackRef(ref);
105
129
  passwordRef.current = ref;
106
130
  };
131
+ // `InputGroup` renders the input, so hoist the model to keep the label, hint, and error wiring.
132
+ const passwordModel = useFormFieldModel({
133
+ id: passwordId,
134
+ isRequired: true,
135
+ error: errors.password ? 'error' : undefined,
136
+ });
137
+ const passwordInputProps = useFormFieldInput(passwordModel);
107
138
  return (
108
- <form onSubmit={onSubmit} action="." noValidate={true}>
139
+ <form ref={formRef} onSubmit={onSubmit} action="." noValidate={true}>
109
140
  <Flex cs={styles}>
110
141
  <FormField
111
142
  orientation="vertical"
@@ -143,35 +174,33 @@ export default () => {
143
174
  </FormField.Field>
144
175
  <FormField.Hint>{errors.email?.message}</FormField.Hint>
145
176
  </FormField>
146
- <FormField
147
- orientation="vertical"
148
- id={passwordId}
149
- isRequired={true}
150
- error={!!errors.password ? 'error' : undefined}
151
- >
177
+ <FormField model={passwordModel} orientation="vertical">
152
178
  <FormField.Label>Password</FormField.Label>
153
- <Flex cs={{gap: system.gap.md}}>
154
- <FormField.Field>
155
- <FormField.Input
156
- as={TextInput}
157
- {...passwordRegistration}
158
- type={showPassword ? 'text' : 'password'}
159
- autoComplete="current-password"
160
- spellCheck={false}
161
- ref={combinePasswordRef}
162
- />
163
- </FormField.Field>
164
- <TertiaryButton
165
- type="button"
166
- icon={showPassword ? visibleStrikethroughIcon : visibleIcon}
167
- aria-label={showPassword ? 'Hide Password' : 'Show Password'}
168
- aria-controls={`input-${passwordId}`}
169
- onClick={() => {
170
- setShowPassword(state => !state);
171
- passwordRef.current?.focus();
172
- }}
179
+ <FormField.Field as={InputGroup}>
180
+ <InputGroup.Input
181
+ {...passwordInputProps}
182
+ {...passwordRegistration}
183
+ error={errors.password ? ErrorType.Error : undefined}
184
+ type={showPassword ? 'text' : 'password'}
185
+ autoComplete="current-password"
186
+ spellCheck={false}
187
+ ref={combinePasswordRef}
173
188
  />
174
- </Flex>
189
+ <InputGroup.InnerEnd>
190
+ <Tooltip title={showPassword ? 'Hide Password' : 'Show Password'}>
191
+ <TertiaryButton
192
+ type="button"
193
+ size="small"
194
+ icon={showPassword ? visibleStrikethroughIcon : visibleIcon}
195
+ aria-controls={`input-${passwordId}`}
196
+ onClick={() => {
197
+ setShowPassword(state => !state);
198
+ passwordRef.current?.focus();
199
+ }}
200
+ />
201
+ </Tooltip>
202
+ </InputGroup.InnerEnd>
203
+ </FormField.Field>
175
204
  <FormField.Hint>{errors.password?.message || passwordHint}</FormField.Hint>
176
205
  </FormField>
177
206
 
@@ -4,8 +4,24 @@ import Density from './examples/Density';
4
4
 
5
5
  # Canvas Kit Density Examples
6
6
 
7
- Although we currently don't support a density system, it's common practice to provide varying
8
- options to users, especially in the context of forms. Below is an example of how you can style our
9
- from elements to achieve the desired density.
7
+ Canvas Kit does not ship a density system. Forms often still need compact, default, and
8
+ comfortable spacing. This example shows how to style Canvas Kit form elements with
9
+ `createStencil` to get three densities.
10
+
11
+ Density in this demo changes:
12
+
13
+ - **Input height and padding** (`system.size.sm` / `md` / `md + 4px`)
14
+ - **Label and input type** (`subtext.medium` / `subtext.large` / `body.small`)
15
+ - **Space between fields** (`system.gap.md` / `lg` / `xl`)
16
+ - **Space between a label and its input** (tighter when labels are vertical)
17
+
18
+ | Density | Input height | Field spacing | Type |
19
+ | ------- | ------------ | ------------- | ---- |
20
+ | High | 32px | 16px | `subtext.medium` |
21
+ | Medium | 40px | 24px | `subtext.large` |
22
+ | Low | 48px | 32px | `body.small` |
23
+
24
+ `Switch` does not expose a size API, so its dimensions stay the same across densities. Only its
25
+ label type and surrounding field spacing change.
10
26
 
11
27
  <ExampleCodeBlock code={Density} />
@@ -1,30 +1,38 @@
1
1
  import React from 'react';
2
2
 
3
- import {FormField, FormFieldGroup} from '@workday/canvas-kit-react/form-field';
3
+ import {Switch} from '@workday/canvas-kit-preview-react/switch';
4
+ import {useUniqueId} from '@workday/canvas-kit-react/common';
5
+ import {FormField} from '@workday/canvas-kit-react/form-field';
4
6
  import {SegmentedControl} from '@workday/canvas-kit-react/segmented-control';
5
7
  import {Select} from '@workday/canvas-kit-react/select';
6
- import {Switch} from '@workday/canvas-kit-react/switch';
7
- import {Heading, Text} from '@workday/canvas-kit-react/text';
8
+ import {Heading, Subtext, Text} from '@workday/canvas-kit-react/text';
8
9
  import {TextInput} from '@workday/canvas-kit-react/text-input';
9
10
  import {calc, createStencil, createStyles, px2rem} from '@workday/canvas-kit-styling';
10
11
  import {base, system} from '@workday/canvas-tokens-web';
11
12
 
13
+ type DensityLevel = 'high' | 'medium' | 'low';
14
+ type LabelOrientation = 'vertical' | 'horizontalStart' | 'horizontalEnd';
15
+ type ContainerAlignment = 'left' | 'center';
16
+ type DensityTypeLevel = 'subtext.medium' | 'subtext.large' | 'body.small';
17
+
18
+ const densitySummary: Record<
19
+ DensityLevel,
20
+ {inputHeight: string; stackGap: string; typeLevel: DensityTypeLevel}
21
+ > = {
22
+ high: {inputHeight: '32px', stackGap: '16px', typeLevel: 'subtext.medium'},
23
+ medium: {inputHeight: '40px', stackGap: '24px', typeLevel: 'subtext.large'},
24
+ low: {inputHeight: '48px', stackGap: '32px', typeLevel: 'body.small'},
25
+ };
26
+
12
27
  const formStyles = createStyles({
13
28
  margin: `${px2rem(12)} ${'0'}`,
14
29
  maxWidth: px2rem(600),
15
30
  minWidth: '0',
16
31
  });
17
32
 
18
- const formFieldGroupListStyles = createStyles({
19
- display: 'inline-flex',
20
- flexDirection: 'row',
21
- flexWrap: 'wrap',
22
- });
23
-
24
33
  const sideBySideInputs = createStencil({
25
34
  base: {
26
35
  display: 'inline-flex',
27
- gap: system.gap.sm,
28
36
  justifyContent: 'space-between',
29
37
  },
30
38
  modifiers: {
@@ -39,52 +47,21 @@ const sideBySideInputs = createStencil({
39
47
  },
40
48
  vertical: {
41
49
  display: 'inline-flex',
50
+ flexDirection: 'row',
42
51
  },
43
52
  },
44
53
  density: {
45
- high: {},
46
- medium: {},
47
- low: {},
48
- },
49
- },
50
- compound: [
51
- {
52
- modifiers: {labelOrientation: 'horizontalStart', density: 'high'},
53
- styles: {
54
- gap: system.gap.md,
55
- },
56
- },
57
- {
58
- modifiers: {labelOrientation: 'horizontalStart', density: 'medium'},
59
- styles: {
60
- gap: system.gap.lg,
61
- },
62
- },
63
- {
64
- modifiers: {labelOrientation: 'horizontalStart', density: 'low'},
65
- styles: {
66
- gap: system.gap.xl,
67
- },
68
- },
69
- {
70
- modifiers: {labelOrientation: 'horizontalEnd', density: 'high'},
71
- styles: {
54
+ high: {
72
55
  gap: system.gap.md,
73
56
  },
74
- },
75
- {
76
- modifiers: {labelOrientation: 'horizontalEnd', density: 'medium'},
77
- styles: {
57
+ medium: {
78
58
  gap: system.gap.lg,
79
59
  },
80
- },
81
- {
82
- modifiers: {labelOrientation: 'horizontalEnd', density: 'low'},
83
- styles: {
60
+ low: {
84
61
  gap: system.gap.xl,
85
62
  },
86
63
  },
87
- ],
64
+ },
88
65
  });
89
66
 
90
67
  const zipCodeInput = createStyles({
@@ -96,7 +73,10 @@ const zipCodeContainerStyles = createStyles({
96
73
  });
97
74
 
98
75
  const formFieldStencil = createStencil({
99
- base: {},
76
+ base: {
77
+ // FormField's built-in margin would double-space with the stack gap below.
78
+ margin: 0,
79
+ },
100
80
  modifiers: {
101
81
  density: {
102
82
  high: {
@@ -155,32 +135,6 @@ const formFieldStencil = createStencil({
155
135
  ],
156
136
  });
157
137
 
158
- const selectStencil = createStencil({
159
- base: {},
160
- modifiers: {
161
- density: {
162
- high: {
163
- height: system.size.sm,
164
- padding: `${system.padding.xxs} ${system.padding.xs}`,
165
- '& + div': {
166
- height: system.size.sm,
167
- },
168
- },
169
- medium: {
170
- height: system.size.md,
171
- padding: `${system.padding.xs}`,
172
- },
173
- low: {
174
- height: calc.add(system.size.md, base.size100),
175
- padding: `${system.padding.sm} ${system.padding.xs}`,
176
- '& + div': {
177
- height: calc.add(system.size.md, base.size100),
178
- },
179
- },
180
- },
181
- },
182
- });
183
-
184
138
  const inputStencil = createStencil({
185
139
  base: {
186
140
  minWidth: px2rem(200),
@@ -190,29 +144,25 @@ const inputStencil = createStencil({
190
144
  high: {
191
145
  height: system.size.sm,
192
146
  padding: `${system.padding.xxs} ${system.padding.xs}`,
147
+ fontSize: system.legacy.fontSize.subtext.md,
148
+ lineHeight: system.legacy.lineHeight.subtext.md,
193
149
  },
194
150
  medium: {
195
151
  height: system.size.md,
196
152
  padding: `${system.padding.xs}`,
153
+ fontSize: system.legacy.fontSize.subtext.lg,
154
+ lineHeight: system.legacy.lineHeight.subtext.lg,
197
155
  },
198
-
199
156
  low: {
200
157
  height: calc.add(system.size.md, base.size100),
201
158
  padding: `${system.padding.sm} ${system.padding.xs}`,
159
+ fontSize: system.legacy.fontSize.body.sm,
160
+ lineHeight: system.legacy.lineHeight.body.sm,
202
161
  },
203
162
  },
204
163
  },
205
164
  });
206
165
 
207
- const creditCardInputStencil = createStencil({
208
- extends: inputStencil,
209
- base: {
210
- width: calc.add(system.size.md, system.size.md),
211
- minWidth: calc.add(system.size.md, system.size.md),
212
- textAlign: 'center',
213
- },
214
- });
215
-
216
166
  const flexContainerStencil = createStencil({
217
167
  base: {
218
168
  display: 'flex',
@@ -263,37 +213,44 @@ const optionItemStyles = createStyles({
263
213
  maxWidth: 'fit-content',
264
214
  });
265
215
 
266
- // high = 32px height on inputs, space between inputs is 16px
267
- // medium 40px height on inputs, space between inputs is 24px
268
- // low = 48px height on inputs, space between inputs is 32px
269
-
270
216
  export default () => {
271
- const [density, setDensity] = React.useState<'high' | 'medium' | 'low'>('medium');
272
- const [containerAlignment, setContainerAlignment] = React.useState<'left' | 'center'>('left');
273
- const [labelOrientation, setLabelOrientation] = React.useState<
274
- 'vertical' | 'horizontalStart' | 'horizontalEnd'
275
- >('vertical');
217
+ const [density, setDensity] = React.useState<DensityLevel>('medium');
218
+ const [containerAlignment, setContainerAlignment] = React.useState<ContainerAlignment>('left');
219
+ const [labelOrientation, setLabelOrientation] = React.useState<LabelOrientation>('vertical');
220
+ const [fastShipping, setFastShipping] = React.useState(false);
221
+ const densityLabelId = useUniqueId();
222
+ const containerAlignmentLabelId = useUniqueId();
223
+ const labelOrientationLabelId = useUniqueId();
224
+
225
+ const handleDensity = (data: {id: string}) => {
226
+ setDensity(data.id as DensityLevel);
227
+ };
276
228
 
277
- const handleDensity = data => {
278
- setDensity(data.id);
229
+ const handleContainerAlignment = (data: {id: string}) => {
230
+ setContainerAlignment(data.id as ContainerAlignment);
279
231
  };
280
232
 
281
- const handleContainerAlignment = data => {
282
- setContainerAlignment(data.id);
233
+ const handleLabelOrientation = (data: {id: string}) => {
234
+ setLabelOrientation(data.id as LabelOrientation);
283
235
  };
284
236
 
285
- const handleLabelOrientation = data => {
286
- setLabelOrientation(data.id);
237
+ const handleFastShipping = (event: React.ChangeEvent<HTMLInputElement>) => {
238
+ setFastShipping(event.target.checked);
287
239
  };
288
240
 
241
+ const labelTypeLevel = densitySummary[density].typeLevel;
242
+ const fieldCs = formFieldStencil({density, labelOrientation});
243
+ const inputCs = inputStencil({density});
244
+
289
245
  return (
290
- <div>
291
- <Heading size="small">Choose Your Density and Alignment</Heading>
246
+ <>
292
247
  <div className={optionStyles}>
293
248
  <div className={optionItemStyles}>
294
- <Text>Density</Text>
295
- <SegmentedControl onSelect={data => handleDensity(data)} size="small">
296
- <SegmentedControl.List aria-label="choose a density">
249
+ <Text as="h3" id={densityLabelId}>
250
+ Density
251
+ </Text>
252
+ <SegmentedControl initialValue={density} onSelect={handleDensity} size="small">
253
+ <SegmentedControl.List aria-label="Density" aria-labelledby={densityLabelId}>
297
254
  <SegmentedControl.Item data-id="high">High</SegmentedControl.Item>
298
255
  <SegmentedControl.Item data-id="medium">Medium</SegmentedControl.Item>
299
256
  <SegmentedControl.Item data-id="low">Low</SegmentedControl.Item>
@@ -301,9 +258,18 @@ export default () => {
301
258
  </SegmentedControl>
302
259
  </div>
303
260
  <div className={optionItemStyles}>
304
- <Text>Label Orientation</Text>
305
- <SegmentedControl onSelect={data => handleLabelOrientation(data)} size="small">
306
- <SegmentedControl.List aria-label="choose a label orientation">
261
+ <Text as="h3" id={labelOrientationLabelId}>
262
+ Label Orientation
263
+ </Text>
264
+ <SegmentedControl
265
+ initialValue={labelOrientation}
266
+ onSelect={handleLabelOrientation}
267
+ size="small"
268
+ >
269
+ <SegmentedControl.List
270
+ aria-label="Label Orientation"
271
+ aria-labelledby={labelOrientationLabelId}
272
+ >
307
273
  <SegmentedControl.Item data-id="vertical">Vertical</SegmentedControl.Item>
308
274
  <SegmentedControl.Item data-id="horizontalStart">
309
275
  Horizontal Start
@@ -313,131 +279,92 @@ export default () => {
313
279
  </SegmentedControl>
314
280
  </div>
315
281
  <div className={optionItemStyles}>
316
- <Text>Container Alignment</Text>
317
- <SegmentedControl onSelect={data => handleContainerAlignment(data)} size="small">
318
- <SegmentedControl.List aria-label="choose a density">
282
+ <Text as="h3" id={containerAlignmentLabelId}>
283
+ Container Alignment
284
+ </Text>
285
+ <SegmentedControl
286
+ initialValue={containerAlignment}
287
+ onSelect={handleContainerAlignment}
288
+ size="small"
289
+ >
290
+ <SegmentedControl.List
291
+ aria-label="Container Alignment"
292
+ aria-labelledby={containerAlignmentLabelId}
293
+ >
319
294
  <SegmentedControl.Item data-id="left">Left</SegmentedControl.Item>
320
295
  <SegmentedControl.Item data-id="center">Center</SegmentedControl.Item>
321
296
  </SegmentedControl.List>
322
297
  </SegmentedControl>
323
298
  </div>
299
+ <Subtext size="large">
300
+ Input height: {densitySummary[density].inputHeight}. Field spacing:{' '}
301
+ {densitySummary[density].stackGap}. Type: {densitySummary[density].typeLevel}.
302
+ </Subtext>
324
303
  </div>
325
304
 
326
305
  <div {...containerAlignmentStencil({alignment: containerAlignment})}>
327
- <form action="#" className={formStyles}>
306
+ <form className={formStyles} autoComplete="off">
307
+ <Heading size="small">Shipping Address</Heading>
328
308
  <div {...flexContainerStencil({density})}>
329
- <FormField
330
- orientation={labelOrientation}
331
- grow
332
- cs={formFieldStencil({density, labelOrientation})}
333
- id="foo"
334
- >
335
- <FormField.Label>Choose Country</FormField.Label>
336
- <Select items={['Dominican Republic', 'Spain', 'United States']}>
337
- <FormField.Input
338
- cs={selectStencil({density})}
339
- placeholder="Choose a country"
340
- as={Select.Input}
341
- />
342
- <Select.Popper>
343
- <Select.Card>
344
- <Select.List>{item => <Select.Item>{item}</Select.Item>}</Select.List>
345
- </Select.Card>
346
- </Select.Popper>
347
- </Select>
348
- </FormField>
349
- <FormField
350
- grow
351
- orientation={labelOrientation}
352
- cs={formFieldStencil({density, labelOrientation})}
353
- >
354
- <FormField.Label>Full Name</FormField.Label>
355
- <FormField.Input as={TextInput} cs={inputStencil({density})} />
309
+ <FormField grow orientation={labelOrientation} cs={fieldCs}>
310
+ <FormField.Label typeLevel={labelTypeLevel}>Full Name</FormField.Label>
311
+ <FormField.Input as={TextInput} autoComplete="name" cs={inputCs} />
356
312
  </FormField>
357
- <FormField
358
- grow
359
- orientation={labelOrientation}
360
- cs={formFieldStencil({density, labelOrientation})}
361
- >
362
- <FormField.Label>Phone Number</FormField.Label>
363
- <FormField.Input as={TextInput} cs={inputStencil({density})} />
364
- </FormField>
365
- <FormField
366
- grow
367
- orientation={labelOrientation}
368
- cs={formFieldStencil({density, labelOrientation})}
369
- >
370
- <FormField.Label>Street Address</FormField.Label>
371
- <FormField.Input as={TextInput} cs={inputStencil({density})} />
372
- </FormField>
373
- <FormField
374
- grow
375
- orientation={labelOrientation}
376
- cs={formFieldStencil({density, labelOrientation})}
377
- >
378
- <FormField.Label>City</FormField.Label>
379
- <FormField.Input as={TextInput} cs={inputStencil({density})} />
313
+ <div {...sideBySideInputs({labelOrientation, density})}>
314
+ <FormField grow orientation={labelOrientation} cs={fieldCs}>
315
+ <FormField.Label typeLevel={labelTypeLevel}>Phone Number</FormField.Label>
316
+ <FormField.Input as={TextInput} autoComplete="tel" cs={inputCs} />
317
+ </FormField>
318
+ <FormField grow orientation={labelOrientation} cs={fieldCs}>
319
+ <FormField.Label typeLevel={labelTypeLevel}>Email</FormField.Label>
320
+ <FormField.Input as={TextInput} autoComplete="email" cs={inputCs} />
321
+ </FormField>
322
+ </div>
323
+ <FormField grow orientation={labelOrientation} cs={fieldCs}>
324
+ <FormField.Label typeLevel={labelTypeLevel}>Street Address</FormField.Label>
325
+ <FormField.Input as={TextInput} autoComplete="street-address" cs={inputCs} />
380
326
  </FormField>
381
- <div {...sideBySideInputs({labelOrientation: labelOrientation, density})}>
382
- <FormField
383
- grow
384
- orientation={labelOrientation}
385
- cs={formFieldStencil({density, labelOrientation})}
386
- >
387
- <FormField.Label>State</FormField.Label>
388
- <FormField.Input as={TextInput} cs={inputStencil({density})} />
327
+
328
+ <div {...sideBySideInputs({labelOrientation, density})}>
329
+ <FormField grow orientation={labelOrientation} cs={fieldCs}>
330
+ <FormField.Label typeLevel={labelTypeLevel}>City</FormField.Label>
331
+ <FormField.Input as={TextInput} autoComplete="address-level2" cs={inputCs} />
389
332
  </FormField>
390
- <FormField
391
- grow
392
- orientation={labelOrientation}
393
- cs={[formFieldStencil({density, labelOrientation}), zipCodeContainerStyles]}
394
- >
395
- <FormField.Label>Zip Code</FormField.Label>
396
- <FormField.Input cs={[inputStencil({density}), zipCodeInput]} as={TextInput} />
333
+ <FormField grow orientation={labelOrientation} cs={fieldCs}>
334
+ <FormField.Label typeLevel={labelTypeLevel}>State</FormField.Label>
335
+ <FormField.Input as={TextInput} autoComplete="address-level1" cs={inputCs} />
336
+ </FormField>
337
+ <FormField grow orientation={labelOrientation} cs={[fieldCs, zipCodeContainerStyles]}>
338
+ <FormField.Label typeLevel={labelTypeLevel}>Zip Code</FormField.Label>
339
+ <FormField.Input
340
+ cs={[inputCs, zipCodeInput]}
341
+ as={TextInput}
342
+ autoComplete="postal-code"
343
+ />
397
344
  </FormField>
398
345
  </div>
399
- <FormField
400
- orientation={labelOrientation}
401
- cs={formFieldStencil({density, labelOrientation})}
402
- >
403
- <FormField.Label>Enable Fast Shipping</FormField.Label>
346
+ <FormField grow orientation={labelOrientation} cs={fieldCs}>
347
+ <FormField.Label typeLevel={labelTypeLevel}>Country</FormField.Label>
404
348
  <FormField.Field>
405
- <FormField.Input as={Switch} />
349
+ <Select items={['Dominican Republic', 'Spain', 'United States']}>
350
+ <FormField.Input cs={inputCs} placeholder="Choose a country" as={Select.Input} />
351
+ <Select.Popper>
352
+ <Select.Card>
353
+ <Select.List>{item => <Select.Item>{item}</Select.Item>}</Select.List>
354
+ </Select.Card>
355
+ </Select.Popper>
356
+ </Select>
357
+ </FormField.Field>
358
+ </FormField>
359
+ <FormField orientation={labelOrientation} cs={fieldCs}>
360
+ <FormField.Label typeLevel={labelTypeLevel}>Enable Fast Shipping</FormField.Label>
361
+ <FormField.Field>
362
+ <FormField.Input as={Switch} checked={fastShipping} onChange={handleFastShipping} />
406
363
  </FormField.Field>
407
364
  </FormField>
408
- <FormFieldGroup
409
- cs={formFieldStencil({density, labelOrientation})}
410
- orientation={labelOrientation}
411
- >
412
- <FormFieldGroup.Label>Credit Card</FormFieldGroup.Label>
413
- <FormFieldGroup.Field>
414
- <FormFieldGroup.List cs={formFieldGroupListStyles}>
415
- <FormFieldGroup.Input
416
- as={TextInput}
417
- placeholder="XXXX"
418
- cs={creditCardInputStencil({density})}
419
- />
420
- <FormFieldGroup.Input
421
- as={TextInput}
422
- placeholder="XXXX"
423
- cs={creditCardInputStencil({density})}
424
- />
425
- <FormFieldGroup.Input
426
- as={TextInput}
427
- placeholder="XXXX"
428
- cs={creditCardInputStencil({density})}
429
- />
430
- <FormFieldGroup.Input
431
- cs={creditCardInputStencil({density})}
432
- placeholder="XXXX"
433
- as={TextInput}
434
- />
435
- </FormFieldGroup.List>
436
- </FormFieldGroup.Field>
437
- </FormFieldGroup>
438
365
  </div>
439
366
  </form>
440
367
  </div>
441
- </div>
368
+ </>
442
369
  );
443
370
  };
@@ -1,12 +1,12 @@
1
1
  import {ExampleCodeBlock, SymbolDoc} from '@workday/canvas-kit-docs';
2
2
 
3
- import StartIcon from './examples/StartIcon';
4
- import EndIcon from './examples/EndIcon';
5
3
  import Avatar from './examples/Avatar';
6
4
  import Depth from './examples/Depth';
7
- import RTL from './examples/RTL';
8
- import LongTitle from './examples/LongTitle';
5
+ import EndIcon from './examples/EndIcon';
9
6
  import HoistedModel from './examples/HoistedModel';
7
+ import LongTitle from './examples/LongTitle';
8
+ import RTL from './examples/RTL';
9
+ import StartIcon from './examples/StartIcon';
10
10
 
11
11
 
12
12
  # Canvas Kit Expandable
@@ -23,8 +23,8 @@ yarn add @workday/canvas-kit-react
23
23
 
24
24
  ### Start Icon
25
25
 
26
- For a basic expandable container with a chevron icon before the title, place`Expandable.Icon` before
27
- `Expandable.Title` as children of `Expandable.Target` and pass the `iconPosition` prop to
26
+ For a basic expandable container with a chevron icon before the title, place `Expandable.Icon`
27
+ before `Expandable.Title` as children of `Expandable.Target` and pass the `iconPosition` prop to
28
28
  `Expandable.Icon` with a value of `start`. `Expandable.Icon` will use a right chevron icon when
29
29
  collapsed and a down chevron icon when expanded.
30
30
 
@@ -46,18 +46,12 @@ To include an avatar image, `Expandable.Avatar` should be placed between `Expand
46
46
  to `Expandable.Icon` depending on whether the `Expandable.Icon` is placed before or after
47
47
  `Expandable.Title`.
48
48
 
49
- **Note:** If using an avatar image, do not also include an Icon component in addition to the Expandable.Icon.
50
- This component is meant to have either an avatar image or an icon in addition to the Expandable.Icon, or neither
51
- an avatar image nor an extra icon.
52
-
49
+ **Note:** If using an avatar image, do not also include an Icon component in addition to the
50
+ Expandable.Icon. This component is meant to have either an avatar image or an icon in addition to
51
+ the Expandable.Icon, or neither an avatar image nor an extra icon.
53
52
 
54
53
  <ExampleCodeBlock code={Avatar} />
55
54
 
56
- > **Accessibility Note:** In this situation, the Avatar is decorative and should not be announced to
57
- > screen readers. The `<Expandable.Avatar>` component has `isDecorative` set to `true` by default to
58
- > hide it from screen readers, as avatars in expandable headers are typically decorative when paired
59
- > with adjacent text.
60
-
61
55
  ### Right to Left (RTL)
62
56
 
63
57
  Expandable container has bidirectional support and should function as expected with RTL languages as
@@ -78,8 +72,6 @@ Long titles will wrap to the next line and increase the height of the container.
78
72
 
79
73
  <ExampleCodeBlock code={LongTitle} />
80
74
 
81
- You can also have direct access to the model if
82
-
83
75
  ### Hoisted Model
84
76
 
85
77
  If you need direct access to the model, you can hoist it with the `useExpandableModel` hook. In the
@@ -87,47 +79,161 @@ example below, we're hoisting the models to expand and collapse all three contai
87
79
 
88
80
  <ExampleCodeBlock code={HoistedModel} />
89
81
 
90
- > **Accessibility Note:** When using multiple Expandable Containers on a page, use the `as` prop to
91
- > render the `<Expandable.Content>` sub-component as an HTML `<section>` element. Then, use
92
- > `aria-labelledby` to reference the unique `id` of the `<Expandable.Title>` element. This practice
93
- > can be useful to screen reader users when multiple Expandable Containers are opened at one time
94
- > for uniquely describing the boundaries of the expandable content.
82
+ For the accordion composition (named regions on each panel), see
83
+ [Accessibility Requirements](#accessibility-requirements).
95
84
 
96
85
  ## Accessibility
97
86
 
98
- Our Expandable component renders a semantic HTML `<button>` element to the DOM, with an optional
99
- parent heading element as defined by the `headingLevel` prop. The `aria-expanded` property is
100
- included on the button to indicate the state of the content to screen readers.
101
-
102
- [Accordion Pattern | APG | WAI | W3C](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/)
103
-
104
- - Use the `headingLevel` prop to assign an appropriate heading level based on the context of the
105
- page content.
106
- - When using Expandable Container for navigation elements, then we don't recommend using the
107
- `headingLevel` prop. This will render only expandable buttons to the DOM, reserving headings for
108
- organizing content in the main body of the page.
109
- - The `as` prop may also be used on `<Expandable.Content>` to render an HTML `<ul>` element for
110
- displaying a list of items. For example, check out
111
- [Side Panel with Navigation](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-examples-side-panel-navigation--docs).
112
-
113
- ### Navigation
114
-
115
- - **Tab key**: Moves focus to the next expandable button or focusable element
116
- - **Shift + Tab**: Moves focus to the previous focusable element
117
- - **Enter or Space**: Toggles the expanded/collapsed state
118
-
119
- ### Screen Reader Experience
120
-
121
- - The expandable button will be announced with its title text followed by the button role
122
- - The current state will be announced as either "collapsed" or "expanded" (For example: "Usage
123
- Guidance, button, collapsed" or "Usage Guidance, button, expanded")
124
- - **State Changes:** When activating the button to expand content, screen readers will announce the
125
- new "expanded" state and vice versa when collapsing content.
126
- - **Content Regions:** Screen reader users can use landmark navigation to jump between sections and
127
- each section will be announced with its associated title (For example: "Usage Guidance, landmark
128
- region")
129
- - **Heading Structure:** Using heading levels with expandable buttons allows screen reader users to
130
- navigate by headings, making the document structure and hierarchy easier to understand.
87
+ Ensure users of assistive technology can find the control, understand whether related content is
88
+ shown or hidden, and reveal or hide that content from the keyboard. **`Expandable`** is an inline
89
+ [disclosure (show/hide)](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) widget: a button
90
+ toggles content that stays in the document flow.
91
+
92
+ Use **`Expandable`** for show/hide sections in the page. Compose multiple instances to build an
93
+ [accordion](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/) — see
94
+ [Hoisted Model](#hoisted-model). For a navigation accordion, see
95
+ [Side Panel with Navigation](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-examples-side-panel-navigation--docs).
96
+
97
+ ### Minimum Accessible Structure
98
+
99
+ The following matches the [Start Icon](#start-icon) example layout: **`Expandable.Icon`** before
100
+ **`Expandable.Title`** inside **`Expandable.Target`**, then **`Expandable.Content`**.
101
+ **`headingLevel`** is omitted here on purpose — add it only after asking which level matches the
102
+ page outline (see **Accessibility Requirements**). The Basic Example code uses `headingLevel="h4"`
103
+ as a sample, not a default.
104
+
105
+ ```tsx
106
+ import {Expandable} from '@workday/canvas-kit-react/expandable';
107
+
108
+ <Expandable>
109
+ <Expandable.Target>
110
+ <Expandable.Icon iconPosition="start" />
111
+ <Expandable.Title>Title</Expandable.Title>
112
+ </Expandable.Target>
113
+ <Expandable.Content>Content</Expandable.Content>
114
+ </Expandable>;
115
+ ```
116
+
117
+ ### Built-in Behaviors
118
+
119
+ Canvas Kit applies these automatically via `useExpandableModel` (a `DisclosureModel`) and Expandable
120
+ subcomponents. **Do not duplicate them** in consuming code.
121
+
122
+ **ARIA and DOM** (_applied by hooks/subcomponents_):
123
+
124
+ - **`Expandable.Target`**: native `<button>` with `aria-expanded` (`true` when
125
+ `visibility !== 'hidden'`) and `aria-controls` set to the disclosure `id`. `onClick` toggles
126
+ `events.show()` / `events.hide()`.
127
+ - **`headingLevel`**: when set (`'h1'`–`'h6'`), wraps the button in **`Heading`** rendered as that
128
+ heading element. When omitted, the button is not wrapped in a heading.
129
+ - **`Expandable.Content`**: `id` set to the same disclosure `id`. Collapsed content stays in the DOM
130
+ with `display: none` (not unmounted). Default element is a `<div>`.
131
+ - **`Expandable.Title`**: styled `<div>` for the visible name. It does **not** receive an `id` from
132
+ the model.
133
+ - **`Expandable.Icon`**: decorative chevron that rotates from `visibility`. Expanded/collapsed state
134
+ for assistive technology comes from `aria-expanded`, not from the icon.
135
+ - **`Expandable.Avatar`**: `isDecorative={true}` by default so the image is hidden from assistive
136
+ technology when paired with **`Expandable.Title`**.
137
+ - Disclosure `id`: generated with `useUniqueId` unless you pass `id` to **`useExpandableModel`**.
138
+ Default `initialVisibility` is `'hidden'`.
139
+
140
+ **Keyboard** (_native button behavior_):
141
+
142
+ **`Expandable.Target`** uses native `<button>` keyboard behavior (tab order, <kbd>Enter</kbd> /
143
+
144
+ <kbd>Space</kbd> to toggle). Collapsed content is not in the tab order (`display: none`). Focus
145
+ stays on the button; Canvas Kit does not move focus into **`Expandable.Content`**.
146
+
147
+ **Screen reader expectations** (_when built-in behaviors are used as intended_):
148
+
149
+ - On focus, assistive technology announces the title text, the button role, and expanded/collapsed
150
+ state from `aria-expanded` (for example, "Title, button, collapsed")
151
+ - Activating the button announces the updated expanded/collapsed state from `aria-expanded`
152
+ - While collapsed, content is removed from the accessibility tree (`display: none`)
153
+ - While expanded, content follows the button in reading order
154
+ - When **`headingLevel`** is set, heading navigation includes the heading that wraps the button
155
+ - When **`Expandable.Content as="section"`** is used with `aria-labelledby` pointing at
156
+ **`Expandable.Title`**, region/landmark navigation can name each open section
157
+
158
+ ### Accessibility Requirements
159
+
160
+ Required in application code for an accessible Expandable. Hoist **`useExpandableModel`** when you
161
+ need programmatic show/hide (for example, expand all) or a stable disclosure `id`. Rows marked
162
+ _(conditional)_ apply only when the situation matches—otherwise omit.
163
+
164
+ **If no design spec is provided:** use the **single disclosure** composition
165
+ ([Basic Example](#basic-example)). If the spec is several related headers, use the **accordion**
166
+ composition. Omit **`Expandable.Avatar`**, **`initialVisibility`**, and a custom **`id`**.
167
+
168
+ **Choose a composition** (pick one; **`Expandable.Content as="section"`** and **`as="ul"`** are
169
+ mutually exclusive):
170
+
171
+ - **Single disclosure** — one show/hide section. [Basic Example](#basic-example) /
172
+ [Minimum Accessible Structure](#minimum-accessible-structure). Default Content is a `<div>`.
173
+ - **Accordion** — several related headers. [Hoisted Model](#hoisted-model): unique `id` on
174
+ **`Expandable.Title`**, **`Expandable.Content as="section"`** with `aria-labelledby`.
175
+ - **Navigation list** — disclosed links.
176
+ [Side Panel with Navigation](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-examples-side-panel-navigation--docs):
177
+ **`as="ul"`**, omit **`headingLevel`**.
178
+
179
+ **Heading level:** Ask which heading (`h1`–`h6`) belongs in the document outline before setting
180
+ **`headingLevel`**. Omit **`headingLevel`** for navigation. Do not generate **`headingLevel`** by
181
+ default — the Basic Example code uses `h4` only as a sample.
182
+
183
+ | Requirement | How to satisfy |
184
+ | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
185
+ | Accessible name | Non-empty **`Expandable.Title`** inside **`Expandable.Target`**. |
186
+ | Keyboard-operable toggle | **`Expandable.Target`** (native `<button>`). |
187
+ | Visual expanded/collapsed indicator | **`Expandable.Icon`** with `iconPosition` matching layout (`start` or `end`). |
188
+ | Heading in the document outline _(conditional)_ | **`headingLevel`** on **`Expandable.Target`** when the control titles body content (single disclosure or accordion). Omit **`headingLevel`** for the navigation-list composition. Ask which level. |
189
+ | Accordion named regions _(conditional)_ | Accordion composition: **`Expandable.Content as="section"`** plus `aria-labelledby` referencing a unique `id` on **`Expandable.Title`**. |
190
+ | List markup _(conditional)_ | Navigation-list composition: **`Expandable.Content as="ul"`** (and list items as children). |
191
+ | Decorative avatar _(conditional)_ | **`Expandable.Avatar`** when the design includes an avatar. Keep **`isDecorative`** at the default `true`. |
192
+ | Start expanded _(conditional)_ | `initialVisibility: 'visible'` on **`useExpandableModel`** (or the equivalent model config). |
193
+ | Programmatic expand/collapse _(conditional)_ | Hoist **`useExpandableModel`** and call `model.events.show()` / `model.events.hide()`. |
194
+
195
+ **Summary for code generation:**
196
+
197
+ - **REQUIRED:** **`Expandable.Target`**, non-empty **`Expandable.Title`**, **`Expandable.Content`**,
198
+ **`Expandable.Icon`**
199
+ - **CONDITIONAL:** composition (single disclosure vs accordion vs navigation list),
200
+ **`headingLevel`** (ask which; omit for navigation), accordion **`as="section"`** +
201
+ `aria-labelledby`, navigation **`as="ul"`**, **`Expandable.Avatar`**, hoisted
202
+ **`useExpandableModel`**, **`initialVisibility`**
203
+
204
+ ### Anti-Patterns
205
+
206
+ Do **not** generate code that does the following (see **Accessibility Requirements** above for what
207
+ to supply instead):
208
+
209
+ - Manually set `aria-expanded`, `aria-controls`, or the disclosure `id` on **`Expandable.Target`**
210
+ or **`Expandable.Content`** — `useExpandableTarget` and `useExpandableContent` wire these
211
+ - Rely on **`Expandable.Icon`** or **`Expandable.Avatar`** for the accessible name — use
212
+ **`Expandable.Title`**
213
+ - Override **`Expandable.Target`** with a custom **`as`** — **`Expandable.Target`** already renders
214
+ a native `<button>`
215
+ - Set `id` on **`Expandable`** to identify the outer wrapper — `id` is model config **and** is
216
+ forwarded to the container element, while **`Expandable.Content`** also uses `state.id`, which can
217
+ produce duplicate IDs. For a stable content `id`, hoist **`useExpandableModel({id: '…'})`**.
218
+ **`Expandable.Title`** still needs its own `id` when you set `aria-labelledby` on
219
+ **`Expandable.Content`**
220
+ - Wrap **`Expandable.Target`** in your own heading — use **`headingLevel`** instead
221
+ - Default **`headingLevel`** to `h4` (or any level) without asking, or set **`headingLevel`** on
222
+ navigation disclosures
223
+ - Generate several Basic Example disclosures with default `div` Content when the spec is an
224
+ accordion — use the **accordion** composition (see **Choose a composition**)
225
+ - Set **`as="section"`** and **`as="ul"`** on the same **`Expandable.Content`** — pick one
226
+ composition
227
+ - Change **`Expandable.Title`** to encode state (for example "Collapsed" / "Expanded" or "Show" /
228
+ "Hide") — `aria-expanded` already exposes state
229
+ - Unmount **`Expandable.Content`** when collapsed, or add `aria-hidden` on it — the library uses
230
+ `display: none`
231
+ - Set **`isDecorative={false}`** on **`Expandable.Avatar`** by default, or pair an avatar with an
232
+ extra non-chevron icon
233
+ - Use **`open`**, **`expanded`**, or **`onToggle`** props on **`Expandable`** — visibility is
234
+ `initialVisibility` plus **`useExpandableModel`** events
235
+ - Assume default **`Expandable.Content`** is a landmark region — that is only true with
236
+ **`as="section"`**
131
237
 
132
238
  ## Component API
133
239
 
@@ -10,7 +10,7 @@ const myStencil = createStencil({
10
10
  },
11
11
  });
12
12
 
13
- export const CSProp = () => {
13
+ export default () => {
14
14
  return (
15
15
  <Card cs={myStencil()}>
16
16
  <Card.Heading
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workday/canvas-kit-docs",
3
- "version": "17.0.0-alpha.0605-next.0",
3
+ "version": "17.0.0-alpha.0616-next.0",
4
4
  "description": "Documentation components of Canvas Kit components",
5
5
  "author": "Workday, Inc. (https://www.workday.com)",
6
6
  "license": "Apache-2.0",
@@ -47,10 +47,10 @@
47
47
  "@stackblitz/sdk": "^1.11.0",
48
48
  "@storybook/csf": "0.0.1",
49
49
  "@workday/canvas-expressive-icons-web": "1.0.2",
50
- "@workday/canvas-kit-labs-react": "^17.0.0-alpha.0605-next.0",
51
- "@workday/canvas-kit-preview-react": "^17.0.0-alpha.0605-next.0",
52
- "@workday/canvas-kit-react": "^17.0.0-alpha.0605-next.0",
53
- "@workday/canvas-kit-styling": "^17.0.0-alpha.0605-next.0",
50
+ "@workday/canvas-kit-labs-react": "^17.0.0-alpha.0616-next.0",
51
+ "@workday/canvas-kit-preview-react": "^17.0.0-alpha.0616-next.0",
52
+ "@workday/canvas-kit-react": "^17.0.0-alpha.0616-next.0",
53
+ "@workday/canvas-kit-styling": "^17.0.0-alpha.0616-next.0",
54
54
  "@workday/canvas-system-icons-web": "^5.0.3",
55
55
  "@workday/canvas-tokens-web": "^4.4.0",
56
56
  "markdown-to-jsx": "^7.2.0",
@@ -63,5 +63,5 @@
63
63
  "mkdirp": "^1.0.3",
64
64
  "typescript": "5.0"
65
65
  },
66
- "gitHead": "58ce933e857e255360611f2330a230b167cead8d"
66
+ "gitHead": "593c0f1f5ee5047c7bea41027f76716a0f42caee"
67
67
  }
@@ -1,9 +0,0 @@
1
- import {ExampleCodeBlock} from '@workday/canvas-kit-docs';
2
- import SelectWithFormik from './examples/SelectWithFormik';
3
-
4
-
5
- # Canvas Kit Examples
6
-
7
- ## Select Using Formik
8
-
9
- <ExampleCodeBlock code={SelectWithFormik} />
@@ -1,84 +0,0 @@
1
- import {useFormik} from 'formik';
2
-
3
- import {PrimaryButton} from '@workday/canvas-kit-react/button';
4
- import {FormField} from '@workday/canvas-kit-react/form-field';
5
- import {Flex} from '@workday/canvas-kit-react/layout';
6
- import {Select, useSelectModel} from '@workday/canvas-kit-react/select';
7
- import {px2rem} from '@workday/canvas-kit-styling';
8
- import {cloudArrowUpIcon, cloudIcon, commentIcon, userIcon} from '@workday/canvas-system-icons-web';
9
- import {system} from '@workday/canvas-tokens-web';
10
-
11
- export default [
12
- {
13
- label: 'Dessert Person by Claire Saffitz',
14
- serverId: 1,
15
- icon: commentIcon,
16
- },
17
- {
18
- label: 'Elements of Pizza by Ken Forkish',
19
- serverId: 2,
20
- icon: cloudIcon,
21
- },
22
- {
23
- label: 'Flour Water Salt Yeast by Ken Forkish',
24
- serverId: 3,
25
- icon: cloudArrowUpIcon,
26
- },
27
- {label: 'Mastering Pasta by Marc Verti', serverId: 4, icon: userIcon},
28
- ];
29
-
30
- export default () => {
31
- const formik = useFormik({
32
- initialValues: {
33
- selectedBook: '',
34
- },
35
- onSubmit: values => {
36
- // Send data to server
37
- setTimeout(() => {
38
- alert(JSON.stringify(values, null, 2));
39
- }, 0);
40
- },
41
- });
42
-
43
- const selectModel = useSelectModel({
44
- items: customOptionsMain,
45
- getId: item => {
46
- return item.serverId;
47
- },
48
- getTextValue: item => item.label,
49
- });
50
-
51
- return (
52
- <form onSubmit={formik.handleSubmit} action=".">
53
- <Flex cs={{gap: system.gap.sm, flexDirection: 'column', alignItems: 'flex-start'}}>
54
- <FormField orientation="vertical" cs={{alignSelf: 'stretch', alignItems: 'normal'}}>
55
- <Select model={selectModel}>
56
- <FormField.Label>Choose a book</FormField.Label>
57
- <FormField.Input
58
- as={Select.Input}
59
- name="selectedBook"
60
- onChange={event => formik.setFieldValue('selectedBook', event.currentTarget.value)}
61
- value={formik.values.selectedBook}
62
- grow
63
- />
64
- <Select.Popper>
65
- <Select.Card>
66
- <Select.List cs={{maxHeight: px2rem(200)}}>
67
- {item => {
68
- return (
69
- <Select.Item data-id={item.serverId} data-text={item.label}>
70
- <Select.Item.Icon icon={item.icon} />
71
- {item.label}
72
- </Select.Item>
73
- );
74
- }}
75
- </Select.List>
76
- </Select.Card>
77
- </Select.Popper>
78
- </Select>
79
- </FormField>
80
- <PrimaryButton type="submit">Submit</PrimaryButton>
81
- </Flex>
82
- </form>
83
- );
84
- };