Skip to content

Commit eb87026

Browse files
committed
Extend FormLayoutCustomField options to improve accessibility and design consistency (#174)
New options: - `disabled` - `innerFieldSize` - `labelForId` - `required` - `validationState`
1 parent 1bac30a commit eb87026

File tree

7 files changed

+246
-30
lines changed

7 files changed

+246
-30
lines changed

src/lib/components/layout/FormLayout/FormLayoutCustomField.jsx

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,62 @@
11
import PropTypes from 'prop-types';
22
import React from 'react';
3+
import getRootSizeClassName from '../../../helpers/getRootSizeClassName';
4+
import getRootValidationStateClassName from '../../../helpers/getRootValidationStateClassName';
35
import { withProviderContext } from '../../../provider';
46
import styles from './FormLayoutCustomField.scss';
57

8+
const renderLabel = (id, label, labelForId) => {
9+
if (labelForId && label) {
10+
return (
11+
<label
12+
htmlFor={labelForId}
13+
id={id && `${id}__label`}
14+
className={styles.label}
15+
>
16+
{label}
17+
</label>
18+
);
19+
}
20+
21+
if (label) {
22+
return (
23+
<div
24+
id={id && `${id}__label`}
25+
className={styles.label}
26+
>
27+
{label}
28+
</div>
29+
);
30+
}
31+
32+
return null;
33+
};
34+
635
export const FormLayoutCustomField = ({
736
children,
837
fullWidth,
938
id,
39+
disabled,
40+
innerFieldSize,
1041
label,
42+
labelForId,
1143
layout,
44+
required,
45+
validationState,
1246
}) => (
1347
<div
1448
id={id}
15-
className={`
16-
${styles.root}
17-
${fullWidth ? styles.isRootFullWidth : ''}
18-
${layout === 'vertical' ? styles.rootLayoutVertical : styles.rootLayoutHorizontal}
19-
`.trim()}
49+
className={[
50+
styles.root,
51+
fullWidth ? styles.isRootFullWidth : '',
52+
layout === 'vertical' ? styles.rootLayoutVertical : styles.rootLayoutHorizontal,
53+
disabled ? styles.isRootDisabled : '',
54+
required ? styles.isRootRequired : '',
55+
getRootSizeClassName(innerFieldSize, styles),
56+
getRootValidationStateClassName(validationState, styles),
57+
].join(' ')}
2058
>
21-
{label && (
22-
<div
23-
id={id && `${id}__label`}
24-
className={styles.label}
25-
>
26-
{label}
27-
</div>
28-
)}
59+
{renderLabel(id, label, labelForId)}
2960
<div
3061
id={id && `${id}__field`}
3162
className={styles.field}
@@ -37,17 +68,26 @@ export const FormLayoutCustomField = ({
3768

3869
FormLayoutCustomField.defaultProps = {
3970
children: null,
71+
disabled: false,
4072
fullWidth: false,
4173
id: undefined,
74+
innerFieldSize: null,
4275
label: null,
76+
labelForId: undefined,
4377
layout: 'vertical',
78+
required: false,
79+
validationState: null,
4480
};
4581

4682
FormLayoutCustomField.propTypes = {
4783
/**
4884
* Custom HTML or React component(s).
4985
*/
5086
children: PropTypes.node,
87+
/**
88+
* If `true`, label will be shown as disabled.
89+
*/
90+
disabled: PropTypes.bool,
5191
/**
5292
* If `true`, the field will span the full width of its parent.
5393
*/
@@ -56,14 +96,30 @@ FormLayoutCustomField.propTypes = {
5696
* ID of the root HTML element.
5797
*/
5898
id: PropTypes.string,
99+
/**
100+
* Size of contained form field used to properly align label.
101+
*/
102+
innerFieldSize: PropTypes.oneOf(['small', 'medium', 'large']),
59103
/**
60104
* Optional label of the field.
61105
*/
62106
label: PropTypes.string,
107+
/**
108+
* Optional ID of labelled field to keep accessibility features.
109+
*/
110+
labelForId: PropTypes.string,
63111
/**
64112
* Layout of the field, controlled by parent FormLayout.
65113
*/
66114
layout: PropTypes.oneOf(['horizontal', 'vertical']),
115+
/**
116+
* If `true`, label will be styled as required.
117+
*/
118+
required: PropTypes.bool,
119+
/**
120+
* Alter the field to provide feedback based on validation result.
121+
*/
122+
validationState: PropTypes.oneOf(['invalid', 'valid', 'warning']),
67123
};
68124

69125
export const FormLayoutCustomFieldWithContext = withProviderContext(FormLayoutCustomField, 'FormLayoutCustomField');

src/lib/components/layout/FormLayout/FormLayoutCustomField.scss

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
1+
@use '../../../styles/tools/form-fields/foundation';
12
@use '../../../styles/tools/form-fields/box-field-layout';
3+
@use '../../../styles/tools/form-fields/box-field-sizes';
4+
@use '../../../styles/tools/form-fields/variants';
25

6+
// Foundation
37
.root {
48
@include box-field-layout.in-form-layout();
9+
@include variants.visual(custom);
510
}
611

12+
.label {
13+
@include foundation.label();
14+
}
15+
16+
.isRootRequired .label {
17+
@include foundation.label-required();
18+
}
19+
20+
// States
21+
.isRootStateInvalid {
22+
@include variants.validation(invalid);
23+
}
24+
25+
.isRootStateValid {
26+
@include variants.validation(valid);
27+
}
28+
29+
.isRootStateWarning {
30+
@include variants.validation(warning);
31+
}
32+
33+
// Layouts
734
.rootLayoutVertical,
835
.rootLayoutHorizontal {
936
@include box-field-layout.vertical();
@@ -21,3 +48,16 @@
2148
.isRootFullWidth .field {
2249
justify-self: stretch;
2350
}
51+
52+
// Sizes
53+
.rootSizeSmall {
54+
@include box-field-sizes.size(small);
55+
}
56+
57+
.rootSizeMedium {
58+
@include box-field-sizes.size(medium);
59+
}
60+
61+
.rootSizeLarge {
62+
@include box-field-sizes.size(large);
63+
}

src/lib/components/layout/FormLayout/README.mdx

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,15 +253,123 @@ FormLayout elements. FormLayoutCustomFields are designed to work solely inside
253253
the FormLayout component.
254254

255255
<Playground>
256-
<FormLayout fieldLayout="horizontal">
256+
<FormLayout fieldLayout="horizontal" labelWidth="auto">
257257
<TextField id="my-text-field-custom-1" label="A form element" />
258-
<FormLayoutCustomField label="Optional label">
259-
<Placeholder bordered>Custom content</Placeholder>
258+
<FormLayoutCustomField label="Optional custom field label">
259+
<Placeholder bordered>Custom field content</Placeholder>
260260
</FormLayoutCustomField>
261261
<TextField id="my-text-field-custom-2" label="Another form element" />
262262
</FormLayout>
263263
</Playground>
264264

265+
👉 While you can set FormLayoutCustomField as `disabled`, `valid` or `required`
266+
and its styles may affect contained form fields through CSS cascade, don't
267+
forget to mirror the aforementioned properties to the contained form fields too
268+
as API options as such are **not** inherited.
269+
270+
### Label Alignment
271+
272+
If you are in a situation with one or more box form fields inside your
273+
FormLayoutCustomField, you may want to have its label aligned with the fields
274+
inside. Since it's
275+
[not quite possible to do this automatically](https://github.com/react-ui-org/react-ui/issues/265)
276+
due to limited browser support, there is `innerFieldSize` option which accepts
277+
any of existing box field sizes (small, medium, or large) and is intended right
278+
for this task.
279+
280+
<Playground>
281+
<FormLayout fieldLayout="horizontal" labelWidth="auto">
282+
<TextField id="my-text-field-custom-alignment-1" label="A form element" />
283+
<FormLayoutCustomField
284+
innerFieldSize="medium"
285+
label="Custom field label aligned to inner text input"
286+
>
287+
<TextField
288+
id="my-text-field-custom-alignment-2"
289+
isLabelVisible={false}
290+
label="A form element"
291+
placeholder="Text field with invisible label"
292+
/>
293+
</FormLayoutCustomField>
294+
<TextField
295+
id="my-text-field-custom-alignment-3"
296+
label="Another form element"
297+
/>
298+
</FormLayout>
299+
</Playground>
300+
301+
### Validation States
302+
303+
Custom fields support the same validation states as regular form fields to
304+
provide labels with optional feedback style.
305+
306+
<Playground>
307+
<FormLayout fieldLayout="horizontal" labelWidth="auto">
308+
<TextField id="my-text-field-custom-validation-1" label="A form element" />
309+
<FormLayoutCustomField
310+
label="Custom field label in valid state"
311+
validationState="valid"
312+
>
313+
<Placeholder bordered>Custom field content</Placeholder>
314+
</FormLayoutCustomField>
315+
<TextField
316+
id="my-text-field-custom-validation-2"
317+
label="Another form element"
318+
/>
319+
</FormLayout>
320+
</Playground>
321+
322+
### Accessibility
323+
324+
If possible, use the `labelForId` option to provide ID of contained form field
325+
so the field remains accessible via custom field label.
326+
327+
You can also specify size of contained form field so custom field label is
328+
properly vertically aligned.
329+
330+
<Playground>
331+
{() => {
332+
const [isChecked, setIsChecked] = React.useState(false);
333+
return (
334+
<FormLayout fieldLayout="horizontal" labelWidth="auto">
335+
<TextField
336+
id="my-text-field-custom-accessibility-1"
337+
label="A form element"
338+
/>
339+
<FormLayoutCustomField
340+
fullWidth
341+
label="Custom field label aligned with medium form field"
342+
labelForId="my-text-field-custom-accessibility-2"
343+
innerFieldSize="medium"
344+
>
345+
<Toolbar align="middle" dense>
346+
<ToolbarItem>
347+
<TextField
348+
id="my-text-field-custom-accessibility-2"
349+
isLabelVisible={false}
350+
label="A form element"
351+
placeholder="Text field with invisible label"
352+
/>
353+
</ToolbarItem>
354+
<ToolbarItem>
355+
<CheckboxField
356+
changeHandler={() => setIsChecked(!isChecked)}
357+
checked={isChecked}
358+
id="my-checkbox-field-custom-accessibility-1"
359+
label="Another form field"
360+
/>
361+
</ToolbarItem>
362+
</Toolbar>
363+
</FormLayoutCustomField>
364+
<TextField
365+
id="my-text-field-custom-accessibility-3"
366+
label="Another form element"
367+
/>
368+
</FormLayout>
369+
)
370+
}}
371+
</Playground>
372+
265373
## Full Example
266374

267375
This is a demo of all components supported by FormLayout.
@@ -403,7 +511,7 @@ This is a demo of all components supported by FormLayout.
403511

404512
<Props table of={FormLayout} />
405513

406-
### FormLayoutCustomField
514+
### FormLayoutCustomField API
407515

408516
A place for custom content inside FormLayout.
409517

@@ -417,3 +525,13 @@ A place for custom content inside FormLayout.
417525
| `--rui-form-layout-horizontal-label-limited-width` | Label width in limited-width layout |
418526
| `--rui-form-layout-horizontal-label-default-width` | Label width in the default layout |
419527
| `--rui-form-layout-row-gap` | Gap between individual rows |
528+
529+
### FormLayoutCustomField Theming
530+
531+
FormLayoutCustomField can be styled using a small subset of
532+
[other form fields theming options](/customize/theming/forms).
533+
534+
| Custom Property | Description |
535+
|------------------------------------------------------|--------------------------------------------------------------|
536+
| `--rui-form-field-custom-default-surrounding-text-color` | Custom field label color in default state |
537+
| `--rui-form-field-custom-disabled-surrounding-text-color` | Custom field label color in disabled-like state |

src/lib/components/layout/FormLayout/__tests__/FormLayoutCustomField.test.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@ describe('rendering', () => {
2828
it('renders correctly with all props', () => {
2929
const tree = shallow((
3030
<FormLayoutCustomField
31+
disabled
3132
fullWidth
3233
label="Label"
34+
labelForId="target-id"
3335
id="my-custom-field"
36+
innerFieldSize="small"
3437
layout="horizontal"
38+
required
3539
>
3640
<span>Custom text in form 1</span>
3741
<span>Custom text in form 2</span>

src/lib/components/layout/FormLayout/__tests__/__snapshots__/FormLayoutCustomField.test.jsx.snap

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
exports[`rendering renders correctly with a single child 1`] = `
44
<div
5-
className="root
6-
7-
rootLayoutVertical"
5+
className="root rootLayoutVertical "
86
>
97
<div
108
className="field"
@@ -18,17 +16,16 @@ exports[`rendering renders correctly with a single child 1`] = `
1816

1917
exports[`rendering renders correctly with all props 1`] = `
2018
<div
21-
className="root
22-
isRootFullWidth
23-
rootLayoutHorizontal"
19+
className="root isRootFullWidth rootLayoutHorizontal isRootDisabled isRootRequired rootSizeSmall "
2420
id="my-custom-field"
2521
>
26-
<div
22+
<label
2723
className="label"
24+
htmlFor="target-id"
2825
id="my-custom-field__label"
2926
>
3027
Label
31-
</div>
28+
</label>
3229
<div
3330
className="field"
3431
id="my-custom-field__field"
@@ -48,9 +45,7 @@ exports[`rendering renders correctly with all props 1`] = `
4845

4946
exports[`rendering renders correctly with multiple children 1`] = `
5047
<div
51-
className="root
52-
53-
rootLayoutVertical"
48+
className="root rootLayoutVertical "
5449
>
5550
<div
5651
className="field"

0 commit comments

Comments
 (0)