Skip to content

Commit 51f46e0

Browse files
authored
feat(textfield): Add proper ARIA labeling to validation icon (#8429)
1 parent 2d9544d commit 51f46e0

File tree

6 files changed

+57
-7
lines changed

6 files changed

+57
-7
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"valid": "Valid"
3+
}

packages/@react-spectrum/textfield/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
},
4242
"dependencies": {
4343
"@react-aria/focus": "^3.20.5",
44+
"@react-aria/i18n": "^3.12.10",
4445
"@react-aria/interactions": "^3.25.3",
4546
"@react-aria/textfield": "^3.17.5",
4647
"@react-aria/utils": "^3.29.1",

packages/@react-spectrum/textfield/src/TextFieldBase.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ import AlertMedium from '@spectrum-icons/ui/AlertMedium';
1414
import CheckmarkMedium from '@spectrum-icons/ui/CheckmarkMedium';
1515
import {classNames, createFocusableRef} from '@react-spectrum/utils';
1616
import {Field} from '@react-spectrum/label';
17-
import {mergeProps} from '@react-aria/utils';
17+
// @ts-ignore
18+
import intlMessages from '../intl/*.json';
19+
import {mergeProps, useId} from '@react-aria/utils';
1820
import {PressEvents, RefObject, ValidationResult} from '@react-types/shared';
1921
import React, {cloneElement, forwardRef, HTMLAttributes, InputHTMLAttributes, LabelHTMLAttributes, ReactElement, Ref, TextareaHTMLAttributes, useImperativeHandle, useRef} from 'react';
2022
import {SpectrumTextFieldProps, TextFieldRef} from '@react-types/textfield';
2123
import styles from '@adobe/spectrum-css-temp/components/textfield/vars.css';
2224
import {useFocusRing} from '@react-aria/focus';
2325
import {useHover} from '@react-aria/interactions';
26+
import {useLocalizedStringFormatter} from '@react-aria/i18n';
2427

2528
interface TextFieldBaseProps extends Omit<SpectrumTextFieldProps, 'onChange' | 'validate'>, PressEvents, Partial<ValidationResult> {
2629
wrapperChildren?: ReactElement | ReactElement[],
@@ -91,7 +94,11 @@ export const TextFieldBase = forwardRef(function TextFieldBase(props: TextFieldB
9194
} as any);
9295
}
9396

94-
let validationIcon = isInvalid ? <AlertMedium /> : <CheckmarkMedium />;
97+
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/textfield');
98+
let validId = useId();
99+
let validationIcon = isInvalid
100+
? <AlertMedium />
101+
: <CheckmarkMedium id={validId} aria-label={stringFormatter.format('valid')} />;
95102
let validation = cloneElement(validationIcon, {
96103
UNSAFE_className: classNames(
97104
styles,
@@ -122,7 +129,18 @@ export const TextFieldBase = forwardRef(function TextFieldBase(props: TextFieldB
122129
)
123130
}>
124131
<ElementType
125-
{...mergeProps(inputProps, hoverProps, focusProps)}
132+
{...mergeProps(
133+
inputProps,
134+
hoverProps,
135+
focusProps,
136+
validationState === 'valid' && !isLoading && !isDisabled
137+
? {
138+
'aria-describedby': inputProps['aria-describedby']
139+
? `${inputProps['aria-describedby']} ${validId}`
140+
: validId
141+
}
142+
: undefined
143+
)}
126144
ref={inputRef as any}
127145
rows={multiLine ? 1 : undefined}
128146
className={

packages/@react-spectrum/textfield/stories/Textfield.stories.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,25 @@ export const WithErrorMessage: TextFieldStory = {
115115
name: 'with error message'
116116
};
117117

118+
export const WithValidState: TextFieldStory = {
119+
render: (args) => render({
120+
...args,
121+
122+
validationState: 'valid'
123+
}),
124+
name: 'with valid state (shows validation icon)'
125+
};
126+
127+
export const WithValidStateAndDescription: TextFieldStory = {
128+
render: (args) => render({
129+
...args,
130+
131+
validationState: 'valid',
132+
description: 'This email address is valid and will be used for notifications.'
133+
}),
134+
name: 'with valid state and description'
135+
};
136+
118137
export const WithDescriptionErrorMessageAndValidation: TextFieldStory = {
119138
render: (args) => renderWithDescriptionErrorMessageAndValidation(args),
120139
name: 'with description, error message and validation'

packages/@react-spectrum/textfield/test/TextField.test.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,9 @@ describe('Shared TextField behavior', () => {
337337
let input = tree.getByTestId(testId);
338338
let helpText = tree.getByText('Enter a single digit number.');
339339
expect(helpText).toHaveAttribute('id');
340-
expect(input).toHaveAttribute('aria-describedby', `${helpText.id}`);
340+
let validIcon = tree.getByLabelText('Valid');
341+
expect(validIcon).toHaveAttribute('id');
342+
expect(input).toHaveAttribute('aria-describedby', `${helpText.id} ${validIcon.id}`);
341343
expect(input.value).toBe('0');
342344
let newValue = 's';
343345
fireEvent.change(input, {target: {value: newValue}});
@@ -356,7 +358,9 @@ describe('Shared TextField behavior', () => {
356358
expect(input.value).toBe(newValue);
357359
helpText = tree.getByText('Enter a single digit number.');
358360
expect(helpText).toHaveAttribute('id');
359-
expect(input).toHaveAttribute('aria-describedby', `${helpText.id}`);
361+
validIcon = tree.getByLabelText('Valid');
362+
expect(validIcon).toHaveAttribute('id');
363+
expect(input).toHaveAttribute('aria-describedby', `${helpText.id} ${validIcon.id}`);
360364
});
361365

362366
it.each`
@@ -387,7 +391,9 @@ describe('Shared TextField behavior', () => {
387391
let tree = renderComponent(Example);
388392
let input = tree.getByTestId(testId);
389393
let helpText;
390-
expect(tree.getByTestId(testId)).not.toHaveAttribute('aria-describedby');
394+
let validIcon = tree.getByLabelText('Valid');
395+
expect(validIcon).toHaveAttribute('id');
396+
expect(tree.getByTestId(testId)).toHaveAttribute('aria-describedby', validIcon.id);
391397

392398
fireEvent.change(input, {target: {value: 's'}});
393399

@@ -418,7 +424,9 @@ describe('Shared TextField behavior', () => {
418424
expect(input.value).toEqual('4');
419425
});
420426

421-
expect(input).not.toHaveAttribute('aria-describedby');
427+
validIcon = tree.getByLabelText('Valid');
428+
expect(validIcon).toHaveAttribute('id');
429+
expect(input).toHaveAttribute('aria-describedby', validIcon.id);
422430
});
423431

424432
it.each`

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8290,6 +8290,7 @@ __metadata:
82908290
dependencies:
82918291
"@adobe/spectrum-css-temp": "npm:3.0.0-alpha.1"
82928292
"@react-aria/focus": "npm:^3.20.5"
8293+
"@react-aria/i18n": "npm:^3.12.10"
82938294
"@react-aria/interactions": "npm:^3.25.3"
82948295
"@react-aria/textfield": "npm:^3.17.5"
82958296
"@react-aria/utils": "npm:^3.29.1"

0 commit comments

Comments
 (0)