11import React from 'react' ;
22import {
3+ Platform ,
34 Pressable ,
45 Text ,
56 TextInput as NativeTextInput ,
@@ -28,7 +29,6 @@ import type { InternalTheme, ThemeProp } from '../../theme/types';
2829export type TextInputAnimationState = {
2930 animatedLabelWrapperStyle : StyleProp < AnimatedStyle < StyleProp < ViewStyle > > > ;
3031 animatedLabelTextStyle : StyleProp < AnimatedStyle < StyleProp < TextStyle > > > ;
31- animatedContainerStyle : StyleProp < AnimatedStyle < StyleProp < ViewStyle > > > ;
3232 animatedActiveOutlineStyle ?: StyleProp < AnimatedStyle < StyleProp < ViewStyle > > > ;
3333} ;
3434
@@ -57,13 +57,18 @@ export type TextInputColors = {
5757} ;
5858
5959export type GetAccessibilityDataReturn = {
60- input : AccessibilityProps & { 'aria-invalid' ?: boolean } ;
61- supportingText : AccessibilityProps ;
62- counter : AccessibilityProps ;
60+ input : AccessibilityProps & {
61+ 'aria-invalid' ?: TextInputProps [ 'aria-invalid' ] ;
62+ 'aria-describedby' ?: string ;
63+ } ;
64+ label : { nativeID : string } ;
65+ supportingText : AccessibilityProps & { nativeID : string } ;
66+ counter : AccessibilityProps & { nativeID : string } ;
6367} ;
6468
6569export type GetAccessibilityDataProps = {
6670 data : TextInputProps ;
71+ id : string ;
6772 inputLength : number ;
6873 hasError : boolean ;
6974 hasCounter : boolean ;
@@ -76,6 +81,7 @@ export type TextInputSharedApi = {
7681 input : React . RefObject < NativeTextInput | null > ;
7782 theme : InternalTheme ;
7883 isFocused : boolean ;
84+ isHovered ?: boolean ;
7985 isRTL : boolean ;
8086 isDisabled : boolean ;
8187 hasAccessory : boolean ;
@@ -147,7 +153,6 @@ export type TextInputHookReturn = SharedTextInputStyleData & {
147153 animatedActiveOutlineStyles :
148154 | StyleProp < AnimatedStyle < StyleProp < ViewStyle > > >
149155 | undefined ;
150- animatedContainerStyle : StyleProp < AnimatedStyle < StyleProp < ViewStyle > > > ;
151156 animatedLabelWrapperStyles : StyleProp < AnimatedStyle < StyleProp < ViewStyle > > > ;
152157 containerStyles : StyleProp < ViewStyle > ;
153158 fieldStyles : StyleProp < ViewStyle > ;
@@ -167,12 +172,16 @@ export type TextInputHookReturn = SharedTextInputStyleData & {
167172 onFocus : ( e : FocusEvent ) => void ;
168173 onBlur : ( e : BlurEvent ) => void ;
169174 focusInput : ( ) => void ;
175+ onHoverIn : ( ) => void ;
176+ onHoverOut : ( ) => void ;
170177} ;
171178
172179export type TextInputRenderProps = React . ComponentPropsWithoutRef <
173180 typeof NativeTextInput
174181> & {
175182 ref ?: React . RefObject < NativeTextInput | null > ;
183+ 'aria-describedby' ?: string ;
184+ 'aria-invalid' ?: TextInputProps [ 'aria-invalid' ] ;
176185} ;
177186
178187export type TextInputHandles = Pick <
@@ -181,6 +190,17 @@ export type TextInputHandles = Pick<
181190> ;
182191
183192export type TextInputProps = NativeTextInputProps & {
193+ /**
194+ * Overrides the field's invalid state on web, including grammar or spelling
195+ * errors. Defaults to the error state or an exceeded character counter.
196+ */
197+ 'aria-invalid' ?: React . AriaAttributes [ 'aria-invalid' ] ;
198+ /**
199+ * Space-separated IDs of additional descriptions on web. The IDs of rendered
200+ * supporting text and the character counter are appended automatically.
201+ * On Android and iOS, provide external descriptions with `accessibilityHint`.
202+ */
203+ 'aria-describedby' ?: string ;
184204 /**
185205 * Imperative handle exposing a subset of native `TextInput` methods
186206 * with side-effect handling (e.g. `clear()` syncs internal state and animations).
@@ -204,11 +224,15 @@ export type TextInputProps = NativeTextInputProps & {
204224 label ?: string ;
205225 /**
206226 * Supporting text to display below the input (Material Design 3).
227+ * Associated with the field through `aria-describedby` on web and included
228+ * in the native accessibility hint. When `error` is true, it is announced
229+ * as an alert. It does not become part of the field's accessible name.
207230 */
208231 supportingText ?: string ;
209232 /**
210233 * When `true`, displays a character counter below the input on the trailing
211234 * side, showing `currentLength/maxLength`. Requires `maxLength` to be set.
235+ * Associated with the field alongside supporting text.
212236 */
213237 counter ?: boolean ;
214238 /**
@@ -324,7 +348,6 @@ function TextInput({
324348 animatedActiveOutlineStyles,
325349 animatedLabelWrapperStyles,
326350 animatedLabelTextStyles,
327- animatedContainerStyle,
328351 containerStyles,
329352 inputStyles,
330353 prefixStyles,
@@ -343,6 +366,8 @@ function TextInput({
343366 onChangeText,
344367 onFocus,
345368 onBlur,
369+ onHoverIn,
370+ onHoverOut,
346371 } = useTextInput ( {
347372 ref,
348373 error,
@@ -360,8 +385,25 @@ function TextInput({
360385 } ) ;
361386
362387 return (
363- < Pressable onPress = { focusInput } accessible = { false } role = "none" >
364- < View style = { fieldStyles } >
388+ < Pressable
389+ onPress = { focusInput }
390+ onHoverIn = { Platform . OS === 'web' ? undefined : onHoverIn }
391+ onHoverOut = { Platform . OS === 'web' ? undefined : onHoverOut }
392+ accessible = { false }
393+ role = "none"
394+ >
395+ < View
396+ style = { fieldStyles }
397+ // Nested web Pressables contain hover events; track the whole field.
398+ onPointerEnter = {
399+ Platform . OS === 'web'
400+ ? ( event ) => {
401+ if ( event . nativeEvent . pointerType !== 'touch' ) onHoverIn ( ) ;
402+ }
403+ : undefined
404+ }
405+ onPointerLeave = { Platform . OS === 'web' ? onHoverOut : undefined }
406+ >
365407 { /* Disabled tint overlay — filled variant only. A childless
366408 absolutely-positioned View whose translucent fill is applied via the
367409 `opacity` style, so it never affects label/input rendering and works
@@ -383,7 +425,10 @@ function TextInput({
383425
384426 { ! ! label && (
385427 < Animated . View aria-hidden style = { animatedLabelWrapperStyles } >
386- < Animated . Text style = { animatedLabelTextStyles } >
428+ < Animated . Text
429+ { ...accessibilityProps . label }
430+ style = { animatedLabelTextStyles }
431+ >
387432 { label }
388433 </ Animated . Text >
389434 </ Animated . View >
@@ -398,17 +443,19 @@ function TextInput({
398443 } )
399444 : null }
400445
401- < Animated . View style = { [ containerStyles , animatedContainerStyle ] } >
446+ { /* Keep the field visible to native accessibility even before focus. */ }
447+ < View style = { containerStyles } >
402448 { hasPrefix && < Text style = { prefixStyles } > { prefix } </ Text > }
403449
404450 { render ( {
405451 ref : input ,
406452 selectionColor,
407453 cursorColor,
408454 placeholderTextColor,
409- ...accessibilityProps . input ,
410455 ...rest ,
456+ ...accessibilityProps . input ,
411457 editable : isEditable ,
458+ readOnly : disabled ? true : rest . readOnly ,
412459 placeholder,
413460 style : inputStyles ,
414461 onChangeText,
@@ -417,7 +464,7 @@ function TextInput({
417464 } ) }
418465
419466 { hasSuffix && < Text style = { suffixStyles } > { suffix } </ Text > }
420- </ Animated . View >
467+ </ View >
421468
422469 { renderTrailingAccessory ? (
423470 renderTrailingAccessory ( {
@@ -434,6 +481,7 @@ function TextInput({
434481 < View style = { styles . addendum } >
435482 { ! ! supportingText && (
436483 < Text
484+ key = { hasError ? 'error' : 'supporting' }
437485 { ...accessibilityProps . supportingText }
438486 style = { supportingTextStyles }
439487 >
0 commit comments