Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/6.x/docs/guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,20 @@ import { TextInput, type TextInputProps } from 'react-native-paper';
/>
```

`TextInput.Icon` is decorative when no press handlers are provided. Decorative icons are
hidden from assistive technology and do not create a keyboard focus stop.
Icons with `onPress`, `onLongPress`, `onPressIn`, or `onPressOut` require an accessible name:

```tsx
<TextInput
label="Search"
startAccessory={(props) => <TextInput.Icon {...props} icon="magnify" />}
endAccessory={(props) => (
<TextInput.Icon {...props} icon="close" aria-label="Clear search" onPress={() => setValue('')} />
)}
/>
```

#### Label and supporting text

- **`label: React.Element | string`** → **`string`**
Expand Down Expand Up @@ -283,6 +297,26 @@ import { TextInput, type TextInputProps } from 'react-native-paper';
/>
```

Supporting text and the character counter describe the field without becoming
part of its accessible name. On web, their generated `nativeID` values are
referenced by the input's `aria-describedby`. Additional IDs passed through
`aria-describedby` are preserved. On Android and iOS, these descriptions are
included in `accessibilityHint`, alongside any hint you provide, because React
Native does not support native described-by relationships.

Error supporting text uses `role="alert"`. Android uses an assertive live region;
iOS announces changed error messages through `AccessibilityInfo`.
Custom input renderers should forward the accessibility props they receive.
Explicit `aria-invalid` values are preserved; when omitted, validity is derived
from `error` and the character counter.
Empty fields remain visible to native accessibility before focus and after
clearing. The field content no longer fades with the floating label.

The filled resting indicator now uses `onSurfaceVariant` and changes to
`onSurface` on hover. An invalid filled field uses `error` at rest and
`onErrorContainer` on hover. The focused indicator continues to use `primary` (or
`error` for an invalid field). Outlined fields continue to use `outline` at rest.

#### Removed props

No direct `TextInput` equivalents for:
Expand Down
17 changes: 13 additions & 4 deletions example/src/Examples/TextInputExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,12 @@ const TextInputDemo = ({ variant }: TextInputDemoProps) => {
);

const trailingIcon = (props: TextInputAccessoryProps) => (
<TextInput.Icon {...props} icon="close" onPress={() => setValue('')} />
<TextInput.Icon
{...props}
icon="close"
aria-label="Clear text"
onPress={() => setValue('')}
/>
);

const inputColor = theme.colors.onSurfaceVariant;
Expand All @@ -100,8 +105,8 @@ const TextInputDemo = ({ variant }: TextInputDemoProps) => {
{ label: 'Error', key: 'error' },
{ label: 'Disabled', key: 'disabled' },
{ label: 'Readonly', key: 'readOnly' },
{ label: 'Leading icon', key: 'leadingIcon' },
{ label: 'Trailing icon', key: 'trailingIcon' },
{ label: 'Decorative leading icon', key: 'leadingIcon' },
{ label: 'Clear text action', key: 'trailingIcon' },
{ label: 'Counter', key: 'counter' },
{ label: 'Prefix', key: 'showPrefix' },
{ label: 'Suffix', key: 'showSuffix' },
Expand All @@ -123,7 +128,11 @@ const TextInputDemo = ({ variant }: TextInputDemoProps) => {
variant={variant}
label={modifiers.label || undefined}
placeholder={modifiers.placeholder || undefined}
supportingText={modifiers.helperText || undefined}
supportingText={
controls.error
? 'Please check the entered text'
: modifiers.helperText || undefined
}
error={controls.error}
disabled={controls.disabled}
editable={!controls.readOnly}
Expand Down
72 changes: 60 additions & 12 deletions src/components/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import {
Platform,
Pressable,
Text,
TextInput as NativeTextInput,
Expand Down Expand Up @@ -28,7 +29,6 @@ import type { InternalTheme, ThemeProp } from '../../theme/types';
export type TextInputAnimationState = {
animatedLabelWrapperStyle: StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>;
animatedLabelTextStyle: StyleProp<AnimatedStyle<StyleProp<TextStyle>>>;
animatedContainerStyle: StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>;
animatedActiveOutlineStyle?: StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>;
};

Expand Down Expand Up @@ -57,13 +57,18 @@ export type TextInputColors = {
};

export type GetAccessibilityDataReturn = {
input: AccessibilityProps & { 'aria-invalid'?: boolean };
supportingText: AccessibilityProps;
counter: AccessibilityProps;
input: AccessibilityProps & {
'aria-invalid'?: TextInputProps['aria-invalid'];
'aria-describedby'?: string;
};
label: { nativeID: string };
supportingText: AccessibilityProps & { nativeID: string };
counter: AccessibilityProps & { nativeID: string };
};

export type GetAccessibilityDataProps = {
data: TextInputProps;
id: string;
inputLength: number;
hasError: boolean;
hasCounter: boolean;
Expand All @@ -76,6 +81,7 @@ export type TextInputSharedApi = {
input: React.RefObject<NativeTextInput | null>;
theme: InternalTheme;
isFocused: boolean;
isHovered?: boolean;
isRTL: boolean;
isDisabled: boolean;
hasAccessory: boolean;
Expand Down Expand Up @@ -147,7 +153,6 @@ export type TextInputHookReturn = SharedTextInputStyleData & {
animatedActiveOutlineStyles:
| StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>
| undefined;
animatedContainerStyle: StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>;
animatedLabelWrapperStyles: StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>;
containerStyles: StyleProp<ViewStyle>;
fieldStyles: StyleProp<ViewStyle>;
Expand All @@ -167,12 +172,16 @@ export type TextInputHookReturn = SharedTextInputStyleData & {
onFocus: (e: FocusEvent) => void;
onBlur: (e: BlurEvent) => void;
focusInput: () => void;
onHoverIn: () => void;
onHoverOut: () => void;
};

export type TextInputRenderProps = React.ComponentPropsWithoutRef<
typeof NativeTextInput
> & {
ref?: React.RefObject<NativeTextInput | null>;
'aria-describedby'?: string;
'aria-invalid'?: TextInputProps['aria-invalid'];
};

export type TextInputHandles = Pick<
Expand All @@ -181,6 +190,17 @@ export type TextInputHandles = Pick<
>;

export type TextInputProps = NativeTextInputProps & {
/**
* Overrides the field's invalid state on web, including grammar or spelling
* errors. Defaults to the error state or an exceeded character counter.
*/
'aria-invalid'?: React.AriaAttributes['aria-invalid'];
/**
* Space-separated IDs of additional descriptions on web. The IDs of rendered
* supporting text and the character counter are appended automatically.
* On Android and iOS, provide external descriptions with `accessibilityHint`.
*/
'aria-describedby'?: string;
/**
* Imperative handle exposing a subset of native `TextInput` methods
* with side-effect handling (e.g. `clear()` syncs internal state and animations).
Expand All @@ -204,11 +224,15 @@ export type TextInputProps = NativeTextInputProps & {
label?: string;
/**
* Supporting text to display below the input (Material Design 3).
* Associated with the field through `aria-describedby` on web and included
* in the native accessibility hint. When `error` is true, it is announced
* as an alert. It does not become part of the field's accessible name.
*/
supportingText?: string;
/**
* When `true`, displays a character counter below the input on the trailing
* side, showing `currentLength/maxLength`. Requires `maxLength` to be set.
* Associated with the field alongside supporting text.
*/
counter?: boolean;
/**
Expand Down Expand Up @@ -324,7 +348,6 @@ function TextInput({
animatedActiveOutlineStyles,
animatedLabelWrapperStyles,
animatedLabelTextStyles,
animatedContainerStyle,
containerStyles,
inputStyles,
prefixStyles,
Expand All @@ -343,6 +366,8 @@ function TextInput({
onChangeText,
onFocus,
onBlur,
onHoverIn,
onHoverOut,
} = useTextInput({
ref,
error,
Expand All @@ -360,8 +385,25 @@ function TextInput({
});

return (
<Pressable onPress={focusInput} accessible={false} role="none">
<View style={fieldStyles}>
<Pressable
onPress={focusInput}
onHoverIn={Platform.OS === 'web' ? undefined : onHoverIn}
onHoverOut={Platform.OS === 'web' ? undefined : onHoverOut}
accessible={false}
role="none"
>
<View
style={fieldStyles}
// Nested web Pressables contain hover events; track the whole field.
onPointerEnter={
Platform.OS === 'web'
? (event) => {
if (event.nativeEvent.pointerType !== 'touch') onHoverIn();
}
: undefined
}
onPointerLeave={Platform.OS === 'web' ? onHoverOut : undefined}
>
{/* Disabled tint overlay — filled variant only. A childless
absolutely-positioned View whose translucent fill is applied via the
`opacity` style, so it never affects label/input rendering and works
Expand All @@ -383,7 +425,10 @@ function TextInput({

{!!label && (
<Animated.View aria-hidden style={animatedLabelWrapperStyles}>
<Animated.Text style={animatedLabelTextStyles}>
<Animated.Text
{...accessibilityProps.label}
style={animatedLabelTextStyles}
>
{label}
</Animated.Text>
</Animated.View>
Expand All @@ -398,17 +443,19 @@ function TextInput({
})
: null}

<Animated.View style={[containerStyles, animatedContainerStyle]}>
{/* Keep the field visible to native accessibility even before focus. */}
<View style={containerStyles}>
{hasPrefix && <Text style={prefixStyles}>{prefix}</Text>}

{render({
ref: input,
selectionColor,
cursorColor,
placeholderTextColor,
...accessibilityProps.input,
...rest,
...accessibilityProps.input,
editable: isEditable,
readOnly: disabled ? true : rest.readOnly,
placeholder,
style: inputStyles,
onChangeText,
Expand All @@ -417,7 +464,7 @@ function TextInput({
})}

{hasSuffix && <Text style={suffixStyles}>{suffix}</Text>}
</Animated.View>
</View>

{renderTrailingAccessory ? (
renderTrailingAccessory({
Expand All @@ -434,6 +481,7 @@ function TextInput({
<View style={styles.addendum}>
{!!supportingText && (
<Text
key={hasError ? 'error' : 'supporting'}
{...accessibilityProps.supportingText}
style={supportingTextStyles}
>
Expand Down
Loading
Loading