react-widgets
Version:
An à la carte set of polished, extensible, and accessible inputs built for React
506 lines (418 loc) • 15.9 kB
JavaScript
'use strict';
var babelHelpers = require('./util/babelHelpers.js');
exports.__esModule = true;
var _react = require('react');
var _react2 = babelHelpers.interopRequireDefault(_react);
var _classnames = require('classnames');
var _classnames2 = babelHelpers.interopRequireDefault(_classnames);
var _util_ = require('./util/_');
var _util_2 = babelHelpers.interopRequireDefault(_util_);
var _utilFilter = require('./util/filter');
var _utilFilter2 = babelHelpers.interopRequireDefault(_utilFilter);
var _Popup = require('./Popup');
var _Popup2 = babelHelpers.interopRequireDefault(_Popup);
var _WidgetButton = require('./WidgetButton');
var _WidgetButton2 = babelHelpers.interopRequireDefault(_WidgetButton);
var _ComboboxInput = require('./ComboboxInput');
var _ComboboxInput2 = babelHelpers.interopRequireDefault(_ComboboxInput);
var _utilCompat = require('./util/compat');
var _utilCompat2 = babelHelpers.interopRequireDefault(_utilCompat);
var _utilPropTypes = require('./util/propTypes');
var _utilPropTypes2 = babelHelpers.interopRequireDefault(_utilPropTypes);
var _List = require('./List');
var _List2 = babelHelpers.interopRequireDefault(_List);
var _ListGroupable = require('./ListGroupable');
var _ListGroupable2 = babelHelpers.interopRequireDefault(_ListGroupable);
var _utilValidateListInterface = require('./util/validateListInterface');
var _utilValidateListInterface2 = babelHelpers.interopRequireDefault(_utilValidateListInterface);
var _uncontrollable = require('uncontrollable');
var _uncontrollable2 = babelHelpers.interopRequireDefault(_uncontrollable);
var _utilDataHelpers = require('./util/dataHelpers');
var _utilInteraction = require('./util/interaction');
var _utilWidgetHelpers = require('./util/widgetHelpers');
var defaultSuggest = function defaultSuggest(f) {
return f === true ? 'startsWith' : f ? f : 'eq';
};
var omit = _util_2['default'].omit;
var pick = _util_2['default'].pick;
var propTypes = {
//-- controlled props -----------
value: _react2['default'].PropTypes.any,
onChange: _react2['default'].PropTypes.func,
open: _react2['default'].PropTypes.bool,
onToggle: _react2['default'].PropTypes.func,
//------------------------------------
itemComponent: _utilPropTypes2['default'].elementType,
listComponent: _utilPropTypes2['default'].elementType,
groupComponent: _utilPropTypes2['default'].elementType,
groupBy: _utilPropTypes2['default'].accessor,
data: _react2['default'].PropTypes.array,
valueField: _react2['default'].PropTypes.string,
textField: _utilPropTypes2['default'].accessor,
name: _react2['default'].PropTypes.string,
onSelect: _react2['default'].PropTypes.func,
autoFocus: _react2['default'].PropTypes.bool,
disabled: _utilPropTypes2['default'].disabled.acceptsArray,
readOnly: _utilPropTypes2['default'].readOnly.acceptsArray,
suggest: _utilPropTypes2['default'].filter,
filter: _utilPropTypes2['default'].filter,
busy: _react2['default'].PropTypes.bool,
dropUp: _react2['default'].PropTypes.bool,
duration: _react2['default'].PropTypes.number, //popup
placeholder: _react2['default'].PropTypes.string,
messages: _react2['default'].PropTypes.shape({
open: _utilPropTypes2['default'].message,
emptyList: _utilPropTypes2['default'].message,
emptyFilter: _utilPropTypes2['default'].message
})
};
var ComboBox = _react2['default'].createClass(babelHelpers.createDecoratedObject([{
key: 'displayName',
initializer: function initializer() {
return 'ComboBox';
}
}, {
key: 'mixins',
initializer: function initializer() {
return [require('./mixins/TimeoutMixin'), require('./mixins/DataFilterMixin'), require('./mixins/PopupScrollToMixin'), require('./mixins/RtlParentContextMixin'), require('./mixins/AriaDescendantMixin')('input')];
}
}, {
key: 'propTypes',
initializer: function initializer() {
return propTypes;
}
}, {
key: 'getInitialState',
value: function getInitialState() {
var _props = this.props;
var value = _props.value;
var data = _props.data;
var valueField = _props.valueField;
var items = this.process(data, value);
var idx = _utilDataHelpers.dataIndexOf(items, value, valueField);
return {
selectedItem: items[idx],
focusedItem: items[! ~idx ? 0 : idx],
processedData: items,
open: false
};
}
}, {
key: 'getDefaultProps',
value: function getDefaultProps() {
return {
data: [],
value: '',
open: false,
suggest: false,
filter: false,
delay: 500,
messages: msgs(),
ariaActiveDescendantKey: 'combobox'
};
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate() {
this.refs.list && _utilValidateListInterface2['default'](this.refs.list);
}
}, {
key: 'shouldComponentUpdate',
value: function shouldComponentUpdate(nextProps, nextState) {
var isSuggesting = this.refs.input && this.refs.input.isSuggesting(),
stateChanged = !_util_2['default'].isShallowEqual(nextState, this.state),
valueChanged = !_util_2['default'].isShallowEqual(nextProps, this.props);
return isSuggesting || stateChanged || valueChanged;
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
var value = nextProps.value;
var data = nextProps.data;
var valueField = nextProps.valueField;
var textField = nextProps.textField;
var rawIdx = _utilDataHelpers.dataIndexOf(data, value, valueField),
valueItem = rawIdx === -1 ? nextProps.value : nextProps.data[rawIdx],
isSuggesting = this.refs.input.isSuggesting(),
items = this.process(nextProps.data, nextProps.value, (rawIdx === -1 || isSuggesting) && _utilDataHelpers.dataText(valueItem, textField)),
idx = _utilDataHelpers.dataIndexOf(items, value, valueField),
focused = this.filterIndexOf(items, _utilDataHelpers.dataText(valueItem, textField));
this._searchTerm = '';
this.setState({
processedData: items,
selectedItem: items[idx],
focusedItem: items[idx === -1 ? focused !== -1 ? focused : 0 // focus the closest match
: idx]
});
}
}, {
key: 'render',
value: function render() {
var _cx,
_this = this;
var _props2 = this.props;
var className = _props2.className;
var tabIndex = _props2.tabIndex;
var filter = _props2.filter;
var suggest = _props2.suggest;
var valueField = _props2.valueField;
var textField = _props2.textField;
var groupBy = _props2.groupBy;
var messages = _props2.messages;
var data = _props2.data;
var busy = _props2.busy;
var dropUp = _props2.dropUp;
var name = _props2.name;
var autoFocus = _props2.autoFocus;
var placeholder = _props2.placeholder;
var value = _props2.value;
var open = _props2.open;
var List = _props2.listComponent;
List = List || groupBy && _ListGroupable2['default'] || _List2['default'];
var elementProps = omit(this.props, Object.keys(propTypes));
var listProps = pick(this.props, Object.keys(List.propTypes));
var popupProps = pick(this.props, Object.keys(_Popup2['default'].propTypes));
var _state = this.state;
var focusedItem = _state.focusedItem;
var selectedItem = _state.selectedItem;
var focused = _state.focused;
var items = this._data(),
disabled = _utilInteraction.isDisabled(this.props),
readOnly = _utilInteraction.isReadOnly(this.props),
valueItem = _utilDataHelpers.dataItem(data, value, valueField),
// take value from the raw data
inputID = _utilWidgetHelpers.instanceId(this, '_input'),
listID = _utilWidgetHelpers.instanceId(this, '_listbox'),
completeType = suggest ? filter ? 'both' : 'inline' : filter ? 'list' : '';
var shouldRenderList = _utilWidgetHelpers.isFirstFocusedRender(this) || open;
messages = msgs(messages);
return _react2['default'].createElement(
'div',
babelHelpers._extends({}, elementProps, {
ref: 'element',
onKeyDown: this._keyDown,
onFocus: this._focus.bind(null, true),
onBlur: this._focus.bind(null, false),
tabIndex: '-1',
className: _classnames2['default'](className, 'rw-combobox', 'rw-widget', (_cx = {
'rw-state-focus': focused,
'rw-state-disabled': disabled,
'rw-state-readonly': readOnly,
'rw-rtl': this.isRtl()
}, _cx['rw-open' + (dropUp ? '-up' : '')] = open, _cx))
}),
_react2['default'].createElement(
_WidgetButton2['default'],
{
tabIndex: '-1',
className: 'rw-select',
onClick: this.toggle,
disabled: !!(disabled || readOnly)
},
_react2['default'].createElement(
'i',
{ className: _classnames2['default']('rw-i rw-i-caret-down', { 'rw-loading': busy }) },
_react2['default'].createElement(
'span',
{ className: 'rw-sr' },
_util_2['default'].result(messages.open, this.props)
)
)
),
_react2['default'].createElement(_ComboboxInput2['default'], {
ref: 'input',
id: inputID,
autoFocus: autoFocus,
tabIndex: tabIndex,
suggest: suggest,
name: name,
role: 'combobox',
'aria-owns': listID,
'aria-busy': !!busy,
'aria-autocomplete': completeType,
'aria-expanded': open,
'aria-haspopup': true,
placeholder: placeholder,
disabled: disabled,
readOnly: readOnly,
className: 'rw-input',
value: _utilDataHelpers.dataText(valueItem, textField),
onChange: this._inputTyping,
onKeyDown: this._inputKeyDown
}),
_react2['default'].createElement(
_Popup2['default'],
babelHelpers._extends({}, popupProps, {
onOpening: function () {
return _this.refs.list.forceUpdate();
}
}),
_react2['default'].createElement(
'div',
null,
shouldRenderList && _react2['default'].createElement(List, babelHelpers._extends({ ref: 'list'
}, listProps, {
id: listID,
data: items,
selected: selectedItem,
focused: focusedItem,
'aria-hidden': !open,
'aria-labelledby': inputID,
'aria-live': open && 'polite',
onSelect: this._onSelect,
onMove: this._scrollTo,
messages: {
emptyList: data.length ? messages.emptyFilter : messages.emptyList
} }))
)
)
);
}
}, {
key: '_onSelect',
decorators: [_utilInteraction.widgetEditable],
value: function _onSelect(data) {
this.close();
_utilWidgetHelpers.notify(this.props.onSelect, data);
this.change(data);
this.focus();
}
}, {
key: '_inputKeyDown',
value: function _inputKeyDown(e) {
this._deleting = e.key === 'Backspace' || e.key === 'Delete';
this._isTyping = true;
}
}, {
key: '_inputTyping',
value: function _inputTyping(e) {
var _props3 = this.props;
var data = _props3.data;
var textField = _props3.textField;
var shouldSuggest = !!this.props.suggest,
strVal = e.target.value,
suggestion;
suggestion = this._deleting || !shouldSuggest ? strVal : this.suggest(this._data(), strVal);
suggestion = suggestion || strVal;
data = _util_2['default'].find(data, function (item) {
return _utilDataHelpers.dataText(item, textField).toLowerCase() === suggestion.toLowerCase();
});
this.change(!this._deleting && data ? data : strVal, true);
this.open();
}
}, {
key: 'focus',
value: function focus() {
this.refs.input.focus();
}
}, {
key: '_focus',
decorators: [_utilInteraction.widgetEnabled],
value: function _focus(focused, e) {
var _this2 = this;
!focused && this.refs.input.accept(); //not suggesting anymore
this.setTimeout('focus', function () {
if (!focused) _this2.close();
if (focused !== _this2.state.focused) {
_utilWidgetHelpers.notify(_this2.props[focused ? 'onFocus' : 'onBlur'], e);
_this2.setState({ focused: focused });
}
});
}
}, {
key: '_keyDown',
decorators: [_utilInteraction.widgetEditable],
value: function _keyDown(e) {
var self = this,
key = e.key,
alt = e.altKey,
list = this.refs.list,
focusedItem = this.state.focusedItem,
selectedItem = this.state.selectedItem,
isOpen = this.props.open;
_utilWidgetHelpers.notify(this.props.onKeyDown, [e]);
if (e.defaultPrevented) return;
if (key === 'End') if (isOpen) this.setState({ focusedItem: list.last() });else select(list.last(), true);else if (key === 'Home') if (isOpen) this.setState({ focusedItem: list.first() });else select(list.first(), true);else if (key === 'Escape' && isOpen) this.close();else if (key === 'Enter' && isOpen) {
select(this.state.focusedItem, true);
} else if (key === 'ArrowDown') {
if (alt) this.open();else {
if (isOpen) this.setState({ focusedItem: list.next(focusedItem) });else select(list.next(selectedItem), true);
}
} else if (key === 'ArrowUp') {
if (alt) this.close();else {
if (isOpen) this.setState({ focusedItem: list.prev(focusedItem) });else select(list.prev(selectedItem), true);
}
}
function select(item, fromList) {
if (!item) return self.change(_utilCompat2['default'].findDOMNode(self.refs.input).value, false);
self.refs.input.accept(true); //removes caret
if (fromList) return self._onSelect(item);
self.change(item, false);
}
}
}, {
key: 'change',
value: function change(data, typing) {
this._typedChange = !!typing;
_utilWidgetHelpers.notify(this.props.onChange, data);
}
}, {
key: 'open',
value: function open() {
if (!this.props.open) _utilWidgetHelpers.notify(this.props.onToggle, true);
}
}, {
key: 'close',
value: function close() {
if (this.props.open) _utilWidgetHelpers.notify(this.props.onToggle, false);
}
}, {
key: 'toggle',
decorators: [_utilInteraction.widgetEditable],
value: function toggle() {
this.focus();
this.props.open ? this.close() : this.open();
}
}, {
key: 'suggest',
value: function suggest(data, value) {
var _props4 = this.props;
var textField = _props4.textField;
var suggest = _props4.suggest;
var minLength = _props4.minLength;
var word = _utilDataHelpers.dataText(value, textField),
suggestion;
suggest = defaultSuggest(suggest);
if (!(word || '').trim() || word.length < (minLength || 1)) return '';
suggestion = typeof value === 'string' ? _util_2['default'].find(data, getFilter(suggest, word, textField)) : value;
if (suggestion && (!this.state || !this.state.deleting)) return _utilDataHelpers.dataText(suggestion, textField);
return '';
}
}, {
key: '_data',
value: function _data() {
return this.state.processedData;
}
}, {
key: 'process',
value: function process(data, values, searchTerm) {
if (this.props.filter && searchTerm) data = this.filter(data, searchTerm);
return data;
}
}]));
exports['default'] = _uncontrollable2['default'](ComboBox, { open: 'onToggle', value: 'onChange' });
function msgs(msgs) {
return babelHelpers._extends({
open: 'open combobox',
emptyList: 'There are no items in this list',
emptyFilter: 'The filter returned no results'
}, msgs);
}
function getFilter(suggest, word, textField) {
return typeof suggest === 'string' ? function (item) {
return _utilFilter2['default'][suggest](_utilDataHelpers.dataText(item, textField).toLowerCase(), word.toLowerCase());
} : function (item) {
return suggest(item, word);
};
}
module.exports = exports['default'];