@workday/canvas-kit-docs 17.0.0-alpha.0613-next.0 → 17.0.0-alpha.0619-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/docs.js +4 -4
- 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/common/mdx/examples/CreateComponent.tsx +3 -3
- package/dist/mdx/react/select/Select.mdx +239 -77
- package/dist/mdx/react/select/examples/GroupedItems.tsx +24 -30
- package/dist/mdx/react/select/examples/WithIcons.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/docs.js
CHANGED
|
@@ -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`
|
|
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
|
|
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
|
|
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
|
|
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",
|
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.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.
|
|
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.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
|
-
|
|
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} />
|