@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.
- package/dist/es6/lib/specs.js +20 -0
- package/dist/es6/lib/stackblitzFiles/packageJSONFile.js +5 -5
- package/dist/es6/lib/stackblitzFiles/packageJSONFile.ts +5 -5
- package/dist/mdx/accessibility/AccessibilityOverview.mdx +3 -1
- package/dist/mdx/preview-react/_examples/mdx/FormLibraryExample.mdx +33 -1
- package/dist/mdx/preview-react/_examples/mdx/examples/TextInputWithReactHookForm.tsx +94 -65
- package/dist/mdx/react/_examples/mdx/Density.mdx +19 -3
- package/dist/mdx/react/_examples/mdx/examples/Density.tsx +139 -212
- package/dist/mdx/react/expandable/Expandable.mdx +161 -55
- package/dist/mdx/styling/mdx/examples/CSProp.tsx +1 -1
- package/package.json +6 -6
- package/dist/mdx/react/_examples/mdx/FormsWithFormik.mdx +0 -9
- package/dist/mdx/react/_examples/mdx/examples/SelectWithFormik.tsx +0 -84
package/dist/es6/lib/specs.js
CHANGED
|
@@ -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.
|
|
22
|
-
"@workday/canvas-kit-preview-react": "16.1.
|
|
23
|
-
"@workday/canvas-kit-react": "16.1.
|
|
24
|
-
"@workday/canvas-kit-react-fonts": "^16.1.
|
|
25
|
-
"@workday/canvas-kit-styling": "16.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.
|
|
23
|
-
"@workday/canvas-kit-preview-react": "16.1.
|
|
24
|
-
"@workday/canvas-kit-react": "16.1.
|
|
25
|
-
"@workday/canvas-kit-react-fonts": "^16.1.
|
|
26
|
-
"@workday/canvas-kit-styling": "16.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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
30
|
-
return {values, errors: {}};
|
|
31
|
-
} catch (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
<
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 {
|
|
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 {
|
|
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<
|
|
272
|
-
const [containerAlignment, setContainerAlignment] = React.useState<
|
|
273
|
-
const [labelOrientation, setLabelOrientation] = React.useState<
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
278
|
-
|
|
229
|
+
const handleContainerAlignment = (data: {id: string}) => {
|
|
230
|
+
setContainerAlignment(data.id as ContainerAlignment);
|
|
279
231
|
};
|
|
280
232
|
|
|
281
|
-
const
|
|
282
|
-
|
|
233
|
+
const handleLabelOrientation = (data: {id: string}) => {
|
|
234
|
+
setLabelOrientation(data.id as LabelOrientation);
|
|
283
235
|
};
|
|
284
236
|
|
|
285
|
-
const
|
|
286
|
-
|
|
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
|
-
|
|
291
|
-
<Heading size="small">Choose Your Density and Alignment</Heading>
|
|
246
|
+
<>
|
|
292
247
|
<div className={optionStyles}>
|
|
293
248
|
<div className={optionItemStyles}>
|
|
294
|
-
<Text>
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
306
|
+
<form className={formStyles} autoComplete="off">
|
|
307
|
+
<Heading size="small">Shipping Address</Heading>
|
|
328
308
|
<div {...flexContainerStencil({density})}>
|
|
329
|
-
<FormField
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
<
|
|
358
|
-
grow
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
<FormField
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
cs={
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
>
|
|
395
|
-
<FormField.Label>Zip Code</FormField.Label>
|
|
396
|
-
<FormField.Input
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
|
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`
|
|
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
|
|
50
|
-
This component is meant to have either an avatar image or an icon in addition to
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workday/canvas-kit-docs",
|
|
3
|
-
"version": "17.0.0-alpha.
|
|
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.
|
|
51
|
-
"@workday/canvas-kit-preview-react": "^17.0.0-alpha.
|
|
52
|
-
"@workday/canvas-kit-react": "^17.0.0-alpha.
|
|
53
|
-
"@workday/canvas-kit-styling": "^17.0.0-alpha.
|
|
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": "
|
|
66
|
+
"gitHead": "593c0f1f5ee5047c7bea41027f76716a0f42caee"
|
|
67
67
|
}
|
|
@@ -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
|
-
};
|