From 355b8deff166107ef4ebe7cd76b834d7d4436fca Mon Sep 17 00:00:00 2001 From: Daniel Lo Nigro Date: Sun, 27 Nov 2016 18:26:57 -0800 Subject: [PATCH 1/3] [added] Allow Tab to select option --- lib/Autocomplete.js | 79 ++++++++++++++++++------------ lib/__tests__/Autocomplete-test.js | 42 ++++++++++++++++ 2 files changed, 89 insertions(+), 32 deletions(-) diff --git a/lib/Autocomplete.js b/lib/Autocomplete.js index df15edd4..f4c750b8 100644 --- a/lib/Autocomplete.js +++ b/lib/Autocomplete.js @@ -10,6 +10,7 @@ let Autocomplete = React.createClass({ value: React.PropTypes.any, onChange: React.PropTypes.func, onSelect: React.PropTypes.func, + selectOnTab: React.PropTypes.bool.isRequired, shouldItemRender: React.PropTypes.func, sortItems: React.PropTypes.func, getItemValue: React.PropTypes.func.isRequired, @@ -34,10 +35,11 @@ let Autocomplete = React.createClass({ }, inputProps: {}, onChange () {}, - onSelect (value, item) {}, + onSelect (value, item, selectionMethod) {}, renderMenu (items, value, style) { return
}, + selectOnTab: true, shouldItemRender () { return true }, menuStyle: { borderRadius: '3px', @@ -164,35 +166,7 @@ let Autocomplete = React.createClass({ }, Enter (event) { - if (this.state.isOpen === false) { - // menu is closed so there is no selection to accept -> do nothing - return - } - else if (this.state.highlightedIndex == null) { - // input has focus but no menu item is selected + enter is hit -> close the menu, highlight whatever's in input - this.setState({ - isOpen: false - }, () => { - this.refs.input.select() - }) - } - else { - // text entered + menu item has been highlighted + enter is hit -> update value to that of selected menu item, close the menu - event.preventDefault() - var item = this.getFilteredItems()[this.state.highlightedIndex] - var value = this.props.getItemValue(item) - this.setState({ - isOpen: false, - highlightedIndex: null - }, () => { - //this.refs.input.focus() // TODO: file issue - this.refs.input.setSelectionRange( - value.length, - value.length - ) - this.props.onSelect(value, item) - }) - } + this.handleKeyboardSelection(event); }, Escape (event) { @@ -200,6 +174,48 @@ let Autocomplete = React.createClass({ highlightedIndex: null, isOpen: false }) + }, + + Tab (event) { + if (this.props.selectOnTab) { + this.handleKeyboardSelection(event); + } + } + }, + + handleKeyboardSelection (event) { + if (this.state.isOpen === false) { + // menu is closed so there is no selection to accept -> do nothing + return + } + else if (this.state.highlightedIndex == null) { + // input has focus but no menu item is selected + enter is hit -> close the menu, highlight whatever's in input + this.setState({ + isOpen: false + }, () => { + this.refs.input.select() + }) + } + else { + // text entered + menu item has been highlighted + enter is hit -> update value to that of selected menu item, close the menu + if (event.key === 'Enter') { + // If enter was pressed, we want to prevent the default event handler from executing. + // However, if tab was pressed, we *do* want the default handler to kick in. + event.preventDefault() + } + var item = this.getFilteredItems()[this.state.highlightedIndex] + var value = this.props.getItemValue(item) + this.setState({ + isOpen: false, + highlightedIndex: null + }, () => { + //this.refs.input.focus() // TODO: file issue + this.refs.input.setSelectionRange( + value.length, + value.length + ) + this.props.onSelect(value, item, event.key) + }) } }, @@ -262,7 +278,7 @@ let Autocomplete = React.createClass({ isOpen: false, highlightedIndex: null }, () => { - this.props.onSelect(value, item) + this.props.onSelect(value, item, 'click') this.refs.input.focus() }) }, @@ -373,4 +389,3 @@ let Autocomplete = React.createClass({ }) module.exports = Autocomplete - diff --git a/lib/__tests__/Autocomplete-test.js b/lib/__tests__/Autocomplete-test.js index ac67f2c8..803d9b3b 100644 --- a/lib/__tests__/Autocomplete-test.js +++ b/lib/__tests__/Autocomplete-test.js @@ -293,6 +293,48 @@ describe('Autocomplete kewDown->Enter event handlers', () => { }); +describe('Autocomplete kewDown->Tab event handlers', () => { + it('should invoke `onSelect` with the selected menu item and close the menu', () => { + var autocompleteWrapper = mount(AutocompleteComponentJSX({})); + var autocompleteInputWrapper = autocompleteWrapper.find('input'); + + let value = 'Ar'; + let defaultPrevented = false; + autocompleteWrapper.setState({'isOpen': true}); + autocompleteInputWrapper.simulate('focus'); + autocompleteWrapper.setProps({ value, onSelect(v) { value = v; } }); + + // simulate keyUp of last key, triggering autocomplete suggestion + selection of the suggestion in the menu + autocompleteInputWrapper.simulate('keyUp', { key : 'r', keyCode: 82, which: 82 }); + + // Hit tab, updating state.value with the selected Autocomplete suggestion + autocompleteInputWrapper.simulate('keyDown', { key : 'Tab', keyCode: 9, which: 9, preventDefault() { defaultPrevented = true; } }); + expect(value).toEqual('Arizona'); + expect(autocompleteWrapper.state('isOpen')).toBe(false); + expect(defaultPrevented).toBe(false); + }); + + it('should not do anything if selectOnTab is false', () => { + var autocompleteWrapper = mount(AutocompleteComponentJSX({ + selectOnTab: false, + })); + var autocompleteInputWrapper = autocompleteWrapper.find('input'); + + let value = 'Ar'; + autocompleteWrapper.setState({'isOpen': true}); + autocompleteInputWrapper.simulate('focus'); + autocompleteWrapper.setProps({ value, onSelect(v) { value = v; } }); + + // simulate keyUp of last key, triggering autocomplete suggestion + selection of the suggestion in the menu + autocompleteInputWrapper.simulate('keyUp', { key : 'r', keyCode: 82, which: 82 }); + + // Pressing tab should not change the state of the component + autocompleteInputWrapper.simulate('keyDown', { key : 'Tab', keyCode: 9, which: 9 }); + expect(value).toEqual('Ar'); + expect(autocompleteWrapper.state('isOpen')).toBe(true); + }); +}); + describe('Autocomplete kewDown->Escape event handlers', () => { var autocompleteWrapper = mount(AutocompleteComponentJSX({})); From 7e8b73bfec49b91a2155687775118555f420764b Mon Sep 17 00:00:00 2001 From: Daniel Lo Nigro Date: Sun, 27 Nov 2016 19:00:28 -0800 Subject: [PATCH 2/3] Avoid "This synthetic event is reused for performance reasons" by caching the key earlier --- lib/Autocomplete.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Autocomplete.js b/lib/Autocomplete.js index f4c750b8..82427e7f 100644 --- a/lib/Autocomplete.js +++ b/lib/Autocomplete.js @@ -184,6 +184,7 @@ let Autocomplete = React.createClass({ }, handleKeyboardSelection (event) { + var key = event.key; if (this.state.isOpen === false) { // menu is closed so there is no selection to accept -> do nothing return @@ -198,7 +199,7 @@ let Autocomplete = React.createClass({ } else { // text entered + menu item has been highlighted + enter is hit -> update value to that of selected menu item, close the menu - if (event.key === 'Enter') { + if (key === 'Enter') { // If enter was pressed, we want to prevent the default event handler from executing. // However, if tab was pressed, we *do* want the default handler to kick in. event.preventDefault() @@ -214,7 +215,7 @@ let Autocomplete = React.createClass({ value.length, value.length ) - this.props.onSelect(value, item, event.key) + this.props.onSelect(value, item, key) }) } }, From aba1367dc95eba8ca1359d57f858538e47688258 Mon Sep 17 00:00:00 2001 From: Daniel Lo Nigro Date: Sun, 27 Nov 2016 19:10:10 -0800 Subject: [PATCH 3/3] More comprehensive tests, check `selectionMethod` --- lib/__tests__/Autocomplete-test.js | 46 ++++++++++++++---------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/lib/__tests__/Autocomplete-test.js b/lib/__tests__/Autocomplete-test.js index 803d9b3b..eb6910c6 100644 --- a/lib/__tests__/Autocomplete-test.js +++ b/lib/__tests__/Autocomplete-test.js @@ -274,18 +274,18 @@ describe('Autocomplete kewDown->Enter event handlers', () => { }); it('should invoke `onSelect` with the selected menu item and close the menu', () => { - let value = 'Ar'; + const onSelect = jest.fn(); let defaultPrevented = false; autocompleteWrapper.setState({'isOpen': true}); autocompleteInputWrapper.simulate('focus'); - autocompleteWrapper.setProps({ value, onSelect(v) { value = v; } }); - + autocompleteWrapper.setProps({ value: 'Ar', onSelect }); + // simulate keyUp of last key, triggering autocomplete suggestion + selection of the suggestion in the menu - autocompleteInputWrapper.simulate('keyUp', { key : 'r', keyCode: 82, which: 82 }); + autocompleteInputWrapper.simulate('keyUp', { key : 'r', keyCode: 82, which: 82 }); // Hit enter, updating state.value with the selected Autocomplete suggestion autocompleteInputWrapper.simulate('keyDown', { key : 'Enter', keyCode: 13, which: 13, preventDefault() { defaultPrevented = true; } }); - expect(value).toEqual('Arizona'); + expect(onSelect).toBeCalledWith('Arizona', {abbr: 'AZ', name: 'Arizona'}, 'Enter'); expect(autocompleteWrapper.state('isOpen')).toBe(false); expect(defaultPrevented).toBe(true); @@ -295,42 +295,43 @@ describe('Autocomplete kewDown->Enter event handlers', () => { describe('Autocomplete kewDown->Tab event handlers', () => { it('should invoke `onSelect` with the selected menu item and close the menu', () => { - var autocompleteWrapper = mount(AutocompleteComponentJSX({})); - var autocompleteInputWrapper = autocompleteWrapper.find('input'); + const onSelect = jest.fn(); + const autocompleteWrapper = mount(AutocompleteComponentJSX({})); + const autocompleteInputWrapper = autocompleteWrapper.find('input'); - let value = 'Ar'; let defaultPrevented = false; autocompleteWrapper.setState({'isOpen': true}); autocompleteInputWrapper.simulate('focus'); - autocompleteWrapper.setProps({ value, onSelect(v) { value = v; } }); + autocompleteWrapper.setProps({ value: 'Ar', onSelect }); // simulate keyUp of last key, triggering autocomplete suggestion + selection of the suggestion in the menu autocompleteInputWrapper.simulate('keyUp', { key : 'r', keyCode: 82, which: 82 }); // Hit tab, updating state.value with the selected Autocomplete suggestion autocompleteInputWrapper.simulate('keyDown', { key : 'Tab', keyCode: 9, which: 9, preventDefault() { defaultPrevented = true; } }); - expect(value).toEqual('Arizona'); + + expect(onSelect).toBeCalledWith('Arizona', {abbr: 'AZ', name: 'Arizona'}, 'Tab'); expect(autocompleteWrapper.state('isOpen')).toBe(false); expect(defaultPrevented).toBe(false); }); it('should not do anything if selectOnTab is false', () => { - var autocompleteWrapper = mount(AutocompleteComponentJSX({ + const onSelect = jest.fn(); + const autocompleteWrapper = mount(AutocompleteComponentJSX({ selectOnTab: false, })); - var autocompleteInputWrapper = autocompleteWrapper.find('input'); + const autocompleteInputWrapper = autocompleteWrapper.find('input'); - let value = 'Ar'; autocompleteWrapper.setState({'isOpen': true}); autocompleteInputWrapper.simulate('focus'); - autocompleteWrapper.setProps({ value, onSelect(v) { value = v; } }); + autocompleteWrapper.setProps({ value: 'Ar', onSelect }); // simulate keyUp of last key, triggering autocomplete suggestion + selection of the suggestion in the menu autocompleteInputWrapper.simulate('keyUp', { key : 'r', keyCode: 82, which: 82 }); // Pressing tab should not change the state of the component autocompleteInputWrapper.simulate('keyDown', { key : 'Tab', keyCode: 9, which: 9 }); - expect(value).toEqual('Ar'); + expect(onSelect).not.toBeCalled(); expect(autocompleteWrapper.state('isOpen')).toBe(true); }); }); @@ -358,20 +359,17 @@ describe('Autocomplete click event handlers', () => { var autocompleteInputWrapper = autocompleteWrapper.find('input'); it('should update input value from selected menu item and close the menu', () => { - let value = 'Ar'; - autocompleteWrapper.setProps({ - value, - onSelect(v) { value = v; }, - }); + const onSelect = jest.fn(); + autocompleteWrapper.setProps({ value: 'Ar', onSelect}); autocompleteWrapper.setState({ isOpen: true }); - autocompleteInputWrapper.simulate('change', { target: { value } }); - + autocompleteInputWrapper.simulate('change', { target: { value: 'Ar' } }); + // simulate keyUp of last key, triggering autocomplete suggestion + selection of the suggestion in the menu - autocompleteInputWrapper.simulate('keyUp', { key : 'r', keyCode: 82, which: 82 }); + autocompleteInputWrapper.simulate('keyUp', { key : 'r', keyCode: 82, which: 82 }); // Click inside input, updating state.value with the selected Autocomplete suggestion autocompleteInputWrapper.simulate('click'); - expect(value).toEqual('Arizona'); + expect(onSelect).toBeCalledWith('Arizona', {abbr: 'AZ', name: 'Arizona'}, 'click'); expect(autocompleteWrapper.state('isOpen')).toBe(false); });