diff --git a/README.md b/README.md index 4ab43b05ae..b841b6ded4 100644 --- a/README.md +++ b/README.md @@ -357,6 +357,7 @@ function onInputKeyDown(event) { | className | string | undefined | className for the outer element | | clearable | bool | true | should it be possible to reset value | | clearAllText | string | 'Clear all' | title for the "clear" control when `multi` is true | +| clearOptionsOnSelection | bool | true | clears options after selecting an option for Async component when `multi` is true | | clearRenderer | func | undefined | Renders a custom clear to be shown in the right-hand side of the select when clearable true: `clearRenderer()` | | clearValueText | string | 'Clear value' | title for the "clear" control | | resetValue | any | null | value to use when you clear the control | diff --git a/examples/src/components/GithubUsers.js b/examples/src/components/GithubUsers.js index 05939d7174..d06ab935d4 100644 --- a/examples/src/components/GithubUsers.js +++ b/examples/src/components/GithubUsers.js @@ -22,7 +22,7 @@ const GithubUsers = React.createClass({ switchToMulti () { this.setState({ multi: true, - value: [this.state.value], + value: this.state.value ? [this.state.value] : null }); }, switchToSingle () { diff --git a/src/Async.js b/src/Async.js index 23d8e9c61a..5c2956e2c4 100644 --- a/src/Async.js +++ b/src/Async.js @@ -14,6 +14,7 @@ const propTypes = { ]), loadOptions: React.PropTypes.func.isRequired, // callback to load options asynchronously; (inputValue: string, callback: Function): ?Promise multi: React.PropTypes.bool, // multi-value input + clearOptionsOnSelection: React.PropTypes.bool, // clears options after selecting an option when `multi` is true; defaults to true options: PropTypes.array.isRequired, // array of options placeholder: React.PropTypes.oneOfType([ // field placeholder, displayed when there's no value (shared with Select) React.PropTypes.string, @@ -30,6 +31,8 @@ const propTypes = { ]), onInputChange: React.PropTypes.func, // optional for keeping track of what is being typed value: React.PropTypes.any, // initial field value + onBlur: React.PropTypes.func, // onBlur handler: function (event) {} + onBlurResetsInput: React.PropTypes.bool, // whether input is cleared on blur }; const defaultCache = {}; @@ -43,6 +46,7 @@ const defaultProps = { loadingPlaceholder: 'Loading...', options: [], searchPromptText: 'Type to search', + clearOptionsOnSelection: true, }; export default class Async extends Component { @@ -191,10 +195,24 @@ export default class Async extends Component { options: (isLoading && loadingPlaceholder) ? [] : options, ref: (ref) => (this.select = ref), onChange: (newValues) => { - if (this.props.multi && this.props.value && (newValues.length > this.props.value.length)) { - this.clearOptions(); + if (this.props.multi && this.props.clearOptionsOnSelection) { + // this.props.value may be null or undefined, so we have to confirm it has length prop + const prevValueLength = (this.props.value && this.props.value.length) ? this.props.value.length : 0; + if (newValues.length > prevValueLength) { + this.clearOptions(); + } + } + if (this.props.onChange) { + this.props.onChange(newValues); + } + }, + onBlur: (...args) => { + if (this.props.onBlurResetsInput !== false) { + this._onInputChange(''); + } + if (this.props.onBlur) { + this.props.onBlur(...args); } - this.props.onChange(newValues); } }; @@ -202,7 +220,7 @@ export default class Async extends Component { ...this.props, ...props, isLoading, - onInputChange: this._onInputChange + onInputChange: this._onInputChange, }); } } diff --git a/test/Async-test.js b/test/Async-test.js index 8742e315ff..a797f19f6a 100644 --- a/test/Async-test.js +++ b/test/Async-test.js @@ -41,9 +41,9 @@ describe('Async', () => { function createOptionsResponse (options) { return { - options: options.map((option) => ({ + options: options.map((option, idx) => ({ label: option, - value: option + value: idx })) }; } @@ -341,6 +341,66 @@ describe('Async', () => { }); }); + describe('multi', () => { + describe('options', () => { + function loadOptions (input, resolve) { + resolve(null, createOptionsResponse(['foo', 'bar', 'fog'])); + } + + it('should be cleared on selection by default', () => { + createControl({ + multi: true, + loadOptions + }); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 0); // autoLoad is false, no options + typeSearchText('fo'); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 2); // input changes to 'fo', filter from ['foo', 'bar', 'fog'] by input + TestUtils.Simulate.keyDown(filterInputNode, { keyCode: 13, key: 'Enter' }); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 0); // on selection, input reset to ''(hard coded behavior), force clearing options + }); + + it('should not be cleared on selection when clearOptionsOnSelection is false', () => { + createControl({ + multi: true, + loadOptions, + clearOptionsOnSelection: false + }); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 0); // autoLoad is false, no options + typeSearchText('fo'); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 2); // input changes to 'fo', filter from ['foo', 'bar', 'fog'] by input + TestUtils.Simulate.keyDown(filterInputNode, { keyCode: 13, key: 'Enter' }); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 3); // on selection, input reset to ''(hard coded behavior), filter from ['foo', 'bar', 'fog'] by input + }); + + it('should be reset on blur by default', () => { + createControl({ + multi: true, + loadOptions + }); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 0); // autoLoad is false, no options + typeSearchText('bar'); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 1); // input changes to 'bar', filter from ['foo', 'bar', 'fog'] by input + TestUtils.Simulate.blur(filterInputNode); + findAndFocusInputControl(); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 3); // input resets to '', filter from ['foo', 'bar', 'fog'] by input + }); + + it('should not be reset on blur when onBlurResetsInput is false', () => { + createControl({ + multi: true, + loadOptions, + onBlurResetsInput: false + }); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 0); // autoLoad is false, no options + typeSearchText('bar'); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 1); // input changes to 'bar', filter from ['foo', 'bar', 'fog'] by input + TestUtils.Simulate.blur(filterInputNode); + findAndFocusInputControl(); + expect(asyncNode.querySelectorAll('[role=option]').length, 'to equal', 1); // input remains 'bar', filter from ['foo', 'bar', 'fog'] by input + }); + }); + }); + describe('noResultsText', () => { beforeEach(() => {