Skip to content

Commit 9f797db

Browse files
aoelenlevithomason
authored andcommitted
feat(FormField): make form field error accessible (#3822)
* feat(FormField): make form field error accessible * Change type of id on FormField * Add alert role on error labels and include error example
1 parent 72c4508 commit 9f797db

6 files changed

Lines changed: 61 additions & 11 deletions

File tree

docs/src/examples/collections/Form/Shorthand/FormExampleFieldControlId.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ const FormExampleFieldControlId = () => (
3737
label='Opinion'
3838
placeholder='Opinion'
3939
/>
40+
<Form.Field
41+
id='form-input-control-error-email'
42+
control={Input}
43+
label='Email'
44+
placeholder='joe@schmoe.com'
45+
error={{
46+
content: 'Please enter a valid email address',
47+
pointing: 'below',
48+
}}
49+
/>
4050
<Form.Field
4151
id='form-button-control-public'
4252
control={Button}

docs/src/examples/collections/Form/Shorthand/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ const FormTypesExamples = () => (
9393

9494
<ComponentExample
9595
title='Accessible Labels'
96-
description='Adding an id to a shorthand Form.Field adds a matching htmlFor prop to the label.'
96+
description='Adding an id to a shorthand Form.Field adds a matching htmlFor prop to the label. In case of an error, the aria-describedby prop is used to connect the error label to the form field.'
9797
examplePath='collections/Form/Shorthand/FormExampleFieldControlId'
9898
/>
9999

docs/src/examples/collections/Form/States/FormExampleFieldErrorLabel.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const FormExampleFieldErrorLabel = () => (
88
fluid
99
label='First name'
1010
placeholder='First name'
11+
id='form-input-first-name'
1112
/>
1213
<Form.Input
1314
error='Please enter your last name'

src/collections/Form/FormField.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export interface StrictFormFieldProps {
3838
/** Individual fields may display an error state along with a message. */
3939
error?: boolean | SemanticShorthandItem<LabelProps>
4040

41+
/** The id of the control */
42+
id?: number | string
43+
4144
/** A field can have its label next to instead of above it. */
4245
inline?: boolean
4346

src/collections/Form/FormField.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function FormField(props) {
4141
required,
4242
type,
4343
width,
44+
id,
4445
} = props
4546

4647
const classes = cx(
@@ -58,7 +59,13 @@ function FormField(props) {
5859
const errorPointing = _.get(error, 'pointing', 'above')
5960
const errorLabel = Label.create(error, {
6061
autoGenerateKey: false,
61-
defaultProps: { prompt: true, pointing: errorPointing },
62+
defaultProps: {
63+
prompt: true,
64+
pointing: errorPointing,
65+
id: id ? `${id}-error-message` : undefined,
66+
role: 'alert',
67+
'aria-atomic': true,
68+
},
6269
})
6370

6471
const errorLabelBefore = (errorPointing === 'below' || errorPointing === 'right') && errorLabel
@@ -89,15 +96,21 @@ function FormField(props) {
8996
// ----------------------------------------
9097
// Checkbox/Radio Control
9198
// ----------------------------------------
92-
const controlProps = { ...rest, content, children, disabled, required, type }
99+
100+
const ariaDescribedBy = id && error ? `${id}-error-message` : null
101+
const ariaAttrs = {
102+
'aria-describedby': ariaDescribedBy,
103+
'aria-invalid': error !== undefined ? true : undefined,
104+
}
105+
const controlProps = { ...rest, content, children, disabled, required, type, id }
93106

94107
// wrap HTML checkboxes/radios in the label
95108
if (control === 'input' && (type === 'checkbox' || type === 'radio')) {
96109
return (
97110
<ElementType className={classes}>
98111
<label>
99112
{errorLabelBefore}
100-
{createElement(control, controlProps)} {label}
113+
{createElement(control, { ...ariaAttrs, ...controlProps })} {label}
101114
{errorLabelAfter}
102115
</label>
103116
</ElementType>
@@ -109,7 +122,7 @@ function FormField(props) {
109122
return (
110123
<ElementType className={classes}>
111124
{errorLabelBefore}
112-
{createElement(control, { ...controlProps, label })}
125+
{createElement(control, { ...ariaAttrs, ...controlProps, label })}
113126
{errorLabelAfter}
114127
</ElementType>
115128
)
@@ -122,11 +135,11 @@ function FormField(props) {
122135
return (
123136
<ElementType className={classes}>
124137
{createHTMLLabel(label, {
125-
defaultProps: { htmlFor: _.get(controlProps, 'id') },
138+
defaultProps: { htmlFor: id },
126139
autoGenerateKey: false,
127140
})}
128141
{errorLabelBefore}
129-
{createElement(control, controlProps)}
142+
{createElement(control, { ...ariaAttrs, ...controlProps })}
130143
{errorLabelAfter}
131144
</ElementType>
132145
)
@@ -161,6 +174,9 @@ FormField.propTypes = {
161174
/** Individual fields may display an error state along with a message. */
162175
error: PropTypes.oneOfType([PropTypes.bool, customPropTypes.itemShorthand]),
163176

177+
/** The id of the control */
178+
id: PropTypes.string,
179+
164180
/** A field can have its label next to instead of above it. */
165181
inline: PropTypes.bool,
166182

test/specs/collections/Form/FormField-test.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,45 @@ describe('FormField', () => {
4141
autoGenerateKey: false,
4242
propKey: 'error',
4343
requiredProps: { label: faker.lorem.word() },
44-
shorthandDefaultProps: { prompt: true, pointing: 'above' },
44+
shorthandDefaultProps: {
45+
prompt: true,
46+
pointing: 'above',
47+
role: 'alert',
48+
'aria-atomic': true,
49+
},
4550
})
4651
common.implementsLabelProp(FormField, {
4752
autoGenerateKey: false,
4853
propKey: 'error',
4954
requiredProps: { control: 'radio' },
50-
shorthandDefaultProps: { prompt: true, pointing: 'above' },
55+
shorthandDefaultProps: {
56+
prompt: true,
57+
pointing: 'above',
58+
role: 'alert',
59+
'aria-atomic': true,
60+
},
5161
})
5262
common.implementsLabelProp(FormField, {
5363
autoGenerateKey: false,
5464
propKey: 'error',
5565
requiredProps: { control: Checkbox },
56-
shorthandDefaultProps: { prompt: true, pointing: 'above' },
66+
shorthandDefaultProps: {
67+
prompt: true,
68+
pointing: 'above',
69+
role: 'alert',
70+
'aria-atomic': true,
71+
},
5772
})
5873
common.implementsLabelProp(FormField, {
5974
autoGenerateKey: false,
6075
propKey: 'error',
6176
requiredProps: { control: 'input' },
62-
shorthandDefaultProps: { prompt: true, pointing: 'above' },
77+
shorthandDefaultProps: {
78+
prompt: true,
79+
pointing: 'above',
80+
role: 'alert',
81+
'aria-atomic': true,
82+
},
6383
})
6484

6585
it('positioned in DOM according to passed "pointing" prop', () => {

0 commit comments

Comments
 (0)