From 3727af1025b3c4e9557eb31ffa1072537f46c7b6 Mon Sep 17 00:00:00 2001 From: Ken Hibino Date: Wed, 21 Jun 2017 21:30:26 -0700 Subject: [PATCH] Add onSuggestionHighlighted prop (#339) --- README.md | 13 ++++++++ src/Autosuggest.js | 20 ++++++++++++ test/focus-first-suggestion/AutosuggestApp.js | 3 ++ .../AutosuggestApp.test.js | 24 ++++++++------ test/multi-section/AutosuggestApp.js | 3 ++ test/multi-section/AutosuggestApp.test.js | 25 +++++++++++++++ test/plain-list/AutosuggestApp.js | 5 +++ test/plain-list/AutosuggestApp.test.js | 32 +++++++++++++++++-- 8 files changed, 114 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 62532bf4..bada702a 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ class Example extends React.Component { | [`renderSuggestion`](#renderSuggestionProp) | Function | ✓ | Use your imagination to define how suggestions are rendered. | | [`inputProps`](#inputPropsProp) | Object | ✓ | Pass through arbitrary props to the input. It must contain at least `value` and `onChange`. | | [`onSuggestionSelected`](#onSuggestionSelectedProp) | Function | | Will be called every time suggestion is selected via mouse or keyboard. | +| [`onSuggestionHighlighted`](#onSuggestionHighlightedProp) | Function | | Will be called every time the highlighted suggestion changes. | | [`shouldRenderSuggestions`](#shouldRenderSuggestionsProp) | Function | | When the input is focused, Autosuggest will consult this function when to render suggestions. Use it, for example, if you want to display suggestions when input value is at least 2 characters long. | | [`alwaysRenderSuggestions`](#alwaysRenderSuggestionsProp) | Boolean | | Set it to `true` if you'd like to render suggestions even when the input is not focused. | | [`highlightFirstSuggestion`](#highlightFirstSuggestionProp) | Boolean | | Set it to `true` if you'd like Autosuggest to automatically highlight the first suggestion. | @@ -372,6 +373,18 @@ where: * `'click'` - user clicked (or tapped) on the suggestion * `'enter'` - user selected the suggestion using Enter + +#### onSuggestionHighlighted (optional) + +This function is called when the highlighted suggestion changes. It has the following signature: + +```js +function onSuggestionHighlighted({ suggestion }) +``` + +where: +* `suggestion` - the highlighted suggestion, or `null` if there is no highlighted suggestion. + #### shouldRenderSuggestions (optional) diff --git a/src/Autosuggest.js b/src/Autosuggest.js index 438e74c1..de818cf3 100644 --- a/src/Autosuggest.js +++ b/src/Autosuggest.js @@ -34,6 +34,7 @@ export default class Autosuggest extends Component { } }, onSuggestionSelected: PropTypes.func, + onSuggestionHighlighted: PropTypes.func, renderInputComponent: PropTypes.func, renderSuggestionsContainer: PropTypes.func, getSuggestionValue: PropTypes.func.isRequired, @@ -138,6 +139,25 @@ export default class Autosuggest extends Component { } } + componentDidUpdate(prevProps, prevState) { + const { onSuggestionHighlighted } = this.props; + + if (!onSuggestionHighlighted) { + return; + } + + const { highlightedSectionIndex, highlightedSuggestionIndex } = this.state; + + if ( + highlightedSectionIndex !== prevState.highlightedSectionIndex || + highlightedSuggestionIndex !== prevState.highlightedSuggestionIndex + ) { + const suggestion = this.getHighlightedSuggestion(); + + onSuggestionHighlighted({ suggestion }); + } + } + componentWillUnmount() { document.removeEventListener('mousedown', this.onDocumentMouseDown); } diff --git a/test/focus-first-suggestion/AutosuggestApp.js b/test/focus-first-suggestion/AutosuggestApp.js index c81c1f42..4fe26e61 100644 --- a/test/focus-first-suggestion/AutosuggestApp.js +++ b/test/focus-first-suggestion/AutosuggestApp.js @@ -41,6 +41,8 @@ export const onSuggestionsClearRequested = sinon.spy(() => { export const onSuggestionSelected = sinon.spy(); +export const onSuggestionHighlighted = sinon.spy(); + export default class AutosuggestApp extends Component { constructor() { super(); @@ -66,6 +68,7 @@ export default class AutosuggestApp extends Component { onSuggestionsFetchRequested={onSuggestionsFetchRequested} onSuggestionsClearRequested={onSuggestionsClearRequested} onSuggestionSelected={onSuggestionSelected} + onSuggestionHighlighted={onSuggestionHighlighted} getSuggestionValue={getSuggestionValue} renderSuggestion={renderSuggestion} inputProps={inputProps} diff --git a/test/focus-first-suggestion/AutosuggestApp.test.js b/test/focus-first-suggestion/AutosuggestApp.test.js index 811f5802..96b5a918 100644 --- a/test/focus-first-suggestion/AutosuggestApp.test.js +++ b/test/focus-first-suggestion/AutosuggestApp.test.js @@ -19,7 +19,8 @@ import { } from '../helpers'; import AutosuggestApp, { onChange, - onSuggestionSelected + onSuggestionSelected, + onSuggestionHighlighted } from './AutosuggestApp'; describe('Autosuggest with highlightFirstSuggestion={true}', () => { @@ -109,12 +110,9 @@ describe('Autosuggest with highlightFirstSuggestion={true}', () => { }); describe('inputProps.onChange', () => { - beforeEach(() => { + it('should be called once with the right parameters when Enter is pressed after autohighlight', () => { focusAndSetInputValue('p'); onChange.reset(); - }); - - it('should be called once with the right parameters when Enter is pressed after autohighlight', () => { clickEnter(); expect(onChange).to.have.been.calledOnce; expect(onChange).to.be.calledWith(syntheticEventMatcher, { @@ -125,12 +123,9 @@ describe('Autosuggest with highlightFirstSuggestion={true}', () => { }); describe('onSuggestionSelected', () => { - beforeEach(() => { + it('should be called once with the right parameters when Enter is pressed after autohighlight', () => { focusAndSetInputValue('p'); onSuggestionSelected.reset(); - }); - - it('should be called once with the right parameters when Enter is pressed after autohighlight', () => { clickEnter(); expect(onSuggestionSelected).to.have.been.calledOnce; expect( @@ -144,4 +139,15 @@ describe('Autosuggest with highlightFirstSuggestion={true}', () => { }); }); }); + + describe('onSuggestionHighlighted', () => { + it('should be called once with the highlighed suggestion when the first suggestion is autohighlighted', () => { + onSuggestionHighlighted.reset(); + focusAndSetInputValue('p'); + expect(onSuggestionHighlighted).to.have.been.calledOnce; + expect(onSuggestionHighlighted).to.have.been.calledWithExactly({ + suggestion: { name: 'Perl', year: 1987 } + }); + }); + }); }); diff --git a/test/multi-section/AutosuggestApp.js b/test/multi-section/AutosuggestApp.js index 186d2d9d..56837075 100644 --- a/test/multi-section/AutosuggestApp.js +++ b/test/multi-section/AutosuggestApp.js @@ -54,6 +54,8 @@ export const onSuggestionsClearRequested = sinon.spy(() => { export const onSuggestionSelected = sinon.spy(); +export const onSuggestionHighlighted = sinon.spy(); + export const renderSectionTitle = sinon.spy(section => { return {section.title}; }); @@ -106,6 +108,7 @@ export default class AutosuggestApp extends Component { onSuggestionsFetchRequested={onSuggestionsFetchRequested} onSuggestionsClearRequested={onSuggestionsClearRequested} onSuggestionSelected={onSuggestionSelected} + onSuggestionHighlighted={onSuggestionHighlighted} getSuggestionValue={getSuggestionValue} renderSuggestion={renderSuggestion} inputProps={inputProps} diff --git a/test/multi-section/AutosuggestApp.test.js b/test/multi-section/AutosuggestApp.test.js index 778ab732..d114f87f 100644 --- a/test/multi-section/AutosuggestApp.test.js +++ b/test/multi-section/AutosuggestApp.test.js @@ -18,6 +18,7 @@ import { clickEscape, clickEnter, clickDown, + clickUp, setInputValue, focusAndSetInputValue, clickClearButton @@ -25,6 +26,7 @@ import { import AutosuggestApp, { onSuggestionsFetchRequested, onSuggestionSelected, + onSuggestionHighlighted, renderSectionTitle, getSectionSuggestions, setHighlightFirstSuggestion @@ -85,6 +87,29 @@ describe('Autosuggest with multiSection={true}', () => { }); }); + describe('onSuggestionHighlighted', () => { + it('should be called once with the suggestion that becomes highlighted', () => { + focusAndSetInputValue('c'); + onSuggestionHighlighted.reset(); + clickDown(); + expect(onSuggestionHighlighted).to.have.been.calledOnce; + expect(onSuggestionHighlighted).to.have.been.calledWithExactly({ + suggestion: { name: 'C', year: 1972 } + }); + }); + + it('should be called once with null when there is no more highlighted suggestion', () => { + focusAndSetInputValue('c'); + clickDown(); + onSuggestionHighlighted.reset(); + clickUp(); + expect(onSuggestionHighlighted).to.have.been.calledOnce; + expect(onSuggestionHighlighted).to.have.been.calledWithExactly({ + suggestion: null + }); + }); + }); + describe('onSuggestionsFetchRequested', () => { it('should be called once with the right parameters when input gets focus and shouldRenderSuggestions returns true', () => { onSuggestionsFetchRequested.reset(); diff --git a/test/plain-list/AutosuggestApp.js b/test/plain-list/AutosuggestApp.js index dea8adf2..697c50e7 100644 --- a/test/plain-list/AutosuggestApp.js +++ b/test/plain-list/AutosuggestApp.js @@ -62,6 +62,10 @@ export const onSuggestionSelected = sinon.spy(() => { addEvent('onSuggestionSelected'); }); +export const onSuggestionHighlighted = sinon.spy(() => { + addEvent('onSuggestionHighlighted'); +}); + export default class AutosuggestApp extends Component { constructor() { super(); @@ -98,6 +102,7 @@ export default class AutosuggestApp extends Component { onSuggestionsFetchRequested={onSuggestionsFetchRequested} onSuggestionsClearRequested={onSuggestionsClearRequested} onSuggestionSelected={onSuggestionSelected} + onSuggestionHighlighted={onSuggestionHighlighted} getSuggestionValue={getSuggestionValue} renderSuggestion={renderSuggestion} inputProps={inputProps} diff --git a/test/plain-list/AutosuggestApp.test.js b/test/plain-list/AutosuggestApp.test.js index 61bbba77..4872d38d 100644 --- a/test/plain-list/AutosuggestApp.test.js +++ b/test/plain-list/AutosuggestApp.test.js @@ -38,7 +38,8 @@ import AutosuggestApp, { shouldRenderSuggestions, onSuggestionsFetchRequested, onSuggestionsClearRequested, - onSuggestionSelected + onSuggestionSelected, + onSuggestionHighlighted } from './AutosuggestApp'; describe('Default Autosuggest', () => { @@ -604,7 +605,34 @@ describe('Default Autosuggest', () => { onChange.reset(); clearEvents(); clickSuggestion(1); - expect(getEvents()).to.deep.equal(['onChange', 'onSuggestionSelected']); + expect( + getEvents().filter(event => event === 'onChange' || event === 'onSuggestionSelected') + ).to.deep.equal(['onChange', 'onSuggestionSelected']); + }); + }); + + describe('onSuggestionHighlighted', () => { + beforeEach(() => { + focusAndSetInputValue('j'); + onSuggestionHighlighted.reset(); + }); + + it('should be called once with the highlighted suggestion when mouse enters a suggestion', () => { + mouseEnterSuggestion(0); + expect(onSuggestionHighlighted).to.have.been.calledOnce; + expect(onSuggestionHighlighted).to.have.been.calledWithExactly({ + suggestion: { name: 'Java', year: 1995 } + }); + }); + + it('should be called once with null when mouse leaves a suggestion and there is no more highlighted suggestion', () => { + mouseEnterSuggestion(0); + onSuggestionHighlighted.reset(); + mouseLeaveSuggestion(0); + expect(onSuggestionHighlighted).to.have.been.calledOnce; + expect(onSuggestionHighlighted).to.have.been.calledWithExactly({ + suggestion: null + }); }); });