Skip to content

Commit 281beb0

Browse files
authored
Associate FormControl.Validation with the relevant input (#818)
* add tests for FormControl hint and validation association * implement FormControl validation association * add changeset * default aria-describedby to undefined
1 parent d7195fd commit 281beb0

File tree

3 files changed

+62
-2
lines changed

3 files changed

+62
-2
lines changed

.changeset/cuddly-kids-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react-brand': patch
3+
---
4+
5+
`FormControl.Validation` is now associated with the relevant input using `aria-describedby`.

packages/react/src/forms/FormControl/FormControl.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,51 @@ describe('FormControl', () => {
219219
expect(labelEl?.classList).toContain('FormControl-label--large')
220220
expect(inputEl.classList).toContain(`TextInput--large`)
221221
})
222+
223+
it('associates the hint with the input', () => {
224+
const {getByLabelText, getByText} = render(
225+
<FormControl>
226+
<FormControl.Label>My Label</FormControl.Label>
227+
<TextInput />
228+
<FormControl.Hint>A useful hint</FormControl.Hint>
229+
</FormControl>,
230+
)
231+
232+
const input = getByLabelText('My Label')
233+
const hint = getByText('A useful hint')
234+
235+
expect(input).toHaveAttribute('aria-describedby', hint.id)
236+
})
237+
238+
it('associates the validation with the input', () => {
239+
const {getByLabelText, getByText} = render(
240+
<FormControl>
241+
<FormControl.Label>My Label</FormControl.Label>
242+
<TextInput />
243+
<FormControl.Validation>LGTM</FormControl.Validation>
244+
</FormControl>,
245+
)
246+
247+
const input = getByLabelText('My Label')
248+
const validation = getByText('LGTM')
249+
250+
expect(input).toHaveAttribute('aria-describedby', validation.id)
251+
})
252+
253+
it('associates both a hint and validation with the input', () => {
254+
const {getByLabelText, getByText} = render(
255+
<FormControl>
256+
<FormControl.Label>My Label</FormControl.Label>
257+
<TextInput />
258+
<FormControl.Hint>A useful hint</FormControl.Hint>
259+
<FormControl.Validation>LGTM</FormControl.Validation>
260+
</FormControl>,
261+
)
262+
263+
const input = getByLabelText('My Label')
264+
const hint = getByText('A useful hint')
265+
const validation = getByText('LGTM')
266+
267+
expect(input).toHaveAttribute('aria-describedby', `${hint.id} ${validation.id}`)
268+
})
222269
})

packages/react/src/forms/FormControl/FormControl.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ const Root = ({
7272
)
7373

7474
const containsHint = childrenArr.some(child => React.isValidElement(child) && child.type === FormControlHint)
75+
const containsValidation = childrenArr.some(
76+
child => React.isValidElement(child) && child.type === FormControlValidation,
77+
)
7578

7679
return (
7780
<section
@@ -88,7 +91,10 @@ const Root = ({
8891
{React.Children.map(children, child => {
8992
if (!React.isValidElement(child)) return
9093

91-
const describedBy = containsHint ? `${uniqueId}-hint` : undefined
94+
const describedBy =
95+
[containsHint && `${uniqueId}-hint`, containsValidation && `${uniqueId}-validation`]
96+
.filter(Boolean)
97+
.join(' ') || undefined
9298

9399
switch (child.type) {
94100
case TextInput:
@@ -140,6 +146,7 @@ const Root = ({
140146
return React.cloneElement(child as React.ReactElement, {
141147
className: clsx(isInlineControl && styles['FormControl-validation-checkbox'], child.props.className),
142148
validationStatus,
149+
id: `${uniqueId}-validation`,
143150
})
144151

145152
case FormControlHint:
@@ -213,7 +220,7 @@ type FormControlValidationProps = {
213220
validationStatus?: FormValidationStatus
214221
} & BaseProps<HTMLSpanElement>
215222

216-
const FormControlValidation = ({children, validationStatus, className}: FormControlValidationProps) => {
223+
const FormControlValidation = ({children, validationStatus, className, ...props}: FormControlValidationProps) => {
217224
return (
218225
<span
219226
className={clsx(
@@ -222,6 +229,7 @@ const FormControlValidation = ({children, validationStatus, className}: FormCont
222229
validationStatus && styles[`FormControl-validation--${validationStatus}`],
223230
className,
224231
)}
232+
{...props}
225233
>
226234
{validationStatus === 'error' && (
227235
<span className={styles['FormControl-validation-error-icon']}>

0 commit comments

Comments
 (0)