UNPKG

react-native-google-places-autocomplete

Version:

Customizable Google Places autocomplete component for iOS and Android React-Native apps

1,219 lines (1,081 loc) 34.5 kB
/* eslint-disable react-native/no-inline-styles */ import debounce from 'lodash.debounce'; import Qs from 'qs'; import uuid from 'react-native-uuid'; import React, { forwardRef, useMemo, useEffect, useImperativeHandle, useRef, useState, useCallback, } from 'react'; import { ActivityIndicator, FlatList, Image, Keyboard, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; // ============================================================================ // CONSTANTS // ============================================================================ const defaultStyles = { container: { flex: 1, }, textInputContainer: { flexDirection: 'row', }, textInput: { backgroundColor: '#FFFFFF', height: 44, borderRadius: 5, paddingVertical: 5, paddingHorizontal: 10, fontSize: 15, flex: 1, marginBottom: 5, }, listView: { backgroundColor: '#FFFFFF', }, row: { backgroundColor: '#FFFFFF', padding: 13, minHeight: 44, flexDirection: 'row', }, loader: { flexDirection: 'row', justifyContent: 'flex-end', height: 20, }, description: {}, separator: { height: StyleSheet.hairlineWidth, backgroundColor: '#c8c7cc', }, poweredContainer: { justifyContent: 'flex-end', alignItems: 'center', borderBottomRightRadius: 5, borderBottomLeftRadius: 5, borderColor: '#c8c7cc', borderTopWidth: 0.5, }, powered: {}, }; // ============================================================================ // COMPONENT // ============================================================================ export const GooglePlacesAutocomplete = forwardRef((props, ref) => { // ========================================================================== // PROPS DESTRUCTURING // ========================================================================== const { autoFillOnNotFound = false, currentLocation = false, currentLocationLabel = 'Current location', debounce: debounceMs = 0, disableScroll = false, enableHighAccuracyLocation = true, enablePoweredByContainer = true, fetchDetails = false, filterReverseGeocodingByTypes = [], GooglePlacesDetailsQuery = {}, GooglePlacesSearchQuery = { rankby: 'distance', type: 'restaurant', }, GoogleReverseGeocodingQuery = {}, isRowScrollable = true, keyboardShouldPersistTaps = 'always', listHoverColor = '#ececec', listUnderlayColor = '#c8c7cc', listViewDisplayed: listViewDisplayedProp = 'auto', keepResultsAfterBlur = false, minLength = 0, nearbyPlacesAPI = 'GooglePlacesSearch', numberOfLines = 1, onFail = () => {}, onNotFound = () => {}, onPress = () => {}, onTimeout = () => console.warn('google places autocomplete: request timeout'), placeholder = '', predefinedPlaces: predefinedPlacesProp = [], predefinedPlacesAlwaysVisible = false, query = { key: 'missing api key', language: 'en', types: 'geocode', }, styles = {}, suppressDefaultStyles = false, textInputHide = false, textInputProps = {}, timeout = 20000, isNewPlacesAPI = false, fields = '*', ...restProps } = props; // ========================================================================== // STATE & REFS // ========================================================================== const predefinedPlaces = useMemo(() => predefinedPlacesProp || [], [ predefinedPlacesProp, ]); // Store results array - useRef prevents re-renders when updating results, allows access to latest results in callbacks const resultsRef = useRef([]); // Store active XMLHttpRequest objects - needed to abort requests when component unmounts or new search starts const requestsRef = useRef([]); // Track if navigator warning has been shown - prevents duplicate console warnings const hasWarnedAboutNavigator = useRef(false); // Reference to TextInput component - enables imperative methods (focus, blur, clear) via ref const inputRef = useRef(null); // Store current query object - allows access to latest query in callbacks without stale closures const queryRef = useRef(query); // Store previous query string - used to detect query changes without causing re-renders const prevQueryStringRef = useRef(JSON.stringify(query)); // Store latest _request function - ensures debounced function always calls current version with latest closures const requestRef = useRef(_request); const queryString = useMemo(() => JSON.stringify(query), [query]); const [stateText, setStateText] = useState(''); const [dataSource, setDataSource] = useState([]); const [listViewDisplayed, setListViewDisplayed] = useState( listViewDisplayedProp === 'auto' ? false : listViewDisplayedProp, ); const [listWasDismissed, setListWasDismissed] = useState(false); const [url, setUrl] = useState(''); const [listLoaderDisplayed, setListLoaderDisplayed] = useState(false); const [sessionToken, setSessionToken] = useState(uuid.v4()); // ========================================================================== // UTILITY FUNCTIONS // ========================================================================== const hasNavigator = useCallback(() => { if (navigator?.geolocation) { return true; } if (!hasWarnedAboutNavigator.current) { if (Platform.OS === 'web') { console.warn( 'Geolocation is not available. For web, ensure your site is served over HTTPS or localhost to use geolocation features.', ); } else { console.warn( 'Geolocation is not available. For React Native, you may need to install and configure @react-native-community/geolocation or expo-location to enable currentLocation.', ); } hasWarnedAboutNavigator.current = true; } return false; }, []); const supportedPlatform = () => { if (Platform.OS === 'web' && !props.requestUrl) { console.warn( 'This library cannot be used for the web unless you specify the requestUrl prop. See https://git.io/JflFv for more for details.', ); return false; } return true; }; const getRequestUrl = (requestUrl) => { if (requestUrl) { if (requestUrl.useOnPlatform === 'all') { return requestUrl.url; } if (requestUrl.useOnPlatform === 'web') { return Platform.select({ web: requestUrl.url, default: 'https://maps.googleapis.com/maps/api', }); } } return 'https://maps.googleapis.com/maps/api'; }; const getRequestHeaders = (requestUrl) => { return requestUrl?.headers || {}; }; const setRequestHeaders = (request, headers) => { Object.keys(headers).forEach((headerKey) => request.setRequestHeader(headerKey, headers[headerKey]), ); }; const requestShouldUseWithCredentials = useCallback(() => { return url === 'https://maps.googleapis.com/maps/api'; }, [url]); const _abortRequests = useCallback(() => { requestsRef.current.forEach((request) => { request.onreadystatechange = null; request.abort(); }); requestsRef.current = []; }, []); // ========================================================================== // DATA PROCESSING FUNCTIONS // ========================================================================== const buildRowsFromResults = useCallback( (results, text) => { let res = []; // Show predefined places if: // 1. No text entered and no results, OR // 2. predefinedPlacesAlwaysVisible is true const shouldDisplayPredefinedPlaces = (!text || text.length === 0) && results.length === 0; if ( shouldDisplayPredefinedPlaces || predefinedPlacesAlwaysVisible === true ) { if (predefinedPlaces.length > 0) { res = [ ...predefinedPlaces.filter((place) => place?.description?.length), ]; } if (currentLocation === true && hasNavigator()) { res.unshift({ description: currentLocationLabel, isCurrentLocation: true, }); } } res = res.map((place) => ({ ...place, isPredefinedPlace: true, })); return [...res, ...results]; }, [ predefinedPlacesAlwaysVisible, predefinedPlaces, currentLocation, currentLocationLabel, hasNavigator, ], ); const _filterResultsByTypes = useCallback((unfilteredResults, types) => { if (!types || types.length === 0) return unfilteredResults; const results = []; for (let i = 0; i < unfilteredResults.length; i++) { let found = false; for (let j = 0; j < types.length; j++) { if (unfilteredResults[i].types?.indexOf(types[j]) !== -1) { found = true; break; } } if (found === true) { results.push(unfilteredResults[i]); } } return results; }, []); const _filterResultsByPlacePredictions = (unfilteredResults) => { const results = []; for (let i = 0; i < unfilteredResults.length; i++) { if (unfilteredResults[i].placePrediction) { results.push({ description: unfilteredResults[i].placePrediction.text?.text, place_id: unfilteredResults[i].placePrediction.placeId, reference: unfilteredResults[i].placePrediction.placeId, structured_formatting: { main_text: unfilteredResults[i].placePrediction.structuredFormat?.mainText ?.text, secondary_text: unfilteredResults[i].placePrediction.structuredFormat ?.secondaryText?.text, }, types: unfilteredResults[i].placePrediction.types ?? [], }); } } return results; }; const _getPredefinedPlace = (rowData) => { if (rowData.isPredefinedPlace !== true) { return rowData; } if (predefinedPlaces.length > 0) { for (let i = 0; i < predefinedPlaces.length; i++) { if (predefinedPlaces[i].description === rowData.description) { return predefinedPlaces[i]; } } } return rowData; }; // ========================================================================== // API REQUEST FUNCTIONS // ========================================================================== const _requestNearby = useCallback( (latitude, longitude) => { _abortRequests(); if ( latitude !== undefined && longitude !== undefined && latitude !== null && longitude !== null ) { const request = new XMLHttpRequest(); requestsRef.current.push(request); request.timeout = timeout; request.ontimeout = onTimeout; request.onreadystatechange = () => { if (request.readyState !== 4) { setListLoaderDisplayed(true); return; } setListLoaderDisplayed(false); if (request.status === 200) { const responseJSON = JSON.parse(request.responseText); _disableRowLoaders(); if (typeof responseJSON.results !== 'undefined') { let results = []; if (nearbyPlacesAPI === 'GoogleReverseGeocoding') { results = _filterResultsByTypes( responseJSON.results, filterReverseGeocodingByTypes, ); } else { results = responseJSON.results; } resultsRef.current = results; const newDataSource = buildRowsFromResults(results); setDataSource(newDataSource); // Auto-show list when results arrive if in 'auto' mode if ( listViewDisplayedProp === 'auto' && newDataSource.length > 0 ) { setListWasDismissed(false); setListViewDisplayed(true); } } if (typeof responseJSON.error_message !== 'undefined') { if (!onFail) { console.warn( 'google places autocomplete: ' + responseJSON.error_message, ); } else { onFail(responseJSON.error_message); } } } }; let requestUrl = ''; if (nearbyPlacesAPI === 'GoogleReverseGeocoding') { // your key must be allowed to use Google Maps Geocoding API requestUrl = `${url}/geocode/json?` + Qs.stringify({ latlng: latitude + ',' + longitude, key: query.key, ...GoogleReverseGeocodingQuery, }); } else { requestUrl = `${url}/place/nearbysearch/json?` + Qs.stringify({ location: latitude + ',' + longitude, key: query.key, ...GooglePlacesSearchQuery, }); } request.open('GET', requestUrl); request.withCredentials = requestShouldUseWithCredentials(); setRequestHeaders(request, getRequestHeaders(props.requestUrl)); request.send(); } else { resultsRef.current = []; setDataSource(buildRowsFromResults([])); } }, [ _abortRequests, timeout, onTimeout, _disableRowLoaders, nearbyPlacesAPI, _filterResultsByTypes, filterReverseGeocodingByTypes, buildRowsFromResults, listViewDisplayedProp, onFail, url, query, GoogleReverseGeocodingQuery, GooglePlacesSearchQuery, requestShouldUseWithCredentials, props.requestUrl, ], ); const _request = (text) => { _abortRequests(); if (!url) { return; } if (supportedPlatform() && text && text.length >= minLength) { const request = new XMLHttpRequest(); requestsRef.current.push(request); request.timeout = timeout; request.ontimeout = onTimeout; request.onreadystatechange = () => { if (request.readyState !== 4) { setListLoaderDisplayed(true); return; } setListLoaderDisplayed(false); if (request.status === 200) { const responseJSON = JSON.parse(request.responseText); if (typeof responseJSON.predictions !== 'undefined') { const results = nearbyPlacesAPI === 'GoogleReverseGeocoding' ? _filterResultsByTypes( responseJSON.predictions, filterReverseGeocodingByTypes, ) : responseJSON.predictions; resultsRef.current = results; const newDataSource = buildRowsFromResults(results, text); setDataSource(newDataSource); // Auto-show list when results arrive if in 'auto' mode if (listViewDisplayedProp === 'auto' && newDataSource.length > 0) { setListWasDismissed(false); setListViewDisplayed(true); } } if (typeof responseJSON.suggestions !== 'undefined') { const results = _filterResultsByPlacePredictions( responseJSON.suggestions, ); resultsRef.current = results; const newDataSource = buildRowsFromResults(results, text); setDataSource(newDataSource); // Auto-show list when results arrive if in 'auto' mode if (listViewDisplayedProp === 'auto' && newDataSource.length > 0) { setListWasDismissed(false); setListViewDisplayed(true); } } if (typeof responseJSON.error_message !== 'undefined') { if (!onFail) { console.warn( 'google places autocomplete: ' + responseJSON.error_message, ); } else { onFail(responseJSON.error_message); } } } else { console.warn( 'google places autocomplete: request could not be completed or has been aborted', ); } }; if (props.preProcess) { setStateText(props.preProcess(text)); } if (isNewPlacesAPI) { const keyQueryParam = query.key ? '?' + Qs.stringify({ key: query.key, }) : ''; request.open('POST', `${url}/v1/places:autocomplete${keyQueryParam}`); } else { request.open( 'GET', `${url}/place/autocomplete/json?input=` + encodeURIComponent(text) + '&' + Qs.stringify(query), ); } request.withCredentials = requestShouldUseWithCredentials(); setRequestHeaders(request, getRequestHeaders(props.requestUrl)); if (isNewPlacesAPI) { const { key, locationbias, types, ...rest } = query; request.send( JSON.stringify({ input: text, sessionToken, ...rest, }), ); } else { request.send(); } } else { resultsRef.current = []; setDataSource(buildRowsFromResults([])); } }; const getCurrentLocation = useCallback(() => { let options = { enableHighAccuracy: false, timeout: 20000, maximumAge: 1000, }; if (enableHighAccuracyLocation && Platform.OS === 'android') { options = { enableHighAccuracy: true, timeout: 20000, }; } const getCurrentPosition = navigator.geolocation.getCurrentPosition || navigator.geolocation.default?.getCurrentPosition; if (getCurrentPosition) { getCurrentPosition( (position) => { if (nearbyPlacesAPI === 'None') { const currentLocationData = { description: currentLocationLabel, geometry: { location: { lat: position.coords.latitude, lng: position.coords.longitude, }, }, }; _disableRowLoaders(); onPress(currentLocationData, currentLocationData); } else { _requestNearby(position.coords.latitude, position.coords.longitude); } }, (error) => { _disableRowLoaders(); console.error(error.message); }, options, ); } }, [ enableHighAccuracyLocation, currentLocationLabel, nearbyPlacesAPI, _disableRowLoaders, onPress, _requestNearby, ]); // ========================================================================== // EVENT HANDLERS // ========================================================================== const _enableRowLoader = (rowData) => { const rows = buildRowsFromResults(resultsRef.current); for (let i = 0; i < rows.length; i++) { if ( rows[i].place_id === rowData.place_id || (rows[i].isCurrentLocation === true && rowData.isCurrentLocation === true) ) { rows[i].isLoading = true; setDataSource(rows); break; } } }; const _disableRowLoaders = useCallback(() => { for (let i = 0; i < resultsRef.current.length; i++) { if (resultsRef.current[i].isLoading === true) { resultsRef.current[i].isLoading = false; } } setDataSource(buildRowsFromResults(resultsRef.current)); }, [buildRowsFromResults]); const _onPress = (rowData) => { if (rowData.isPredefinedPlace !== true && fetchDetails === true) { if (rowData.isLoading === true) { // already requesting return; } hideListView(true); Keyboard.dismiss(); _abortRequests(); // display loader _enableRowLoader(rowData); // fetch details const request = new XMLHttpRequest(); requestsRef.current.push(request); request.timeout = timeout; request.ontimeout = onTimeout; request.onreadystatechange = () => { if (request.readyState !== 4) return; if (request.status === 200) { const responseJSON = JSON.parse(request.responseText); if ( responseJSON.status === 'OK' || (isNewPlacesAPI && responseJSON.id) ) { const details = isNewPlacesAPI ? responseJSON : responseJSON.result; _disableRowLoaders(); _onBlur(); setStateText(_renderDescription(rowData)); delete rowData.isLoading; onPress(rowData, details); } else { _disableRowLoaders(); if (autoFillOnNotFound) { setStateText(_renderDescription(rowData)); delete rowData.isLoading; } if (!onNotFound) { console.warn( 'google places autocomplete: ' + responseJSON.status, ); } else { onNotFound(responseJSON); } } } else { _disableRowLoaders(); if (!onFail) { console.warn( 'google places autocomplete: request could not be completed or has been aborted', ); } else { onFail('request could not be completed or has been aborted'); } } }; if (isNewPlacesAPI) { request.open( 'GET', `${url}/v1/places/${rowData.place_id}?` + Qs.stringify({ key: query.key, sessionToken, fields, }), ); setSessionToken(uuid.v4()); } else { request.open( 'GET', `${url}/place/details/json?` + Qs.stringify({ key: query.key, placeid: rowData.place_id, language: query.language, ...GooglePlacesDetailsQuery, }), ); } request.withCredentials = requestShouldUseWithCredentials(); setRequestHeaders(request, getRequestHeaders(props.requestUrl)); request.send(); } else if (rowData.isCurrentLocation === true) { hideListView(true); // display loader _enableRowLoader(rowData); setStateText(_renderDescription(rowData)); delete rowData.isLoading; getCurrentLocation(); } else { hideListView(true); setStateText(_renderDescription(rowData)); _onBlur(); delete rowData.isLoading; const predefinedPlace = _getPredefinedPlace(rowData); // sending predefinedPlace as details for predefined places onPress(predefinedPlace, predefinedPlace); } }; const _onChangeText = (text) => { setListWasDismissed(false); setStateText(text); debounceData(text); }; const _handleChangeText = (text) => { _onChangeText(text); const onChangeText = textInputProps?.onChangeText; if (onChangeText) { onChangeText(text); } }; const hideListView = useCallback( (force = false) => { if (!keepResultsAfterBlur || force) { setListWasDismissed(true); setListViewDisplayed(false); } }, [keepResultsAfterBlur], ); const isNewFocusInAutocompleteResultList = ({ relatedTarget, currentTarget, }) => { if (!relatedTarget) return false; let node = relatedTarget.parentNode; while (node) { if (node.id === 'result-list-id') return true; node = node.parentNode; } return false; }; const _onBlur = (e) => { if (e && isNewFocusInAutocompleteResultList(e)) return; hideListView(); inputRef?.current?.blur(); }; const _onFocus = () => { setListWasDismissed(false); setListViewDisplayed(true); }; // ========================================================================== // RENDER FUNCTIONS // ========================================================================== const _renderDescription = (rowData) => { if (props.renderDescription) { return props.renderDescription(rowData); } return rowData.description || rowData.formatted_address || rowData.name; }; const _getRowLoader = () => { return <ActivityIndicator animating={true} size='small' />; }; const _renderLoader = (rowData) => { if (rowData.isLoading === true) { return ( <View style={[ suppressDefaultStyles ? {} : defaultStyles.loader, styles?.loader, ]} > {_getRowLoader()} </View> ); } return null; }; const _renderRowData = (rowData, index) => { if (props.renderRow) { return props.renderRow(rowData, index); } return ( <Text style={[ suppressDefaultStyles ? {} : defaultStyles.description, styles?.description, rowData.isPredefinedPlace ? styles?.predefinedPlacesDescription : {}, ]} numberOfLines={numberOfLines} > {_renderDescription(rowData)} </Text> ); }; const _renderRow = (rowData = {}, index) => { return ( <ScrollView contentContainerStyle={ isRowScrollable ? { minWidth: '100%' } : { width: '100%' } } scrollEnabled={isRowScrollable} keyboardShouldPersistTaps={keyboardShouldPersistTaps} horizontal={true} showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} > <Pressable style={({ hovered, pressed }) => [ isRowScrollable ? { minWidth: '100%' } : { width: '100%' }, { backgroundColor: pressed ? listUnderlayColor : hovered ? listHoverColor : undefined, }, ]} onPress={() => _onPress(rowData)} onBlur={_onBlur} > <View style={[ suppressDefaultStyles ? {} : defaultStyles.row, styles?.row, rowData.isPredefinedPlace ? styles?.specialItemRow : {}, ]} > {_renderLoader(rowData)} {_renderRowData(rowData, index)} </View> </Pressable> </ScrollView> ); }; const _renderSeparator = (sectionID, rowID) => { if (rowID === dataSource.length - 1) { return null; } return ( <View key={`${sectionID}-${rowID}`} style={[ suppressDefaultStyles ? {} : defaultStyles.separator, styles?.separator, ]} /> ); }; const _shouldShowPoweredLogo = () => { if (!enablePoweredByContainer || dataSource.length === 0) { return false; } for (let i = 0; i < dataSource.length; i++) { const row = dataSource[i]; if (!('isCurrentLocation' in row) && !('isPredefinedPlace' in row)) { return true; } } return false; }; const _renderPoweredLogo = () => { if (!_shouldShowPoweredLogo()) { return null; } return ( <View style={[ suppressDefaultStyles ? {} : defaultStyles.row, defaultStyles.poweredContainer, styles?.poweredContainer, ]} > <Image style={[ suppressDefaultStyles ? {} : defaultStyles.powered, styles?.powered, ]} resizeMode='contain' source={require('./images/powered_by_google_on_white.png')} /> </View> ); }; const _renderLeftButton = () => { if (props.renderLeftButton) { return props.renderLeftButton(); } return null; }; const _renderRightButton = () => { if (props.renderRightButton) { return props.renderRightButton(); } return null; }; const _getFlatList = () => { const keyExtractor = (item, index) => { // Use stable keys based on item data if (item.place_id) { return `place_${item.place_id}_${index}`; } if (item.isCurrentLocation) { return 'current_location'; } if (item.isPredefinedPlace && item.description) { return `predefined_${item.description}_${index}`; } // Fallback to index-based key (should rarely happen) return `item_${index}`; }; // Show list if: // 1. Platform is supported // 2. There's data to show (dataSource has items) // 3. listViewDisplayed is true OR we're in 'auto' mode (auto-shows when data exists) const isAutoMode = listViewDisplayedProp === 'auto' || listViewDisplayedProp === undefined; const shouldShowList = supportedPlatform() && dataSource.length > 0 && (listViewDisplayed === true || (isAutoMode && !listWasDismissed)); if (shouldShowList) { return ( <FlatList nativeID='result-list-id' scrollEnabled={!disableScroll} nestedScrollEnabled={true} keyboardShouldPersistTaps={keyboardShouldPersistTaps} style={[ suppressDefaultStyles ? {} : defaultStyles.listView, styles?.listView, ]} data={dataSource} keyExtractor={keyExtractor} extraData={[dataSource, props]} ItemSeparatorComponent={_renderSeparator} renderItem={({ item, index }) => _renderRow(item, index)} ListEmptyComponent={ listLoaderDisplayed ? props.listLoaderComponent : stateText.length > minLength && props.listEmptyComponent } ListHeaderComponent={ props.renderHeaderComponent && props.renderHeaderComponent(stateText) } ListFooterComponent={_renderPoweredLogo} {...restProps} /> ); } return null; }; // ========================================================================== // EFFECTS // ========================================================================== // Update query ref when query changes useEffect(() => { queryRef.current = query; }, [query]); // Initialize URL from requestUrl prop useEffect(() => { setUrl(getRequestUrl(props.requestUrl)); }, [props.requestUrl]); // Initialize dataSource on mount useEffect(() => { setDataSource(buildRowsFromResults([])); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Keep requestRef updated requestRef.current = _request; // Debounce setup const debounceData = useMemo(() => { return debounce((text) => requestRef.current(text), debounceMs); }, [debounceMs]); useEffect(() => { return () => { // Cleanup debounced function on unmount if (debounceData.cancel) { debounceData.cancel(); } }; }, [debounceData]); // Reload search when query changes (using string comparison to avoid object reference issues) useEffect(() => { const queryChanged = prevQueryStringRef.current !== queryString; if (queryChanged) { prevQueryStringRef.current = queryString; if (stateText && stateText.length >= minLength) { debounceData(stateText); } } return () => { _abortRequests(); }; }, [queryString, debounceData, stateText, minLength, _abortRequests]); // Auto-show list when dataSource has items in 'auto' mode useEffect(() => { if ( listViewDisplayedProp === 'auto' && dataSource.length > 0 && !listViewDisplayed && !listWasDismissed ) { setListViewDisplayed(true); } }, [ dataSource.length, listViewDisplayedProp, listViewDisplayed, listWasDismissed, ]); // ========================================================================== // IMPERATIVE HANDLE // ========================================================================== useImperativeHandle( ref, () => ({ setAddressText: (address) => { setStateText(address); }, getAddressText: () => stateText, blur: () => inputRef.current?.blur(), focus: () => inputRef.current?.focus(), isFocused: () => inputRef.current?.isFocused(), clear: () => inputRef.current?.clear(), getCurrentLocation, }), [stateText, getCurrentLocation], ); // ========================================================================== // MAIN RENDER // ========================================================================== const { onFocus: textInputOnFocus, onBlur: textInputOnBlur, onChangeText: textInputOnChangeText, // destructuring here stops this being set after onChangeText={_handleChangeText} clearButtonMode, InputComp, ...userProps } = textInputProps || {}; const TextInputComp = InputComp || TextInput; return ( <View style={[ suppressDefaultStyles ? {} : defaultStyles.container, styles?.container, ]} pointerEvents='box-none' > {!textInputHide && ( <View style={[ suppressDefaultStyles ? {} : defaultStyles.textInputContainer, styles?.textInputContainer, ]} > {_renderLeftButton()} <TextInputComp ref={inputRef} style={[ suppressDefaultStyles ? {} : defaultStyles.textInput, styles?.textInput, ]} value={stateText} placeholder={placeholder} onFocus={ textInputOnFocus ? (e) => { _onFocus(); textInputOnFocus(e); } : _onFocus } onBlur={ textInputOnBlur ? (e) => { _onBlur(e); textInputOnBlur(e); } : _onBlur } clearButtonMode={clearButtonMode || 'while-editing'} onChangeText={_handleChangeText} {...userProps} /> {_renderRightButton()} </View> )} {props.inbetweenCompo} {_getFlatList()} {props.children} </View> ); }); GooglePlacesAutocomplete.displayName = 'GooglePlacesAutocomplete'; export default { GooglePlacesAutocomplete };