Skip to content

Commit

Permalink
[added] disable individual dropdown/combobox options
Browse files Browse the repository at this point in the history
jquense committed Nov 2, 2015
1 parent 5d1b530 commit 1058e3f
Showing 9 changed files with 146 additions and 104 deletions.
30 changes: 21 additions & 9 deletions docs/components/pages/ComboBox.api.md
Original file line number Diff line number Diff line change
@@ -21,53 +21,65 @@ the string value of the {widgetName} will be returned.

### onSelect?{ type: 'Function(Any value)' }

This handler fires when an item has been selected from the list. It fires before the `onChange` handler, and fires
This handler fires when an item has been selected from the list. It fires before the `onChange` handler, and fires
regardless of whether the value has actually changed.

<EditableExample codeText={require('../examples/onSelect')(widgetName)}/>

### data?{ type: 'Array<Any>' }

An array of possible values for the {widgetName}. If an array of `objects` is provided you
should usethe `valueField` and `textField` props, to specify which object
should use the `valueField` and `textField` props, to specify which object
properties comprise the value field (such as an id) and the field used to label the item.

### valueField?{ type: 'String' }

A dataItem field name for uniquely identifying items in the `data` list. A `valueField` is required
A dataItem field name for uniquely identifying items in the `data` list. A `valueField` is required
when the `value` prop is not itself a dataItem. A `valueField` is useful when specifying the selected item, by
its `id` instead of using the model as the value.


When a `valueField` is not provided, the {widgetName} will use strict equality checks (`===`) to locate
When a `valueField` is not provided, the {widgetName} will use strict equality checks (`===`) to locate
the `value` in the `data` list.

<EditableExample codeText={require('../examples/valueField')(widgetName)}/>

### textField?{ type: 'String | Function(dataItem)' }

Specify which data item field to display in the ${widgetName} and selected item. The textField` prop
Specify which data item field to display in the ${widgetName} and selected item. The textField` prop
may also also used as to find an item in the list as you type. Providing an accessor function allows for computed text values

<EditableExample codeText={require('../examples/textField')(widgetName, false, true)}/>

### itemComponent?{ type: 'Component' }

This component is used to render each possible item in the DropdownList. The default component
This component is used to render each possible item in the ${widgetName}. The default component
renders the text of the selected item (specified by `textfield`)

<EditableExample codeText={require('../examples/itemComponent')(widgetName)}/>

### disabled?{ type: '[Boolean, Array]' }

Disable the widget, if an `Array` of values is passed in only those values will be disabled.

<EditableExample codeText={require('../examples/disabled')(widgetName, 'disabled', false)}/>

### readOnly?{ type: '[Boolean, Array]' }

Place the {widgetName} in a read-only mode, If an `Array` of values is passed in only those values will be read-only.

<EditableExample codeText={require('../examples/disabled')(widgetName, 'readOnly', false)}/>

### groupBy?{ type: 'String | Function(Any dataItem)' }

Determines how to group the {widgetName} dropdown list. Providing a `string` will group
Determines how to group the {widgetName}. Providing a `string` will group
the `data` array by that property. You can also provide a function which should return the group value.

<EditableExample codeText={require('../examples/groupby')(widgetName)}/>

### groupComponent?{ type: 'Component' }

This component is used to render each option group, when `groupBy` is specified. By
This component is used to render each option group, when `groupBy` is specified. By
default the `groupBy` value will be used.

<EditableExample codeText={require('../examples/groupComponent')(widgetName)}/>
@@ -80,7 +92,7 @@ are always "startsWith", meaning it will search from the start of the `textField
### filter?{ type: '[Boolean, String, Function(dataItem, searchTerm)]', default: 'false' }

Specify a filtering method used to reduce the items in the dropdown as you type. It can be used in conjunction with
the `suggest` prop or instead of it. There are a few prebuilt filtering methods that can be specified
the `suggest` prop or instead of it. There are a few built-in filtering methods that can be specified
by passing the `String` name. You can explicitly opt out of filtering by setting filter
to `false`

36 changes: 24 additions & 12 deletions docs/components/pages/DropdownList.api.md
Original file line number Diff line number Diff line change
@@ -12,59 +12,71 @@ Change event Handler that is called when the value is changed.

### onSelect?{ type: 'Function(Any value)' }

This handler fires when an item has been selected from the list. It fires before the `onChange` handler, and fires
This handler fires when an item has been selected from the list. It fires before the `onChange` handler, and fires
regardless of whether the value has actually changed.

<EditableExample codeText={require('../examples/onSelect')(widgetName)}/>

### data?{ type: 'Array<Any>' }

provide an array of possible values for the DropdownList. If an array of `objects` is provided you
provide an array of possible values for the ${widgetName}. If an array of `objects` is provided you
should use the `valueField` and `textField` props, to specify which object
properties comprise the value field (such as an id) and the field used to label the item.

### valueField?{ type: 'String' }

A dataItem field name for uniquely identifying items in the `data` list. A `valueField` is required
A dataItem field name for uniquely identifying items in the `data` list. A `valueField` is required
when the `value` prop is not itself a dataItem. A `valueField` is useful when specifying the selected item, by
its `id` instead of using the model as the value.

When a `valueField` is not provided, the {widgetName} will use strict equality checks (`===`) to locate
When a `valueField` is not provided, the {widgetName} will use strict equality checks (`===`) to locate
the `value` in the `data` list.

<EditableExample codeText={require('../examples/valueField')(widgetName)}/>

### textField?{ type: 'String | Function(dataItem)' }

{`Specify which data item field to display in the ${widgetName} and selected item. The `}`textField`{`prop
{`Specify which data item field to display in the ${widgetName} and selected item. The `}`textField`{`prop
may also also used as to find an item in the list as you type. Providing an accessor function allows for computed text values`}

<EditableExample codeText={require('../examples/textField')(widgetName)}/>

### valueComponent?{ type: 'Component' }

This component is used to render the selected value of the combobox. The default component
This component is used to render the selected value of the ${widgetName}. The default component
renders the text of the selected item (specified by `textfield`)

<EditableExample codeText={require('../examples/valueComponent')(widgetName)}/>

### itemComponent?{ type: 'Component' }

This component is used to render each possible item in the DropdownList. The default component
This component is used to render each possible item in the ${widgetName}. The default component
renders the text of the selected item (specified by `textfield`)

<EditableExample codeText={require('../examples/itemComponent')(widgetName)}/>

### disabled?{ type: '[Boolean, Array]' }

Disable the widget, if an `Array` of values is passed in only those values will be disabled.

<EditableExample codeText={require('../examples/disabled')(widgetName, 'disabled', false)}/>

### readOnly?{ type: '[Boolean, Array]' }

Place the {widgetName} in a read-only mode, If an `Array` of values is passed in only those values will be read-only.

<EditableExample codeText={require('../examples/disabled')(widgetName, 'readOnly', false)}/>

### groupBy?{ type: 'String | Function(Any dataItem)' }

Determines how to group the {widgetName} dropdown list. Providing a `string` will group
Determines how to group the {widgetName}. Providing a `string` will group
the `data` array by that property. You can also provide a function which should return the group value.

<EditableExample codeText={require('../examples/groupby')(widgetName)}/>

### groupComponent?{ type: 'Component' }

This component is used to render each option group, when `groupBy` is specified. By
This component is used to render each option group, when `groupBy` is specified. By
default the `groupBy` value will be used.


@@ -79,7 +91,7 @@ Text to display when the value is empty.

The string value of the current search being typed into the {widgetName}. When
unset (`undefined`) the {widgetName} will handle the filtering internally.
The `defaultSearchTerm` prop can be used to set an initialization value for uncontrolled widgets. searchTerm is only
The `defaultSearchTerm` prop can be used to set an initialization value for uncontrolled widgets. `searchTerm` is only
relevant when the `filter` prop is set.


@@ -105,7 +117,7 @@ when the `open` prop is set otherwise the widget open buttons won't work.

### filter?{ type: '[String, Function(dataItem, searchTerm)]', default: 'false' }

Specify a filtering method used to reduce the items in the dropdown as you type. There are a few prebuilt filtering
Specify a filtering method used to reduce the items in the dropdown as you type. There are a few built-in filtering
methods that can be specified by passing the `String` name.

To handle custom filtering techniques provide a `function` that returns `true` or `false` for each passed in item
@@ -168,4 +180,4 @@ Text to display when the the current filter does not return any results.
- <kbd>home</kbd> move focus to first item
- <kbd>end</kbd> move focus to last item
- <kbd>enter</kbd> select focused item
- <kbd>any key</kbd> search list for item starting with key
- <kbd>any key</kbd> search list for item starting with key
10 changes: 6 additions & 4 deletions src/Combobox.jsx
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import GroupableList from './ListGroupable';
import validateList from './util/validateListInterface';
import createUncontrolledWidget from 'uncontrollable';
import { dataItem, dataText, dataIndexOf } from './util/dataHelpers';
import { widgetEditable, widgetEnabled } from './util/interaction';
import { widgetEditable, widgetEnabled, isDisabled, isReadOnly } from './util/interaction';
import { instanceId, notify, isFirstFocusedRender } from './util/widgetHelpers';

let defaultSuggest = f => f === true ? 'startsWith' : f ? f : 'eq'
@@ -41,8 +41,8 @@ let propTypes = {
onSelect: React.PropTypes.func,

autoFocus: React.PropTypes.bool,
disabled: CustomPropTypes.disabled,
readOnly: CustomPropTypes.readOnly,
disabled: CustomPropTypes.disabled.acceptsArray,
readOnly: CustomPropTypes.readOnly.acceptsArray,

suggest: CustomPropTypes.filter,
filter: CustomPropTypes.filter,
@@ -144,7 +144,7 @@ var ComboBox = React.createClass({
className, tabIndex, filter, suggest
, valueField, textField, groupBy
, messages, data, busy, dropUp, name, autoFocus
, placeholder, value, open, disabled, readOnly
, placeholder, value, open
, listComponent: List } = this.props;

List = List || (groupBy && GroupableList) || PlainList
@@ -156,6 +156,8 @@ var ComboBox = React.createClass({
let { focusedItem, selectedItem, focused } = this.state;

let items = this._data()
, disabled = isDisabled(this.props)
, readOnly = isReadOnly(this.props)
, valueItem = dataItem(data, value, valueField) // take value from the raw data
, inputID = instanceId(this, '_input')
, listID = instanceId(this, '_listbox')
11 changes: 6 additions & 5 deletions src/DropdownList.jsx
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import validateList from './util/validateListInterface';
import createUncontrolledWidget from 'uncontrollable';

import { dataItem, dataText, dataIndexOf } from './util/dataHelpers';
import { widgetEditable, widgetEnabled } from './util/interaction';
import { widgetEditable, widgetEnabled, isDisabled, isReadOnly } from './util/interaction';
import { instanceId, notify, isFirstFocusedRender } from './util/widgetHelpers';

let { omit, pick, result } = _;
@@ -48,9 +48,8 @@ var propTypes = {
dropUp: React.PropTypes.bool,
duration: React.PropTypes.number, //popup

disabled: CustomPropTypes.disabled,

readOnly: CustomPropTypes.readOnly,
disabled: CustomPropTypes.disabled.acceptsArray,
readOnly: CustomPropTypes.readOnly.acceptsArray,

messages: React.PropTypes.shape({
open: CustomPropTypes.message,
@@ -123,7 +122,7 @@ var DropdownList = React.createClass({
className, tabIndex, filter
, valueField, textField, groupBy
, messages, data, busy, dropUp
, placeholder, value, open, disabled, readOnly
, placeholder, value, open
, valueComponent: ValueComponent
, listComponent: List } = this.props;

@@ -136,6 +135,8 @@ var DropdownList = React.createClass({
let { focusedItem, selectedItem, focused } = this.state;

let items = this._data()
, disabled = isDisabled(this.props)
, readOnly = isReadOnly(this.props)
, valueItem = dataItem(data, value, valueField) // take value from the raw data
, listID = instanceId(this, '__listbox');

22 changes: 14 additions & 8 deletions src/List.jsx
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import cn from 'classnames';
import _ from './util/_';
import { dataText, dataValue } from './util/dataHelpers';
import { instanceId, notify } from './util/widgetHelpers';
import { isDisabledItem, isReadOnlyItem } from './util/interaction';

let optionId = (id, idx)=> `${id}__option__${idx}`;

@@ -26,12 +27,13 @@ export default React.createClass({
optionComponent: CustomPropTypes.elementType,
itemComponent: CustomPropTypes.elementType,

selectedIndex: React.PropTypes.number,
focusedIndex: React.PropTypes.number,
valueField: React.PropTypes.string,
selected: React.PropTypes.any,
focused: React.PropTypes.any,
valueField: CustomPropTypes.accessor,
textField: CustomPropTypes.accessor,

optionID: React.PropTypes.func,
disabled: CustomPropTypes.disabled.acceptsArray,
readOnly: CustomPropTypes.readOnly.acceptsArray,

messages: React.PropTypes.shape({
emptyList: CustomPropTypes.message
@@ -41,7 +43,6 @@ export default React.createClass({

getDefaultProps(){
return {
optID: '',
onSelect: ()=>{},
optionComponent: ListOption,
ariaActiveDescendantKey: 'list',
@@ -72,7 +73,6 @@ export default React.createClass({
, focused, selected, messages, onSelect
, itemComponent: ItemComponent
, optionComponent: Option
, optionID
, ...props } = this.props
, id = instanceId(this)
, items;
@@ -83,22 +83,28 @@ export default React.createClass({
{_.result(messages.emptyList, this.props)}
</li>
) : data.map((item, idx) => {
var currentId = optionId(id, idx);
var currentId = optionId(id, idx)
, isDisabled = isDisabledItem(item, props)
, isReadOnly = isReadOnlyItem(item, props);

return (
<Option
key={'item_' + idx}
id={currentId}
dataItem={item}
disabled={isDisabled}
readOnly={isReadOnly}
focused={focused === item}
selected={selected === item}
onClick={onSelect.bind(null, item)}
onClick={isDisabled || isReadOnly ? undefined : onSelect.bind(null, item)}
>
{ ItemComponent
? <ItemComponent
item={item}
value={dataValue(item, valueField)}
text={dataText(item, textField)}
disabled={isDisabled}
readOnly={isReadOnly}
/>
: dataText(item, textField)
}
21 changes: 14 additions & 7 deletions src/ListGroupable.jsx
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import _ from './util/_';
import warning from 'warning';
import { dataText, dataValue } from './util/dataHelpers';
import { instanceId, notify } from './util/widgetHelpers';
import { isDisabledItem, isReadOnlyItem } from './util/interaction';

let optionId = (id, idx)=> `${id}__option__${idx}`;

@@ -31,10 +32,11 @@ export default React.createClass({
selected: React.PropTypes.any,
focused: React.PropTypes.any,

valueField: React.PropTypes.string,
valueField: CustomPropTypes.accessor,
textField: CustomPropTypes.accessor,

optID: React.PropTypes.string,
disabled: CustomPropTypes.disabled.acceptsArray,
readOnly: CustomPropTypes.readOnly.acceptsArray,

groupBy: CustomPropTypes.accessor,

@@ -46,7 +48,6 @@ export default React.createClass({

getDefaultProps(){
return {
optID: '',
onSelect: function(){},
data: [],
optionComponent: ListOption,
@@ -155,7 +156,9 @@ export default React.createClass({
, itemComponent: ItemComponent
, optionComponent: Option } = this.props

let currentID = optionId(instanceId(this), idx);
let currentID = optionId(instanceId(this), idx)
, isDisabled = isDisabledItem(item, this.props)
, isReadOnly = isReadOnlyItem(item, this.props);

if (focused === item)
this._currentActiveID = currentID;
@@ -167,13 +170,17 @@ export default React.createClass({
dataItem={item}
focused={focused === item}
selected={selected === item}
onClick={onSelect.bind(null, item)}
disabled={isDisabled}
readOnly={isReadOnly}
onClick={isDisabled || isReadOnly ? undefined : onSelect.bind(null, item)}
>
{ ItemComponent
? <ItemComponent
item={item}
value={dataValue(item, valueField)}
text={dataText(item, textField)}
disabled={isDisabled}
readOnly={isReadOnly}
/>
: dataText(item, textField)
}
@@ -197,7 +204,7 @@ export default React.createClass({
, `[React Widgets] You are seem to be trying to group this list by a `
+ `property \`${groupBy}\` that doesn't exist in the dataset items, this may be a typo`)

return data.reduce( (grps, item) => {
return data.reduce((grps, item) => {
var group = iter(item);

_.has(grps, group)
@@ -215,7 +222,7 @@ export default React.createClass({
.reduce( (flat, grp) => flat.concat(groups[grp]), [])
},

move() {
move(){
var selected = this.getItemDOMNode(this.props.focused);

if( !selected ) return
12 changes: 8 additions & 4 deletions src/ListOption.jsx
Original file line number Diff line number Diff line change
@@ -5,20 +5,24 @@ let ListOption = React.createClass({
propTypes: {
dataItem: React.PropTypes.any,
focused: React.PropTypes.bool,
selected: React.PropTypes.bool
selected: React.PropTypes.bool,
disabled: React.PropTypes.bool,
readOnly: React.PropTypes.bool
},

render() {
let { className, children, focused, selected, ...props } = this.props;
let { className, children, focused, selected, disabled, readOnly, ...props } = this.props;
let classes = {
'rw-state-focus': focused,
'rw-state-selected': selected
'rw-state-selected': selected,
'rw-state-disabled': disabled,
'rw-state-readonly': readOnly
};

return (
<li
role='option'
tabIndex='-1'
tabIndex={!(disabled || readOnly) ? '-1' : undefined}
aria-selected={!!selected}
className={cn('rw-list-option', className, classes)}
{...props}
16 changes: 11 additions & 5 deletions src/less/core.less
Original file line number Diff line number Diff line change
@@ -82,11 +82,6 @@
border-radius: @input-border-radius;
margin-bottom: 2px;

// .rw-open-up & {
// margin-bottom: 0;
// margin-top: 2px;
// }

.rw-rtl &{
padding-left: 1.9em;
padding-right: 0;
@@ -207,6 +202,17 @@ ul.rw-list {
&.rw-state-selected {
.state-select();
}

&.rw-state-disabled,
&.rw-state-readonly {
color: @gray-light;
cursor: not-allowed;

&:hover {
background: none;
border-color: transparent;
}
}
}
}

92 changes: 42 additions & 50 deletions src/mixins/ListMovementMixin.js
Original file line number Diff line number Diff line change
@@ -1,76 +1,68 @@
'use strict';
import React from 'react';
import filter from '../util/filter';
import { dataText } from '../util/dataHelpers';
import CustomPropTypes from '../util/propTypes';
import { isDisabledItem, isReadOnlyItem } from '../util/interaction';

module.exports = {
const EMPTY_VALUE = {};

var isDisabledOrReadonly = (item, props) => isDisabledItem(item, props) || isReadOnlyItem(item, props)

export default {

propTypes: {
textField: React.PropTypes.string
textField: CustomPropTypes.accessor,
valueField: CustomPropTypes.accessor,
disabled: CustomPropTypes.disabled.acceptsArray,
readOnly: CustomPropTypes.readOnly.acceptsArray
},

first() {
return this._data()[0]
return this.next(EMPTY_VALUE)
},

last() {
var data = this._data()
return data[data.length - 1]
},

prev(item, word) {
var textField = this.props.textField
, data = this._data()
, idx = data.indexOf(item)
let data = this._data()
, item = data[data.length - 1];

if (idx === -1) idx = data.length;

return word
? findPrevInstance(textField, data, word, idx)
: --idx < 0 ? data[0] : data[idx]
return isDisabledOrReadonly(item, this.props)
? this.prev(item) : item
},

next(item, word) {
var textField = this.props.textField
, data = this._data()
, idx = data.indexOf(item)
prev(item, word){
var data = this._data()
, nextIdx = data.indexOf(item)
, matches = matcher(word, item, this.props.textField);

return word
? findNextInstance(textField, data, word, idx)
: ++idx === data.length ? data[data.length - 1] : data[idx]
}
if (nextIdx < 0 || nextIdx == null)
nextIdx = 0

}
nextIdx--;

function findNextInstance(textField, data, word, startIndex){
var matches = filter.startsWith
, idx = -1
, len = data.length
, foundStart, itemText;
while (nextIdx > -1 && (isDisabledOrReadonly(data[nextIdx], this.props) || !matches(data[nextIdx])))
nextIdx--

word = word.toLowerCase()
return nextIdx >= 0 ? data[nextIdx] : item;
},

while (++idx < len){
foundStart = foundStart || idx > startIndex
itemText = foundStart && dataText(data[idx], textField).toLowerCase()
next(item, word) {
var data = this._data()
, nextIdx = data.indexOf(item) + 1
, len = data.length
, matches = matcher(word, item, this.props.textField);

if( foundStart && matches(itemText, word) )
return data[idx]
while (nextIdx < len && (isDisabledOrReadonly(data[nextIdx], this.props) || !matches(data[nextIdx])))
nextIdx++

return nextIdx < len ? data[nextIdx] : item
}
}

function findPrevInstance(textField, data, word, startIndex){
var matches = filter.startsWith
, idx = data.length
, foundStart, itemText;
function matcher(word, item, textField){
if (!word) return ()=> true

word = word.toLowerCase()

while (--idx >= 0 ){
foundStart = foundStart || idx < startIndex
itemText = foundStart && dataText(data[idx], textField).toLowerCase()

if( foundStart && matches(itemText, word) )
return data[idx]
}
return item => filter.startsWith(
dataText(item, textField).toLowerCase()
, word
)
}

0 comments on commit 1058e3f

Please sign in to comment.