diff --git a/lib/Autocomplete.js b/lib/Autocomplete.js index df15edd4..82427e7f 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,49 @@ let Autocomplete = React.createClass({ highlightedIndex: null, isOpen: false }) + }, + + Tab (event) { + if (this.props.selectOnTab) { + this.handleKeyboardSelection(event); + } + } + }, + + handleKeyboardSelection (event) { + var key = event.key; + 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 (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, key) + }) } }, @@ -262,7 +279,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 +390,3 @@ let Autocomplete = React.createClass({ }) module.exports = Autocomplete - diff --git a/lib/__tests__/Autocomplete-test.js b/lib/__tests__/Autocomplete-test.js index ac67f2c8..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); @@ -293,6 +293,49 @@ 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', () => { + const onSelect = jest.fn(); + const autocompleteWrapper = mount(AutocompleteComponentJSX({})); + const autocompleteInputWrapper = autocompleteWrapper.find('input'); + + let defaultPrevented = false; + autocompleteWrapper.setState({'isOpen': true}); + autocompleteInputWrapper.simulate('focus'); + 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(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', () => { + const onSelect = jest.fn(); + const autocompleteWrapper = mount(AutocompleteComponentJSX({ + selectOnTab: false, + })); + const autocompleteInputWrapper = autocompleteWrapper.find('input'); + + autocompleteWrapper.setState({'isOpen': true}); + autocompleteInputWrapper.simulate('focus'); + 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(onSelect).not.toBeCalled(); + expect(autocompleteWrapper.state('isOpen')).toBe(true); + }); +}); + describe('Autocomplete kewDown->Escape event handlers', () => { var autocompleteWrapper = mount(AutocompleteComponentJSX({})); @@ -316,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); });