UNPKG

office-ui-fabric-react

Version:

Reusable React components for building experiences for Office 365.

870 lines (868 loc) • 48.6 kB
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