diff --git a/src/Autosuggest.js b/src/Autosuggest.js index a2e81a75..17d6b57b 100644 --- a/src/Autosuggest.js +++ b/src/Autosuggest.js @@ -109,10 +109,13 @@ export default class Autosuggest extends Component { this.justPressedUpDown = false; this.justMouseEntered = false; + + this.pressedSuggestion = null; } componentDidMount() { document.addEventListener('mousedown', this.onDocumentMouseDown); + document.addEventListener('mouseup', this.onDocumentMouseUp); this.input = this.autowhatever.input; this.suggestionsContainer = this.autowhatever.itemsContainer; @@ -169,6 +172,7 @@ export default class Autosuggest extends Component { componentWillUnmount() { document.removeEventListener('mousedown', this.onDocumentMouseDown); + document.removeEventListener('mouseup', this.onDocumentMouseUp); } updateHighlightedSuggestion(sectionIndex, suggestionIndex, prevValue) { @@ -332,6 +336,9 @@ export default class Autosuggest extends Component { onSuggestionMouseEnter = (event, { sectionIndex, itemIndex }) => { this.updateHighlightedSuggestion(sectionIndex, itemIndex); + if (event.target === this.pressedSuggestion) { + this.justSelectedSuggestion = true; + } this.justMouseEntered = true; @@ -344,8 +351,20 @@ export default class Autosuggest extends Component { this.updateHighlightedSuggestion(this.props.multiSection ? 0 : null, 0); }; - onSuggestionMouseDown = () => { - this.justSelectedSuggestion = true; + onDocumentMouseUp = () => { + if (this.pressedSuggestion && !this.justSelectedSuggestion) { + this.pressedSuggestion = null; + this.input.focus(); + } + }; + + onSuggestionMouseDown = e => { + // Checking if this.justSelectedSuggestion is already true to not duplicate touch events in chrome + // See: https://github.com/facebook/react/issues/9809#issuecomment-413978405 + if (!this.justSelectedSuggestion) { + this.justSelectedSuggestion = true; + this.pressedSuggestion = e.target; + } }; onSuggestionsClearRequested = () => { @@ -427,8 +446,23 @@ export default class Autosuggest extends Component { onBlur && onBlur(this.blurEvent, { highlightedSuggestion }); }; - resetHighlightedSuggestionOnMouseLeave = () => { + onSuggestionMouseLeave = e => { this.resetHighlightedSuggestion(false); // shouldResetValueBeforeUpDown + if (this.justSelectedSuggestion && e.target === this.pressedSuggestion) { + this.justSelectedSuggestion = false; + } + }; + + onSuggestionTouchStart = () => { + this.justSelectedSuggestion = true; + // todo: event.preventDefault when https://github.com/facebook/react/issues/2043 + // todo: gets released so onSuggestionMouseDown won't fire in chrome + }; + + onSuggestionTouchMove = () => { + this.justSelectedSuggestion = false; + this.pressedSuggestion = null; + this.input.focus(); }; itemProps = ({ sectionIndex, itemIndex }) => { @@ -436,9 +470,10 @@ export default class Autosuggest extends Component { 'data-section-index': sectionIndex, 'data-suggestion-index': itemIndex, onMouseEnter: this.onSuggestionMouseEnter, - onMouseLeave: this.resetHighlightedSuggestionOnMouseLeave, + onMouseLeave: this.onSuggestionMouseLeave, onMouseDown: this.onSuggestionMouseDown, - onTouchStart: this.onSuggestionMouseDown, // Because on iOS `onMouseDown` is not triggered + onTouchStart: this.onSuggestionTouchStart, + onTouchMove: this.onSuggestionTouchMove, onClick: this.onSuggestionClick }; }; diff --git a/test/focus-first-suggestion/AutosuggestApp.test.js b/test/focus-first-suggestion/AutosuggestApp.test.js index 9a4cc66c..6048865b 100644 --- a/test/focus-first-suggestion/AutosuggestApp.test.js +++ b/test/focus-first-suggestion/AutosuggestApp.test.js @@ -128,16 +128,15 @@ describe('Autosuggest with highlightFirstSuggestion={true}', () => { onSuggestionSelected.reset(); clickEnter(); expect(onSuggestionSelected).to.have.been.calledOnce; - expect(onSuggestionSelected).to.have.been.calledWithExactly( - syntheticEventMatcher, - { - suggestion: { name: 'Perl', year: 1987 }, - suggestionValue: 'Perl', - suggestionIndex: 0, - sectionIndex: null, - method: 'enter' - } - ); + expect( + onSuggestionSelected + ).to.have.been.calledWithExactly(syntheticEventMatcher, { + suggestion: { name: 'Perl', year: 1987 }, + suggestionValue: 'Perl', + suggestionIndex: 0, + sectionIndex: null, + method: 'enter' + }); }); }); diff --git a/test/helpers.js b/test/helpers.js index 966132be..69a1883a 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -92,13 +92,11 @@ export const getSuggestion = suggestionIndex => { throw Error( ` Cannot find suggestion #${suggestionIndex}. - ${ - suggestions.length === 0 - ? 'No suggestions found.' - : `Only ${suggestions.length} suggestion${ - suggestions.length === 1 ? '' : 's' - } found.` - } + ${suggestions.length === 0 + ? 'No suggestions found.' + : `Only ${suggestions.length} suggestion${suggestions.length === 1 + ? '' + : 's'} found.`} ` ); } @@ -181,6 +179,30 @@ const mouseDownDocument = target => { ); }; +export const mouseUpDocument = target => { + document.dispatchEvent( + new window.CustomEvent( + 'mouseup', + target + ? { + detail: { + // must be 'detail' accoring to docs: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events#Adding_custom_data_–_CustomEvent() + target + } + } + : null + ) + ); +}; + +const touchStartSuggestion = suggestionIndex => { + Simulate.touchStart(getSuggestion(suggestionIndex)); +}; + +const touchMoveSuggestion = suggestionIndex => { + Simulate.touchMove(getSuggestion(suggestionIndex)); +}; + // It doesn't feel right to emulate all the DOM events by copying the implementation. // Please show me a better way to emulate this. export const clickSuggestion = suggestionIndex => { @@ -195,6 +217,41 @@ export const clickSuggestion = suggestionIndex => { clock.tick(1); }; +// Simulates only mouse events since on touch devices dragging considered as a scroll and is a different case. +export const dragSuggestionOut = suggestionIndex => { + const suggestion = getSuggestion(suggestionIndex); + + mouseEnterSuggestion(suggestionIndex); + mouseDownDocument(suggestion); + mouseDownSuggestion(suggestionIndex); + mouseLeaveSuggestion(suggestionIndex); + mouseUpDocument(); +}; + +export const dragSuggestionOutAndIn = suggestionIndex => { + const suggestion = getSuggestion(suggestionIndex); + + mouseEnterSuggestion(suggestionIndex); + mouseDownDocument(suggestion); + mouseDownSuggestion(suggestionIndex); + mouseLeaveSuggestion(suggestionIndex); + mouseEnterSuggestion(suggestionIndex); + mouseUpDocument(); + blurInput(); + focusInput(); + Simulate.click(suggestion); + clock.tick(1); +}; + +// Simulates mouse events as well as touch events since some browsers (chrome) mirror them and we should handle this. +// Order of events is implemented according to docs: https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent +export const dragSuggestionOutTouch = suggestionIndex => { + touchStartSuggestion(suggestionIndex); + touchMoveSuggestion(suggestionIndex); + mouseDownSuggestion(suggestionIndex); + mouseUpDocument(); +}; + export const clickSuggestionsContainer = () => { mouseDownDocument(suggestionsContainer); blurInput(); diff --git a/test/plain-list/AutosuggestApp.test.js b/test/plain-list/AutosuggestApp.test.js index 98418122..6a1b4b73 100644 --- a/test/plain-list/AutosuggestApp.test.js +++ b/test/plain-list/AutosuggestApp.test.js @@ -18,6 +18,8 @@ import { mouseLeaveSuggestion, clickSuggestion, clickSuggestionsContainer, + dragSuggestionOut, + dragSuggestionOutTouch, focusInput, blurInput, clickEscape, @@ -28,7 +30,9 @@ import { focusAndSetInputValue, isInputFocused, clearEvents, - getEvents + getEvents, + mouseUpDocument, + dragSuggestionOutAndIn } from '../helpers'; import AutosuggestApp, { getSuggestionValue, @@ -316,6 +320,41 @@ describe('Default Autosuggest', () => { }); }); + // Tests for these bugs + // https://github.com/moroshko/react-autosuggest/issues/412#issuecomment-318627754 + // https://github.com/moroshko/react-autosuggest/issues/380 + describe('when suggestion is dragged', () => { + beforeEach(() => { + focusAndSetInputValue('p'); + }); + + it('should keep the focus on input when suggestion is dragged', () => { + dragSuggestionOut(1); + expect(isInputFocused()).to.equal(true); + }); + + it('should clear suggestions if input blurred after suggestion drag', () => { + dragSuggestionOut(1); + blurInput(); + expectSuggestions([]); + }); + + it('should keep the focus on input when suggestion is dragged on touch devices', () => { + dragSuggestionOutTouch(1); + expect(isInputFocused()).to.equal(true); + }); + + it("should select a suggestion if it's dragged and mouse enters back", () => { + dragSuggestionOutAndIn(1); + expectInputValue('PHP'); + }); + + it('should not focus input on document mouse up', () => { + mouseUpDocument(); + expect(isInputFocused()).to.equal(false); + }); + }); + describe('getSuggestionValue', () => { beforeEach(() => { getSuggestionValue.reset(); @@ -563,32 +602,30 @@ describe('Default Autosuggest', () => { it('should be called once with the right parameters when suggestion is clicked', () => { clickSuggestion(1); expect(onSuggestionSelected).to.have.been.calledOnce; - expect(onSuggestionSelected).to.have.been.calledWithExactly( - syntheticEventMatcher, - { - suggestion: { name: 'JavaScript', year: 1995 }, - suggestionValue: 'JavaScript', - suggestionIndex: 1, - sectionIndex: null, - method: 'click' - } - ); + expect( + onSuggestionSelected + ).to.have.been.calledWithExactly(syntheticEventMatcher, { + suggestion: { name: 'JavaScript', year: 1995 }, + suggestionValue: 'JavaScript', + suggestionIndex: 1, + sectionIndex: null, + method: 'click' + }); }); it('should be called once with the right parameters when Enter is pressed and suggestion is highlighted', () => { clickDown(); clickEnter(); expect(onSuggestionSelected).to.have.been.calledOnce; - expect(onSuggestionSelected).to.have.been.calledWithExactly( - syntheticEventMatcher, - { - suggestion: { name: 'Java', year: 1995 }, - suggestionValue: 'Java', - suggestionIndex: 0, - sectionIndex: null, - method: 'enter' - } - ); + expect( + onSuggestionSelected + ).to.have.been.calledWithExactly(syntheticEventMatcher, { + suggestion: { name: 'Java', year: 1995 }, + suggestionValue: 'Java', + suggestionIndex: 0, + sectionIndex: null, + method: 'enter' + }); }); it('should not be called when Enter is pressed and there is no highlighted suggestion', () => {