office-ui-fabric-react
Version:
Reusable React components for building experiences for Office 365.
870 lines (868 loc) • 48.6 kB
JavaScript
define(["require", "exports", "tslib", "react", "../../common/DirectionalHint", "../../Callout", "../../Label", "../../Button", "../pickers/AutoFill/BaseAutoFill", "../../Utilities", "../../utilities/selectableOption/SelectableOption.Props", "./ComboBox.scss"], function (require, exports, tslib_1, React, DirectionalHint_1, Callout_1, Label_1, Button_1, BaseAutoFill_1, Utilities_1, SelectableOption_Props_1, stylesImport) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var styles = stylesImport;
var SearchDirection;
(function (SearchDirection) {
SearchDirection[SearchDirection["backward"] = -1] = "backward";
SearchDirection[SearchDirection["none"] = 0] = "none";
SearchDirection[SearchDirection["forward"] = 1] = "forward";
})(SearchDirection || (SearchDirection = {}));
var ComboBox = (function (_super) {
tslib_1.__extends(ComboBox, _super);
function ComboBox(props) {
var _this = _super.call(this, props) || this;
// This is used to clear any pending autocomplete
// text (used when autocomplete is true and allowFreeform is false)
_this._readOnlyPendingAutoCompleteTimeout = 1000 /* ms */;
_this._warnMutuallyExclusive({
'defaultSelectedKey': 'selectedKey',
'value': 'defaultSelectedKey',
'selectedKey': 'value'
});
_this._id = props.id || Utilities_1.getId('ComboBox');
var selectedKey = props.defaultSelectedKey !== undefined ? props.defaultSelectedKey : props.selectedKey;
_this._lastReadOnlyAutoCompleteChangeTimeoutId = -1;
var index = _this._getSelectedIndex(props.options, selectedKey);
_this.state = {
isOpen: false,
selectedIndex: index,
focused: false,
suggestedDisplayValue: '',
currentOptions: _this.props.options,
currentPendingValueValidIndex: -1,
currentPendingValue: ''
};
return _this;
}
ComboBox.prototype.componentDidMount = function () {
// hook up resolving the options if needed on focus
this._events.on(this._comboBoxWrapper, 'focus', this._onResolveOptions, true);
};
ComboBox.prototype.componentWillReceiveProps = function (newProps) {
// In controlled component usage where selectedKey is provided, update the selectedIndex
// and currentOptions state if the key or options change
if (newProps.selectedKey !== undefined &&
(newProps.selectedKey !== this.props.selectedKey || newProps.options !== this.props.options)) {
var index = this._getSelectedIndex(newProps.options, newProps.selectedKey);
this.setState({
selectedIndex: index,
currentOptions: newProps.options
});
}
};
ComboBox.prototype.componentDidUpdate = function (prevProps, prevState) {
var allowFreeform = this.props.allowFreeform;
var _a = this.state, isOpen = _a.isOpen, focused = _a.focused, selectedIndex = _a.selectedIndex;
// If we are newly open, make sure the currently
// selected option is scrolled into view
if (!prevState.isOpen && isOpen) {
this._scrollIntoView();
}
// If we are open or we are focused but are not the activeElement,
// set focus on the input
if (isOpen || (focused && document.activeElement !== this._comboBox.inputElement)) {
this.focus();
}
// If we just opened/closed the menu OR
// updated the selectedIndex with the menu closed OR
// we are focused and are not allowing freeform
// we need to fix up focus and set selection
if (prevState.isOpen !== isOpen ||
(!isOpen && prevState.selectedIndex !== selectedIndex) ||
(!allowFreeform && focused)) {
this._select();
}
};
ComboBox.prototype.componentWillUnmount = function () {
_super.prototype.componentWillUnmount.call(this);
// remove the eventHanlder that was added in componentDidMount
this._events.off(this._comboBoxWrapper);
};
// Primary Render
ComboBox.prototype.render = function () {
var id = this._id;
var _a = this.props, className = _a.className, label = _a.label, disabled = _a.disabled, ariaLabel = _a.ariaLabel, required = _a.required, errorMessage = _a.errorMessage, _b = _a.onRenderContainer, onRenderContainer = _b === void 0 ? this._onRenderContainer : _b, allowFreeform = _a.allowFreeform, autoComplete = _a.autoComplete, buttonIconProps = _a.buttonIconProps;
var _c = this.state, isOpen = _c.isOpen, selectedIndex = _c.selectedIndex, focused = _c.focused, suggestedDisplayValue = _c.suggestedDisplayValue;
this._currentVisibleValue = this._getVisibleValue();
var divProps = Utilities_1.getNativeProps(this.props, Utilities_1.divProperties);
return (React.createElement("div", tslib_1.__assign({}, divProps, { ref: 'root', className: Utilities_1.css('ms-ComboBox-container') }),
label && (React.createElement(Label_1.Label, { id: id + '-label', required: required, htmlFor: id }, label)),
React.createElement("div", { ref: this._resolveRef('_comboBoxWrapper'), id: id + 'wrapper', className: Utilities_1.css('ms-ComboBox', styles.wrapper, (errorMessage && errorMessage.length > 0 ? styles.wrapperForError : null), styles.root, className, (_d = {
'is-open': isOpen
},
_d['is-disabled ' + styles.rootIsDisabled] = disabled,
_d['is-required '] = required,
_d[styles.focused] = focused,
_d[styles.readOnly] = !allowFreeform,
_d)) },
React.createElement(BaseAutoFill_1.BaseAutoFill, { "data-is-interactable": !disabled, ref: this._resolveRef('_comboBox'), id: id + '-input', className: Utilities_1.css('ms-ComboBox-Input', styles.input), type: 'text', key: selectedIndex, onFocus: this._select, onBlur: this._onBlur, onKeyDown: this._onInputKeyDown, onKeyUp: this._onInputKeyUp, onClick: allowFreeform ? this.focus : this._onComboBoxClick, onInputValueChange: this._onInputChange, "aria-expanded": isOpen, "aria-autocomplete": (!disabled && autoComplete), role: 'combobox', "aria-readonly": ((allowFreeform || disabled) ? null : 'true'), readOnly: disabled || !allowFreeform, "aria-labelledby": (label && (id + '-label')), "aria-label": ((ariaLabel && !label) && ariaLabel), "aria-describedby": (id + '-option'), "aria-activedescendant": (isOpen && selectedIndex >= 0 ? (id + '-list' + selectedIndex) : null), "aria-disabled": disabled, "aria-owns": (id + '-list'), spellCheck: false, defaultVisibleValue: this._currentVisibleValue, suggestedDisplayValue: suggestedDisplayValue, updateValueInWillReceiveProps: this._onUpdateValueInAutoFillWillReceiveProps, shouldSelectFullInputValueInComponentDidUpdate: this._onShouldSelectFullInputValueInAutoFillComponentDidUpdate }),
React.createElement(Button_1.IconButton, { className: Utilities_1.css('ms-ComboBox-Button', styles.caretDown), role: 'presentation', "aria-hidden": 'true', tabIndex: -1, onClick: this._onComboBoxClick, iconProps: buttonIconProps, disabled: disabled })),
isOpen && (onRenderContainer(tslib_1.__assign({}, this.props), this._onRenderContainer)),
errorMessage &&
React.createElement("div", { className: Utilities_1.css(styles.errorMessage) }, errorMessage)));
var _d;
};
/**
* Set focus on the input
*/
ComboBox.prototype.focus = function () {
if (this._comboBox) {
this._comboBox.focus();
}
};
/**
* componentWillReceiveProps handler for the auto fill component
* Checks/updates the iput value to set, if needed
* @param {IBaseAutoFillProps} defaultVisibleValue - the defaultVisibleValue that got passed
* in to the auto fill's componentWillReceiveProps
* @returns {string} - the updated value to set, if needed
*/
ComboBox.prototype._onUpdateValueInAutoFillWillReceiveProps = function () {
if (this._comboBox === null || this._comboBox === undefined) {
return null;
}
if (this._currentVisibleValue && this._currentVisibleValue !== '' && this._comboBox.value !== this._currentVisibleValue) {
return this._currentVisibleValue;
}
return this._comboBox.value;
};
/**
* componentDidUpdate handler for the auto fill component
*
* @param { string } defaultVisibleValue - the current defaultVisibleValue in the auto fill's componentDidUpdate
* @param { string } suggestedDisplayValue - the current suggestedDisplayValue in the auto fill's componentDidUpdate
* @returns {boolean} - should the full value of the input be selected?
* True if the defaultVisibleValue equals the suggestedDisplayValue, false otherwise
*/
ComboBox.prototype._onShouldSelectFullInputValueInAutoFillComponentDidUpdate = function () {
return this._currentVisibleValue === this.state.suggestedDisplayValue;
};
/**
* Get the correct value to pass to the input
* to show to the user based off of the current props and state
* @returns {string} the value to pass to the input
*/
ComboBox.prototype._getVisibleValue = function () {
var _a = this.props, value = _a.value, allowFreeform = _a.allowFreeform, autoComplete = _a.autoComplete;
var _b = this.state, selectedIndex = _b.selectedIndex, currentPendingValueValidIndex = _b.currentPendingValueValidIndex, currentOptions = _b.currentOptions, currentPendingValue = _b.currentPendingValue, suggestedDisplayValue = _b.suggestedDisplayValue;
// If the user passed is a value prop, use that
if (value) {
return value;
}
var index = selectedIndex;
if (allowFreeform) {
// If we are allowing freeform and autocomplete is also true
// and we've got a pending value that matches an option, remember
// the matched option's index
if (autoComplete && this._indexWithinBounds(currentOptions, currentPendingValueValidIndex)) {
index = currentPendingValueValidIndex;
}
// Since we are allowing freeform, if there is currently a nonempty pending value, use that
// otherwise use the index determined above (falling back to '' if we did not get a valid index)
return currentPendingValue !== '' ? currentPendingValue :
(this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : '');
}
else {
// If we are not allowing freeform and have a
// valid index that matches the pending value,
// we know we will need some version of the pending value
if (this._indexWithinBounds(currentOptions, currentPendingValueValidIndex)) {
// If autoComplete is on, return the
// raw pending value, otherwise remember
// the matched option's index
if (autoComplete) {
return currentPendingValue;
}
index = currentPendingValueValidIndex;
}
// If we have a valid index then return the text value of that option,
// otherwise return the suggestedDisplayValue
return this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : suggestedDisplayValue;
}
};
/**
* Is the index within the bounds of the array?
* @param options - options to check if the index is valid for
* @param index - the index to check
* @returns {boolean} - true if the index is valid for the given options, false otherwise
*/
ComboBox.prototype._indexWithinBounds = function (options, index) {
return index >= 0 && index < options.length;
};
/**
* Handler for typing changes on the input
* @param updatedValue - the newly changed value
*/
ComboBox.prototype._onInputChange = function (updatedValue) {
if (this.props.disabled) {
this._handleInputWhenDisabled(null /* event */);
return;
}
this.props.allowFreeform ?
this._processInputChangeWithFreeform(updatedValue) :
this._processInputChangeWithoutFreeform(updatedValue);
};
/**
* Process the new input's new value when the comboBox
* allows freeform entry
* @param updatedValue - the input's newly changed value
*/
ComboBox.prototype._processInputChangeWithFreeform = function (updatedValue) {
var currentOptions = this.state.currentOptions;
// if the new value is empty, nothing needs to be done
if (updatedValue === '') {
return;
}
// Remember the original value and then,
// make the value lowercase for comparison
var originalUpdatedValue = updatedValue;
updatedValue = updatedValue.toLocaleLowerCase();
var newSuggestedDisplayValue = '';
var newCurrentPendingValueValidIndex = -1;
// If autoComplete is on, attempt to find a match from the available options
if (this.props.autoComplete) {
// If autoComplete is on, attempt to find a match where the text of an option starts with the updated value
var items = currentOptions.map(function (item, index) { return tslib_1.__assign({}, item, { index: index }); }).filter(function (option) { return option.itemType !== SelectableOption_Props_1.SelectableOptionMenuItemType.Header && option.itemType !== SelectableOption_Props_1.SelectableOptionMenuItemType.Divider; }).filter(function (option) { return option.text.toLocaleLowerCase().indexOf(updatedValue) === 0; });
if (items.length > 0) {
// If the user typed out the complete option text, we don't need any suggested display text anymore
newSuggestedDisplayValue = items[0].text.toLocaleLowerCase() !== updatedValue ? items[0].text : '';
// remember the index of the match we found
newCurrentPendingValueValidIndex = items[0].index;
}
}
else {
// If autoComplete is off, attempt to find a match only when the value is exactly equal to the text of an option
var items = currentOptions.map(function (item, index) { return tslib_1.__assign({}, item, { index: index }); }).filter(function (option) { return option.itemType !== SelectableOption_Props_1.SelectableOptionMenuItemType.Header && option.itemType !== SelectableOption_Props_1.SelectableOptionMenuItemType.Divider; }).filter(function (option) { return option.text.toLocaleLowerCase() === updatedValue; });
// if we fould a match remember the index
if (items.length === 1) {
newCurrentPendingValueValidIndex = items[0].index;
}
}
// Set the updated state
this._setPendingInfo(originalUpdatedValue, newCurrentPendingValueValidIndex, newSuggestedDisplayValue);
};
/**
* Process the new input's new value when the comboBox
* does not allow freeform entry
* @param updatedValue - the input's newly changed value
*/
ComboBox.prototype._processInputChangeWithoutFreeform = function (updatedValue) {
var _this = this;
var _a = this.state, currentPendingValue = _a.currentPendingValue, currentPendingValueValidIndex = _a.currentPendingValueValidIndex, currentOptions = _a.currentOptions, selectedIndex = _a.selectedIndex;
if (this.props.autoComplete) {
// If autoComplete is on while allow freeform is off,
// we will remember the keypresses and build up a string to attempt to match
// as long as characters are typed within a the timeout span of each other,
// otherwise we will clear the string and start building a new one on the next keypress.
// Also, only do this processing if we have a non-empty value
if (updatedValue !== '') {
// If we have a pending autocomplete clearing task,
// we know that the user is typing with keypresses happening
// within the timeout of each other so remove the clearing task
// and continue building the pending value with the udpated value
if (this._lastReadOnlyAutoCompleteChangeTimeoutId > 0) {
this._async.clearTimeout(this._lastReadOnlyAutoCompleteChangeTimeoutId);
this._lastReadOnlyAutoCompleteChangeTimeoutId = -1;
updatedValue = currentPendingValue + updatedValue;
}
var originalUpdatedValue = updatedValue;
updatedValue = updatedValue.toLocaleLowerCase();
// If autoComplete is on, attempt to find a match where the text of an option starts with the updated value
var items = currentOptions.map(function (item, index) { return tslib_1.__assign({}, item, { index: index }); }).filter(function (option) { return option.itemType !== SelectableOption_Props_1.SelectableOptionMenuItemType.Header && option.itemType !== SelectableOption_Props_1.SelectableOptionMenuItemType.Divider; }).filter(function (option) { return option.text.toLocaleLowerCase().indexOf(updatedValue) === 0; });
// If we found a match, udpdate the state
if (items.length > 0) {
this._setPendingInfo(originalUpdatedValue, items[0].index, items[0].text);
}
// Schedule a timeout to clear the pending value after the timeout span
this._lastReadOnlyAutoCompleteChangeTimeoutId =
this._async.setTimeout(function () { _this._lastReadOnlyAutoCompleteChangeTimeoutId = -1; }, this._readOnlyPendingAutoCompleteTimeout);
return;
}
}
// If we get here, either autoComplete is on or we did not find a match with autoComplete on.
// Remember we are not allowing freeform, so at this point, if we have a pending valid value index
// use that; otherwise use the selectedIndex
var index = currentPendingValueValidIndex >= 0 ? currentPendingValueValidIndex : selectedIndex;
// Since we are not allowing freeform, we need to
// set both the pending and suggested values/index
// to allow us to select all content in the input to
// give the illusion that we are readonly (e.g. freeform off)
this._setPendingInfoFromIndex(index);
};
/**
* Walk along the options starting at the index, stepping by the delta (positive or negative)
* looking for the next valid selectable index (e.g. skipping headings and dividers)
* @param index - the index to get the next selectable index from
* @param delta - optional delta to step by when finding the next index, defaults to 0
* @returns {number} - the next valid selectable index. If the new index is outside of the bounds,
* it will snap to the edge of the options array. If delta == 0 and the given index is not selectable
*/
ComboBox.prototype._getNextSelectableIndex = function (index, searchDirection) {
var currentOptions = this.state.currentOptions;
var newIndex = index + searchDirection;
newIndex = Math.max(0, Math.min(currentOptions.length - 1, newIndex));
var option = currentOptions[newIndex];
// attempt to skip headers and dividers
if ((option.itemType === SelectableOption_Props_1.SelectableOptionMenuItemType.Header ||
option.itemType === SelectableOption_Props_1.SelectableOptionMenuItemType.Divider)) {
// Should we continue looking for an index to select?
if (searchDirection !== SearchDirection.none &&
((newIndex !== 0 && searchDirection < SearchDirection.none) ||
(newIndex !== currentOptions.length - 1 && searchDirection > SearchDirection.none))) {
newIndex = this._getNextSelectableIndex(newIndex, searchDirection);
}
else {
// If we cannot perform a useful search just return the index we were given
return index;
}
}
// We have the next valid selectable index, return it
return newIndex;
};
/**
* Set the selected index. Note, this is
* the "real" selected index, not the pending selected index
* @param index - the index to set (or the index to set from if a search direction is provided)
* @param searchDirection - the direction to search along the options from the given index
*/
ComboBox.prototype._setSelectedIndex = function (index, searchDirection) {
if (searchDirection === void 0) { searchDirection = SearchDirection.none; }
var onChanged = this.props.onChanged;
var _a = this.state, selectedIndex = _a.selectedIndex, currentOptions = _a.currentOptions;
// Find the next selectable index, if searchDirection is none
// we will get our starting index back
index = this._getNextSelectableIndex(index, searchDirection);
// Are we at a new index? If so, update the state, otherwise
// there is nothing to do
if (index !== selectedIndex) {
var option = currentOptions[index];
// Set the selected option
this.setState({
selectedIndex: index
});
// Did the creator give us an onChanged callback?
if (onChanged) {
onChanged(option, index);
}
// if we have a new selected index,
// clear all of the pending info
this._clearPendingInfo();
}
};
/**
* Focus (and select) the content of the input
* and set the focused state
*/
ComboBox.prototype._select = function () {
this._comboBox.inputElement.select();
if (!this.state.focused) {
this.setState({ focused: true });
}
};
/**
* Callback issued when the options should be resolved, if they have been updated or
* if they need to be passed in the first time. This only does work if an onResolveOptions
* callback was passed in
*/
ComboBox.prototype._onResolveOptions = function () {
var _this = this;
if (this.props.onResolveOptions) {
// get the options
var newOptions = this.props.onResolveOptions(tslib_1.__assign({}, this.state.currentOptions));
// Check to see if the returned value is an array, if it is update the state
// If the returned value is not an array then check to see if it's a promise or PromiseLike. If it is then resolve it asynchronously.
if (Array.isArray(newOptions)) {
this.setState({
currentOptions: newOptions
});
}
else if (newOptions && newOptions.then) {
// Ensure that the promise will only use the callback if it was the most recent one
// and update the state when the promise returns
var promise_1 = this._currentPromise = newOptions;
promise_1.then(function (newOptionsFromPromise) {
if (promise_1 === _this._currentPromise) {
_this.setState({
currentOptions: newOptionsFromPromise
});
}
});
}
}
};
/**
* OnBlur handler. Set the focused state to false
* and submit any pending value
*/
ComboBox.prototype._onBlur = function () {
if (this.state.focused) {
this.setState({ focused: false });
this._submitPendingValue();
}
};
/**
* Submit a pending value if there is one
*/
ComboBox.prototype._submitPendingValue = function () {
var _a = this.props, onChanged = _a.onChanged, allowFreeform = _a.allowFreeform;
var _b = this.state, currentPendingValue = _b.currentPendingValue, currentPendingValueValidIndex = _b.currentPendingValueValidIndex, currentOptions = _b.currentOptions;
// If we allow freeform and we have a pending value, we
// need to handle that
if (allowFreeform && currentPendingValue !== '') {
// Check to see if the user typed an exact match
if (currentPendingValueValidIndex >= 0) {
var pendingOptionText = currentOptions[currentPendingValueValidIndex].text.toLocaleLowerCase();
// By exact match, that means: our pending value is the same as the the pending option text OR
// the peding option starts with the pending value and we have an "autoComplete" selection
// where the total lenght is equal to pending option length; update the state
if (currentPendingValue.toLocaleLowerCase() === pendingOptionText ||
(pendingOptionText.indexOf(currentPendingValue.toLocaleLowerCase()) === 0 &&
this._comboBox.isValueSelected &&
currentPendingValue.length + (this._comboBox.selectionEnd - this._comboBox.selectionStart) === pendingOptionText.length)) {
this._setSelectedIndex(currentPendingValueValidIndex);
this._clearPendingInfo();
return;
}
}
// Create a new option
var newOption = { key: currentPendingValue, text: currentPendingValue };
var newOptions = currentOptions.concat([newOption]);
var newSelectedIndex = this._getSelectedIndex(newOptions, currentPendingValue);
this.setState({
currentOptions: newOptions,
selectedIndex: newSelectedIndex
});
if (onChanged) {
onChanged(null, null, currentPendingValue);
}
}
else if (currentPendingValueValidIndex >= 0) {
// Since we are not allowing freeform, we must have a matching
// to be able to update state
this._setSelectedIndex(currentPendingValueValidIndex);
}
// Finally, clear the pending info
this._clearPendingInfo();
};
// Render Callout container and pass in list
ComboBox.prototype._onRenderContainer = function (props) {
var _a = props.onRenderList, onRenderList = _a === void 0 ? this._onRenderList : _a, calloutProps = props.calloutProps;
return (React.createElement(Callout_1.Callout, tslib_1.__assign({ isBeakVisible: false, gapSpace: 0, doNotLayer: false, directionalHint: DirectionalHint_1.DirectionalHint.bottomLeftEdge, directionalHintFixed: true }, calloutProps, { className: Utilities_1.css('ms-ComboBox-callout', styles.callout, calloutProps ? calloutProps.className : undefined), targetElement: this._comboBoxWrapper, onDismiss: this._onDismiss, setInitialFocus: false }),
React.createElement("div", { ref: this._resolveRef('_comboBoxMenu'), style: { width: this._comboBoxWrapper.clientWidth - 2 } }, onRenderList(tslib_1.__assign({}, props), this._onRenderList))));
};
// Render List of items
ComboBox.prototype._onRenderList = function (props) {
var _this = this;
var _a = this.props.onRenderItem, onRenderItem = _a === void 0 ? this._onRenderItem : _a;
var id = this._id;
var selectedIndex = this.state.selectedIndex;
return (React.createElement("div", { id: id + '-list', className: Utilities_1.css('ms-ComboBox-items', styles.items), "aria-labelledby": id + '-label', role: 'listbox' }, this.state.currentOptions.map(function (item, index) { return onRenderItem(tslib_1.__assign({}, item, { index: index }), _this._onRenderItem); })));
};
// Render items
ComboBox.prototype._onRenderItem = function (item) {
switch (item.itemType) {
case SelectableOption_Props_1.SelectableOptionMenuItemType.Divider:
return this._renderSeparator(item);
case SelectableOption_Props_1.SelectableOptionMenuItemType.Header:
return this._renderHeader(item);
default:
return this._renderOption(item);
}
};
// Render separator
ComboBox.prototype._renderSeparator = function (item) {
var index = item.index, key = item.key;
if (index > 0) {
return React.createElement("div", { role: 'separator', key: key, className: Utilities_1.css('ms-ComboBox-divider', styles.divider) });
}
return null;
};
ComboBox.prototype._renderHeader = function (item) {
var _a = this.props.onRenderOption, onRenderOption = _a === void 0 ? this._onRenderOption : _a;
return (React.createElement("div", { key: item.key, className: Utilities_1.css('ms-ComboBox-header', styles.header), role: 'header' }, onRenderOption(item, this._onRenderOption)));
};
// Render menu item
ComboBox.prototype._renderOption = function (item) {
var _this = this;
var _a = this.props.onRenderOption, onRenderOption = _a === void 0 ? this._onRenderOption : _a;
var id = this._id;
var isSelected = this._isOptionSelected(item.index);
return (React.createElement(Button_1.CommandButton, { id: id + '-list' + item.index, key: item.key, "data-index": item.index, className: Utilities_1.css('ms-ComboBox-item', styles.item, (_b = {},
_b['is-selected ' + styles.itemIsSelected] = isSelected,
_b['is-disabled ' + styles.itemIsDisabled] = this.props.disabled === true,
_b)), onClick: function () { return _this._onItemClick(item.index); }, role: 'option', "aria-selected": isSelected ? 'true' : 'false', ariaLabel: item.text },
" ",
React.createElement("span", { ref: this._resolveRef(isSelected ? '_selectedElement' : '') }, onRenderOption(item, this._onRenderOption))));
var _b;
};
/**
* Use the current valid pending index if it exists OR
* we do not have a valid index and we currently have a pending input value,
* otherwise use the selected index
* */
ComboBox.prototype._isOptionSelected = function (index) {
var _a = this.state, currentPendingValueValidIndex = _a.currentPendingValueValidIndex, currentPendingValue = _a.currentPendingValue, selectedIndex = _a.selectedIndex;
return ((currentPendingValueValidIndex >= 0 || currentPendingValue !== '') ?
currentPendingValueValidIndex === index : selectedIndex === index);
};
/**
* Scroll the selected element into view
*/
ComboBox.prototype._scrollIntoView = function () {
if (this._selectedElement) {
var alignToTop = true;
if (this._comboBoxMenu.offsetParent) {
var scrollableParentRect = this._comboBoxMenu.offsetParent.getBoundingClientRect();
var selectedElementRect = this._selectedElement.offsetParent.getBoundingClientRect();
if (scrollableParentRect.top + scrollableParentRect.height <= selectedElementRect.top) {
alignToTop = false;
}
}
this._selectedElement.offsetParent.scrollIntoView(alignToTop);
}
};
// Render content of item
ComboBox.prototype._onRenderOption = function (item) {
return React.createElement("span", { className: Utilities_1.css('ms-ComboBox-optionText', styles.optionText) }, item.text);
};
/**
* Click handler for the menu items
* to select the item and also close the menu
* @param index - the index of the item that was clicked
*/
ComboBox.prototype._onItemClick = function (index) {
this._setSelectedIndex(index);
this.setState({
isOpen: false
});
};
/**
* Handles dismissing (cancelling) the menu
*/
ComboBox.prototype._onDismiss = function () {
// reset the selected index
// to the last valud state
this._resetSelectedIndex();
// close the menu and focus the input
this.setState({ isOpen: false });
this._comboBox.focus();
};
/**
* Get the index of the option that is marked as selected
* @param options - the comboBox options
* @param selectedKey - the known selected key to find
* @returns {number} - the index of the selected option, -1 if not found
*/
ComboBox.prototype._getSelectedIndex = function (options, selectedKey) {
return Utilities_1.findIndex(options, (function (option) { return (option.isSelected || option.selected || (selectedKey != null) && option.key === selectedKey); }));
};
/**
* Reset the selected index by clearing the
* input (of any pending text), clearing the pending state,
* and setting the suggested display value to the last
* selected state text
*/
ComboBox.prototype._resetSelectedIndex = function () {
var _a = this.state, selectedIndex = _a.selectedIndex, currentOptions = _a.currentOptions;
this._comboBox.clear();
this._clearPendingInfo();
if (selectedIndex > 0 && selectedIndex < currentOptions.length) {
this.setState({
suggestedDisplayValue: currentOptions[selectedIndex].text
});
}
};
/**
* Clears the pending info state
*/
ComboBox.prototype._clearPendingInfo = function () {
this._setPendingInfo('' /* suggestedDisplayValue */, -1 /* currentPendingValueValidIndex */, '' /* currentPendingValue */);
};
/**
* Set the pending info
* @param currentPendingValue - new pending value to set
* @param currentPendingValueValidIndex - new pending value index to set
* @param suggestedDisplayValue - new suggest display value to set
*/
ComboBox.prototype._setPendingInfo = function (currentPendingValue, currentPendingValueValidIndex, suggestedDisplayValue) {
this.setState({
currentPendingValue: currentPendingValue,
currentPendingValueValidIndex: currentPendingValueValidIndex,
suggestedDisplayValue: suggestedDisplayValue
});
};
/**
* Set the pending info from the given index
* @param index - the index to set the pending info from
*/
ComboBox.prototype._setPendingInfoFromIndex = function (index) {
var currentOptions = this.state.currentOptions;
if (index > 0 && index < currentOptions.length) {
var option = currentOptions[index];
this._setPendingInfo(option.text, index, option.text);
}
else {
this._clearPendingInfo();
}
};
/**
* Sets either the pending info or the
* selected index depending of if the comboBox is open
* @param index - the index to search from
* @param searchDirection - the direction to search
*/
ComboBox.prototype._setInfoForIndexAndDirection = function (index, searchDirection) {
var _a = this.state, isOpen = _a.isOpen, selectedIndex = _a.selectedIndex;
if (isOpen) {
index = this._getNextSelectableIndex(index, searchDirection);
this._setPendingInfoFromIndex(index);
}
else {
this._setSelectedIndex(selectedIndex, searchDirection);
}
};
/**
* Handle keydown on the input
* @param ev - The keyboard event that was fired
*/
ComboBox.prototype._onInputKeyDown = function (ev) {
var _a = this.props, disabled = _a.disabled, allowFreeform = _a.allowFreeform, autoComplete = _a.autoComplete;
var _b = this.state, isOpen = _b.isOpen, currentPendingValueValidIndex = _b.currentPendingValueValidIndex, selectedIndex = _b.selectedIndex, currentOptions = _b.currentOptions;
if (disabled) {
this._handleInputWhenDisabled(ev);
return;
}
var index = currentPendingValueValidIndex >= 0 ? currentPendingValueValidIndex : selectedIndex;
switch (ev.which) {
case Utilities_1.KeyCodes.enter:
// On enter submit the pending value
this._submitPendingValue();
// if we are open or
// if we are not allowing freeform or
// our we have no pending value
// and no valid pending index
// flip the open state
if ((isOpen ||
((!allowFreeform ||
this.state.currentPendingValue === undefined ||
this.state.currentPendingValue === null ||
this.state.currentPendingValue.length <= 0) &&
this.state.currentPendingValueValidIndex < 0))) {
this.setState({
isOpen: !isOpen
});
}
// Allow TAB to propigate
if (ev.which === Utilities_1.KeyCodes.tab) {
return;
}
break;
case Utilities_1.KeyCodes.tab:
// On enter submit the pending value
this._submitPendingValue();
// If we are not allowing freeform
// or the comboBox is open, flip the open state
if (isOpen) {
this.setState({
isOpen: !isOpen
});
}
// Allow TAB to propigate
return;
case Utilities_1.KeyCodes.escape:
// reset the selected index
this._resetSelectedIndex();
// Close the menu if opened
if (isOpen) {
this.setState({
isOpen: false
});
}
break;
case Utilities_1.KeyCodes.up:
// Go to the previous option
this._setInfoForIndexAndDirection(index, SearchDirection.backward);
break;
case Utilities_1.KeyCodes.down:
// Expand the comboBox on ALT + DownArrow
if (ev.altKey || ev.metaKey) {
this.setState({ isOpen: true });
}
else {
// Got to the next option
this._setInfoForIndexAndDirection(index, SearchDirection.forward);
}
break;
case Utilities_1.KeyCodes.home:
case Utilities_1.KeyCodes.end:
if (allowFreeform) {
return;
}
// Set the initial values to respond to HOME
// which goes to the first selectable option
index = -1;
var directionToSearch = SearchDirection.forward;
// If end, update the values to respond to END
// which goes to the last selectable option
if (ev.which === Utilities_1.KeyCodes.end) {
index = currentOptions.length;
directionToSearch = SearchDirection.backward;
}
this._setInfoForIndexAndDirection(index, directionToSearch);
break;
case Utilities_1.KeyCodes.space:
// event handled in _onComboBoxKeyUp
if (!allowFreeform && !autoComplete) {
break;
}
default:
// are we processing a function key? if so bail out
if (ev.which >= 112 /* F1 */ && ev.which <= 123 /* F12 */) {
return;
}
// If we get here and we got either and ALT key
// or meta key and we are current open, let's close the menu
if ((ev.altKey || ev.metaKey) && isOpen) {
this.setState({ isOpen: !isOpen });
}
// If we are not allowing freeform and
// allowing autoComplete, handle the input here
// since we have marked the input as readonly
if (!allowFreeform && autoComplete) {
this._onInputChange(String.fromCharCode(ev.which));
break;
}
// allow the key to propigate by default
return;
}
ev.stopPropagation();
ev.preventDefault();
};
/**
* Handle keyup on the input
* @param ev - the keyboard event that was fired
*/
ComboBox.prototype._onInputKeyUp = function (ev) {
var _a = this.props, disabled = _a.disabled, allowFreeform = _a.allowFreeform, autoComplete = _a.autoComplete;
if (disabled) {
this._handleInputWhenDisabled(ev);
return;
}
switch (ev.which) {
case Utilities_1.KeyCodes.space:
// If we are not allowing freeform and are not autoComplete
// make space expand/collapse the comboBox
// and allow the event to propagate
if (!allowFreeform && !autoComplete) {
this.setState({
isOpen: !this.state.isOpen
});
return;
}
break;
default:
return;
}
ev.stopPropagation();
ev.preventDefault();
};
/**
* Handle dismissing the menu and
* eating the required key event when disabled
* @param ev - the keyboard event that was fired
*/
ComboBox.prototype._handleInputWhenDisabled = function (ev) {
// If we are disabled, close the menu (if needed)
// and eat all keystokes other than TAB or ESC
if (this.props.disabled) {
if (this.state.isOpen) {
this.setState({ isOpen: false });
}
// When disabled stop propagation and prevent default
// of the event unless we have a tab, escape, or function key
if (ev !== null &&
ev.which !== Utilities_1.KeyCodes.tab &&
ev.which !== Utilities_1.KeyCodes.escape &&
(ev.which < 112 /* F1 */ || ev.which > 123 /* F12 */)) {
ev.stopPropagation();
ev.preventDefault();
}
}
};
/**
* Click handler for the button of the comboBox
* and the input when not allowing freeform. This
* toggles the expand/collapse state of the comboBox (if enbled)
*/
ComboBox.prototype._onComboBoxClick = function () {
var disabled = this.props.disabled;
var isOpen = this.state.isOpen;
if (!disabled) {
this.setState({
isOpen: !isOpen
});
}
};
return ComboBox;
}(Utilities_1.BaseComponent));
ComboBox.defaultProps = {
options: [],
allowFreeform: false,
autoComplete: true,
buttonIconProps: { iconName: 'ChevronDown' }
};
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "focus", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onUpdateValueInAutoFillWillReceiveProps", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onShouldSelectFullInputValueInAutoFillComponentDidUpdate", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_getVisibleValue", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onInputChange", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_select", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onResolveOptions", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onBlur", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onRenderContainer", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onRenderList", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onRenderItem", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_renderOption", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onRenderOption", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onDismiss", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onInputKeyDown", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onInputKeyUp", null);
tslib_1.__decorate([
Utilities_1.autobind
], ComboBox.prototype, "_onComboBoxClick", null);
exports.ComboBox = ComboBox;
});
//# sourceMappingURL=ComboBox.js.map