diff --git a/README.md b/README.md index 236e89a495..a022326a9a 100644 --- a/README.md +++ b/README.md @@ -381,7 +381,7 @@ function onInputKeyDown(event) { | `onInputKeyDown` | function | undefined | input keyDown handler; call `event.preventDefault()` to override default `Select` behaviour: `function(event) {}` | | `onMenuScrollToBottom` | function | undefined | called when the menu is scrolled to the bottom | | `onOpen` | function | undefined | handler for when the menu opens: `function () {}` | -| `onSelectResetsInput` | boolean | true | whether the input value should be reset when options are selected, for `multi` +| `onSelectResetsInput` | boolean | true | whether the input value should be reset when options are selected. Also input value will be set to empty if 'onSelectResetsInput=true' and Select will get new value that not equal previous value. | | `onValueClick` | function | undefined | onClick handler for value labels: `function (value, event) {}` | | `openOnClick` | boolean | true | open the options menu when the control is clicked (requires searchable = true) | | `openOnFocus` | boolean | false | open the options menu when the control gets focus | diff --git a/src/Select.js b/src/Select.js index 022004763f..a7222eea5c 100644 --- a/src/Select.js +++ b/src/Select.js @@ -33,6 +33,26 @@ const stringOrNumber = PropTypes.oneOfType([ let instanceId = 1; +const shouldShowValue = (state, props) => { + const { inputValue, isPseudoFocused, isFocused } = state; + const { onSelectResetsInput } = props; + + if (!inputValue) return true; + + if (!onSelectResetsInput){ + return !(!isFocused && isPseudoFocused || isFocused && !isPseudoFocused); + } + + return false; +}; + +const shouldShowPlaceholder = (state, props, isOpen) => { + const { inputValue, isPseudoFocused, isFocused } = state; + const { onSelectResetsInput } = props; + + return !inputValue || !onSelectResetsInput && !isOpen && !isPseudoFocused && !isFocused; +}; + class Select extends React.Component { constructor (props) { super(props); @@ -102,7 +122,7 @@ class Select extends React.Component { this.setState({ required: false }); } - if (this.state.inputValue && this.props.value !== nextProps.value && this.props.onSelectResetsInput) { + if (this.state.inputValue && this.props.value !== nextProps.value && nextProps.onSelectResetsInput) { this.setState({ inputValue: this.handleInputValueChange('') }); } } @@ -347,7 +367,7 @@ class Select extends React.Component { let toOpen = this.state.isOpen || this._openAfterFocus || this.props.openOnFocus; toOpen = this._focusAfterClear ? false : toOpen; //if focus happens after clear values, don't open dropdown yet. - + if (this.props.onFocus) { this.props.onFocus(event); } @@ -771,7 +791,8 @@ class Select extends React.Component { let renderLabel = this.props.valueRenderer || this.getOptionLabel; let ValueComponent = this.props.valueComponent; if (!valueArray.length) { - return !this.state.inputValue ?
{this.props.placeholder}
: null; + const showPlaceholder = shouldShowPlaceholder(this.state, this.props, isOpen); + return showPlaceholder ?
{this.props.placeholder}
: null; } let onClick = this.props.onValueClick ? this.handleValueClick : null; if (this.props.multi) { @@ -791,7 +812,7 @@ class Select extends React.Component { ); }); - } else if (!this.state.inputValue) { + } else if (shouldShowValue(this.state, this.props)) { if (isOpen) onClick = null; return ( this.input = ref, required: this.state.required, - value: this.state.inputValue, + value, }; if (this.props.inputRenderer) { diff --git a/test/Select-test.js b/test/Select-test.js index 874d1b1f5a..3b00677e4d 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -40,6 +40,7 @@ class PropsWrapper extends React.Component { super(props); this.state = props || {}; } + setPropsForChild(props) { this.setState(props); } @@ -148,7 +149,7 @@ describe('Select', () => { onChange={onChange} onInputChange={onInputChange} {...props} - /> + /> ); if (options.initialFocus !== false) { findAndFocusInputControl(); @@ -177,7 +178,7 @@ describe('Select', () => { onChange={onChange} onInputChange={onInputChange} {...props} - /> + /> ); instance = wrapper.getChild(); @@ -275,7 +276,7 @@ describe('Select', () => { expect(ReactDOM.findDOMNode(selectInputElement).name, 'to equal', 'form-field-name'); }); - it('should show the options on mouse click', function () { + it('should show the options on mouse click', () => { TestUtils.Simulate.mouseDown(ReactDOM.findDOMNode(instance).querySelector('.Select-control'), { button: 0 }); var node = ReactDOM.findDOMNode(instance); expect(node, 'queried for', '.Select-option', 'to have length', 3); @@ -348,14 +349,14 @@ describe('Select', () => { }); - it('should display the options menu when tapped', function() { + it('should display the options menu when tapped', () => { TestUtils.Simulate.touchStart(getSelectControl(instance)); TestUtils.Simulate.touchEnd(getSelectControl(instance)); var node = ReactDOM.findDOMNode(instance); expect(node, 'queried for', '.Select-option', 'to have length', 3); }); - it('should not display the options menu when touched and dragged', function() { + it('should not display the options menu when touched and dragged', () => { TestUtils.Simulate.touchStart(getSelectControl(instance)); TestUtils.Simulate.touchMove(getSelectControl(instance)); TestUtils.Simulate.touchEnd(getSelectControl(instance)); @@ -721,7 +722,6 @@ describe('Select', () => { }); it('selects the initial value', () => { - expect(instance, 'to contain',
Two
@@ -868,7 +868,7 @@ describe('Select', () => { typeSearchText('1'); expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', - 'to satisfy', [ + 'to satisfy', [ expect.it('to have text', 'One'), expect.it('to have text', 'Ten'), expect.it('to have text', 'Twenty-one') @@ -1016,18 +1016,18 @@ describe('Select', () => { }); it('set the initial value of the hidden input control', () => { - expect(ReactDOM.findDOMNode(wrapper).querySelector(FORM_VALUE_SELECTOR).value, 'to equal', 'true' ); + expect(ReactDOM.findDOMNode(wrapper).querySelector(FORM_VALUE_SELECTOR).value, 'to equal', 'true'); }); it('updates the value when the value prop is set', () => { wrapper.setPropsForChild({ value: false }); expect(ReactDOM.findDOMNode(instance), 'queried for first', DISPLAYED_SELECTION_SELECTOR, - 'to have text', 'No'); + 'to have text', 'No'); }); it('updates the value of the hidden input control after new value prop', () => { wrapper.setPropsForChild({ value: false }); - expect(ReactDOM.findDOMNode(wrapper).querySelector(FORM_VALUE_SELECTOR).value, 'to equal', 'false' ); + expect(ReactDOM.findDOMNode(wrapper).querySelector(FORM_VALUE_SELECTOR).value, 'to equal', 'false'); }); it('calls onChange with the new value as a boolean', () => { @@ -1040,7 +1040,7 @@ describe('Select', () => { it('supports setting the value via prop', () => { wrapper.setPropsForChild({ value: false }); expect(ReactDOM.findDOMNode(instance), 'queried for first', DISPLAYED_SELECTION_SELECTOR, - 'to have text', 'No'); + 'to have text', 'No'); }); it('displays the X button for false value', () => { @@ -1068,7 +1068,7 @@ describe('Select', () => { it('selects the initial value', () => { expect(instance, 'to contain', - +
Yes
No
); @@ -1088,7 +1088,7 @@ describe('Select', () => { }); expect(instance, 'to contain', - +
No
); }); @@ -1100,7 +1100,7 @@ describe('Select', () => { }); expect(instance, 'to contain', - +
Yes
); }); @@ -1113,7 +1113,7 @@ describe('Select', () => { }); expect(instance, 'to contain', - +
No
); }); @@ -1153,18 +1153,18 @@ describe('Select', () => { typeSearchText('fal'); expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', - 'to satisfy', [ - expect.it('to have text', 'No'), - ]); + 'to satisfy', [ + expect.it('to have text', 'No'), + ]); }); it('finds text at end', () => { typeSearchText('se'); expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', - 'to satisfy', [ - expect.it('to have text', 'No'), - ]); + 'to satisfy', [ + expect.it('to have text', 'No'), + ]); }); }); @@ -1182,18 +1182,18 @@ describe('Select', () => { typeSearchText('fa'); expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', - 'to satisfy', [ - expect.it('to have text', 'No') - ]); + 'to satisfy', [ + expect.it('to have text', 'No') + ]); }); it('does not match text at end', () => { typeSearchText('se'); expect(ReactDOM.findDOMNode(instance), 'to contain elements matching', - '.Select-noresults'); + '.Select-noresults'); expect(ReactDOM.findDOMNode(instance), 'to contain no elements matching', - '.Select-option'); + '.Select-option'); }); }); @@ -1210,19 +1210,19 @@ describe('Select', () => { typeSearchText('al'); expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', - 'to satisfy', [ - expect.it('to have text', 'No'), - ]); + 'to satisfy', [ + expect.it('to have text', 'No'), + ]); }); it('finds text at end', () => { typeSearchText('e'); expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', - 'to satisfy', [ - expect.it('to have text', 'Yes'), - expect.it('to have text', 'No') - ]); + 'to satisfy', [ + expect.it('to have text', 'Yes'), + expect.it('to have text', 'No') + ]); }); }); @@ -1240,18 +1240,18 @@ describe('Select', () => { typeSearchText('tr'); expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', - 'to satisfy', [ - expect.it('to have text', 'Yes') - ]); + 'to satisfy', [ + expect.it('to have text', 'Yes') + ]); }); it('does not match text at end', () => { typeSearchText('e'); expect(ReactDOM.findDOMNode(instance), 'to contain elements matching', - '.Select-noresults'); + '.Select-noresults'); expect(ReactDOM.findDOMNode(instance), 'to contain no elements matching', - '.Select-option'); + '.Select-option'); }); }); }); @@ -1298,7 +1298,7 @@ describe('Select', () => { value: 'three' }); - expect(ReactDOM.findDOMNode(wrapper).querySelector(FORM_VALUE_SELECTOR).value, 'to equal', 'three' ); + expect(ReactDOM.findDOMNode(wrapper).querySelector(FORM_VALUE_SELECTOR).value, 'to equal', 'three'); }); it('display the raw value if the option is not available', () => { @@ -1361,7 +1361,7 @@ describe('Select', () => { expect(ReactDOM.findDOMNode(instance).querySelectorAll('.Select-option')[1], 'to have attributes', { class: 'is-disabled' - }); + }); }); it('is not selectable by clicking', () => { @@ -1488,7 +1488,7 @@ describe('Select', () => { expect(onChange, 'was not called'); // And the menu is still open expect(ReactDOM.findDOMNode(instance), 'to contain no elements matching', DISPLAYED_SELECTION_SELECTOR); - expect(ReactDOM.findDOMNode(instance), 'queried for' , '.Select-option', + expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', 'to satisfy', [ expect.it('to have text', 'Two') ]); @@ -1501,7 +1501,7 @@ describe('Select', () => { expect(onChange, 'was not called'); // And the menu is still open expect(ReactDOM.findDOMNode(instance), 'to contain no elements matching', DISPLAYED_SELECTION_SELECTOR); - expect(ReactDOM.findDOMNode(instance), 'queried for' , '.Select-option', + expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', 'to satisfy', [ expect.it('to have text', 'Two') ]); @@ -1514,7 +1514,7 @@ describe('Select', () => { expect(onChange, 'was not called'); // And the menu is still open expect(ReactDOM.findDOMNode(instance), 'to contain no elements matching', DISPLAYED_SELECTION_SELECTOR); - expect(ReactDOM.findDOMNode(instance), 'queried for' , '.Select-option', + expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', 'to satisfy', [ expect.it('to have text', 'Two') ]); @@ -1848,7 +1848,7 @@ describe('Select', () => { it('filters the existing selections from the options', () => { - setValueProp(['four','three']); + setValueProp(['four', 'three']); typeSearchText('o'); @@ -1861,7 +1861,7 @@ describe('Select', () => { it('removes the last selected option with backspace', () => { - setValueProp(['four','three']); + setValueProp(['four', 'three']); onChange.reset(); // Ignore previous onChange calls pressBackspace(); expect(onChange, 'was called with', [{ label: 'Four', value: 'four' }]); @@ -1887,7 +1887,7 @@ describe('Select', () => { it('removes the last selected option with delete', () => { - setValueProp(['four','three']); + setValueProp(['four', 'three']); onChange.reset(); // Ignore previous onChange calls pressDelete(); expect(onChange, 'was called with', [{ label: 'Four', value: 'four' }]); @@ -2129,63 +2129,103 @@ describe('Select', () => { }); - describe('with multi=true different onSelectResetsInput', () => { - it('should have retained inputValue after accepting selection with onSelectResetsInput=false', () => { - options = [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - { value: 'three', label: 'Three' }, - { value: 'four', label: 'Four' } - ]; + describe('onSelectResetsInput', () => { + options = [ + { value: 'one', label: 'One' }, + { value: 'two', label: 'Two' }, + { value: 'three', label: 'Three' }, + { value: 'four', label: 'Four' } + ]; + + describe('with single select', () => { + it('should have retained inputValue after accepting selection with onSelectResetsInput=false', () => { + // Render an instance of the component + wrapper = createControlWithWrapper({ + value: '', + options: options, + onSelectResetsInput: false, + onCloseResetsInput: false, + onBlurResetsInput: false, + }); - // Render an instance of the component - wrapper = createControlWithWrapper({ - value: '', - options: options, - multi: true, - closeOnSelect: false, - removeSelected: false, - onSelectResetsInput: false - }); + clickArrowToOpen(); + typeSearchText('tw'); + pressEnterToAccept(); + setValueProp('two'); // trigger componentWillReceiveProps - instance.setState({ - isFocused: true + expect(instance.state.inputValue, 'to equal', 'tw'); + expect(instance, 'to contain',
Two
); + expect(instance, 'to contain', ); + + instance.setState({ + isFocused: false, + isPseudoFocused: false, + }); + + expect(instance, 'to contain', ); + expect(instance, 'to contain',
Two
); + + instance.setState({ + isFocused: true, + }); + + expect(instance, 'to contain', ); + expect(instance, 'not to contain',
Two
); }); - clickArrowToOpen(); - typeSearchText('two'); - pressEnterToAccept(); - setValueProp('two'); // trigger componentWillReceiveProps + it('should have reset the inputValue after accepting selection when onSelectResetsInput= true or not set', () => { + // Render an instance of the component + wrapper = createControlWithWrapper({ + value: '', + options: options, + }); + + clickArrowToOpen(); + typeSearchText('tw'); + expect(instance.state.inputValue, 'to equal', 'tw'); + pressEnterToAccept(); + setValueProp('two'); // trigger componentWillReceiveProps - expect(instance.state.inputValue, 'to equal', 'two'); + expect(instance.state.inputValue, 'to equal', ''); + expect(instance, 'to contain',
Two
); + expect(instance, 'to contain', ); + }); }); - it('should have reset the inputValue after accepting selection when onSelectResetsInput= true or not set', () => { - options = [ - { value: 'one', label: 'One' }, - { value: 'two', label: 'Two' }, - { value: 'three', label: 'Three' }, - { value: 'four', label: 'Four' } - ]; + describe('with multi select', () => { + it('should have retained inputValue after accepting selection with onSelectResetsInput=false', () => { + // Render an instance of the component + wrapper = createControlWithWrapper({ + value: '', + options: options, + multi: true, + onSelectResetsInput: false + }); - // Render an instance of the component - wrapper = createControlWithWrapper({ - value: '', - options: options, - multi: true, - closeOnSelect: false, - removeSelected: false - }); + clickArrowToOpen(); + typeSearchText('two'); + pressEnterToAccept(); + setValueProp('two'); // trigger componentWillReceiveProps - instance.setState({ - isFocused: true + expect(instance.state.inputValue, 'to equal', 'two'); }); - clickArrowToOpen(); - typeSearchText('two'); - pressEnterToAccept(); + it('should have reset the inputValue after accepting selection when onSelectResetsInput= true or not set', () => { + // Render an instance of the component + wrapper = createControlWithWrapper({ + value: '', + options: options, + multi: true, + }); + + clickArrowToOpen(); + typeSearchText('two'); + expect(instance.state.inputValue, 'to equal', 'two'); + pressEnterToAccept(); + setValueProp('two'); // trigger componentWillReceiveProps - expect(instance.state.inputValue, 'to equal', ''); + expect(instance.state.inputValue, 'to equal', ''); + }); }); }); @@ -2717,16 +2757,16 @@ describe('Select', () => { instance = ReactDOM.render(, node); - expect(instance.state.isFocused, 'to equal', false); + expect(instance.state.isFocused, 'to equal', false); + findAndFocusInputControl(); + expect(instance.state.isFocused, 'to equal', true); + ReactDOM.render(