Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion examples/src/components/GithubUsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
26 changes: 22 additions & 4 deletions src/Async.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = {};
Expand All @@ -43,6 +46,7 @@ const defaultProps = {
loadingPlaceholder: 'Loading...',
options: [],
searchPromptText: 'Type to search',
clearOptionsOnSelection: true,
};

export default class Async extends Component {
Expand Down Expand Up @@ -191,18 +195,32 @@ 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);
}
};

return children({
...this.props,
...props,
isLoading,
onInputChange: this._onInputChange
onInputChange: this._onInputChange,
});
}
}
Expand Down
64 changes: 62 additions & 2 deletions test/Async-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}))
};
}
Expand Down Expand Up @@ -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(() => {
Expand Down