Skip to content

Commit 6a92286

Browse files
authored
Merge pull request #21 from phazei/accessibility
Accessibility options, blur method, custom clear element.
2 parents 0f033c2 + fe41690 commit 6a92286

File tree

2 files changed

+155
-29
lines changed

2 files changed

+155
-29
lines changed

docs/styling-guide.md

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Styles = {
2323
placeholder?: {
2424
color?: string;
2525
};
26+
clearButtonText?: ViewStyle;
2627
}
2728
```
2829
@@ -34,7 +35,13 @@ Understanding the component's structure helps with styling. Here's how the compo
3435
<View style={[styles.container, style.container]}>
3536
<View>
3637
<TextInput style={[styles.input, style.input, ...]} />
37-
<TouchableOpacity> (Clear button) </TouchableOpacity>
38+
<TouchableOpacity>
39+
{clearElement || (
40+
<View style={styles.clearTextWrapper}>
41+
<Text style={[styles.clearText, style.clearButtonText]}>×</Text>
42+
</View>
43+
)}
44+
</TouchableOpacity>
3845
<ActivityIndicator /> (Loading indicator)
3946
</View>
4047
<View style={[styles.suggestionsContainer, style.suggestionsContainer]}>
@@ -75,6 +82,10 @@ const styles = {
7582
},
7683
placeholder: {
7784
color: '#888888',
85+
},
86+
clearButtonText: {
87+
color: '#FF0000', // Red X
88+
fontSize: 20,
7889
}
7990
};
8091
```
@@ -129,6 +140,11 @@ const materialStyles = {
129140
},
130141
placeholder: {
131142
color: '#9E9E9E',
143+
},
144+
clearButtonText: {
145+
color: '#FFFFFF',
146+
fontSize: 22,
147+
fontWeight: '400',
132148
}
133149
};
134150
```
@@ -180,10 +196,49 @@ const iosStyles = {
180196
},
181197
placeholder: {
182198
color: '#8E8E93',
199+
},
200+
clearButtonText: {
201+
color: '#FFFFFF',
202+
fontSize: 22,
203+
fontWeight: '400',
183204
}
184205
};
185206
```
186207
208+
## ✨ NEW: Custom Close Element
209+
210+
You can now provide a custom close element instead of the default "×" text:
211+
212+
```javascript
213+
import { Ionicons } from '@expo/vector-icons';
214+
215+
<GooglePlacesTextInput
216+
apiKey="YOUR_KEY"
217+
clearElement={
218+
<Icon name="close-circle" size={24} color="#999" />
219+
}
220+
// ...other props
221+
/>
222+
```
223+
224+
## ✨ NEW: Accessibility Labels
225+
226+
The component now supports comprehensive accessibility customization:
227+
228+
```javascript
229+
<GooglePlacesTextInput
230+
apiKey="YOUR_KEY"
231+
accessibilityLabels={{
232+
input: 'Search for places',
233+
clearButton: 'Clear search text',
234+
loadingIndicator: 'Searching for places',
235+
suggestionItem: (prediction) =>
236+
`Select ${prediction.structuredFormat.mainText.text}, ${prediction.structuredFormat.secondaryText?.text || ''}`
237+
}}
238+
// ...other props
239+
/>
240+
```
241+
187242
## Styling the Suggestions List
188243
189244
The suggestions list is implemented as a FlatList with customizable height:
@@ -231,6 +286,26 @@ The clear button is automatically styled based on platform (iOS or Android) but
231286
/>
232287
```
233288
289+
## ✨ NEW: Programmatic Control
290+
291+
The component now exposes a `blur()` method in addition to the existing `clear()` and `focus()` methods:
292+
293+
```javascript
294+
const inputRef = useRef();
295+
296+
// Blur the input
297+
inputRef.current?.blur();
298+
299+
// Clear the input
300+
inputRef.current?.clear();
301+
302+
// Focus the input
303+
inputRef.current?.focus();
304+
305+
// Get current session token
306+
const token = inputRef.current?.getSessionToken();
307+
```
308+
234309
## RTL Support
235310

236311
The component automatically handles RTL layouts based on the text direction. You can also force RTL with the `forceRTL` prop:

src/GooglePlacesTextInput.tsx

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
useRef,
66
useState,
77
} from 'react';
8+
import type { ReactNode } from 'react';
89
import type {
910
StyleProp,
1011
TextStyle,
@@ -72,11 +73,24 @@ interface GooglePlacesTextInputStyles {
7273
secondary?: StyleProp<TextStyle>;
7374
};
7475
loadingIndicator?: {
75-
color?: string; // ✅ Keep as string, not StyleProp
76+
color?: string;
7677
};
7778
placeholder?: {
78-
color?: string; // ✅ Keep as string, not StyleProp
79+
color?: string;
7980
};
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;
8094
}
8195

8296
type TextInputInheritedProps = Pick<TextInputProps, 'onFocus' | 'onBlur'>;
@@ -98,6 +112,7 @@ interface GooglePlacesTextInputProps extends TextInputInheritedProps {
98112
showClearButton?: boolean;
99113
forceRTL?: boolean;
100114
style?: GooglePlacesTextInputStyles;
115+
clearElement?: ReactNode;
101116
hideOnKeyboardDismiss?: boolean;
102117
scrollEnabled?: boolean;
103118
nestedScrollEnabled?: boolean;
@@ -106,10 +121,12 @@ interface GooglePlacesTextInputProps extends TextInputInheritedProps {
106121
detailsFields?: string[];
107122
onError?: (error: any) => void;
108123
enableDebug?: boolean;
124+
accessibilityLabels?: GooglePlacesAccessibilityLabels;
109125
}
110126

111127
interface GooglePlacesTextInputRef {
112128
clear: () => void;
129+
blur: () => void;
113130
focus: () => void;
114131
getSessionToken: () => string | null;
115132
}
@@ -140,6 +157,7 @@ const GooglePlacesTextInput = forwardRef<
140157
showClearButton = true,
141158
forceRTL = undefined,
142159
style = {},
160+
clearElement,
143161
hideOnKeyboardDismiss = false,
144162
scrollEnabled = true,
145163
nestedScrollEnabled = true,
@@ -150,6 +168,7 @@ const GooglePlacesTextInput = forwardRef<
150168
enableDebug = false,
151169
onFocus,
152170
onBlur,
171+
accessibilityLabels = {},
153172
},
154173
ref
155174
) => {
@@ -210,6 +229,9 @@ const GooglePlacesTextInput = forwardRef<
210229
setShowSuggestions(false);
211230
setSessionToken(generateSessionToken());
212231
},
232+
blur: () => {
233+
inputRef.current?.blur();
234+
},
213235
focus: () => {
214236
inputRef.current?.focus();
215237
},
@@ -452,8 +474,18 @@ const GooglePlacesTextInput = forwardRef<
452474
const backgroundColor =
453475
suggestionsContainerStyle?.backgroundColor || '#efeff1';
454476

477+
const defaultAccessibilityLabel = `${mainText.text}${
478+
secondaryText ? `, ${secondaryText.text}` : ''
479+
}`;
480+
const accessibilityLabel =
481+
accessibilityLabels.suggestionItem?.(item.placePrediction) ||
482+
defaultAccessibilityLabel;
483+
455484
return (
456485
<TouchableOpacity
486+
accessibilityRole="button"
487+
accessibilityLabel={accessibilityLabel}
488+
accessibilityHint="Double tap to select this place"
457489
style={[
458490
styles.suggestionItem,
459491
{ backgroundColor },
@@ -541,7 +573,7 @@ const GooglePlacesTextInput = forwardRef<
541573
});
542574
}
543575
// eslint-disable-next-line react-hooks/exhaustive-deps
544-
}, []); // ✅ Only run on mount
576+
}, []);
545577

546578
return (
547579
<View style={[styles.container, style.container]}>
@@ -556,6 +588,8 @@ const GooglePlacesTextInput = forwardRef<
556588
onFocus={handleFocus}
557589
onBlur={handleBlur}
558590
clearButtonMode="never" // Disable iOS native clear button
591+
accessibilityRole="search"
592+
accessibilityLabel={accessibilityLabels.input || placeHolderText}
559593
/>
560594

561595
{/* Clear button - shown only if showClearButton is true */}
@@ -574,15 +608,28 @@ const GooglePlacesTextInput = forwardRef<
574608
setSessionToken(generateSessionToken());
575609
inputRef.current?.focus();
576610
}}
611+
accessibilityRole="button"
612+
accessibilityLabel={
613+
accessibilityLabels.clearButton || 'Clear input text'
614+
}
577615
>
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+
)}
586633
</TouchableOpacity>
587634
)}
588635

@@ -592,6 +639,10 @@ const GooglePlacesTextInput = forwardRef<
592639
style={[styles.loadingIndicator, getIconPosition(45)]}
593640
size={'small'}
594641
color={style.loadingIndicator?.color || '#000000'}
642+
accessibilityLiveRegion="polite"
643+
accessibilityLabel={
644+
accessibilityLabels.loadingIndicator || 'Loading suggestions'
645+
}
595646
/>
596647
)}
597648
</View>
@@ -610,6 +661,8 @@ const GooglePlacesTextInput = forwardRef<
610661
nestedScrollEnabled={nestedScrollEnabled}
611662
bounces={false}
612663
style={style.suggestionsList}
664+
accessibilityRole="list"
665+
accessibilityLabel={`${predictions.length} place suggestion resuts`}
613666
/>
614667
</View>
615668
)}
@@ -662,30 +715,27 @@ const styles = StyleSheet.create({
662715
top: '50%',
663716
transform: [{ translateY: -10 }],
664717
},
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,
667729
fontWeight: '400',
668730
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,
676732
includeFontPadding: false,
677733
},
678-
androidClearButton: {
734+
androidClearText: {
679735
fontSize: 24,
680736
fontWeight: '400',
681737
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,
689739
includeFontPadding: false,
690740
},
691741
});
@@ -698,6 +748,7 @@ export type {
698748
PlaceDetailsFields,
699749
PlacePrediction,
700750
PlaceStructuredFormat,
751+
GooglePlacesAccessibilityLabels,
701752
};
702753

703754
export default GooglePlacesTextInput;

0 commit comments

Comments
 (0)