Fix autocomplete click issue. Listen mouse down instead of click

This commit fixes click issue in the autocomplete predictions list. The previous implementation relied on two event: blur event from the input and click even from the predictions list. Blur event happens before click event and because of this there was a listener to listen the current hover status. However, this strategy doesn't work in touch devices where there's no hovers.

The fix is to listen mousedown/mouseup (and touchstart/touchend) events and save the current "in progress" state to the component state.
This commit is contained in:
Mikko Koski 2017-05-19 10:52:03 +03:00
parent 04780644de
commit 98fd29f92a

View file

@ -17,7 +17,7 @@ const DIRECTION_DOWN = 'down';
// Renders the autocompletion prediction results in a list
const LocationPredictionsList = props => {
const { predictions, highlightedIndex, onSelectItem } = props;
const { predictions, highlightedIndex, onSelectStart, onSelectEnd } = props;
if (predictions.length === 0) {
return null;
}
@ -30,7 +30,10 @@ const LocationPredictionsList = props => {
<li
className={isHighlighted ? css.highlighted : null}
key={prediction.id}
onClick={() => onSelectItem(index)}
onTouchStart={onSelectStart}
onMouseDown={onSelectStart}
onTouchEnd={() => onSelectEnd(index)}
onMouseUp={() => onSelectEnd(index)}
>
{prediction.description}
</li>
@ -58,7 +61,8 @@ LocationPredictionsList.propTypes = {
})
).isRequired,
highlightedIndex: number,
onSelectItem: func.isRequired,
onSelectStart: func.isRequired,
onSelectEnd: func.isRequired,
};
// Get the current value with defaults from the given
@ -91,7 +95,7 @@ class LocationAutocompleteInput extends Component {
this.state = {
inputHasFocus: false,
predictionsHaveHover: false,
selectionInProgress: false,
highlightedIndex: -1, // -1 means no highlight
};
@ -100,6 +104,10 @@ class LocationAutocompleteInput extends Component {
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.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.
@ -268,9 +276,31 @@ class LocationAutocompleteInput extends Component {
});
});
}
finalizeSelection() {
this.setState({ inputHasFocus: false, highlightedIndex: -1 });
this.props.input.onBlur(currentValue(this.props));
}
handleOnBlur() {
if (!this.state.selectionInProgress) {
this.finalizeSelection();
}
}
handlePredictionsSelectStart() {
this.setState({selectionInProgress: true})
}
handlePredictionsSelectEnd(index) {
this.setState({selectionInProgress: false});
this.selectItem(index);
this.finalizeSelection();
}
render() {
const { autoFocus, className, placeholder, input } = this.props;
const { name, onFocus, onBlur } = input;
const { name, onFocus } = input;
const { search, predictions } = currentValue(this.props);
const handleOnFocus = e => {
@ -278,21 +308,12 @@ class LocationAutocompleteInput extends Component {
onFocus(e);
};
const handleOnBlur = () => {
this.setState({ inputHasFocus: false, highlightedIndex: -1 });
onBlur(currentValue(this.props));
};
// 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.
//
// We also have to check if the predictions have hover to avoid a
// click triggering a blur event that hides the predictions before
// the click event is sent, resulting in a click to whatever is
// rendered below the predictions.
const renderPredictions = this.state.inputHasFocus || this.state.predictionsHaveHover;
const renderPredictions = this.state.inputHasFocus;
return (
<div className={css.root}>
@ -305,21 +326,17 @@ class LocationAutocompleteInput extends Component {
name={name}
value={search}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onBlur={this.handleOnBlur}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
/>
{renderPredictions
? <div
onMouseEnter={() => this.setState({ predictionsHaveHover: true })}
onMouseLeave={() => this.setState({ predictionsHaveHover: false })}
>
<LocationPredictionsList
predictions={predictions}
highlightedIndex={this.state.highlightedIndex}
onSelectItem={this.selectItem}
/>
</div>
? <LocationPredictionsList
predictions={predictions}
highlightedIndex={this.state.highlightedIndex}
onSelectStart={this.handlePredictionsSelectStart}
onSelectEnd={this.handlePredictionsSelectEnd}
/>
: null}
</div>
);