Skip to content

Commit a796f25

Browse files
committed
fix(text-input): address review feedback
1 parent ced769d commit a796f25

5 files changed

Lines changed: 101 additions & 26 deletions

File tree

‎docs/6.x/docs/guides/migration.md‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,10 @@ referenced by the input's `aria-describedby`. Additional IDs passed through
305305
included in `accessibilityHint`, alongside any hint you provide, because React
306306
Native does not support native described-by relationships.
307307

308+
When no label or explicit accessible name is supplied, the placeholder names the
309+
field, including after text is entered. A disabled field always exposes its
310+
disabled state, even when `aria-disabled={false}` is supplied.
311+
308312
Error supporting text uses `role="alert"`. Android uses an assertive live region;
309313
iOS announces changed error messages through `AccessibilityInfo`.
310314
Custom input renderers should forward the accessibility props they receive.
@@ -313,10 +317,10 @@ from `error` and the character counter.
313317
Empty fields remain visible to native accessibility before focus and after
314318
clearing. The field content no longer fades with the floating label.
315319

316-
The filled resting indicator now uses `onSurfaceVariant` and changes to
317-
`onSurface` on hover. An invalid filled field uses `error` at rest and
318-
`onErrorContainer` on hover. The focused indicator continues to use `primary` (or
319-
`error` for an invalid field). Outlined fields continue to use `outline` at rest.
320+
The filled resting indicator now uses `onSurfaceVariant`; outlined fields use
321+
`outline` at rest. Both change to `onSurface` on hover. Invalid fields use `error`
322+
at rest and `onErrorContainer` on hover. Focus takes precedence over hover and
323+
uses `primary` (or `error` for an invalid field).
320324

321325
#### Removed props
322326

‎scripts/typescript-output-lint.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const output = join(root, 'lib', 'typescript');
1414
* More info: https://github.com/callstack/react-native-paper/pull/3603
1515
*/
1616
const unusedViewProps = [
17-
'nativeID',
1817
'accessibilityActions',
1918
'accessibilityValue',
2019
'onAccessibilityAction',

‎src/components/TextInput/utils.ts‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,15 +190,11 @@ export const getOutlineColor = ({
190190

191191
if (isDisabled) return colors.onSurface;
192192
if (hasError) {
193-
return variant === 'filled' && isHovered && !isFocused
194-
? colors.onErrorContainer
195-
: colors.error;
193+
return isHovered && !isFocused ? colors.onErrorContainer : colors.error;
196194
}
197195
if (isFocused) return colors.primary;
198-
if (variant === 'filled') {
199-
return isHovered ? colors.onSurface : colors.onSurfaceVariant;
200-
}
201-
return colors.outline;
196+
if (isHovered) return colors.onSurface;
197+
return variant === 'filled' ? colors.onSurfaceVariant : colors.outline;
202198
};
203199

204200
/**
@@ -478,6 +474,7 @@ export const getOutlinedTextInputData = (
478474
isDisabled,
479475
isFocused,
480476
hasError,
477+
isHovered: api.isHovered,
481478
});
482479

483480
/**
@@ -611,10 +608,10 @@ export const getAccessibilityData = ({
611608

612609
return {
613610
input: {
614-
'aria-label': explicitLabel ?? label,
611+
'aria-label': explicitLabel ?? label ?? props.placeholder,
615612
'aria-labelledby': labelledBy,
616613
'aria-describedby': describedBy,
617-
'aria-disabled': props['aria-disabled'] ?? isDisabled,
614+
'aria-disabled': isDisabled || (props['aria-disabled'] ?? false),
618615
'aria-invalid': props['aria-invalid'] ?? (hasError || isCounterExceeded),
619616
accessibilityHint: hint,
620617
},

‎src/components/__tests__/TextInputAccessibility.test.tsx‎

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,43 @@ afterEach(() => {
2020
renderInput.mockClear();
2121
});
2222

23+
it.each(['filled', 'outlined'] as const)(
24+
'uses the placeholder as the name of an unlabelled %s field, including after typing',
25+
async (variant) => {
26+
await render(<TextInput variant={variant} placeholder="Search" />);
27+
28+
const input = screen.getByLabelText('Search');
29+
await userEvent.type(input, 'Paper');
30+
expect(screen.getByLabelText('Search')).toHaveDisplayValue('Paper');
31+
}
32+
);
33+
34+
it.each([
35+
{ label: 'Search products' },
36+
{ 'aria-label': 'Search products' },
37+
{ accessibilityLabel: 'Search products' },
38+
])('prefers a supplied name over the placeholder: %j', async (props) => {
39+
await render(<TextInput {...props} placeholder="Search" />);
40+
41+
expect(screen.getByLabelText('Search products')).toBeOnTheScreen();
42+
expect(screen.queryByLabelText('Search')).toBeNull();
43+
});
44+
45+
it.each([
46+
{ disabled: true, 'aria-disabled': false, expectedDisabled: true },
47+
{ disabled: true, 'aria-disabled': true, expectedDisabled: true },
48+
{ disabled: false, 'aria-disabled': true, expectedDisabled: true },
49+
{ disabled: false, 'aria-disabled': false, expectedDisabled: false },
50+
])(
51+
'keeps the disabled semantics consistent: %j',
52+
async ({ expectedDisabled, ...props }) => {
53+
await render(<TextInput {...props} label="Email" readOnly />);
54+
55+
const input = screen.getByLabelText('Email');
56+
expect(input).toHaveProp('aria-disabled', expectedDisabled);
57+
}
58+
);
59+
2360
it.each(['filled', 'outlined'] as const)(
2461
'keeps an empty unfocused %s field visible to assistive technology',
2562
async (variant) => {
@@ -308,7 +345,7 @@ it('renders decorative accessories outside the accessibility tree without button
308345
).toBeOnTheScreen();
309346
});
310347

311-
it('preserves decorative loading and container visuals without accessible controls', async () => {
348+
it('preserves decorative loading and content styles without accessible controls', async () => {
312349
await render(
313350
<TextInput
314351
label="Search"
@@ -330,11 +367,6 @@ it('preserves decorative loading and container visuals without accessible contro
330367
expect(
331368
screen.getByRole('progressbar', { includeHiddenElements: true })
332369
).toBeOnTheScreen();
333-
expect(
334-
screen.getByTestId('search-decoration-container', {
335-
includeHiddenElements: true,
336-
})
337-
).toHaveStyle({ backgroundColor: 'pink', borderWidth: 1 });
338370
expect(
339371
screen.getByTestId('search-decoration', { includeHiddenElements: true })
340372
).toHaveStyle({ padding: 2 });

‎src/components/__tests__/TextInputStates.test.tsx‎

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { afterEach, expect, it, jest } from '@jest/globals';
44
import { renderHook } from '@testing-library/react-native';
55

66
import PaperProvider from '../../core/PaperProvider';
7-
import { getTheme } from '../../core/theming';
87
import { act } from '../../test-utils';
8+
import { LightTheme as theme } from '../../theme/schemes';
99
import { useTextInput } from '../TextInput/hooks';
10+
import { getOutlineColor } from '../TextInput/utils';
1011

11-
const theme = getTheme();
1212
afterEach(() => {
1313
jest.restoreAllMocks();
1414
});
@@ -79,13 +79,56 @@ it('uses MD3 filled resting and hover colors without overriding the focused or d
7979
).toBe(theme.colors.onSurfaceVariant);
8080
});
8181

82-
it('preserves the outlined resting color on hover', async () => {
83-
const { result } = await renderHook(
84-
() => useTextInput({ variant: 'outlined' }),
85-
{ wrapper: PaperProvider }
82+
it('updates the outlined hover color for errors and ignores hover when disabled', async () => {
83+
const { result, rerender } = await renderHook(
84+
(props: { error?: boolean; disabled?: boolean }) =>
85+
useTextInput({ variant: 'outlined', ...props }),
86+
{ initialProps: {}, wrapper: PaperProvider }
87+
);
88+
expect(StyleSheet.flatten(result.current.outlineStyles)?.borderColor).toBe(
89+
theme.colors.outline
90+
);
91+
await act(() => result.current.onHoverIn());
92+
expect(StyleSheet.flatten(result.current.outlineStyles)?.borderColor).toBe(
93+
theme.colors.onSurface
94+
);
95+
await rerender({ error: true });
96+
expect(StyleSheet.flatten(result.current.outlineStyles)?.borderColor).toBe(
97+
theme.colors.onErrorContainer
98+
);
99+
await act(() => result.current.onHoverOut());
100+
expect(StyleSheet.flatten(result.current.outlineStyles)?.borderColor).toBe(
101+
theme.colors.error
86102
);
87103
await act(() => result.current.onHoverIn());
104+
await rerender({ error: true, disabled: true });
105+
expect(StyleSheet.flatten(result.current.outlineStyles)?.borderColor).toBe(
106+
theme.colors.onSurface
107+
);
108+
await act(() => result.current.onHoverOut());
109+
await rerender({});
88110
expect(StyleSheet.flatten(result.current.outlineStyles)?.borderColor).toBe(
89111
theme.colors.outline
90112
);
91113
});
114+
115+
it.each(['filled', 'outlined'] as const)(
116+
'preserves the focused %s indicator when hovered, invalid or disabled',
117+
(variant) => {
118+
const state = {
119+
theme,
120+
variant,
121+
isFocused: true,
122+
isHovered: true,
123+
isDisabled: false,
124+
hasError: false,
125+
};
126+
expect(getOutlineColor(state)).toBe(theme.colors.primary);
127+
expect(getOutlineColor({ ...state, hasError: true })).toBe(
128+
theme.colors.error
129+
);
130+
expect(
131+
getOutlineColor({ ...state, hasError: true, isDisabled: true })
132+
).toBe(theme.colors.onSurface);
133+
}
134+
);

0 commit comments

Comments
 (0)