Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

removeSelected prop (round 2), for optionally keeping selected values in dropdown #1891

Merged
merged 17 commits into from
Oct 25, 2017
Merged
Show file tree
Hide file tree
Changes from 11 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ The built-in Options renderer also support custom classNames, just add a `classN

You can enable multi-value selection by setting `multi={true}`. In this mode:

* Selected options will be removed from the dropdown menu
* Selected options will be removed from the dropdown menu by default. If you want them to remain in the list, set `removeSelected={true}`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@banderson just a minor nit: should this be "If you want them to remain in the list, set removeSelected={false}"?

* The selected values are submitted in multiple `<input type="hidden">` fields, use `joinValues` to submit joined values in a single field instead
* The values of the selected items are joined using the `delimiter` prop to create the input value when `joinValues` is true
* A simple value, if provided, will be split using the `delimiter` prop
Expand Down
10 changes: 9 additions & 1 deletion examples/src/components/Multiselect.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var MultiSelectField = createClass({
},
getInitialState () {
return {
removeSelected: true,
disabled: false,
crazy: false,
options: FLAVOURS,
Expand All @@ -33,6 +34,9 @@ var MultiSelectField = createClass({
console.log('You\'ve selected:', value);
this.setState({ value });
},
toggleRemove (e) {
this.setState({ removeSelected: e.target.checked });
},
toggleDisabled (e) {
this.setState({ disabled: e.target.checked });
},
Expand All @@ -47,9 +51,13 @@ var MultiSelectField = createClass({
return (
<div className="section">
<h3 className="section-heading">{this.props.label}</h3>
<Select multi simpleValue disabled={this.state.disabled} value={this.state.value} placeholder="Select your favourite(s)" options={this.state.options} onChange={this.handleSelectChange} />
<Select multi simpleValue removeSelected={this.state.removeSelected} disabled={this.state.disabled} value={this.state.value} placeholder="Select your favourite(s)" options={this.state.options} onChange={this.handleSelectChange} />

<div className="checkbox-list">
<label className="checkbox">
<input type="checkbox" className="checkbox-control" checked={this.state.removeSelected} onChange={this.toggleRemove} />
<span className="checkbox-label">Remove selected options</span>
</label>
<label className="checkbox">
<input type="checkbox" className="checkbox-control" checked={this.state.disabled} onChange={this.toggleDisabled} />
<span className="checkbox-label">Disable the control</span>
Expand Down
15 changes: 11 additions & 4 deletions src/Select.js
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,12 @@ class Select extends React.Component {
inputValue: this.handleInputValueChange(''),
focusedIndex: null
}, () => {
this.addValue(value);
var valueArray = this.getValueArray(this.props.value);
if (valueArray.some(i => i[this.props.valueKey] === value[this.props.valueKey])) {
this.removeValue(value);
} else {
this.addValue(value);
}
});
} else {
this.setState({
Expand Down Expand Up @@ -556,7 +561,7 @@ class Select extends React.Component {

removeValue (value) {
var valueArray = this.getValueArray(this.props.value);
this.setValue(valueArray.filter(i => i !== value));
this.setValue(valueArray.filter(i => i[this.props.valueKey] !== value[this.props.valueKey]));
this.focus();
}

Expand Down Expand Up @@ -974,7 +979,7 @@ class Select extends React.Component {

render () {
let valueArray = this.getValueArray(this.props.value);
let options = this._visibleOptions = this.filterOptions(this.props.multi ? this.getValueArray(this.props.value) : null);
let options = this._visibleOptions = this.filterOptions(this.props.multi && this.props.removeSelected ? valueArray : null);
let isOpen = this.state.isOpen;
if (this.props.multi && !options.length && valueArray.length && !this.state.inputValue) isOpen = false;
const focusedOptionIndex = this.getFocusableOptionIndex(valueArray[0]);
Expand Down Expand Up @@ -1035,7 +1040,7 @@ class Select extends React.Component {
{this.renderClear()}
{this.renderArrow()}
</div>
{isOpen ? this.renderOuter(options, !this.props.multi ? valueArray : null, focusedOption) : null}
{isOpen ? this.renderOuter(options, valueArray, focusedOption) : null}
</div>
);
}
Expand Down Expand Up @@ -1099,6 +1104,7 @@ Select.propTypes = {
options: PropTypes.array, // array of options
pageSize: PropTypes.number, // number of entries to page when using page up/down keys
placeholder: stringOrNode, // field placeholder, displayed when there's no value
removeSelected: PropTypes.bool, // whether the selected option is removed from the dropdown on multi selects
required: PropTypes.bool, // applies HTML5 required attribute when needed
resetValue: PropTypes.any, // value to use when you clear the control
scrollMenuIntoView: PropTypes.bool, // boolean to enable the viewport to shift so that the full menu fully visible when engaged
Expand Down Expand Up @@ -1146,6 +1152,7 @@ Select.defaultProps = {
optionComponent: Option,
pageSize: 5,
placeholder: 'Select...',
removeSelected: true,
required: false,
scrollMenuIntoView: true,
searchable: true,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/defaultMenuRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function menuRenderer ({
let Option = optionComponent;

return options.map((option, i) => {
let isSelected = valueArray && valueArray.indexOf(option) > -1;
let isSelected = valueArray && valueArray.some(x => x[valueKey] == option[valueKey]);
let isFocused = option === focusedOption;
let optionClass = classNames(optionClassName, {
'Select-option': true,
Expand Down
103 changes: 103 additions & 0 deletions test/Select-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2042,6 +2042,109 @@ describe('Select', () => {

});

describe('with removeSelected=false', () => {
beforeEach(() => {
options = [
{ value: 'one', label: 'One' },
{ value: 'two', label: 'Two' },
{ value: 'three', label: 'Three' },
{ value: 'four', label: 'Four' }
];

// Render an instance of the component
wrapper = createControlWithWrapper({
value: '',
options: options,
multi: true,
removeSelected: false
}, {
wireUpOnChangeToValue: true
});

// We need a hack here.
// JSDOM (at least v3.x) doesn't appear to support div's with tabindex
// This just hacks that we are focused
// This is (obviously) implementation dependent, and may need to change
instance.setState({
isFocused: true
});
});

it('does not remove the selected options from the menu', () => {

clickArrowToOpen();

var items = ReactDOM.findDOMNode(instance).querySelectorAll('.Select-option');

// Click the option "Two" to select it
expect(items[1], 'to have text', 'Two');
TestUtils.Simulate.mouseDown(items[1]);
expect(onChange, 'was called times', 1);

// Now get the list again
items = ReactDOM.findDOMNode(instance).querySelectorAll('.Select-option');
expect(items[0], 'to have text', 'One');
expect(items[1], 'to have text', 'Two');
expect(items[2], 'to have text', 'Three');
expect(items[3], 'to have text', 'Four');
expect(items, 'to have length', 4);

// Click first item, 'One'
TestUtils.Simulate.mouseDown(items[0]);
expect(onChange, 'was called times', 2);

items = ReactDOM.findDOMNode(instance).querySelectorAll('.Select-option');
expect(items[0], 'to have text', 'One');
expect(items[1], 'to have text', 'Two');
expect(items[2], 'to have text', 'Three');
expect(items[3], 'to have text', 'Four');
expect(items, 'to have length', 4);

// Click last item, 'Four'
TestUtils.Simulate.mouseDown(items[3]);
expect(onChange, 'was called times', 3);

items = ReactDOM.findDOMNode(instance).querySelectorAll('.Select-option');
expect(items[0], 'to have text', 'One');
expect(items[1], 'to have text', 'Two');
expect(items[2], 'to have text', 'Three');
expect(items[3], 'to have text', 'Four');
expect(items, 'to have length', 4);

expect(onChange.args, 'to equal', [
[[{ value: 'two', label: 'Two' }]],
[[{ value: 'two', label: 'Two' }, { value: 'one', label: 'One' }]],
[
[
{ value: 'two', label: 'Two' },
{ value: 'one', label: 'One' },
{ value: 'four', label: 'Four' },
],
],
]);
});

it('removes a selected value if chosen again', () => {

clickArrowToOpen();

var items = ReactDOM.findDOMNode(instance).querySelectorAll('.Select-option');

// Click the option "Two" to select it
TestUtils.Simulate.mouseDown(items[1]);
expect(onChange, 'was called times', 1);

// Click the option "Two" again to deselect it
TestUtils.Simulate.mouseDown(items[1]);
expect(onChange, 'was called times', 2);

expect(onChange.args, 'to equal', [
[[{ value: 'two', label: 'Two' }]],
[[]],
]);
});
});

describe('with props', () => {

describe('className', () => {
Expand Down