diff --git a/examples/src/app.js b/examples/src/app.js
index cb06945ba6..5068e08ccf 100644
--- a/examples/src/app.js
+++ b/examples/src/app.js
@@ -5,6 +5,7 @@ import Select from 'react-select';
import CustomRenderField from './components/CustomRenderField';
import MultiSelectField from './components/MultiSelectField';
+import GroupedOptionsField from './components/GroupedOptionsField';
import RemoteSelectField from './components/RemoteSelectField';
import SelectedValuesField from './components/SelectedValuesField';
import StatesField from './components/StatesField';
@@ -36,6 +37,7 @@ React.render(
+
diff --git a/examples/src/components/GroupedOptionsField.js b/examples/src/components/GroupedOptionsField.js
new file mode 100644
index 0000000000..f17fe265c1
--- /dev/null
+++ b/examples/src/components/GroupedOptionsField.js
@@ -0,0 +1,71 @@
+import React from 'react';
+import Select from 'react-select';
+
+function logChange() {
+ console.log.apply(console, [].concat(['Select value changed:'], Array.prototype.slice.apply(arguments)));
+}
+
+var ops = [{
+ label: 'Black',
+ value: 'black',
+}, {
+ label: 'Primary Colors',
+ options: [{
+ label: 'Yellow',
+ value: 'yellow'
+ }, {
+ label: 'Red',
+ value: 'red'
+ }, {
+ label: 'Blue',
+ value: 'blue'
+ }]
+}, {
+ label: 'Secondary Colors',
+ options: [{
+ label: 'Orange',
+ value: 'orange'
+ }, {
+ label: 'Purple',
+ options: [{
+ label: 'Light Purple',
+ value: 'light_purple'
+ }, {
+ label: 'Medium Purple',
+ value: 'medium_purple'
+ }, {
+ label: 'Dark Purple',
+ value: 'dark_purple'
+ }]
+ }, {
+ label: 'Green',
+ value: 'green'
+ }]
+}, {
+ label: 'White',
+ value: 'white',
+}];
+
+var GroupedOptionsField = React.createClass({
+ displayName: 'GroupedOptionsField',
+ propTypes: {
+ delimiter: React.PropTypes.string,
+ label: React.PropTypes.string,
+ multi: React.PropTypes.bool,
+ },
+
+ render () {
+ return (
+
+
{this.props.label}
+
+
+ );
+ }
+});
+
+module.exports = GroupedOptionsField;
+
diff --git a/less/menu.less b/less/menu.less
index 2906579b50..5db6fd2e2d 100644
--- a/less/menu.less
+++ b/less/menu.less
@@ -57,9 +57,20 @@
color: @select-option-disabled-color;
cursor: not-allowed;
}
+}
+.Select-optionGroup-label ~ .Select-option,
+.Select-optionGroup-label ~ .Select-optionGroup {
+ margin-left: @select-padding-horizontal;
}
+.Select-optionGroup-label {
+ box-sizing: border-box;
+ color: @select-option-color;
+ cursor: default;
+ display: block;
+ padding: @select-padding-vertical @select-padding-horizontal;
+}
// no results
diff --git a/src/OptionGroup.js b/src/OptionGroup.js
new file mode 100644
index 0000000000..3201ffd0ff
--- /dev/null
+++ b/src/OptionGroup.js
@@ -0,0 +1,45 @@
+var React = require('react');
+var classes = require('classnames');
+
+var Option = React.createClass({
+ propTypes: {
+ children: React.PropTypes.any,
+ className: React.PropTypes.string,
+ optionGroup: React.PropTypes.shape({
+ label: React.PropTypes.string.isRequired,
+ options: React.PropTypes.array.isRequired,
+ }).isRequired, // object that is base for that option
+ renderFunc: React.PropTypes.func // method passed to ReactSelect component to render label text
+ },
+
+ blockEvent: function(event) {
+ event.preventDefault();
+ if ((event.target.tagName !== 'A') || !('href' in event.target)) {
+ return;
+ }
+
+ if (event.target.target) {
+ window.open(event.target.href);
+ } else {
+ window.location.href = event.target.href;
+ }
+ },
+
+ render: function() {
+ var obj = this.props.optionGroup;
+ var renderedLabel = this.props.renderFunc(obj);
+ return (
+
+
+ {renderedLabel}
+
+ {this.props.children}
+
+ );
+ }
+});
+
+module.exports = Option;
diff --git a/src/Select.js b/src/Select.js
index e1124f7c03..d18026a5e2 100644
--- a/src/Select.js
+++ b/src/Select.js
@@ -8,9 +8,76 @@ var classes = require('classnames');
var Value = require('./Value');
var SingleValue = require('./SingleValue');
var Option = require('./Option');
+var OptionGroup = require('./OptionGroup');
var requestId = 0;
+function defaultFilterOption(option, filterValue) {
+ if (!filterValue) {
+ return true;
+ }
+ var valueTest = String(option.value), labelTest = String(option.label);
+ if (this.props.ignoreCase) {
+ valueTest = valueTest.toLowerCase();
+ labelTest = labelTest.toLowerCase();
+ filterValue = filterValue.toLowerCase();
+ }
+ return (this.props.matchPos === 'start') ? (
+ (this.props.matchProp !== 'label' && valueTest.substr(0, filterValue.length) === filterValue) ||
+ (this.props.matchProp !== 'value' && labelTest.substr(0, filterValue.length) === filterValue)
+ ) : (
+ (this.props.matchProp !== 'label' && valueTest.indexOf(filterValue) >= 0) ||
+ (this.props.matchProp !== 'value' && labelTest.indexOf(filterValue) >= 0)
+ );
+}
+
+function defaultFilterOptions(options, filterValue, exclude) {
+ if (!options) {
+ return [];
+ }
+ var matchingOptions = function(matches, option) {
+ if (isGroup(option)) {
+ var groupMatches = option.options.reduce(matchingOptions, []);
+ if (groupMatches.length > 0) {
+ matches.push(Object.assign({}, option, {
+ options: groupMatches,
+ }));
+ }
+ } else if (
+ (!this.props.multi || exclude.indexOf(option.value) === -1) &&
+ (this.props.filterOption && this.props.filterOption.call(this, option, filterValue))
+ ) {
+ matches.push(option);
+ }
+ return matches;
+ }.bind(this);
+ return options.reduce(matchingOptions, []);
+}
+
+function defaultLabelRenderer(op) {
+ return op.label;
+}
+
+function flattenOptions(options) {
+ if (!options) {
+ return options;
+ }
+ var flatten = function(flat, opt) {
+ if (Array.isArray(opt.options)) {
+ return flat.concat(
+ opt,
+ opt.options.reduce(flatten, [])
+ );
+ }
+ return flat.concat(opt);
+ };
+ return options.reduce(flatten, []);
+}
+
+function isGroup(op) {
+ return op && Array.isArray(op.options);
+}
+
var Select = React.createClass({
displayName: 'Select',
@@ -70,6 +137,8 @@ var Select = React.createClass({
clearable: true,
delimiter: ',',
disabled: false,
+ filterOption: defaultFilterOption,
+ filterOptions: defaultFilterOptions,
ignoreCase: true,
inputProps: {},
matchPos: 'any',
@@ -81,6 +150,7 @@ var Select = React.createClass({
onInputChange: undefined,
onOptionLabelClick: undefined,
optionComponent: Option,
+ optionGroupComponent: OptionGroup,
options: undefined,
placeholder: 'Select...',
searchable: true,
@@ -106,7 +176,8 @@ var Select = React.createClass({
isFocused: false,
isLoading: false,
isOpen: false,
- options: this.props.options
+ options: this.props.options,
+ flatOptions: flattenOptions(this.props.options)
};
},
@@ -163,11 +234,14 @@ var Select = React.createClass({
componentWillReceiveProps: function(newProps) {
var optionsChanged = false;
- if (JSON.stringify(newProps.options) !== JSON.stringify(this.props.options)) {
+ var options = newProps.options;
+ if (JSON.stringify(options) !== JSON.stringify(this.props.options)) {
optionsChanged = true;
+ var filteredOptions = this.filterOptions(options);
this.setState({
- options: newProps.options,
- filteredOptions: this.filterOptions(newProps.options)
+ filteredOptions: filteredOptions,
+ flatOptions: flattenOptions(filteredOptions),
+ options: options
});
}
if (newProps.value !== this.state.value || newProps.placeholder !== this.props.placeholder || optionsChanged) {
@@ -250,15 +324,15 @@ var Select = React.createClass({
values: values,
inputValue: '',
filteredOptions: filteredOptions,
+ flatOptions: flattenOptions(filteredOptions),
placeholder: !this.props.multi && values.length ? values[0].label : placeholder,
focusedOption: focusedOption
};
},
getFirstFocusableOption: function (options) {
-
for (var optionIndex = 0; optionIndex < options.length; ++optionIndex) {
- if (!options[optionIndex].disabled) {
+ if (!options[optionIndex].disabled && !isGroup(options[optionIndex])) {
return options[optionIndex];
}
}
@@ -512,6 +586,7 @@ var Select = React.createClass({
isOpen: true,
inputValue: event.target.value,
filteredOptions: filteredOptions,
+ flatOptions: flattenOptions(filteredOptions),
focusedOption: this._getNewFocusedOption(filteredOptions)
}, this._bindCloseMenuIfClickedOutside);
}
@@ -538,6 +613,7 @@ var Select = React.createClass({
var newState = {
options: options,
filteredOptions: filteredOptions,
+ flatOptions: flattenOptions(filteredOptions),
focusedOption: this._getNewFocusedOption(filteredOptions)
};
for (var key in state) {
@@ -564,6 +640,7 @@ var Select = React.createClass({
var newState = {
options: data.options,
filteredOptions: filteredOptions,
+ flatOptions: flattenOptions(filteredOptions),
focusedOption: this._getNewFocusedOption(filteredOptions)
};
for (var key in state) {
@@ -581,28 +658,8 @@ var Select = React.createClass({
var exclude = (values || this.state.values).map(function(i) {
return i.value;
});
- if (this.props.filterOptions) {
- return this.props.filterOptions.call(this, options, filterValue, exclude);
- } else {
- var filterOption = function(op) {
- if (this.props.multi && exclude.indexOf(op.value) > -1) return false;
- if (this.props.filterOption) return this.props.filterOption.call(this, op, filterValue);
- var valueTest = String(op.value), labelTest = String(op.label);
- if (this.props.ignoreCase) {
- valueTest = valueTest.toLowerCase();
- labelTest = labelTest.toLowerCase();
- filterValue = filterValue.toLowerCase();
- }
- return !filterValue || (this.props.matchPos === 'start') ? (
- (this.props.matchProp !== 'label' && valueTest.substr(0, filterValue.length) === filterValue) ||
- (this.props.matchProp !== 'value' && labelTest.substr(0, filterValue.length) === filterValue)
- ) : (
- (this.props.matchProp !== 'label' && valueTest.indexOf(filterValue) >= 0) ||
- (this.props.matchProp !== 'value' && labelTest.indexOf(filterValue) >= 0)
- );
- };
- return (options || []).filter(filterOption, this);
- }
+ var result = this.props.filterOptions.call(this, options, filterValue, exclude);
+ return result;
},
selectFocusedOption: function() {
@@ -631,8 +688,8 @@ var Select = React.createClass({
focusAdjacentOption: function(dir) {
this._focusedOptionReveal = true;
- var ops = this.state.filteredOptions.filter(function(op) {
- return !op.disabled;
+ var ops = this.state.flatOptions.filter(function(op) {
+ return !op.disabled && !isGroup(op);
});
if (!this.state.isOpen) {
this.setState({
@@ -677,11 +734,8 @@ var Select = React.createClass({
buildMenu: function() {
var focusedValue = this.state.focusedOption ? this.state.focusedOption.value : null;
- var renderLabel = this.props.optionRenderer || function(op) {
- return op.label;
- };
if (this.state.filteredOptions.length > 0) {
- focusedValue = focusedValue == null ? this.state.filteredOptions[0] : focusedValue;
+ focusedValue = focusedValue == null ? this.state.filteredOptions[0].value : focusedValue;
}
// Add the current value to the filtered options in last resort
var options = this.state.filteredOptions;
@@ -695,56 +749,80 @@ var Select = React.createClass({
};
options.unshift(newOption);
}
- var ops = Object.keys(options).map(function(key) {
- var op = options[key];
- var isSelected = this.state.value === op.value;
- var isFocused = focusedValue === op.value;
- var optionClass = classes({
- 'Select-option': true,
- 'is-selected': isSelected,
- 'is-focused': isFocused,
- 'is-disabled': op.disabled
- });
- var ref = isFocused ? 'focused' : null;
- var mouseEnter = this.focusOption.bind(this, op);
- var mouseLeave = this.unfocusOption.bind(this, op);
- var mouseDown = this.selectValue.bind(this, op);
- var optionResult = React.createElement(this.props.optionComponent, {
- key: 'option-' + op.value,
- className: optionClass,
- renderFunc: renderLabel,
- mouseEnter: mouseEnter,
- mouseLeave: mouseLeave,
- mouseDown: mouseDown,
- click: mouseDown,
- addLabelText: this.props.addLabelText,
- option: op,
- ref: ref
- });
- return optionResult;
- }, this);
- if (ops.length) {
- return ops;
+ if (!options.length) {
+ return this.renderNoResults();
+ }
+
+ return this.renderOptions(focusedValue, options);
+ },
+
+ renderNoResults() {
+ var noResultsText, promptClass;
+ if (this.state.isLoading) {
+ promptClass = 'Select-searching';
+ noResultsText = this.props.searchingText;
+ } else if (this.state.inputValue || !this.props.asyncOptions) {
+ promptClass = 'Select-noresults';
+ noResultsText = this.props.noResultsText;
} else {
- var noResultsText, promptClass;
- if (this.state.isLoading) {
- promptClass = 'Select-searching';
- noResultsText = this.props.searchingText;
- } else if (this.state.inputValue || !this.props.asyncOptions) {
- promptClass = 'Select-noresults';
- noResultsText = this.props.noResultsText;
- } else {
- promptClass = 'Select-search-prompt';
- noResultsText = this.props.searchPromptText;
+ promptClass = 'Select-search-prompt';
+ noResultsText = this.props.searchPromptText;
+ }
+
+ return (
+
+ {noResultsText}
+
+ );
+ },
+
+ renderOptions(focusedValue, options) {
+ console.log(options);
+ return options.map(function(option) {
+ if (isGroup(option)) {
+ return this.renderOptionGroup(focusedValue, option);
}
+ return this.renderOption(focusedValue, option);
+ }, this);
+ },
- return (
-
- {noResultsText}
-
- );
- }
+ renderOption(focusedValue, op) {
+ var renderLabel = this.props.optionRenderer || defaultLabelRenderer;
+ var isSelected = this.state.value === op.value;
+ var isFocused = focusedValue === op.value;
+ var optionClass = classes({
+ 'Select-option': true,
+ 'is-selected': isSelected,
+ 'is-focused': isFocused,
+ 'is-disabled': op.disabled
+ });
+ var ref = isFocused ? 'focused' : null;
+ var mouseEnter = this.focusOption.bind(this, op);
+ var mouseLeave = this.unfocusOption.bind(this, op);
+ var mouseDown = this.selectValue.bind(this, op);
+ return React.createElement(this.props.optionComponent, {
+ key: 'option-' + op.value,
+ className: optionClass,
+ renderFunc: renderLabel,
+ mouseEnter: mouseEnter,
+ mouseLeave: mouseLeave,
+ mouseDown: mouseDown,
+ click: mouseDown,
+ addLabelText: this.props.addLabelText,
+ option: op,
+ ref: ref
+ });
+ },
+
+ renderOptionGroup(focusedValue, group) {
+ var renderLabel = this.props.optionGroupRenderer || defaultLabelRenderer;
+ return React.createElement(this.props.optionGroupComponent, {
+ key: 'optionGroup-' + group.label,
+ children: this.renderOptions(focusedValue, group.options),
+ renderFunc: renderLabel,
+ optionGroup: group
+ });
},
handleOptionLabelClick: function (value, event) {