UNPKG

office-ui-fabric-react

Version:

Reusable React components for building experiences for Office 365.

855 lines (854 loc) • 74.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var tslib_1 = require("tslib"); var React = require("react"); var Button_1 = require("../../Button"); var Callout_1 = require("../../Callout"); var Checkbox_1 = require("../../Checkbox"); var KeytipData_1 = require("../../KeytipData"); var Label_1 = require("../../Label"); var Utilities_1 = require("../../Utilities"); var SelectableOption_types_1 = require("../../utilities/selectableOption/SelectableOption.types"); var index_1 = require("../Autofill/index"); var ComboBox_classNames_1 = require("./ComboBox.classNames"); var ComboBox_styles_1 = require("./ComboBox.styles"); var SearchDirection; (function (SearchDirection) { SearchDirection[SearchDirection["backward"] = -1] = "backward"; SearchDirection[SearchDirection["none"] = 0] = "none"; SearchDirection[SearchDirection["forward"] = 1] = "forward"; })(SearchDirection || (SearchDirection = {})); var HoverStatus; (function (HoverStatus) { // This is used when the user was hovering // and has since moused out of the menu items HoverStatus[HoverStatus["clearAll"] = -2] = "clearAll"; // This is the default "normal" state // when no hover has happened or a hover is in progress HoverStatus[HoverStatus["default"] = -1] = "default"; })(HoverStatus || (HoverStatus = {})); var ScrollIdleDelay = 250 /* ms */; var TouchIdleDelay = 500; /* ms */ // This is used to clear any pending autocomplete // text (used when autocomplete is true and allowFreeform is false) var ReadOnlyPendingAutoCompleteTimeout = 1000 /* ms */; // Internal class that is used to wrap all ComboBox options // This is used to customize when we want to rerender components, // so we don't rerender every option every time render is executed var ComboBoxOptionWrapper = /** @class */ (function (_super) { tslib_1.__extends(ComboBoxOptionWrapper, _super); function ComboBoxOptionWrapper() { return _super !== null && _super.apply(this, arguments) || this; } ComboBoxOptionWrapper.prototype.render = function () { return this.props.render(); }; ComboBoxOptionWrapper.prototype.shouldComponentUpdate = function (newProps) { // The render function will always be different, so we ignore that prop return !Utilities_1.shallowCompare(tslib_1.__assign({}, this.props, { render: undefined }), tslib_1.__assign({}, newProps, { render: undefined })); }; return ComboBoxOptionWrapper; }(React.Component)); var ComboBox = /** @class */ (function (_super) { tslib_1.__extends(ComboBox, _super); function ComboBox(props) { var _this = _super.call(this, props) || this; _this._root = Utilities_1.createRef(); // The input aspect of the comboBox _this._autofill = Utilities_1.createRef(); // The wrapping div of the input and button _this._comboBoxWrapper = Utilities_1.createRef(); // The callout element _this._comboBoxMenu = Utilities_1.createRef(); // The menu item element that is currently selected _this._selectedElement = Utilities_1.createRef(); /** * Set focus on the input */ _this.focus = function (shouldOpenOnFocus) { if (_this._autofill.current) { _this._autofill.current.focus(); if (shouldOpenOnFocus) { _this.setState({ isOpen: true }); } } }; /** * Close menu callout if it is open */ _this.dismissMenu = function () { var isOpen = _this.state.isOpen; isOpen && _this.setState({ isOpen: false }); }; /** * componentWillReceiveProps handler for the auto fill component * Checks/updates the iput value to set, if needed * @param { IAutofillProps } defaultVisibleValue - the defaultVisibleValue that got passed * in to the auto fill's componentWillReceiveProps * @returns { string } - the updated value to set, if needed */ _this._onUpdateValueInAutofillWillReceiveProps = function () { var comboBox = _this._autofill.current; if (!comboBox) { return null; } if (comboBox.value === null || comboBox.value === undefined) { return null; } if (_this._currentVisibleValue && _this._currentVisibleValue !== '' && comboBox.value !== _this._currentVisibleValue) { return _this._currentVisibleValue; } return 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 */ _this._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 */ _this._getVisibleValue = function () { var _a = _this.props, text = _a.text, value = _a.value, allowFreeform = _a.allowFreeform, autoComplete = _a.autoComplete; var _b = _this.state, selectedIndices = _b.selectedIndices, currentPendingValueValidIndex = _b.currentPendingValueValidIndex, currentOptions = _b.currentOptions, currentPendingValue = _b.currentPendingValue, suggestedDisplayValue = _b.suggestedDisplayValue, isOpen = _b.isOpen, focused = _b.focused; var currentPendingIndexValid = _this._indexWithinBounds(currentOptions, currentPendingValueValidIndex); // If the user passed is a value prop, use that // unless we are open and have a valid current pending index if (!(isOpen && currentPendingIndexValid) && (text && !currentPendingValue)) { return text; } if (!(isOpen && currentPendingIndexValid) && (value && !currentPendingValue)) { return value; } // Values to display in the BaseAutoFill area var displayValues = []; if (_this.props.multiSelect) { // MUlti-select if (focused) { var index = -1; if (autoComplete === 'on' && currentPendingIndexValid) { index = currentPendingValueValidIndex; } displayValues.push(currentPendingValue !== '' ? currentPendingValue : (_this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : '')); } else { for (var idx = 0; selectedIndices && (idx < selectedIndices.length); idx++) { var index = selectedIndices[idx]; displayValues.push(_this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : suggestedDisplayValue); } } } else { // Single-select var index = _this._getFirstSelectedIndex(); 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 === 'on' && currentPendingIndexValid) { 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) displayValues.push(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 (currentPendingIndexValid && autoComplete === 'on') { // If autoComplete is on, return the // raw pending value, otherwise remember // the matched option's index index = currentPendingValueValidIndex; displayValues.push(currentPendingValue); } else { displayValues.push(_this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : suggestedDisplayValue); } } } // If we have a valid index then return the text value of that option, // otherwise return the suggestedDisplayValue var displayString = ''; for (var idx = 0; idx < displayValues.length; idx++) { if (idx > 0) { displayString += ', '; } displayString += displayValues[idx]; } return displayString; }; /** * Handler for typing changes on the input * @param updatedValue - the newly changed value */ _this._onInputChange = function (updatedValue) { if (_this.props.disabled) { _this._handleInputWhenDisabled(null /* event */); return; } _this.props.allowFreeform ? _this._processInputChangeWithFreeform(updatedValue) : _this._processInputChangeWithoutFreeform(updatedValue); }; /** * Focus (and select) the content of the input * and set the focused state */ _this._select = function () { if (_this._autofill.current && _this._autofill.current.inputElement) { _this._autofill.current.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 */ _this._onResolveOptions = function () { 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 */ _this._onBlur = function (event) { // Do nothing if the blur is coming from something // inside the comboBox root or the comboBox menu since // it we are not really bluring from the whole comboBox if (event.relatedTarget && (_this._root.current && _this._root.current.contains(event.relatedTarget) || _this._comboBoxMenu.current && _this._comboBoxMenu.current.contains(event.relatedTarget))) { event.preventDefault(); event.stopPropagation(); return; } if (_this.state.focused) { _this.setState({ focused: false }); if (!_this.props.multiSelect) { _this._submitPendingValue(event); } } }; // Render Callout container and pass in list _this._onRenderContainer = function (props) { var onRenderList = props.onRenderList, calloutProps = props.calloutProps, dropdownWidth = props.dropdownWidth, _a = props.onRenderLowerContent, onRenderLowerContent = _a === void 0 ? _this._onRenderLowerContent : _a, useComboBoxAsMenuWidth = props.useComboBoxAsMenuWidth; return (React.createElement(Callout_1.Callout, tslib_1.__assign({ isBeakVisible: false, gapSpace: 0, doNotLayer: false, directionalHint: 4 /* bottomLeftEdge */, directionalHintFixed: true }, calloutProps, { className: Utilities_1.css(_this._classNames.callout, calloutProps ? calloutProps.className : undefined), target: _this._comboBoxWrapper.current, onDismiss: _this._onDismiss, onScroll: _this._onScroll, setInitialFocus: false, calloutWidth: useComboBoxAsMenuWidth && _this._comboBoxWrapper.current ? _this._comboBoxWrapper.current.clientWidth + 2 : dropdownWidth }), React.createElement("div", { className: _this._classNames.optionsContainerWrapper, ref: _this._comboBoxMenu }, onRenderList(tslib_1.__assign({}, props), _this._onRenderList)), onRenderLowerContent(_this.props, _this._onRenderLowerContent))); }; // Render List of items _this._onRenderList = function (props) { var onRenderItem = props.onRenderItem, options = props.options; var id = _this._id; return (React.createElement("div", { id: id + '-list', className: _this._classNames.optionsContainer, "aria-labelledby": id + '-label', role: 'listbox' }, options.map(function (item) { return onRenderItem(item, _this._onRenderItem); }))); }; // Render items _this._onRenderItem = function (item) { switch (item.itemType) { case SelectableOption_types_1.SelectableOptionMenuItemType.Divider: return _this._renderSeparator(item); case SelectableOption_types_1.SelectableOptionMenuItemType.Header: return _this._renderHeader(item); default: return _this._renderOption(item); } }; // Default _onRenderLowerContent function returns nothing _this._onRenderLowerContent = function () { return null; }; _this._renderOption = function (item) { var _a = _this.props.onRenderOption, onRenderOption = _a === void 0 ? _this._onRenderOptionContent : _a; var id = _this._id; var isSelected = _this._isOptionSelected(item.index); var optionStyles = _this._getCurrentOptionStyles(item); var checkboxStyles = function () { return optionStyles; }; var getOptionComponent = function () { return (!_this.props.multiSelect ? (React.createElement(Button_1.CommandButton, { id: id + '-list' + item.index, key: item.key, "data-index": item.index, styles: _this._getCurrentOptionStyles(item), checked: isSelected, className: 'ms-ComboBox-option', onClick: _this._onItemClick(item.index), onMouseEnter: _this._onOptionMouseEnter.bind(_this, item.index), onMouseMove: _this._onOptionMouseMove.bind(_this, item.index), onMouseLeave: _this._onOptionMouseLeave, role: 'option', "aria-selected": isSelected ? 'true' : 'false', ariaLabel: _this._getPreviewText(item), disabled: item.disabled }, " ", React.createElement("span", { ref: isSelected ? _this._selectedElement : undefined }, onRenderOption(item, _this._onRenderOptionContent)))) : (React.createElement(Checkbox_1.Checkbox, { id: id + '-list' + item.index, ariaLabel: _this._getPreviewText(item), key: item.key, "data-index": item.index, styles: checkboxStyles, className: 'ms-ComboBox-option', "data-is-focusable": true, onChange: _this._onItemClick(item.index), label: item.text, role: 'option', "aria-selected": isSelected ? 'true' : 'false', checked: isSelected }, onRenderOption(item, _this._onRenderOptionContent)))); }; return (React.createElement(ComboBoxOptionWrapper, { key: item.key, index: item.index, disabled: item.disabled, isSelected: isSelected, text: item.text, render: getOptionComponent })); }; /** * Scroll handler for the callout to make sure the mouse events * for updating focus are not interacting during scroll */ _this._onScroll = function () { if (!_this._isScrollIdle && _this._scrollIdleTimeoutId !== undefined) { _this._async.clearTimeout(_this._scrollIdleTimeoutId); _this._scrollIdleTimeoutId = undefined; } else { _this._isScrollIdle = false; } _this._scrollIdleTimeoutId = _this._async.setTimeout(function () { _this._isScrollIdle = true; }, ScrollIdleDelay); }; _this._onRenderOptionContent = function (item) { var optionClassNames = ComboBox_classNames_1.getComboBoxOptionClassNames(_this._getCurrentOptionStyles(item)); return React.createElement("span", { className: optionClassNames.optionText }, item.text); }; /** * Handles dismissing (cancelling) the menu */ _this._onDismiss = function () { // reset the selected index // to the last valud state _this._resetSelectedIndex(); // close the menu and focus the input _this.setState({ isOpen: false }); if (_this._autofill.current && _this._focusInputAfterClose) { _this._autofill.current.focus(); } }; /** * Handle keydown on the input * @param ev - The keyboard event that was fired */ _this._onInputKeyDown = function (ev) { var _a = _this.props, disabled = _a.disabled, allowFreeform = _a.allowFreeform, autoComplete = _a.autoComplete; var _b = _this.state, isOpen = _b.isOpen, currentOptions = _b.currentOptions, currentPendingValueValidIndexOnHover = _b.currentPendingValueValidIndexOnHover; if (disabled) { _this._handleInputWhenDisabled(ev); return; } var index = _this._getPendingSelectedIndex(false /* includeCurrentPendingValue */); switch (ev.which) { case 13 /* enter */: _this._submitPendingValue(ev); if (_this.props.multiSelect && isOpen) { _this.setState({ currentPendingValueValidIndex: index }); } else { // On enter submit the pending value if ((isOpen || ((!allowFreeform || _this.state.currentPendingValue === undefined || _this.state.currentPendingValue === null || _this.state.currentPendingValue.length <= 0) && _this.state.currentPendingValueValidIndex < 0))) { // 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 _this.setState({ isOpen: !isOpen }); } } break; case 9 /* tab */: // On enter submit the pending value if (!_this.props.multiSelect) { _this._submitPendingValue(ev); } // If we are not allowing freeform // or the comboBox is open, flip the open state if (isOpen) { _this._setOpenStateAndFocusOnClose(!isOpen, false /* focusInputAfterClose */); } // Allow TAB to propigate return; case 27 /* escape */: // reset the selected index _this._resetSelectedIndex(); // Close the menu if opened if (isOpen) { _this.setState({ isOpen: false }); } else { return; } break; case 38 /* up */: // if we are in clearAll state (e.g. the user as hovering // and has since mousedOut of the menu items), // go to the last index if (currentPendingValueValidIndexOnHover === HoverStatus.clearAll) { index = _this.state.currentOptions.length; } if ((ev.altKey || ev.metaKey) && isOpen) { _this._setOpenStateAndFocusOnClose(!isOpen, true /* focusInputAfterClose */); return; } // Go to the previous option _this._setPendingInfoFromIndexAndDirection(index, SearchDirection.backward); break; case 40 /* down */: // Expand the comboBox on ALT + DownArrow if (ev.altKey || ev.metaKey) { _this._setOpenStateAndFocusOnClose(true /* isOpen */, true /* focusInputAfterClose */); } else { // if we are in clearAll state (e.g. the user as hovering // and has since mousedOut of the menu items), // go to the first index if (currentPendingValueValidIndexOnHover === HoverStatus.clearAll) { index = -1; } // Got to the next option _this._setPendingInfoFromIndexAndDirection(index, SearchDirection.forward); } break; case 36 /* home */: case 35 /* 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 === 35 /* end */) { index = currentOptions.length; directionToSearch = SearchDirection.backward; } _this._setPendingInfoFromIndexAndDirection(index, directionToSearch); break; case 32 /* space */: // event handled in _onComboBoxKeyUp if (!allowFreeform && autoComplete === 'off') { 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._setOpenStateAndFocusOnClose(!isOpen, true /* focusInputAfterClose */); } // If we are not allowing freeform and // allowing autoComplete, handle the input here // since we have marked the input as readonly if (!allowFreeform && autoComplete === 'on') { _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 */ _this._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 32 /* 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 === 'off') { var isOpen = _this.state.isOpen; _this._setOpenStateAndFocusOnClose(!isOpen, !!isOpen); return; } break; default: return; } ev.stopPropagation(); ev.preventDefault(); }; _this._onOptionMouseLeave = function () { if (!_this._isScrollIdle) { return; } _this.setState({ currentPendingValueValidIndexOnHover: HoverStatus.clearAll }); }; /** * 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) */ _this._onComboBoxClick = function () { var disabled = _this.props.disabled; var isOpen = _this.state.isOpen; if (!disabled) { _this._setOpenStateAndFocusOnClose(!isOpen, false /* focusInputAfterClose */); } }; /** * Click handler for the autofill. */ _this._onAutofillClick = function () { if (_this.props.allowFreeform) { _this.focus(_this.state.isOpen || _this._processingTouch); } else { _this._onComboBoxClick(); } }; _this._onTouchStart = function () { if (_this._comboBoxWrapper.value && !('onpointerdown' in _this._comboBoxWrapper)) { _this._handleTouchAndPointerEvent(); } }; _this._onPointerDown = function (ev) { if (ev.pointerType === 'touch') { _this._handleTouchAndPointerEvent(); ev.preventDefault(); ev.stopImmediatePropagation(); } }; _this._warnMutuallyExclusive({ 'defaultSelectedKey': 'selectedKey', 'text': 'defaultSelectedKey', 'value': 'defaultSelectedKey', 'selectedKey': 'value', 'dropdownWidth': 'useComboBoxAsMenuWidth', }); _this._warnDeprecations({ 'value': 'text' }); _this._id = props.id || Utilities_1.getId('ComboBox'); var selectedKeys = _this._getSelectedKeys(props.defaultSelectedKey, props.selectedKey); _this._isScrollIdle = true; _this._processingTouch = false; var initialSelectedIndices = _this._getSelectedIndices(props.options, selectedKeys); _this.state = { isOpen: false, selectedIndices: initialSelectedIndices, focused: false, suggestedDisplayValue: '', currentOptions: _this.props.options, currentPendingValueValidIndex: -1, currentPendingValue: '', currentPendingValueValidIndexOnHover: HoverStatus.default }; return _this; } ComboBox.prototype.componentDidMount = function () { if (this._comboBoxWrapper.current) { // hook up resolving the options if needed on focus this._events.on(this._comboBoxWrapper.current, 'focus', this._onResolveOptions, true); if ('onpointerdown' in this._comboBoxWrapper.current) { // For ComboBoxes, touching anywhere in the combo box should drop the dropdown, including the input element. // This gives more hit target space for touch environments. We're setting the onpointerdown here, because React // does not support Pointer events yet. this._events.on(this._comboBoxWrapper.value, 'pointerdown', this._onPointerDown, true); } } }; ComboBox.prototype.componentWillReceiveProps = function (newProps) { // Update the selectedIndex and currentOptions state if // the selectedKey, value, or options have changed if (newProps.selectedKey !== this.props.selectedKey || newProps.text !== this.props.text || newProps.value !== this.props.value || newProps.options !== this.props.options) { var selectedKeys = this._getSelectedKeys(undefined, newProps.selectedKey); var indices = this._getSelectedIndices(newProps.options, selectedKeys); this.setState({ selectedIndices: indices, currentOptions: newProps.options }); } }; ComboBox.prototype.componentDidUpdate = function (prevProps, prevState) { var _this = this; var _a = this.props, allowFreeform = _a.allowFreeform, text = _a.text, value = _a.value, onMenuOpen = _a.onMenuOpen, onMenuDismissed = _a.onMenuDismissed; var _b = this.state, isOpen = _b.isOpen, focused = _b.focused, selectedIndices = _b.selectedIndices, currentPendingValueValidIndex = _b.currentPendingValueValidIndex; // If we are newly open or are open and the pending valid index changed, // make sure the currently selected/pending option is scrolled into view if (isOpen && (!prevState.isOpen || prevState.currentPendingValueValidIndex !== currentPendingValueValidIndex)) { // Need this timeout so that the selectedElement ref is correctly updated this._async.setTimeout(function () { return _this._scrollIntoView(); }, 0); } // If we are open or we are just closed, shouldFocusAfterClose is set, // are focused but we are not the activeElement set focus on the input if (isOpen || (prevState.isOpen && !isOpen && this._focusInputAfterClose && focused && this._autofill.current && document.activeElement !== this._autofill.current.inputElement)) { this.focus(); } // If we should focusAfterClose AND // just opened/closed the menu OR // are focused AND // updated the selectedIndex with the menu closed OR // are not allowing freeform OR // the value changed // we need to set selection if (this._focusInputAfterClose && (prevState.isOpen && !isOpen || (focused && ((!isOpen && !this.props.multiSelect && prevState.selectedIndices && selectedIndices && prevState.selectedIndices[0] !== selectedIndices[0]) || !allowFreeform || text !== prevProps.text || value !== prevProps.value)))) { this._select(); } this._notifyPendingValueChanged(prevState); if (isOpen && !prevState.isOpen && onMenuOpen) { onMenuOpen(); } if (!isOpen && prevState.isOpen && onMenuDismissed) { onMenuDismissed(); } }; ComboBox.prototype.componentWillUnmount = function () { _super.prototype.componentWillUnmount.call(this); // remove the eventHanlder that was added in componentDidMount this._events.off(this._comboBoxWrapper.current); }; // Primary Render ComboBox.prototype.render = function () { var _this = this; 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, _c = _a.onRenderList, onRenderList = _c === void 0 ? this._onRenderList : _c, _d = _a.onRenderItem, onRenderItem = _d === void 0 ? this._onRenderItem : _d, _e = _a.onRenderOption, onRenderOption = _e === void 0 ? this._onRenderOptionContent : _e, allowFreeform = _a.allowFreeform, buttonIconProps = _a.buttonIconProps, _f = _a.isButtonAriaHidden, isButtonAriaHidden = _f === void 0 ? true : _f, customStyles = _a.styles, theme = _a.theme, title = _a.title, keytipProps = _a.keytipProps; var _g = this.state, isOpen = _g.isOpen, focused = _g.focused, suggestedDisplayValue = _g.suggestedDisplayValue; this._currentVisibleValue = this._getVisibleValue(); var divProps = Utilities_1.getNativeProps(this.props, Utilities_1.divProperties); var hasErrorMessage = (errorMessage && errorMessage.length > 0) ? true : false; this._classNames = this.props.getClassNames ? this.props.getClassNames(theme, !!isOpen, !!disabled, !!required, !!focused, !!allowFreeform, !!hasErrorMessage, className) : ComboBox_classNames_1.getClassNames(ComboBox_styles_1.getStyles(theme, customStyles), className, !!isOpen, !!disabled, !!required, !!focused, !!allowFreeform, !!hasErrorMessage); var describedBy = id + '-option'; return (React.createElement("div", tslib_1.__assign({}, divProps, { ref: this._root, className: this._classNames.container }), label && (React.createElement(Label_1.Label, { id: id + '-label', disabled: disabled, required: required, htmlFor: id + '-input', className: this._classNames.label }, label)), React.createElement(KeytipData_1.KeytipData, { keytipProps: keytipProps, disabled: disabled }, function (keytipAttributes) { return (React.createElement("div", { "data-ktp-target": keytipAttributes['data-ktp-target'], ref: _this._comboBoxWrapper, id: id + 'wrapper', className: _this._classNames.root }, React.createElement(index_1.Autofill, { "data-ktp-execute-target": keytipAttributes['data-ktp-execute-target'], "data-is-interactable": !disabled, componentRef: _this._autofill, id: id + '-input', className: _this._classNames.input, type: 'text', onFocus: _this._select, onBlur: _this._onBlur, onKeyDown: _this._onInputKeyDown, onKeyUp: _this._onInputKeyUp, onClick: _this._onAutofillClick, onTouchStart: _this._onTouchStart, onInputValueChange: _this._onInputChange, "aria-expanded": isOpen, "aria-autocomplete": _this._getAriaAutoCompleteValue(), role: 'combobox', "aria-readonly": ((allowFreeform || disabled) ? undefined : 'true'), readOnly: disabled || !allowFreeform, "aria-labelledby": (label && (id + '-label')), "aria-label": ((ariaLabel && !label) ? ariaLabel : undefined), "aria-describedby": describedBy + (keytipAttributes['aria-describedby'] || ''), "aria-activedescendant": _this._getAriaActiveDescentValue(), "aria-disabled": disabled, "aria-owns": (id + '-list'), spellCheck: false, defaultVisibleValue: _this._currentVisibleValue, suggestedDisplayValue: suggestedDisplayValue, updateValueInWillReceiveProps: _this._onUpdateValueInAutofillWillReceiveProps, shouldSelectFullInputValueInComponentDidUpdate: _this._onShouldSelectFullInputValueInAutofillComponentDidUpdate, title: title }), React.createElement(Button_1.IconButton, { className: 'ms-ComboBox-CaretDown-button', styles: _this._getCaretButtonStyles(), role: 'presentation', "aria-hidden": isButtonAriaHidden, "data-is-focusable": false, tabIndex: -1, onClick: _this._onComboBoxClick, iconProps: buttonIconProps, disabled: disabled, checked: isOpen }))); }), isOpen && (onRenderContainer(tslib_1.__assign({}, this.props, { onRenderList: onRenderList, onRenderItem: onRenderItem, onRenderOption: onRenderOption, options: this.state.currentOptions.map(function (item, index) { return (tslib_1.__assign({}, item, { index: index })); }) }), this._onRenderContainer)), errorMessage && React.createElement("div", { className: this._classNames.errorMessage }, errorMessage))); }; /** * 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) { if (!options) { return false; } return index >= 0 && index < options.length; }; /** * 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 _this = this; 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 === 'on') { // 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_types_1.SelectableOptionMenuItemType.Header && option.itemType !== SelectableOption_types_1.SelectableOptionMenuItemType.Divider; }) .filter(function (option) { return _this._getPreviewText(option).toLocaleLowerCase().indexOf(updatedValue) === 0; }); if (items.length > 0) { // use ariaLabel as the value when the option is set var text = this._getPreviewText(items[0]); // If the user typed out the complete option text, we don't need any suggested display text anymore newSuggestedDisplayValue = text.toLocaleLowerCase() !== updatedValue ? 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_types_1.SelectableOptionMenuItemType.Header && option.itemType !== SelectableOption_types_1.SelectableOptionMenuItemType.Divider; }) .filter(function (option) { return _this._getPreviewText(option).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; if (this.props.autoComplete === 'on') { // 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 !== undefined) { this._async.clearTimeout(this._lastReadOnlyAutoCompleteChangeTimeoutId); this._lastReadOnlyAutoCompleteChangeTimeoutId = undefined; 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, i) { return tslib_1.__assign({}, item, { index: i }); }) .filter(function (option) { return option.itemType !== SelectableOption_types_1.SelectableOptionMenuItemType.Header && option.itemType !== SelectableOption_types_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, this._getPreviewText(items[0])); } // Schedule a timeout to clear the pending value after the timeout span this._lastReadOnlyAutoCompleteChangeTimeoutId = this._async.setTimeout(function () { _this._lastReadOnlyAutoCompleteChangeTimeoutId = undefined; }, 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 : this._getFirstSelectedIndex(); // 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); }; ComboBox.prototype._getFirstSelectedIndex = function () { return (this.state.selectedIndices && this.state.selectedIndices.length > 0) ? this.state.selectedIndices[0] : -1; }; /** * 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)); if (!this._indexWithinBounds(currentOptions, newIndex)) { return -1; } var option = currentOptions[newIndex]; // attempt to skip headers and dividers if ((option.itemType === SelectableOption_types_1.SelectableOptionMenuItemType.Header || option.itemType === SelectableOption_types_1.SelectableOptionMenuItemType.Divider)) { // Should we continue looking for an index to select? if (searchDirection !== SearchDirection.none && ((newIndex > 0 && searchDirection < SearchDirection.none) || (newIndex >= 0 && newIndex < currentOptions.length && 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, submitPendingValueEvent, searchDirection) { if (searchDirection === void 0) { searchDirection = SearchDirection.none; } var _a = this.props, onChanged = _a.onChanged, onPendingValueChanged = _a.onPendingValueChanged; var currentOptions = this.state.currentOptions; var selectedIndices = this.state.selectedIndices; if (!selectedIndices) { selectedIndices = []; } // Find the next selectable index, if searchDirection is none // we will get our starting index back index = this._getNextSelectableIndex(index, searchDirection); if (!this._indexWithinBounds(currentOptions, index)) { return; } // Are we at a new index? If so, update the state, otherwise // there is nothing to do if (this.props.multiSelect || selectedIndices.length < 1 || (selectedIndices.length === 1 && selectedIndices[0] !== index)) { var option = currentOptions[index]; if (!option) { return; } if (this.props.multiSelect) { option.selected = !option.selected; if (option.selected && selectedIndices.indexOf(index) < 0) { selectedIndices.push(index); }