-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
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
[SelectField] [DropDownMenu] Support multi select #6165
Changes from all commits
91e080f
265dc8c
7c0082d
715a557
b055791
725c4dc
2a181b3
e18f403
af05629
ada7f96
57e574b
5df3184
564125f
697deb4
b738e6e
f780480
aa9dbdd
a3092fa
21e8514
985c477
ba66177
f024b15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import React, {Component} from 'react'; | ||
import SelectField from 'material-ui/SelectField'; | ||
import MenuItem from 'material-ui/MenuItem'; | ||
|
||
const names = [ | ||
'Oliver Hansen', | ||
'Van Henry', | ||
'April Tucker', | ||
'Ralph Hubbard', | ||
'Omar Alexander', | ||
'Carlos Abbott', | ||
'Miriam Wagner', | ||
'Bradley Wilkerson', | ||
'Virginia Andrews', | ||
'Kelly Snyder', | ||
]; | ||
|
||
/** | ||
* `SelectField` can handle multiple selections. It is enabled with the `multiple` property. | ||
*/ | ||
export default class SelectFieldExampleMultiSelect extends Component { | ||
state = { | ||
values: [], | ||
}; | ||
|
||
handleChange = (event, index, values) => this.setState({values}); | ||
|
||
menuItems(values) { | ||
return names.map((name) => ( | ||
<MenuItem | ||
key={name} | ||
insetChildren={true} | ||
checked={values && values.includes(name)} | ||
value={name} | ||
primaryText={name} | ||
/> | ||
)); | ||
} | ||
|
||
render() { | ||
const {values} = this.state; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some would consider destructuration overkill here, I'm fine either way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK |
||
return ( | ||
<SelectField | ||
multiple={true} | ||
hintText="Select a name" | ||
value={values} | ||
onChange={this.handleChange} | ||
> | ||
{this.menuItems(values)} | ||
</SelectField> | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import React, {Component} from 'react'; | ||
import SelectField from 'material-ui/SelectField'; | ||
import MenuItem from 'material-ui/MenuItem'; | ||
|
||
const persons = [ | ||
{value: 0, name: 'Oliver Hansen'}, | ||
{value: 1, name: 'Van Henry'}, | ||
{value: 2, name: 'April Tucker'}, | ||
{value: 3, name: 'Ralph Hubbard'}, | ||
{value: 4, name: 'Omar Alexander'}, | ||
{value: 5, name: 'Carlos Abbott'}, | ||
{value: 6, name: 'Miriam Wagner'}, | ||
{value: 7, name: 'Bradley Wilkerson'}, | ||
{value: 8, name: 'Virginia Andrews'}, | ||
{value: 9, name: 'Kelly Snyder'}, | ||
]; | ||
|
||
/** | ||
* The rendering of selected items can be customized by providing a `selectionRenderer`. | ||
*/ | ||
export default class SelectFieldExampleSelectionRenderer extends Component { | ||
state = { | ||
values: [], | ||
}; | ||
|
||
handleChange = (event, index, values) => this.setState({values}); | ||
|
||
selectionRenderer = (values) => { | ||
switch (values.length) { | ||
case 0: | ||
return ''; | ||
case 1: | ||
return persons[values[0]].name; | ||
default: | ||
return `${values.length} names selected`; | ||
} | ||
} | ||
|
||
menuItems(persons) { | ||
return persons.map((person) => ( | ||
<MenuItem | ||
key={person.value} | ||
insetChildren={true} | ||
checked={this.state.values.includes(person.value)} | ||
value={person.value} | ||
primaryText={person.name} | ||
/> | ||
)); | ||
} | ||
|
||
render() { | ||
return ( | ||
<SelectField | ||
multiple={true} | ||
hintText="Select a name" | ||
value={this.state.values} | ||
onChange={this.handleChange} | ||
selectionRenderer={this.selectionRenderer} | ||
> | ||
{this.menuItems(persons)} | ||
</SelectField> | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -138,12 +138,20 @@ class DropDownMenu extends Component { | |
* Overrides the styles of `Menu` when the `DropDownMenu` is displayed. | ||
*/ | ||
menuStyle: PropTypes.object, | ||
/** | ||
* If true, `value` must be an array and the menu will support | ||
* multiple selections. | ||
*/ | ||
multiple: PropTypes.bool, | ||
/** | ||
* Callback function fired when a menu item is clicked, other than the one currently selected. | ||
* | ||
* @param {object} event TouchTap event targeting the menu item that was clicked. | ||
* @param {number} key The index of the clicked menu item in the `children` collection. | ||
* @param {any} payload The `value` prop of the clicked menu item. | ||
* @param {any} value If `multiple` is true, the menu's `value` | ||
* array with either the menu item's `value` added (if | ||
* it wasn't already selected) or omitted (if it was already selected). | ||
* Otherwise, the `value` of the menu item. | ||
*/ | ||
onChange: PropTypes.func, | ||
/** | ||
|
@@ -158,6 +166,15 @@ class DropDownMenu extends Component { | |
* Override the inline-styles of selected menu items. | ||
*/ | ||
selectedMenuItemStyle: PropTypes.object, | ||
/** | ||
* Callback function fired when a menu item is clicked, other than the one currently selected. | ||
* | ||
* @param {any} value If `multiple` is true, the menu's `value` | ||
* array with either the menu item's `value` added (if | ||
* it wasn't already selected) or omitted (if it was already selected). | ||
* Otherwise, the `value` of the menu item. | ||
*/ | ||
selectionRenderer: PropTypes.func, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine with that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree, that needs to be tested. |
||
/** | ||
* Override the inline-styles of the root element. | ||
*/ | ||
|
@@ -167,7 +184,9 @@ class DropDownMenu extends Component { | |
*/ | ||
underlineStyle: PropTypes.object, | ||
/** | ||
* The value that is currently selected. | ||
* If `multiple` is true, an array of the `value`s of the selected | ||
* menu items. Otherwise, the `value` of the selected menu item. | ||
* If provided, the menu will be a controlled component. | ||
*/ | ||
value: PropTypes.any, | ||
}; | ||
|
@@ -179,6 +198,7 @@ class DropDownMenu extends Component { | |
iconButton: <DropDownArrow />, | ||
openImmediately: false, | ||
maxHeight: 500, | ||
multiple: false, | ||
}; | ||
|
||
static contextTypes = { | ||
|
@@ -273,16 +293,28 @@ class DropDownMenu extends Component { | |
}; | ||
|
||
handleItemTouchTap = (event, child, index) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't realized that the With that in mind, why is I feel like we have quite some logic duplication. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes the idea is to expose Menu's existing multiple functionality. Menu has two callbacks onChange and onItemTouchTap. In the current master branch Menu's onItemTouchTab is propagated up through DropDownMenu's and SelectField's onChange callbacks. So to avoid breaking existing clients I thought that was best left unchanged. Menu's onChange is already using the semantics that if multiple is true then onChange is called with arrays, so I did the same for SelectField and DropDownMenu. The logic duplication is mostly in Menu. If breaking changes could be made then I think Menu's onItemTouchTab should be removed in favor of an updated onChange with a richer set of parameters. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree but I would definitely avoid introducing breaking changes on the |
||
event.persist(); | ||
this.setState({ | ||
open: false, | ||
}, () => { | ||
if (this.props.onChange) { | ||
this.props.onChange(event, index, child.props.value); | ||
if (this.props.multiple) { | ||
if (!this.state.open) { | ||
this.setState({open: true}); | ||
} | ||
} else { | ||
event.persist(); | ||
this.setState({ | ||
open: false, | ||
}, () => { | ||
if (this.props.onChange) { | ||
this.props.onChange(event, index, child.props.value); | ||
} | ||
|
||
this.close(Events.isKeyboard(event)); | ||
}); | ||
this.close(Events.isKeyboard(event)); | ||
}); | ||
} | ||
}; | ||
|
||
handleChange = (event, value) => { | ||
if (this.props.multiple && this.props.onChange) { | ||
this.props.onChange(event, undefined, value); | ||
} | ||
}; | ||
|
||
close = (isKeyboard) => { | ||
|
@@ -307,6 +339,7 @@ class DropDownMenu extends Component { | |
animated, | ||
animation, | ||
autoWidth, | ||
multiple, | ||
children, | ||
className, | ||
disabled, | ||
|
@@ -315,6 +348,7 @@ class DropDownMenu extends Component { | |
listStyle, | ||
maxHeight, | ||
menuStyle: menuStyleProp, | ||
selectionRenderer, | ||
onClose, // eslint-disable-line no-unused-vars | ||
openImmediately, // eslint-disable-line no-unused-vars | ||
menuItemStyle, | ||
|
@@ -334,12 +368,36 @@ class DropDownMenu extends Component { | |
const styles = getStyles(this.props, this.context); | ||
|
||
let displayValue = ''; | ||
React.Children.forEach(children, (child) => { | ||
if (child && value === child.props.value) { | ||
// This will need to be improved (in case primaryText is a node) | ||
displayValue = child.props.label || child.props.primaryText; | ||
if (!multiple) { | ||
React.Children.forEach(children, (child) => { | ||
if (child && value === child.props.value) { | ||
if (selectionRenderer) { | ||
displayValue = selectionRenderer(value); | ||
} else { | ||
// This will need to be improved (in case primaryText is a node) | ||
displayValue = child.props.label || child.props.primaryText; | ||
} | ||
} | ||
}); | ||
} else { | ||
const values = []; | ||
React.Children.forEach(children, (child) => { | ||
if (child && value && value.includes(child.props.value)) { | ||
if (selectionRenderer) { | ||
values.push(child.props.value); | ||
} else { | ||
values.push(child.props.label || child.props.primaryText); | ||
} | ||
} | ||
}); | ||
|
||
displayValue = []; | ||
if (selectionRenderer) { | ||
displayValue = selectionRenderer(values); | ||
} else { | ||
displayValue = values.join(', '); | ||
} | ||
}); | ||
} | ||
|
||
let menuStyle; | ||
if (anchorEl && !autoWidth) { | ||
|
@@ -385,13 +443,15 @@ class DropDownMenu extends Component { | |
onRequestClose={this.handleRequestCloseMenu} | ||
> | ||
<Menu | ||
multiple={multiple} | ||
maxHeight={maxHeight} | ||
desktop={true} | ||
value={value} | ||
onEscKeyDown={this.handleEscKeyDownMenu} | ||
style={menuStyle} | ||
listStyle={listStyle} | ||
onItemTouchTap={this.handleItemTouchTap} | ||
onChange={this.handleChange} | ||
menuItemStyle={menuItemStyle} | ||
selectedMenuItemStyle={selectedMenuItemStyle} | ||
> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could avoid that function indirection, but I'm fine either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. I'll add it inline.