diff --git a/src/components/LocationAutocompleteInput/LocationAutocompleteInput.js b/src/components/LocationAutocompleteInput/LocationAutocompleteInput.js
index 6ec558d9..cce0e22a 100644
--- a/src/components/LocationAutocompleteInput/LocationAutocompleteInput.js
+++ b/src/components/LocationAutocompleteInput/LocationAutocompleteInput.js
@@ -1,511 +1,8 @@
import React, { Component } from 'react';
-import { any, arrayOf, bool, func, number, object, shape, string, oneOfType } from 'prop-types';
+import { func, object, shape, string } from 'prop-types';
import { Field } from 'react-final-form';
-import debounce from 'lodash/debounce';
-import classNames from 'classnames';
-import { propTypes } from '../../util/types';
-import { getPlacePredictions, getPlaceDetails } from '../../util/googleMaps';
import { ValidationError } from '../../components';
-
-import css from './LocationAutocompleteInput.css';
-
-const DEBOUNCE_WAIT_TIME = 200;
-const KEY_CODE_ARROW_UP = 38;
-const KEY_CODE_ARROW_DOWN = 40;
-const KEY_CODE_ENTER = 13;
-const KEY_CODE_TAB = 9;
-const KEY_CODE_ESC = 27;
-const DIRECTION_UP = 'up';
-const DIRECTION_DOWN = 'down';
-const TOUCH_TAP_RADIUS = 5; // Movement within 5px from touch start is considered a tap
-
-const Icon = () => (
-
-);
-
-// Touch devices need to be able to distinguish touches for scrolling and touches to tap
-const getTouchCoordinates = nativeEvent => {
- const touch = nativeEvent && nativeEvent.changedTouches ? nativeEvent.changedTouches[0] : null;
- return touch ? { x: touch.screenX, y: touch.screenY } : null;
-};
-
-// Renders the autocompletion prediction results in a list
-const LocationPredictionsList = props => {
- const {
- rootClassName,
- className,
- predictions,
- highlightedIndex,
- onSelectStart,
- onSelectMove,
- onSelectEnd,
- } = props;
- if (predictions.length === 0) {
- return null;
- }
-
- /* eslint-disable jsx-a11y/no-static-element-interactions */
- const item = (prediction, index) => {
- const isHighlighted = index === highlightedIndex;
-
- return (
-
onSelectStart(getTouchCoordinates(e.nativeEvent))}
- onMouseDown={() => onSelectStart()}
- onTouchMove={e => onSelectMove(getTouchCoordinates(e.nativeEvent))}
- onTouchEnd={() => onSelectEnd(index)}
- onMouseUp={() => onSelectEnd(index)}
- >
- {prediction.description}
-
- );
- };
- /* eslint-enable jsx-a11y/no-static-element-interactions */
-
- const classes = classNames(rootClassName || css.predictionsRoot, className);
-
- return (
-
- );
-};
-
-LocationPredictionsList.defaultProps = {
- rootClassName: null,
- className: null,
- highlightedIndex: null,
-};
-
-LocationPredictionsList.propTypes = {
- rootClassName: string,
- className: string,
- predictions: arrayOf(
- shape({
- id: string.isRequired,
- description: string.isRequired,
- place_id: string.isRequired,
- })
- ).isRequired,
- highlightedIndex: number,
- onSelectStart: func.isRequired,
- onSelectMove: func.isRequired,
- onSelectEnd: func.isRequired,
-};
-
-// Get the current value with defaults from the given
-// LocationAutocompleteInput props.
-const currentValue = props => {
- const value = props.input.value || {};
- const { search = '', predictions = [], selectedPlaceId = null, selectedPlace = null } = value;
- return { search, predictions, selectedPlaceId, selectedPlace };
-};
-
-/*
- Location auto completion input component
-
- This component can work as the `component` prop to Final Form's
- component. It takes a custom input value shape, and
- controls the onChange callback that is called with the input value.
-
- The component works by listening to the underlying input component
- and calling the Google Maps Places API for predictions. When the
- predictions arrive, those are passed to Final Form in the onChange
- callback.
-
- See the LocationAutocompleteInput.example.js file for a usage
- example within a form.
-*/
-class LocationAutocompleteInput extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- inputHasFocus: false,
- selectionInProgress: false,
- touchStartedFrom: null,
- highlightedIndex: -1, // -1 means no highlight
- };
-
- // Ref to the input element.
- this.input = null;
-
- // Current sessionToken used to combine autocomplete calls with place details call
- // This reduces Google Maps pricing.
- this.autocompleteSessionToken = null;
-
- this.changeHighlight = this.changeHighlight.bind(this);
- this.selectItem = this.selectItem.bind(this);
- this.selectItemIfNoneSelected = this.selectItemIfNoneSelected.bind(this);
- this.onKeyDown = this.onKeyDown.bind(this);
- this.onChange = this.onChange.bind(this);
- this.handleOnBlur = this.handleOnBlur.bind(this);
- this.handlePredictionsSelectStart = this.handlePredictionsSelectStart.bind(this);
- this.handlePredictionsSelectMove = this.handlePredictionsSelectMove.bind(this);
- this.handlePredictionsSelectEnd = this.handlePredictionsSelectEnd.bind(this);
- this.finalizeSelection = this.finalizeSelection.bind(this);
-
- // Debounce the method to avoid calling the API too many times
- // when the user is typing fast.
- this.predict = debounce(this.predict.bind(this), DEBOUNCE_WAIT_TIME, { leading: true });
- }
-
- // Interpret input key event
- onKeyDown(e) {
- if (e.keyCode === KEY_CODE_ARROW_UP) {
- // Prevent changing cursor position in input
- e.preventDefault();
- this.changeHighlight(DIRECTION_UP);
- } else if (e.keyCode === KEY_CODE_ARROW_DOWN) {
- // Prevent changing cursor position in input
- e.preventDefault();
- this.changeHighlight(DIRECTION_DOWN);
- } else if (e.keyCode === KEY_CODE_ENTER) {
- const { search, selectedPlace } = currentValue(this.props);
-
- if (search && !selectedPlace) {
- // Prevent form submit, try to select value instead.
- e.preventDefault();
- e.stopPropagation();
- this.selectItemIfNoneSelected();
- }
- } else if (e.keyCode === KEY_CODE_TAB) {
- this.selectItemIfNoneSelected();
- } else if (e.keyCode === KEY_CODE_ESC && this.input) {
- this.input.blur();
- }
- }
-
- // Handle input text change, fetch predictions if the value isn't empty
- onChange(e) {
- const onChange = this.props.input.onChange;
- const { predictions } = currentValue(this.props);
- const newValue = e.target.value;
-
- // Clear the current values since the input content is changed
- onChange({
- search: newValue,
- predictions: newValue ? predictions : [],
- selectedPlaceId: null,
- selectedPlace: null,
- });
-
- // Clear highlighted prediction since the input value changed and
- // results will change as well
- this.setState({ highlightedIndex: -1 });
-
- if (!newValue) {
- // No need to fetch predictions on empty input
- return;
- }
-
- this.predict(newValue);
- }
-
- // Change the currently highlighted item by calculating the new
- // index from the current state and the given direction number
- // (DIRECTION_UP or DIRECTION_DOWN)
- changeHighlight(direction) {
- this.setState((prevState, props) => {
- const { predictions } = currentValue(props);
- const currentIndex = prevState.highlightedIndex;
- let index = currentIndex;
-
- if (direction === DIRECTION_UP) {
- // Keep the first position if already highlighted
- index = currentIndex === 0 ? 0 : currentIndex - 1;
- } else if (direction === DIRECTION_DOWN) {
- index = currentIndex + 1;
- }
-
- // Check that the index is within the bounds
- if (index < 0) {
- index = -1;
- } else if (index >= predictions.length) {
- index = predictions.length - 1;
- }
-
- return { highlightedIndex: index };
- });
- }
-
- // Select the prediction in the given item. This will fetch the
- // place details and set it as the selected place.
- selectItem(index) {
- if (index < 0) {
- return;
- }
- const { predictions } = currentValue(this.props);
- if (index >= predictions.length) {
- return;
- }
- const prediction = predictions[index];
- const placeId = prediction.place_id;
-
- this.props.input.onChange({
- ...this.props.input,
- selectedPlaceId: placeId,
- selectedPlace: null,
- });
-
- this.autocompleteSessionToken =
- this.autocompleteSessionToken || new window.google.maps.places.AutocompleteSessionToken();
- const sessionToken = this.autocompleteSessionToken;
-
- getPlaceDetails(placeId, sessionToken)
- .then(place => {
- this.props.input.onChange({
- search: prediction.description,
- predictions: [],
- selectedPlaceId: placeId,
- selectedPlace: place,
- });
- this.autocompleteSessionToken = null;
- })
- .catch(e => {
- // eslint-disable-next-line no-console
- console.error(e);
- this.props.input.onChange({
- ...this.props.input.value,
- selectedPlaceId: null,
- selectedPlace: null,
- });
- });
- }
- selectItemIfNoneSelected() {
- const { search, selectedPlaceId } = currentValue(this.props);
- if (search && !selectedPlaceId) {
- const index = this.state.highlightedIndex !== -1 ? this.state.highlightedIndex : 0;
- this.selectItem(index);
- }
- }
- predict(search) {
- const mapsLibLoaded = window.google && window.google.maps;
- if (!mapsLibLoaded) {
- throw new Error('Google Maps API must be loaded for LocationAutocompleteInput');
- }
- const onChange = this.props.input.onChange;
-
- this.autocompleteSessionToken =
- this.autocompleteSessionToken || new window.google.maps.places.AutocompleteSessionToken();
- const sessionToken = this.autocompleteSessionToken;
-
- getPlacePredictions(search, sessionToken)
- .then(results => {
- const { search: currentSearch } = currentValue(this.props);
-
- // If the earlier predictions arrive when the user has already
- // changed the search term, ignore and wait until the latest
- // predictions arrive. Without this logic, results for earlier
- // requests would override whatever the user had typed since.
- //
- // This is essentially the same as switchLatest in RxJS or
- // takeLatest in Redux Saga, without canceling the earlier
- // requests.
- if (results.search === currentSearch) {
- onChange({
- search: results.search,
- predictions: results.predictions,
- selectedPlaceId: null,
- selectedPlace: null,
- });
- }
- })
- .catch(e => {
- // eslint-disable-next-line no-console
- console.error(e);
- const value = currentValue(this.props);
- onChange({
- ...value,
- selectedPlaceId: null,
- selectedPlace: null,
- });
- });
- }
-
- finalizeSelection() {
- this.setState({ inputHasFocus: false, highlightedIndex: -1 });
- this.props.input.onBlur(currentValue(this.props));
- }
-
- handleOnBlur() {
- if (this.props.closeOnBlur && !this.state.selectionInProgress) {
- this.finalizeSelection();
- }
- }
-
- handlePredictionsSelectStart(touchCoordinates) {
- this.setState({
- selectionInProgress: true,
- touchStartedFrom: touchCoordinates,
- isSwipe: false,
- });
- }
-
- handlePredictionsSelectMove(touchCoordinates) {
- this.setState(prevState => {
- const touchStartedFrom = prevState.touchStartedFrom;
- const isTouchAction = !!touchStartedFrom;
- const isSwipe = isTouchAction
- ? Math.abs(touchStartedFrom.y - touchCoordinates.y) > TOUCH_TAP_RADIUS
- : false;
-
- return { selectionInProgress: false, isSwipe };
- });
- }
-
- handlePredictionsSelectEnd(index) {
- this.setState(prevState => {
- if (!prevState.isSwipe) {
- this.selectItem(index);
- this.finalizeSelection();
- }
- return { selectionInProgress: false, touchStartedFrom: null, isSwipe: false };
- });
- }
-
- render() {
- const {
- autoFocus,
- rootClassName,
- className,
- iconClassName,
- inputClassName,
- predictionsClassName,
- validClassName,
- placeholder,
- input,
- meta,
- inputRef,
- } = this.props;
- const { name, onFocus } = input;
- const { search, predictions } = currentValue(this.props);
- const { touched, valid } = meta || {};
- const isValid = valid && touched;
-
- const handleOnFocus = e => {
- this.setState({ inputHasFocus: true });
- onFocus(e);
- };
-
- const rootClass = classNames(rootClassName || css.root, className);
- const iconClass = classNames(iconClassName || css.icon);
- const inputClass = classNames(inputClassName || css.input, { [validClassName]: isValid });
- const predictionsClass = classNames(predictionsClassName);
-
- // Only render predictions when the input has focus. For
- // development and easier workflow with the browser devtools, you
- // might want to hardcode this to `true`. Otherwise the dropdown
- // list will disappear.
- const renderPredictions = this.state.inputHasFocus;
-
- return (
-
-
-
-
-
{
- this.input = node;
- if (inputRef) {
- inputRef(node);
- }
- }}
- />
- {renderPredictions ? (
-
- ) : null}
-
- );
- }
-}
-
-LocationAutocompleteInput.defaultProps = {
- autoFocus: false,
- closeOnBlur: true,
- rootClassName: null,
- className: null,
- iconClassName: null,
- inputClassName: null,
- predictionsClassName: null,
- validClassName: null,
- placeholder: '',
- meta: null,
- inputRef: null,
-};
-
-LocationAutocompleteInput.propTypes = {
- autoFocus: bool,
- rootClassName: string,
- className: string,
- closeOnBlur: bool,
- iconClassName: string,
- inputClassName: string,
- predictionsClassName: string,
- validClassName: string,
- placeholder: string,
- input: shape({
- name: string.isRequired,
- value: oneOfType([
- shape({
- search: string,
- predictions: any,
- selectedPlace: propTypes.place,
- }),
- string,
- ]),
- onChange: func.isRequired,
- onFocus: func.isRequired,
- onBlur: func.isRequired,
- }).isRequired,
- meta: shape({
- valid: bool.isRequired,
- touched: bool.isRequired,
- }),
- inputRef: func,
-};
-
-export default LocationAutocompleteInput;
+import LocationAutocompleteInputGoogleMaps from './LocationAutocompleteInputGoogleMaps.js';
class LocationAutocompleteInputComponent extends Component {
render() {
@@ -525,7 +22,7 @@ class LocationAutocompleteInputComponent extends Component {
return (
{labelInfo}
-
+
);
@@ -550,6 +47,8 @@ LocationAutocompleteInputComponent.propTypes = {
meta: object.isRequired,
};
+export default LocationAutocompleteInputGoogleMaps;
+
export const LocationAutocompleteInputField = props => {
return ;
};
diff --git a/src/components/LocationAutocompleteInput/LocationAutocompleteInput.css b/src/components/LocationAutocompleteInput/LocationAutocompleteInputGoogleMaps.css
similarity index 100%
rename from src/components/LocationAutocompleteInput/LocationAutocompleteInput.css
rename to src/components/LocationAutocompleteInput/LocationAutocompleteInputGoogleMaps.css
diff --git a/src/components/LocationAutocompleteInput/LocationAutocompleteInputGoogleMaps.js b/src/components/LocationAutocompleteInput/LocationAutocompleteInputGoogleMaps.js
new file mode 100644
index 00000000..0047ce22
--- /dev/null
+++ b/src/components/LocationAutocompleteInput/LocationAutocompleteInputGoogleMaps.js
@@ -0,0 +1,507 @@
+import React, { Component } from 'react';
+import { any, arrayOf, bool, func, number, shape, string, oneOfType } from 'prop-types';
+import classNames from 'classnames';
+import debounce from 'lodash/debounce';
+import { propTypes } from '../../util/types';
+import { getPlacePredictions, getPlaceDetails } from '../../util/googleMaps';
+
+import css from './LocationAutocompleteInputGoogleMaps.css';
+
+const DEBOUNCE_WAIT_TIME = 200;
+const KEY_CODE_ARROW_UP = 38;
+const KEY_CODE_ARROW_DOWN = 40;
+const KEY_CODE_ENTER = 13;
+const KEY_CODE_TAB = 9;
+const KEY_CODE_ESC = 27;
+const DIRECTION_UP = 'up';
+const DIRECTION_DOWN = 'down';
+const TOUCH_TAP_RADIUS = 5; // Movement within 5px from touch start is considered a tap
+
+const Icon = () => (
+
+);
+
+// Touch devices need to be able to distinguish touches for scrolling and touches to tap
+const getTouchCoordinates = nativeEvent => {
+ const touch = nativeEvent && nativeEvent.changedTouches ? nativeEvent.changedTouches[0] : null;
+ return touch ? { x: touch.screenX, y: touch.screenY } : null;
+};
+
+// Renders the autocompletion prediction results in a list
+const LocationPredictionsList = props => {
+ const {
+ rootClassName,
+ className,
+ predictions,
+ highlightedIndex,
+ onSelectStart,
+ onSelectMove,
+ onSelectEnd,
+ } = props;
+ if (predictions.length === 0) {
+ return null;
+ }
+
+ /* eslint-disable jsx-a11y/no-static-element-interactions */
+ const item = (prediction, index) => {
+ const isHighlighted = index === highlightedIndex;
+
+ return (
+ onSelectStart(getTouchCoordinates(e.nativeEvent))}
+ onMouseDown={() => onSelectStart()}
+ onTouchMove={e => onSelectMove(getTouchCoordinates(e.nativeEvent))}
+ onTouchEnd={() => onSelectEnd(index)}
+ onMouseUp={() => onSelectEnd(index)}
+ >
+ {prediction.description}
+
+ );
+ };
+ /* eslint-enable jsx-a11y/no-static-element-interactions */
+
+ const classes = classNames(rootClassName || css.predictionsRoot, className);
+
+ return (
+
+ );
+};
+
+LocationPredictionsList.defaultProps = {
+ rootClassName: null,
+ className: null,
+ highlightedIndex: null,
+};
+
+LocationPredictionsList.propTypes = {
+ rootClassName: string,
+ className: string,
+ predictions: arrayOf(
+ shape({
+ id: string.isRequired,
+ description: string.isRequired,
+ place_id: string.isRequired,
+ })
+ ).isRequired,
+ highlightedIndex: number,
+ onSelectStart: func.isRequired,
+ onSelectMove: func.isRequired,
+ onSelectEnd: func.isRequired,
+};
+
+// Get the current value with defaults from the given
+// LocationAutocompleteInput props.
+const currentValue = props => {
+ const value = props.input.value || {};
+ const { search = '', predictions = [], selectedPlaceId = null, selectedPlace = null } = value;
+ return { search, predictions, selectedPlaceId, selectedPlace };
+};
+
+/*
+ Location auto completion input component
+
+ This component can work as the `component` prop to Final Form's
+ component. It takes a custom input value shape, and
+ controls the onChange callback that is called with the input value.
+
+ The component works by listening to the underlying input component
+ and calling the Google Maps Places API for predictions. When the
+ predictions arrive, those are passed to Final Form in the onChange
+ callback.
+
+ See the LocationAutocompleteInput.example.js file for a usage
+ example within a form.
+*/
+class LocationAutocompleteInputGoogleMaps extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ inputHasFocus: false,
+ selectionInProgress: false,
+ touchStartedFrom: null,
+ highlightedIndex: -1, // -1 means no highlight
+ };
+
+ // Ref to the input element.
+ this.input = null;
+
+ // Current sessionToken used to combine autocomplete calls with place details call
+ // This reduces Google Maps pricing.
+ this.autocompleteSessionToken = null;
+
+ this.changeHighlight = this.changeHighlight.bind(this);
+ this.selectItem = this.selectItem.bind(this);
+ this.selectItemIfNoneSelected = this.selectItemIfNoneSelected.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onChange = this.onChange.bind(this);
+ this.handleOnBlur = this.handleOnBlur.bind(this);
+ this.handlePredictionsSelectStart = this.handlePredictionsSelectStart.bind(this);
+ this.handlePredictionsSelectMove = this.handlePredictionsSelectMove.bind(this);
+ this.handlePredictionsSelectEnd = this.handlePredictionsSelectEnd.bind(this);
+ this.finalizeSelection = this.finalizeSelection.bind(this);
+
+ // Debounce the method to avoid calling the API too many times
+ // when the user is typing fast.
+ this.predict = debounce(this.predict.bind(this), DEBOUNCE_WAIT_TIME, { leading: true });
+ }
+
+ // Interpret input key event
+ onKeyDown(e) {
+ if (e.keyCode === KEY_CODE_ARROW_UP) {
+ // Prevent changing cursor position in input
+ e.preventDefault();
+ this.changeHighlight(DIRECTION_UP);
+ } else if (e.keyCode === KEY_CODE_ARROW_DOWN) {
+ // Prevent changing cursor position in input
+ e.preventDefault();
+ this.changeHighlight(DIRECTION_DOWN);
+ } else if (e.keyCode === KEY_CODE_ENTER) {
+ const { search, selectedPlace } = currentValue(this.props);
+
+ if (search && !selectedPlace) {
+ // Prevent form submit, try to select value instead.
+ e.preventDefault();
+ e.stopPropagation();
+ this.selectItemIfNoneSelected();
+ }
+ } else if (e.keyCode === KEY_CODE_TAB) {
+ this.selectItemIfNoneSelected();
+ } else if (e.keyCode === KEY_CODE_ESC && this.input) {
+ this.input.blur();
+ }
+ }
+
+ // Handle input text change, fetch predictions if the value isn't empty
+ onChange(e) {
+ const onChange = this.props.input.onChange;
+ const { predictions } = currentValue(this.props);
+ const newValue = e.target.value;
+
+ // Clear the current values since the input content is changed
+ onChange({
+ search: newValue,
+ predictions: newValue ? predictions : [],
+ selectedPlaceId: null,
+ selectedPlace: null,
+ });
+
+ // Clear highlighted prediction since the input value changed and
+ // results will change as well
+ this.setState({ highlightedIndex: -1 });
+
+ if (!newValue) {
+ // No need to fetch predictions on empty input
+ return;
+ }
+
+ this.predict(newValue);
+ }
+
+ // Change the currently highlighted item by calculating the new
+ // index from the current state and the given direction number
+ // (DIRECTION_UP or DIRECTION_DOWN)
+ changeHighlight(direction) {
+ this.setState((prevState, props) => {
+ const { predictions } = currentValue(props);
+ const currentIndex = prevState.highlightedIndex;
+ let index = currentIndex;
+
+ if (direction === DIRECTION_UP) {
+ // Keep the first position if already highlighted
+ index = currentIndex === 0 ? 0 : currentIndex - 1;
+ } else if (direction === DIRECTION_DOWN) {
+ index = currentIndex + 1;
+ }
+
+ // Check that the index is within the bounds
+ if (index < 0) {
+ index = -1;
+ } else if (index >= predictions.length) {
+ index = predictions.length - 1;
+ }
+
+ return { highlightedIndex: index };
+ });
+ }
+
+ // Select the prediction in the given item. This will fetch the
+ // place details and set it as the selected place.
+ selectItem(index) {
+ if (index < 0) {
+ return;
+ }
+ const { predictions } = currentValue(this.props);
+ if (index >= predictions.length) {
+ return;
+ }
+ const prediction = predictions[index];
+ const placeId = prediction.place_id;
+
+ this.props.input.onChange({
+ ...this.props.input,
+ selectedPlaceId: placeId,
+ selectedPlace: null,
+ });
+
+ this.autocompleteSessionToken =
+ this.autocompleteSessionToken || new window.google.maps.places.AutocompleteSessionToken();
+ const sessionToken = this.autocompleteSessionToken;
+
+ getPlaceDetails(placeId, sessionToken)
+ .then(place => {
+ this.props.input.onChange({
+ search: prediction.description,
+ predictions: [],
+ selectedPlaceId: placeId,
+ selectedPlace: place,
+ });
+ this.autocompleteSessionToken = null;
+ })
+ .catch(e => {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ this.props.input.onChange({
+ ...this.props.input.value,
+ selectedPlaceId: null,
+ selectedPlace: null,
+ });
+ });
+ }
+ selectItemIfNoneSelected() {
+ const { search, selectedPlaceId } = currentValue(this.props);
+ if (search && !selectedPlaceId) {
+ const index = this.state.highlightedIndex !== -1 ? this.state.highlightedIndex : 0;
+ this.selectItem(index);
+ }
+ }
+ predict(search) {
+ const mapsLibLoaded = window.google && window.google.maps;
+ if (!mapsLibLoaded) {
+ throw new Error('Google Maps API must be loaded for LocationAutocompleteInput');
+ }
+ const onChange = this.props.input.onChange;
+
+ this.autocompleteSessionToken =
+ this.autocompleteSessionToken || new window.google.maps.places.AutocompleteSessionToken();
+ const sessionToken = this.autocompleteSessionToken;
+
+ getPlacePredictions(search, sessionToken)
+ .then(results => {
+ const { search: currentSearch } = currentValue(this.props);
+
+ // If the earlier predictions arrive when the user has already
+ // changed the search term, ignore and wait until the latest
+ // predictions arrive. Without this logic, results for earlier
+ // requests would override whatever the user had typed since.
+ //
+ // This is essentially the same as switchLatest in RxJS or
+ // takeLatest in Redux Saga, without canceling the earlier
+ // requests.
+ if (results.search === currentSearch) {
+ onChange({
+ search: results.search,
+ predictions: results.predictions,
+ selectedPlaceId: null,
+ selectedPlace: null,
+ });
+ }
+ })
+ .catch(e => {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ const value = currentValue(this.props);
+ onChange({
+ ...value,
+ selectedPlaceId: null,
+ selectedPlace: null,
+ });
+ });
+ }
+
+ finalizeSelection() {
+ this.setState({ inputHasFocus: false, highlightedIndex: -1 });
+ this.props.input.onBlur(currentValue(this.props));
+ }
+
+ handleOnBlur() {
+ if (this.props.closeOnBlur && !this.state.selectionInProgress) {
+ this.finalizeSelection();
+ }
+ }
+
+ handlePredictionsSelectStart(touchCoordinates) {
+ this.setState({
+ selectionInProgress: true,
+ touchStartedFrom: touchCoordinates,
+ isSwipe: false,
+ });
+ }
+
+ handlePredictionsSelectMove(touchCoordinates) {
+ this.setState(prevState => {
+ const touchStartedFrom = prevState.touchStartedFrom;
+ const isTouchAction = !!touchStartedFrom;
+ const isSwipe = isTouchAction
+ ? Math.abs(touchStartedFrom.y - touchCoordinates.y) > TOUCH_TAP_RADIUS
+ : false;
+
+ return { selectionInProgress: false, isSwipe };
+ });
+ }
+
+ handlePredictionsSelectEnd(index) {
+ this.setState(prevState => {
+ if (!prevState.isSwipe) {
+ this.selectItem(index);
+ this.finalizeSelection();
+ }
+ return { selectionInProgress: false, touchStartedFrom: null, isSwipe: false };
+ });
+ }
+
+ render() {
+ const {
+ autoFocus,
+ rootClassName,
+ className,
+ iconClassName,
+ inputClassName,
+ predictionsClassName,
+ validClassName,
+ placeholder,
+ input,
+ meta,
+ inputRef,
+ } = this.props;
+ const { name, onFocus } = input;
+ const { search, predictions } = currentValue(this.props);
+ const { touched, valid } = meta || {};
+ const isValid = valid && touched;
+
+ const handleOnFocus = e => {
+ this.setState({ inputHasFocus: true });
+ onFocus(e);
+ };
+
+ const rootClass = classNames(rootClassName || css.root, className);
+ const iconClass = classNames(iconClassName || css.icon);
+ const inputClass = classNames(inputClassName || css.input, { [validClassName]: isValid });
+ const predictionsClass = classNames(predictionsClassName);
+
+ // Only render predictions when the input has focus. For
+ // development and easier workflow with the browser devtools, you
+ // might want to hardcode this to `true`. Otherwise the dropdown
+ // list will disappear.
+ const renderPredictions = this.state.inputHasFocus;
+
+ return (
+
+
+
+
+
{
+ this.input = node;
+ if (inputRef) {
+ inputRef(node);
+ }
+ }}
+ />
+ {renderPredictions ? (
+
+ ) : null}
+
+ );
+ }
+}
+
+LocationAutocompleteInputGoogleMaps.defaultProps = {
+ autoFocus: false,
+ closeOnBlur: true,
+ rootClassName: null,
+ className: null,
+ iconClassName: null,
+ inputClassName: null,
+ predictionsClassName: null,
+ validClassName: null,
+ placeholder: '',
+ meta: null,
+ inputRef: null,
+};
+
+LocationAutocompleteInputGoogleMaps.propTypes = {
+ autoFocus: bool,
+ rootClassName: string,
+ className: string,
+ closeOnBlur: bool,
+ iconClassName: string,
+ inputClassName: string,
+ predictionsClassName: string,
+ validClassName: string,
+ placeholder: string,
+ input: shape({
+ name: string.isRequired,
+ value: oneOfType([
+ shape({
+ search: string,
+ predictions: any,
+ selectedPlace: propTypes.place,
+ }),
+ string,
+ ]),
+ onChange: func.isRequired,
+ onFocus: func.isRequired,
+ onBlur: func.isRequired,
+ }).isRequired,
+ meta: shape({
+ valid: bool.isRequired,
+ touched: bool.isRequired,
+ }),
+ inputRef: func,
+};
+
+
+export default LocationAutocompleteInputGoogleMaps;