Skip to content
Merged
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
77 changes: 76 additions & 1 deletion docs/styling-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Styles = {
placeholder?: {
color?: string;
};
clearButtonText?: ViewStyle;
}
```

Expand All @@ -34,7 +35,13 @@ Understanding the component's structure helps with styling. Here's how the compo
<View style={[styles.container, style.container]}>
<View>
<TextInput style={[styles.input, style.input, ...]} />
<TouchableOpacity> (Clear button) </TouchableOpacity>
<TouchableOpacity>
{clearElement || (
<View style={styles.clearTextWrapper}>
<Text style={[styles.clearText, style.clearButtonText]}>×</Text>
</View>
)}
</TouchableOpacity>
<ActivityIndicator /> (Loading indicator)
</View>
<View style={[styles.suggestionsContainer, style.suggestionsContainer]}>
Expand Down Expand Up @@ -75,6 +82,10 @@ const styles = {
},
placeholder: {
color: '#888888',
},
clearButtonText: {
color: '#FF0000', // Red X
fontSize: 20,
}
};
```
Expand Down Expand Up @@ -129,6 +140,11 @@ const materialStyles = {
},
placeholder: {
color: '#9E9E9E',
},
clearButtonText: {
color: '#FFFFFF',
fontSize: 22,
fontWeight: '400',
}
};
```
Expand Down Expand Up @@ -180,10 +196,49 @@ const iosStyles = {
},
placeholder: {
color: '#8E8E93',
},
clearButtonText: {
color: '#FFFFFF',
fontSize: 22,
fontWeight: '400',
}
};
```

## ✨ NEW: Custom Close Element

You can now provide a custom close element instead of the default "×" text:

```javascript
import { Ionicons } from '@expo/vector-icons';

<GooglePlacesTextInput
apiKey="YOUR_KEY"
clearElement={
<Icon name="close-circle" size={24} color="#999" />
}
// ...other props
/>
```

## ✨ NEW: Accessibility Labels

The component now supports comprehensive accessibility customization:

```javascript
<GooglePlacesTextInput
apiKey="YOUR_KEY"
accessibilityLabels={{
input: 'Search for places',
clearButton: 'Clear search text',
loadingIndicator: 'Searching for places',
suggestionItem: (prediction) =>
`Select ${prediction.structuredFormat.mainText.text}, ${prediction.structuredFormat.secondaryText?.text || ''}`
}}
// ...other props
/>
```

## Styling the Suggestions List

The suggestions list is implemented as a FlatList with customizable height:
Expand Down Expand Up @@ -231,6 +286,26 @@ The clear button is automatically styled based on platform (iOS or Android) but
/>
```

## ✨ NEW: Programmatic Control

The component now exposes a `blur()` method in addition to the existing `clear()` and `focus()` methods:

```javascript
const inputRef = useRef();

// Blur the input
inputRef.current?.blur();

// Clear the input
inputRef.current?.clear();

// Focus the input
inputRef.current?.focus();

// Get current session token
const token = inputRef.current?.getSessionToken();
```

## RTL Support

The component automatically handles RTL layouts based on the text direction. You can also force RTL with the `forceRTL` prop:
Expand Down
111 changes: 83 additions & 28 deletions src/GooglePlacesTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
useRef,
useState,
} from 'react';
import type { ReactNode } from 'react';
import type {
StyleProp,
TextStyle,
Expand Down Expand Up @@ -72,11 +73,24 @@ interface GooglePlacesTextInputStyles {
secondary?: StyleProp<TextStyle>;
};
loadingIndicator?: {
color?: string; // ✅ Keep as string, not StyleProp
color?: string;
};
placeholder?: {
color?: string; // ✅ Keep as string, not StyleProp
color?: string;
};
clearButtonText?: StyleProp<ViewStyle>;
}

interface GooglePlacesAccessibilityLabels {
input?: string;
clearButton?: string;
loadingIndicator?: string;
/**
* A function that receives a place prediction and returns a descriptive string
* for the suggestion item.
* @example (prediction) => `Select ${prediction.structuredFormat.mainText.text}, ${prediction.structuredFormat.secondaryText?.text}`
*/
suggestionItem?: (prediction: PlacePrediction) => string;
}

type TextInputInheritedProps = Pick<TextInputProps, 'onFocus' | 'onBlur'>;
Expand All @@ -98,6 +112,7 @@ interface GooglePlacesTextInputProps extends TextInputInheritedProps {
showClearButton?: boolean;
forceRTL?: boolean;
style?: GooglePlacesTextInputStyles;
clearElement?: ReactNode;
hideOnKeyboardDismiss?: boolean;
scrollEnabled?: boolean;
nestedScrollEnabled?: boolean;
Expand All @@ -106,10 +121,12 @@ interface GooglePlacesTextInputProps extends TextInputInheritedProps {
detailsFields?: string[];
onError?: (error: any) => void;
enableDebug?: boolean;
accessibilityLabels?: GooglePlacesAccessibilityLabels;
}

interface GooglePlacesTextInputRef {
clear: () => void;
blur: () => void;
focus: () => void;
getSessionToken: () => string | null;
}
Expand Down Expand Up @@ -140,6 +157,7 @@ const GooglePlacesTextInput = forwardRef<
showClearButton = true,
forceRTL = undefined,
style = {},
clearElement,
hideOnKeyboardDismiss = false,
scrollEnabled = true,
nestedScrollEnabled = true,
Expand All @@ -150,6 +168,7 @@ const GooglePlacesTextInput = forwardRef<
enableDebug = false,
onFocus,
onBlur,
accessibilityLabels = {},
},
ref
) => {
Expand Down Expand Up @@ -179,6 +198,10 @@ const GooglePlacesTextInput = forwardRef<
};
}, []);

useEffect(() => {
setInputText(value ?? '');
}, [value]);

// Add keyboard listener
useEffect(() => {
if (hideOnKeyboardDismiss) {
Expand Down Expand Up @@ -206,6 +229,9 @@ const GooglePlacesTextInput = forwardRef<
setShowSuggestions(false);
setSessionToken(generateSessionToken());
},
blur: () => {
inputRef.current?.blur();
},
focus: () => {
inputRef.current?.focus();
},
Expand Down Expand Up @@ -448,8 +474,18 @@ const GooglePlacesTextInput = forwardRef<
const backgroundColor =
suggestionsContainerStyle?.backgroundColor || '#efeff1';

const defaultAccessibilityLabel = `${mainText.text}${
secondaryText ? `, ${secondaryText.text}` : ''
}`;
const accessibilityLabel =
accessibilityLabels.suggestionItem?.(item.placePrediction) ||
defaultAccessibilityLabel;

return (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={accessibilityLabel}
accessibilityHint="Double tap to select this place"
style={[
styles.suggestionItem,
{ backgroundColor },
Expand Down Expand Up @@ -537,7 +573,7 @@ const GooglePlacesTextInput = forwardRef<
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // ✅ Only run on mount
}, []);

return (
<View style={[styles.container, style.container]}>
Expand All @@ -552,6 +588,8 @@ const GooglePlacesTextInput = forwardRef<
onFocus={handleFocus}
onBlur={handleBlur}
clearButtonMode="never" // Disable iOS native clear button
accessibilityRole="search"
accessibilityLabel={accessibilityLabels.input || placeHolderText}
/>

{/* Clear button - shown only if showClearButton is true */}
Expand All @@ -570,15 +608,28 @@ const GooglePlacesTextInput = forwardRef<
setSessionToken(generateSessionToken());
inputRef.current?.focus();
}}
accessibilityRole="button"
accessibilityLabel={
accessibilityLabels.clearButton || 'Clear input text'
}
>
<Text
style={Platform.select({
ios: styles.iOSclearButton,
android: styles.androidClearButton,
})}
>
{'×'}
</Text>
{clearElement || (
<View style={styles.clearTextWrapper}>
<Text
style={[
Platform.select({
ios: styles.iOSclearText,
android: styles.androidClearText,
}),
style.clearButtonText,
]}
accessibilityElementsHidden={true}
importantForAccessibility="no-hide-descendants"
>
{'×'}
</Text>
</View>
)}
</TouchableOpacity>
)}

Expand All @@ -588,6 +639,10 @@ const GooglePlacesTextInput = forwardRef<
style={[styles.loadingIndicator, getIconPosition(45)]}
size={'small'}
color={style.loadingIndicator?.color || '#000000'}
accessibilityLiveRegion="polite"
accessibilityLabel={
accessibilityLabels.loadingIndicator || 'Loading suggestions'
}
/>
)}
</View>
Expand All @@ -606,6 +661,8 @@ const GooglePlacesTextInput = forwardRef<
nestedScrollEnabled={nestedScrollEnabled}
bounces={false}
style={style.suggestionsList}
accessibilityRole="list"
accessibilityLabel={`${predictions.length} place suggestion resuts`}
/>
</View>
)}
Expand Down Expand Up @@ -658,30 +715,27 @@ const styles = StyleSheet.create({
top: '50%',
transform: [{ translateY: -10 }],
},
iOSclearButton: {
fontSize: 18,
clearTextWrapper: {
backgroundColor: '#999',
borderRadius: 12,
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
},
//this is never going to be consistent between different phone fonts and sizes
iOSclearText: {
fontSize: 22,
fontWeight: '400',
color: 'white',
backgroundColor: '#999',
width: 25,
height: 25,
borderRadius: 12.5,
textAlign: 'center',
textAlignVertical: 'center',
lineHeight: 19,
lineHeight: 24,
includeFontPadding: false,
},
androidClearButton: {
androidClearText: {
fontSize: 24,
fontWeight: '400',
color: 'white',
backgroundColor: '#999',
width: 24,
height: 24,
borderRadius: 12,
textAlign: 'center',
textAlignVertical: 'center',
lineHeight: 20,
lineHeight: 25.5,
includeFontPadding: false,
},
});
Expand All @@ -694,6 +748,7 @@ export type {
PlaceDetailsFields,
PlacePrediction,
PlaceStructuredFormat,
GooglePlacesAccessibilityLabels,
};

export default GooglePlacesTextInput;