5
5
useRef ,
6
6
useState ,
7
7
} from 'react' ;
8
+ import type { ReactNode } from 'react' ;
8
9
import type {
9
10
StyleProp ,
10
11
TextStyle ,
@@ -72,11 +73,24 @@ interface GooglePlacesTextInputStyles {
72
73
secondary ?: StyleProp < TextStyle > ;
73
74
} ;
74
75
loadingIndicator ?: {
75
- color ?: string ; // ✅ Keep as string, not StyleProp
76
+ color ?: string ;
76
77
} ;
77
78
placeholder ?: {
78
- color ?: string ; // ✅ Keep as string, not StyleProp
79
+ color ?: string ;
79
80
} ;
81
+ clearButtonText ?: StyleProp < ViewStyle > ;
82
+ }
83
+
84
+ interface GooglePlacesAccessibilityLabels {
85
+ input ?: string ;
86
+ clearButton ?: string ;
87
+ loadingIndicator ?: string ;
88
+ /**
89
+ * A function that receives a place prediction and returns a descriptive string
90
+ * for the suggestion item.
91
+ * @example (prediction) => `Select ${prediction.structuredFormat.mainText.text}, ${prediction.structuredFormat.secondaryText?.text}`
92
+ */
93
+ suggestionItem ?: ( prediction : PlacePrediction ) => string ;
80
94
}
81
95
82
96
type TextInputInheritedProps = Pick < TextInputProps , 'onFocus' | 'onBlur' > ;
@@ -98,6 +112,7 @@ interface GooglePlacesTextInputProps extends TextInputInheritedProps {
98
112
showClearButton ?: boolean ;
99
113
forceRTL ?: boolean ;
100
114
style ?: GooglePlacesTextInputStyles ;
115
+ clearElement ?: ReactNode ;
101
116
hideOnKeyboardDismiss ?: boolean ;
102
117
scrollEnabled ?: boolean ;
103
118
nestedScrollEnabled ?: boolean ;
@@ -106,10 +121,12 @@ interface GooglePlacesTextInputProps extends TextInputInheritedProps {
106
121
detailsFields ?: string [ ] ;
107
122
onError ?: ( error : any ) => void ;
108
123
enableDebug ?: boolean ;
124
+ accessibilityLabels ?: GooglePlacesAccessibilityLabels ;
109
125
}
110
126
111
127
interface GooglePlacesTextInputRef {
112
128
clear : ( ) => void ;
129
+ blur : ( ) => void ;
113
130
focus : ( ) => void ;
114
131
getSessionToken : ( ) => string | null ;
115
132
}
@@ -140,6 +157,7 @@ const GooglePlacesTextInput = forwardRef<
140
157
showClearButton = true ,
141
158
forceRTL = undefined ,
142
159
style = { } ,
160
+ clearElement,
143
161
hideOnKeyboardDismiss = false ,
144
162
scrollEnabled = true ,
145
163
nestedScrollEnabled = true ,
@@ -150,6 +168,7 @@ const GooglePlacesTextInput = forwardRef<
150
168
enableDebug = false ,
151
169
onFocus,
152
170
onBlur,
171
+ accessibilityLabels = { } ,
153
172
} ,
154
173
ref
155
174
) => {
@@ -210,6 +229,9 @@ const GooglePlacesTextInput = forwardRef<
210
229
setShowSuggestions ( false ) ;
211
230
setSessionToken ( generateSessionToken ( ) ) ;
212
231
} ,
232
+ blur : ( ) => {
233
+ inputRef . current ?. blur ( ) ;
234
+ } ,
213
235
focus : ( ) => {
214
236
inputRef . current ?. focus ( ) ;
215
237
} ,
@@ -452,8 +474,18 @@ const GooglePlacesTextInput = forwardRef<
452
474
const backgroundColor =
453
475
suggestionsContainerStyle ?. backgroundColor || '#efeff1' ;
454
476
477
+ const defaultAccessibilityLabel = `${ mainText . text } ${
478
+ secondaryText ? `, ${ secondaryText . text } ` : ''
479
+ } `;
480
+ const accessibilityLabel =
481
+ accessibilityLabels . suggestionItem ?.( item . placePrediction ) ||
482
+ defaultAccessibilityLabel ;
483
+
455
484
return (
456
485
< TouchableOpacity
486
+ accessibilityRole = "button"
487
+ accessibilityLabel = { accessibilityLabel }
488
+ accessibilityHint = "Double tap to select this place"
457
489
style = { [
458
490
styles . suggestionItem ,
459
491
{ backgroundColor } ,
@@ -541,7 +573,7 @@ const GooglePlacesTextInput = forwardRef<
541
573
} ) ;
542
574
}
543
575
// eslint-disable-next-line react-hooks/exhaustive-deps
544
- } , [ ] ) ; // ✅ Only run on mount
576
+ } , [ ] ) ;
545
577
546
578
return (
547
579
< View style = { [ styles . container , style . container ] } >
@@ -556,6 +588,8 @@ const GooglePlacesTextInput = forwardRef<
556
588
onFocus = { handleFocus }
557
589
onBlur = { handleBlur }
558
590
clearButtonMode = "never" // Disable iOS native clear button
591
+ accessibilityRole = "search"
592
+ accessibilityLabel = { accessibilityLabels . input || placeHolderText }
559
593
/>
560
594
561
595
{ /* Clear button - shown only if showClearButton is true */ }
@@ -574,15 +608,28 @@ const GooglePlacesTextInput = forwardRef<
574
608
setSessionToken ( generateSessionToken ( ) ) ;
575
609
inputRef . current ?. focus ( ) ;
576
610
} }
611
+ accessibilityRole = "button"
612
+ accessibilityLabel = {
613
+ accessibilityLabels . clearButton || 'Clear input text'
614
+ }
577
615
>
578
- < Text
579
- style = { Platform . select ( {
580
- ios : styles . iOSclearButton ,
581
- android : styles . androidClearButton ,
582
- } ) }
583
- >
584
- { '×' }
585
- </ Text >
616
+ { clearElement || (
617
+ < View style = { styles . clearTextWrapper } >
618
+ < Text
619
+ style = { [
620
+ Platform . select ( {
621
+ ios : styles . iOSclearText ,
622
+ android : styles . androidClearText ,
623
+ } ) ,
624
+ style . clearButtonText ,
625
+ ] }
626
+ accessibilityElementsHidden = { true }
627
+ importantForAccessibility = "no-hide-descendants"
628
+ >
629
+ { '×' }
630
+ </ Text >
631
+ </ View >
632
+ ) }
586
633
</ TouchableOpacity >
587
634
) }
588
635
@@ -592,6 +639,10 @@ const GooglePlacesTextInput = forwardRef<
592
639
style = { [ styles . loadingIndicator , getIconPosition ( 45 ) ] }
593
640
size = { 'small' }
594
641
color = { style . loadingIndicator ?. color || '#000000' }
642
+ accessibilityLiveRegion = "polite"
643
+ accessibilityLabel = {
644
+ accessibilityLabels . loadingIndicator || 'Loading suggestions'
645
+ }
595
646
/>
596
647
) }
597
648
</ View >
@@ -610,6 +661,8 @@ const GooglePlacesTextInput = forwardRef<
610
661
nestedScrollEnabled = { nestedScrollEnabled }
611
662
bounces = { false }
612
663
style = { style . suggestionsList }
664
+ accessibilityRole = "list"
665
+ accessibilityLabel = { `${ predictions . length } place suggestion resuts` }
613
666
/>
614
667
</ View >
615
668
) }
@@ -662,30 +715,27 @@ const styles = StyleSheet.create({
662
715
top : '50%' ,
663
716
transform : [ { translateY : - 10 } ] ,
664
717
} ,
665
- iOSclearButton : {
666
- fontSize : 18 ,
718
+ clearTextWrapper : {
719
+ backgroundColor : '#999' ,
720
+ borderRadius : 12 ,
721
+ width : 24 ,
722
+ height : 24 ,
723
+ alignItems : 'center' ,
724
+ justifyContent : 'center' ,
725
+ } ,
726
+ //this is never going to be consistent between different phone fonts and sizes
727
+ iOSclearText : {
728
+ fontSize : 22 ,
667
729
fontWeight : '400' ,
668
730
color : 'white' ,
669
- backgroundColor : '#999' ,
670
- width : 25 ,
671
- height : 25 ,
672
- borderRadius : 12.5 ,
673
- textAlign : 'center' ,
674
- textAlignVertical : 'center' ,
675
- lineHeight : 19 ,
731
+ lineHeight : 24 ,
676
732
includeFontPadding : false ,
677
733
} ,
678
- androidClearButton : {
734
+ androidClearText : {
679
735
fontSize : 24 ,
680
736
fontWeight : '400' ,
681
737
color : 'white' ,
682
- backgroundColor : '#999' ,
683
- width : 24 ,
684
- height : 24 ,
685
- borderRadius : 12 ,
686
- textAlign : 'center' ,
687
- textAlignVertical : 'center' ,
688
- lineHeight : 20 ,
738
+ lineHeight : 25.5 ,
689
739
includeFontPadding : false ,
690
740
} ,
691
741
} ) ;
@@ -698,6 +748,7 @@ export type {
698
748
PlaceDetailsFields ,
699
749
PlacePrediction ,
700
750
PlaceStructuredFormat ,
751
+ GooglePlacesAccessibilityLabels ,
701
752
} ;
702
753
703
754
export default GooglePlacesTextInput ;
0 commit comments