Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
60 changes: 60 additions & 0 deletions docs/6.x/docs/guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,40 @@ import { TextInput, type TextInputProps } from 'react-native-paper';
/>
```

`TextInput.Icon` without a press handler is decorative: it is hidden from
assistive technology and is not a keyboard focus stop. An icon with `onPress`,
`onLongPress`, `onPressIn` or `onPressOut` requires an `aria-label` or
`accessibilityLabel` that names its action:

```tsx
const [value, setValue] = React.useState('');

// Before (v5)
<TextInput
label="Search"
value={value}
onChangeText={setValue}
left={<TextInput.Icon icon="magnify" />}
right={<TextInput.Icon icon="close" onPress={() => setValue('')} />}
/>

// After (v6)
<TextInput
label="Search"
value={value}
onChangeText={setValue}
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 @@ -382,6 +416,32 @@ import { TextInput, type TextInputProps } from 'react-native-paper';
/>
```

Supporting text and the character counter describe the field instead of
becoming part of its accessible name. On web, the input references them with
`aria-describedby`, and IDs you pass in `aria-describedby` are kept. On Android
and iOS, they are only exposed through `accessibilityHint`, alongside any hint
you pass, and the visible text is hidden from assistive technology. Users who
turn hints off, for example with the VoiceOver Speak Hints setting, do not hear
them.

When no label or explicit accessible name is supplied, the placeholder names the
field, including after text is entered. A disabled field always exposes its
disabled state, even when `aria-disabled={false}` is supplied.

On web, error supporting text uses `role="alert"`. On Android and iOS, a new or
changed error message is announced through `AccessibilityInfo`, and an error
that is already shown when the field mounts is not announced there. On web,
explicit `aria-invalid` values are kept; when omitted, validity is derived from
`error` and the character counter. Custom input renderers should forward the
accessibility props they receive.

The filled indicator uses `onSurfaceVariant` and the outlined border uses
`outline` at rest. On web, both change to `onSurface` on hover, and an invalid
field changes from `error` to `onErrorContainer`. Focus takes precedence over
hover and uses `primary`, or `error` for an invalid field. A disabled field uses
the disabled colors even when `error` is set. On web it keeps `aria-invalid`; on
Android and iOS the error text stays in its hint.

#### 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
79 changes: 66 additions & 13 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,16 @@ 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, a new or changed
* message is announced. 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 +349,6 @@ function TextInput({
animatedActiveOutlineStyles,
animatedLabelWrapperStyles,
animatedLabelTextStyles,
animatedContainerStyle,
containerStyles,
inputStyles,
prefixStyles,
Expand All @@ -343,6 +367,8 @@ function TextInput({
onChangeText,
onFocus,
onBlur,
onHoverIn,
onHoverOut,
} = useTextInput({
ref,
error,
Expand All @@ -360,8 +386,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 +426,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 +444,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 +465,7 @@ function TextInput({
})}

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

{renderTrailingAccessory ? (
renderTrailingAccessory({
Expand All @@ -427,13 +475,18 @@ function TextInput({
multiline: !!rest.multiline,
})
) : hasError ? (
<TextInputErrorIcon style={trailingAccessoryStyles} theme={theme} />
<TextInputErrorIcon
style={trailingAccessoryStyles}
disabled={isDisabled}
theme={theme}
/>
) : null}
</View>

<View style={styles.addendum}>
{!!supportingText && (
<Text
key={hasError ? 'error' : 'supporting'}
{...accessibilityProps.supportingText}
style={supportingTextStyles}
>
Expand Down
5 changes: 4 additions & 1 deletion src/components/TextInput/TextInputErrorIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ import { View } from 'react-native';
import type { StyleProp, ViewStyle } from 'react-native';

import { ACCESSORY_SIZE } from './constants';
import { getIconColor } from './utils';
import { useInternalTheme } from '../../core/theming';
import type { ThemeProp } from '../../theme/types';
import Icon from '../Icon';

interface TextInputErrorIconProps {
style?: StyleProp<ViewStyle>;
disabled?: boolean;
theme?: ThemeProp;
}

const TextInputErrorIcon = ({
style: wrapperStyle,
disabled = false,
theme: themeOverride,
}: TextInputErrorIconProps) => {
const theme = useInternalTheme(themeOverride);
Expand All @@ -22,7 +25,7 @@ const TextInputErrorIcon = ({
<Icon
source="alert-circle"
size={ACCESSORY_SIZE}
color={theme.colors.error}
color={getIconColor({ theme, hasError: true, isDisabled: disabled })}
/>
</View>
);
Expand Down
Loading
Loading