@workday/canvas-kit-docs 16.2.0-0612-next.0 → 16.2.0-0618-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.
@@ -412448,7 +412448,7 @@ export const docs = (typeof window !== 'undefined' && window.__docs) ||
412448
412448
  {
412449
412449
  "name": "Select",
412450
412450
  "fileName": "/home/runner/work/canvas-kit/canvas-kit/modules/react/select/lib/Select.tsx",
412451
- "description": "Use `Select` to allow users to choose an option from a list or type characters to select a matching option.\n\n**Note: `Select` must wrap `FormField` and `FormField` must wrap all `Select` children to ensure proper accessibility. **\n\n```tsx\n<Select items={options}>\n <FormField label=\"Your Label\">\n <Select.Input onChange={e => handleChange(e)} id=\"contact-select\" />\n <Select.Popper>\n <Select.Card>\n <Select.List>\n {item => <Select.Item>{item.id}</Select.Item>}\n </Select.List>\n </Select.Card>\n </Select.Popper>\n </FormField>\n</Select>\n```",
412451
+ "description": "Use `Select` to allow users to choose an option from a list or type characters to select a matching option.\n\n**Note: Wrap `Select` with `FormField` and compose `FormField.Input as={Select.Input}` so the combobox receives the field label.**\n\n```tsx\n<FormField>\n <FormField.Label>Contact</FormField.Label>\n <FormField.Field>\n <Select items={options}>\n <FormField.Input as={Select.Input} onChange={e => handleChange(e)} />\n <Select.Popper>\n <Select.Card>\n <Select.List>{item => <Select.Item>{item}</Select.Item>}</Select.List>\n </Select.Card>\n </Select.Popper>\n </Select>\n </FormField.Field>\n</FormField>\n```",
412452
412452
  "declarations": [
412453
412453
  {
412454
412454
  "name": "Select",
@@ -412536,7 +412536,7 @@ export const docs = (typeof window !== 'undefined' && window.__docs) ||
412536
412536
  {
412537
412537
  "name": "Input",
412538
412538
  "symbol": "SelectInput",
412539
- "description": "`Select.Input` renders a {@link ComboboxMenu Combobox.Input} that handles keyboard navigation and interaction defined by [WAI](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/).\nThis component can either be [controlled or uncontrolled](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components).\n\n```tsx\n<Select items={options}>\n <FormField label=\"Contact\">\n <Select.Input onChange={(event) => handleChange(event)}>\n ...\n </FormField>\n</Select>\n```",
412539
+ "description": "`Select.Input` renders a {@link ComboboxMenu Combobox.Input} that handles keyboard navigation and interaction defined by [WAI](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/).\nThis component can either be [controlled or uncontrolled](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components).\n\nCompose it as `FormField.Input as={Select.Input}` so label, required, and error wiring apply.\n\n```tsx\n<FormField>\n <FormField.Label>Contact</FormField.Label>\n <FormField.Field>\n <Select items={options}>\n <FormField.Input as={Select.Input} onChange={event => handleChange(event)} />\n ...\n </Select>\n </FormField.Field>\n</FormField>\n```",
412540
412540
  "declarations": [
412541
412541
  {
412542
412542
  "name": "Input",
@@ -412552,7 +412552,7 @@ export const docs = (typeof window !== 'undefined' && window.__docs) ||
412552
412552
  {
412553
412553
  "name": "Card",
412554
412554
  "symbol": "SelectCard",
412555
- "description": "`Select.Card` renders a {@link ComboboxCard Combobox.Card}. You have access to all `Card` props.\n\n**Note: The card will be the width of its corresponding `Select.Input`**.\n\n```tsx\n<Select items={options}>\n <FormField label=\"Your Label\">\n <Select.Input onChange={(event) => handleChange(event)}>\n <Select.Popper>\n <Select.Card>\n ...\n </Select.Card>\n </Select.Popper>\n </FormField>\n</Select>\n```",
412555
+ "description": "`Select.Card` renders a {@link ComboboxCard Combobox.Card}. You have access to all `Card` props.\n\n**Note: The card will be the width of its corresponding `Select.Input`**.\n\n```tsx\n<FormField>\n <FormField.Label>Contact</FormField.Label>\n <FormField.Field>\n <Select items={options}>\n <FormField.Input as={Select.Input} onChange={event => handleChange(event)} />\n <Select.Popper>\n <Select.Card>...</Select.Card>\n </Select.Popper>\n </Select>\n </FormField.Field>\n</FormField>\n```",
412556
412556
  "declarations": [
412557
412557
  {
412558
412558
  "name": "Card",
@@ -412568,7 +412568,7 @@ export const docs = (typeof window !== 'undefined' && window.__docs) ||
412568
412568
  {
412569
412569
  "name": "Item",
412570
412570
  "symbol": "SelectItem",
412571
- "description": "`Select.Item` renders a {@link ComboboxMenuItem Combobox.Menu.Item} with aria role of `option`. You can optionally render a `Icon`.\n\n```tsx\n<Select items={options}>\n <FormField label=\"Your Label\">\n <Select.Input onChange={(event) => handleChange(event)}>\n <Select.Popper>\n <Select.Card>\n <Select.List>\n {(item) => <Select.Item><Select.Item.Icon icon={icon} />{item}</Select.Item>}\n </Select.List\n </Select.Card>\n </Select.Popper>\n </FormField>\n</Select>\n```",
412571
+ "description": "`Select.Item` renders a {@link ComboboxMenuItem Combobox.Menu.Item} with aria role of `option`. You can optionally render a `Icon`.\n\n```tsx\n<FormField>\n <FormField.Label>Contact</FormField.Label>\n <FormField.Field>\n <Select items={options}>\n <FormField.Input as={Select.Input} onChange={event => handleChange(event)} />\n <Select.Popper>\n <Select.Card>\n <Select.List>\n {item => (\n <Select.Item>\n <Select.Item.Icon icon={icon} />\n {item}\n </Select.Item>\n )}\n </Select.List>\n </Select.Card>\n </Select.Popper>\n </Select>\n </FormField.Field>\n</FormField>\n```",
412572
412572
  "declarations": [
412573
412573
  {
412574
412574
  "name": "Item",
@@ -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.2",
22
- "@workday/canvas-kit-preview-react": "16.1.2",
23
- "@workday/canvas-kit-react": "16.1.2",
24
- "@workday/canvas-kit-react-fonts": "^16.1.2",
25
- "@workday/canvas-kit-styling": "16.1.2",
21
+ "@workday/canvas-kit-labs-react": "16.1.4",
22
+ "@workday/canvas-kit-preview-react": "16.1.4",
23
+ "@workday/canvas-kit-react": "16.1.4",
24
+ "@workday/canvas-kit-react-fonts": "^16.1.4",
25
+ "@workday/canvas-kit-styling": "16.1.4",
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.2",
23
- "@workday/canvas-kit-preview-react": "16.1.2",
24
- "@workday/canvas-kit-react": "16.1.2",
25
- "@workday/canvas-kit-react-fonts": "^16.1.2",
26
- "@workday/canvas-kit-styling": "16.1.2",
22
+ "@workday/canvas-kit-labs-react": "16.1.4",
23
+ "@workday/canvas-kit-preview-react": "16.1.4",
24
+ "@workday/canvas-kit-react": "16.1.4",
25
+ "@workday/canvas-kit-react-fonts": "^16.1.4",
26
+ "@workday/canvas-kit-styling": "16.1.4",
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} />