office-ui-fabric-react
Version: 
Reusable React components for building experiences for Microsoft 365.
815 lines • 96.8 kB
JavaScript
define(["require", "exports", "tslib", "react", "../Autofill/index", "../../Utilities", "../../Callout", "../../Checkbox", "../../Button", "../../common/DirectionalHint", "./ComboBox.styles", "./ComboBox.classNames", "../../KeytipData", "../../Label", "../../utilities/selectableOption/index"], function (require, exports, tslib_1, React, index_1, Utilities_1, Callout_1, Checkbox_1, Button_1, DirectionalHint_1, ComboBox_styles_1, ComboBox_classNames_1, KeytipData_1, Label_1, index_2) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    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) {
        /** Used when the user was hovering and has since moused out of the menu items */
        HoverStatus[HoverStatus["clearAll"] = -2] = "clearAll";
        /** 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(tslib_1.__assign({}, this.props), { render: undefined }), tslib_1.__assign(tslib_1.__assign({}, newProps), { render: undefined }));
        };
        return ComboBoxOptionWrapper;
    }(React.Component));
    var COMPONENT_NAME = 'ComboBox';
    var ComboBox = /** @class */ (function (_super) {
        tslib_1.__extends(ComboBox, _super);
        function ComboBox(props) {
            var _this = _super.call(this, props) || this;
            _this._root = React.createRef();
            /** The input aspect of the comboBox */
            _this._autofill = React.createRef();
            /** The wrapping div of the input and button */
            _this._comboBoxWrapper = React.createRef();
            /** The callout element */
            _this._comboBoxMenu = React.createRef();
            /** The menu item element that is currently selected */
            _this._selectedElement = React.createRef();
            /**
             * {@inheritdoc}
             */
            _this.focus = function (shouldOpenOnFocus, useFocusAsync) {
                if (_this._autofill.current) {
                    if (useFocusAsync) {
                        Utilities_1.focusAsync(_this._autofill.current);
                    }
                    else {
                        _this._autofill.current.focus();
                    }
                    if (shouldOpenOnFocus) {
                        _this.setState({
                            isOpen: true,
                        });
                    }
                }
                // Programatically setting focus means that there is nothing else that needs to be done
                // Focus is now contained
                if (!_this._hasFocus()) {
                    _this.setState({ focusState: 'focused' });
                }
            };
            /**
             * 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 defaultVisibleValue - the defaultVisibleValue that got passed
             *  in to the auto fill's componentWillReceiveProps
             * @returns - 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;
                }
                var visibleValue = _this._normalizeToString(_this._currentVisibleValue);
                if (comboBox.value !== visibleValue) {
                    // If visibleValue is empty, ensure that the empty string is used
                    return visibleValue || '';
                }
                return comboBox.value;
            };
            _this._renderComboBoxWrapper = function (multiselectAccessibleText, errorMessageId, keytipAttributes) {
                if (keytipAttributes === void 0) { keytipAttributes = {}; }
                var _a = _this.props, label = _a.label, disabled = _a.disabled, ariaLabel = _a.ariaLabel, ariaDescribedBy = _a.ariaDescribedBy, required = _a.required, errorMessage = _a.errorMessage, buttonIconProps = _a.buttonIconProps, _b = _a.isButtonAriaHidden, isButtonAriaHidden = _b === void 0 ? true : _b, title = _a.title, placeholderProp = _a.placeholder, tabIndex = _a.tabIndex, autofill = _a.autofill, iconButtonProps = _a.iconButtonProps;
                var _c = _this.state, isOpen = _c.isOpen, suggestedDisplayValue = _c.suggestedDisplayValue;
                // If the combobox has focus, is multiselect, and has a display string, then use that placeholder
                // so that the selected items don't appear to vanish. This is not ideal but it's the only reasonable way
                // to correct the behavior where the input is cleared so the user can type. If a full refactor is done, then this
                // should be removed and the multiselect combobox should behave like a picker.
                var placeholder = _this._hasFocus() && _this.props.multiSelect && multiselectAccessibleText
                    ? multiselectAccessibleText
                    : placeholderProp;
                return (React.createElement("div", { "data-ktp-target": keytipAttributes['data-ktp-target'], ref: _this._comboBoxWrapper, id: _this._id + 'wrapper', className: _this._classNames.root },
                    React.createElement(index_1.Autofill, tslib_1.__assign({ "data-ktp-execute-target": keytipAttributes['data-ktp-execute-target'], "data-is-interactable": !disabled, componentRef: _this._autofill, id: _this._id + '-input', className: _this._classNames.input, type: "text", onFocus: _this._onFocus, 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", readOnly: disabled, "aria-labelledby": label && _this._id + '-label', "aria-label": ariaLabel && !label ? ariaLabel : undefined, "aria-describedby": errorMessage !== undefined
                            ? Utilities_1.mergeAriaAttributeValues(ariaDescribedBy, keytipAttributes['aria-describedby'], errorMessageId)
                            : Utilities_1.mergeAriaAttributeValues(ariaDescribedBy, keytipAttributes['aria-describedby']), "aria-activedescendant": _this._getAriaActiveDescendantValue(), "aria-required": required, "aria-disabled": disabled, "aria-owns": isOpen ? _this._id + '-list' : undefined, spellCheck: false, defaultVisibleValue: _this._currentVisibleValue, suggestedDisplayValue: suggestedDisplayValue, updateValueInWillReceiveProps: _this._onUpdateValueInAutofillWillReceiveProps, shouldSelectFullInputValueInComponentDidUpdate: _this._onShouldSelectFullInputValueInAutofillComponentDidUpdate, title: title, preventValueSelection: !_this._hasFocus(), placeholder: placeholder, tabIndex: tabIndex }, autofill)),
                    React.createElement(Button_1.IconButton, tslib_1.__assign({ className: 'ms-ComboBox-CaretDown-button', styles: _this._getCaretButtonStyles(), role: "presentation", "aria-hidden": isButtonAriaHidden, "data-is-focusable": false, tabIndex: -1, onClick: _this._onComboBoxClick, onBlur: _this._onBlur, iconProps: buttonIconProps, disabled: disabled, checked: isOpen }, iconButtonProps))));
            };
            /**
             * componentDidUpdate handler for the auto fill component
             *
             * @param defaultVisibleValue - the current defaultVisibleValue in the auto fill's componentDidUpdate
             * @param suggestedDisplayValue - the current suggestedDisplayValue in the auto fill's componentDidUpdate
             * @returns - 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 the value to pass to the input
             */
            _this._getVisibleValue = function () {
                var _a = _this.props, text = _a.text, 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;
                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 === null || currentPendingValue === undefined)) {
                    return text;
                }
                if (_this.props.multiSelect) {
                    // Multi-select
                    if (_this._hasFocus()) {
                        var index = -1;
                        if (autoComplete === 'on' && currentPendingIndexValid) {
                            index = currentPendingValueValidIndex;
                        }
                        return _this._getPendingString(currentPendingValue, currentOptions, index);
                    }
                    else {
                        return _this._getMultiselectDisplayString(selectedIndices, currentOptions, 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 pending value, use that
                        // otherwise use the index determined above (falling back to '' if we did not get a valid index)
                        return _this._getPendingString(currentPendingValue, currentOptions, index);
                    }
                    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;
                            return _this._normalizeToString(currentPendingValue);
                        }
                        else if (!_this.state.isOpen && currentPendingValue) {
                            return _this._indexWithinBounds(currentOptions, index)
                                ? currentPendingValue
                                : _this._normalizeToString(suggestedDisplayValue);
                        }
                        else {
                            return _this._indexWithinBounds(currentOptions, index)
                                ? currentOptions[index].text
                                : _this._normalizeToString(suggestedDisplayValue);
                        }
                    }
                }
            };
            /**
             * 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._onFocus = function () {
                if (_this._autofill.current && _this._autofill.current.inputElement) {
                    _this._autofill.current.inputElement.select();
                }
                if (!_this._hasFocus()) {
                    _this.setState({ focusState: 'focusing' });
                }
            };
            /**
             * 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.__spreadArrays(_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
             */
            // eslint-disable-next-line deprecation/deprecation
            _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
                var relatedTarget = event.relatedTarget;
                if (event.relatedTarget === null) {
                    // In IE11, due to lack of support, event.relatedTarget is always
                    // null making every onBlur call to be "outside" of the ComboBox
                    // even when it's not. Using document.activeElement is another way
                    // for us to be able to get what the relatedTarget without relying
                    // on the event
                    relatedTarget = document.activeElement;
                }
                if (relatedTarget &&
                    // when event coming from withing the comboBox title
                    ((_this._root.current && _this._root.current.contains(relatedTarget)) ||
                        // when event coming from within the comboBox list menu
                        (_this._comboBoxMenu.current &&
                            (_this._comboBoxMenu.current.contains(relatedTarget) ||
                                // when event coming from the callout containing the comboBox list menu (ex: when scrollBar of the
                                // Callout is clicked) checks if the relatedTarget is a parent of _comboBoxMenu
                                Utilities_1.findElementRecursive(_this._comboBoxMenu.current, function (element) { return element === relatedTarget; }))))) {
                    event.preventDefault();
                    event.stopPropagation();
                    return;
                }
                if (_this._hasFocus()) {
                    _this.setState({ focusState: 'none' });
                    if (!_this.props.multiSelect || _this.props.allowFreeform) {
                        _this._submitPendingValue(event);
                    }
                }
            };
            // Render Callout container and pass in list
            _this._onRenderContainer = function (props) {
                var onRenderList = props.onRenderList, calloutProps = props.calloutProps, dropdownWidth = props.dropdownWidth, dropdownMaxWidth = props.dropdownMaxWidth, _a = props.onRenderUpperContent, onRenderUpperContent = _a === void 0 ? _this._onRenderUpperContent : _a, _b = props.onRenderLowerContent, onRenderLowerContent = _b === void 0 ? _this._onRenderLowerContent : _b, useComboBoxAsMenuWidth = props.useComboBoxAsMenuWidth, persistMenu = props.persistMenu, _c = props.shouldRestoreFocus, shouldRestoreFocus = _c === void 0 ? true : _c;
                var isOpen = _this.state.isOpen;
                var id = _this._id;
                var comboBoxMenuWidth = useComboBoxAsMenuWidth && _this._comboBoxWrapper.current
                    ? _this._comboBoxWrapper.current.clientWidth + 2
                    : undefined;
                return (React.createElement(Callout_1.Callout, tslib_1.__assign({ isBeakVisible: false, gapSpace: 0, doNotLayer: false, directionalHint: DirectionalHint_1.DirectionalHint.bottomLeftEdge, directionalHintFixed: false }, calloutProps, { onLayerMounted: _this._onLayerMounted, className: Utilities_1.css(_this._classNames.callout, calloutProps ? calloutProps.className : undefined), target: _this._comboBoxWrapper.current, onDismiss: _this._onDismiss, onMouseDown: _this._onCalloutMouseDown, onScroll: _this._onScroll, setInitialFocus: false, calloutWidth: useComboBoxAsMenuWidth && _this._comboBoxWrapper.current
                        ? comboBoxMenuWidth && comboBoxMenuWidth
                        : dropdownWidth, calloutMaxWidth: dropdownMaxWidth ? dropdownMaxWidth : comboBoxMenuWidth, hidden: persistMenu ? !isOpen : undefined, shouldRestoreFocus: shouldRestoreFocus }),
                    onRenderUpperContent(_this.props, _this._onRenderUpperContent),
                    React.createElement("div", { className: _this._classNames.optionsContainerWrapper, ref: _this._comboBoxMenu }, onRenderList(tslib_1.__assign(tslib_1.__assign({}, props), { id: id }), _this._onRenderList)),
                    onRenderLowerContent(_this.props, _this._onRenderLowerContent)));
            };
            _this._onLayerMounted = function () {
                _this._onCalloutLayerMounted();
                if (_this.props.calloutProps && _this.props.calloutProps.onLayerMounted) {
                    _this.props.calloutProps.onLayerMounted();
                }
            };
            _this._onRenderLabel = function (onRenderLabelProps) {
                var _a = onRenderLabelProps.props, label = _a.label, disabled = _a.disabled, required = _a.required;
                if (label) {
                    return (React.createElement(Label_1.Label, { id: _this._id + '-label', disabled: disabled, required: required, className: _this._classNames.label },
                        label,
                        onRenderLabelProps.multiselectAccessibleText && (React.createElement("span", { className: _this._classNames.screenReaderText }, onRenderLabelProps.multiselectAccessibleText))));
                }
                return null;
            };
            // 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 index_2.SelectableOptionMenuItemType.Divider:
                        return _this._renderSeparator(item);
                    case index_2.SelectableOptionMenuItemType.Header:
                        return _this._renderHeader(item);
                    default:
                        return _this._renderOption(item);
                }
            };
            // Default _onRenderLowerContent function returns nothing
            _this._onRenderLowerContent = function () {
                return null;
            };
            // Default _onRenderUpperContent function returns nothing
            _this._onRenderUpperContent = 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 isChecked = _this._isOptionChecked(item.index);
                var optionStyles = _this._getCurrentOptionStyles(item);
                var optionClassNames = ComboBox_classNames_1.getComboBoxOptionClassNames(_this._getCurrentOptionStyles(item));
                var title = _this._getPreviewText(item);
                var onRenderCheckboxLabel = function () { return onRenderOption(item, _this._onRenderOptionContent); };
                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: optionStyles, checked: isSelected, className: 'ms-ComboBox-option', onClick: _this._onItemClick(item), 
                        // eslint-disable-next-line react/jsx-no-bind
                        onMouseEnter: _this._onOptionMouseEnter.bind(_this, item.index), 
                        // eslint-disable-next-line react/jsx-no-bind
                        onMouseMove: _this._onOptionMouseMove.bind(_this, item.index), onMouseLeave: _this._onOptionMouseLeave, role: "option", "aria-selected": isSelected ? 'true' : 'false', ariaLabel: item.ariaLabel, disabled: item.disabled, title: title }, React.createElement("span", { className: optionClassNames.optionTextWrapper, ref: isSelected ? _this._selectedElement : undefined }, onRenderOption(item, _this._onRenderOptionContent)))) : (React.createElement(Checkbox_1.Checkbox, { id: id + '-list' + item.index, ariaLabel: item.ariaLabel, key: item.key, "data-index": item.index, styles: optionStyles, className: 'ms-ComboBox-option', "data-is-focusable": true, onChange: _this._onItemClick(item), label: item.text, role: "option", checked: isChecked, title: title, disabled: item.disabled, 
                        // eslint-disable-next-line react/jsx-no-bind
                        onRenderLabel: onRenderCheckboxLabel, inputProps: {
                            'aria-selected': isSelected ? 'true' : 'false',
                        } }));
                };
                return (React.createElement(ComboBoxOptionWrapper, { key: item.key, index: item.index, disabled: item.disabled, isSelected: isSelected, isChecked: isChecked, text: item.text, 
                    // eslint-disable-next-line react/jsx-no-bind
                    render: getOptionComponent, data: item.data }));
            };
            /**
             * Mouse clicks to headers, dividers and scrollbar should not make input lose focus
             */
            _this._onCalloutMouseDown = function (ev) {
                ev.preventDefault();
            };
            /**
             * 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 () {
                var onMenuDismiss = _this.props.onMenuDismiss;
                if (onMenuDismiss) {
                    onMenuDismiss();
                }
                // In persistMode we need to simulate callout layer mount
                // since that only happens once. We do it on dismiss since
                // it works either way.
                if (_this.props.persistMenu) {
                    _this._onCalloutLayerMounted();
                }
                // close the menu
                _this._setOpenStateAndFocusOnClose(false /* isOpen */, false /* focusInputAfterClose */);
                // reset the selected index
                // to the last value state
                _this._resetSelectedIndex();
            };
            _this._onAfterClearPendingInfo = function () {
                _this._processingClearPendingInfo = false;
            };
            /**
             * 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;
                // Take note if we are processing an alt (option) or meta (command) keydown.
                // See comment in _onInputKeyUp for reasoning.
                _this._lastKeyDownWasAltOrMeta = _this._isAltOrMeta(ev);
                if (disabled) {
                    _this._handleInputWhenDisabled(ev);
                    return;
                }
                var index = _this._getPendingSelectedIndex(false /* includeCurrentPendingValue */);
                switch (ev.which) {
                    case Utilities_1.KeyCodes.enter:
                        if (_this._autofill.current && _this._autofill.current.inputElement) {
                            _this._autofill.current.inputElement.select();
                        }
                        _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 Utilities_1.KeyCodes.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 Utilities_1.KeyCodes.escape:
                        // reset the selected index
                        _this._resetSelectedIndex();
                        // Close the menu if opened
                        if (isOpen) {
                            _this.setState({
                                isOpen: false,
                            });
                        }
                        else {
                            return;
                        }
                        break;
                    case Utilities_1.KeyCodes.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) {
                            // Close the menu if it is open and break so
                            // that the event get stopPropagation and prevent default.
                            // Otherwise, we need to let the event continue to propagate
                            if (isOpen) {
                                _this._setOpenStateAndFocusOnClose(!isOpen, true /* focusInputAfterClose */);
                                break;
                            }
                            return;
                        }
                        // Go to the previous option
                        _this._setPendingInfoFromIndexAndDirection(index, SearchDirection.backward);
                        break;
                    case Utilities_1.KeyCodes.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 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._setPendingInfoFromIndexAndDirection(index, directionToSearch);
                        break;
                    /* eslint-disable no-fallthrough */
                    case Utilities_1.KeyCodes.space:
                        // event handled in _onComboBoxKeyUp
                        if (!allowFreeform && autoComplete === 'off') {
                            break;
                        }
                    default:
                        /* eslint-enable no-fallthrough */
                        // 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, let the event propagate
                        if (ev.keyCode === Utilities_1.KeyCodes.alt || ev.key === 'Meta' /* && isOpen */) {
                            return;
                        }
                        // 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(ev.key);
                            break;
                        }
                        // allow the key to propagate 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;
                var isOpen = _this.state.isOpen;
                // We close the menu on key up only if ALL of the following are true:
                // - Most recent key down was alt or meta (command)
                // - The alt/meta key down was NOT followed by some other key (such as down/up arrow to
                //   expand/collapse the menu)
                // - We're not on a Mac (or iOS)
                // This is because on Windows, pressing alt moves focus to the application menu bar or similar,
                // closing any open context menus. There is not a similar behavior on Macs.
                var keyPressIsAltOrMetaAlone = _this._lastKeyDownWasAltOrMeta && _this._isAltOrMeta(ev);
                _this._lastKeyDownWasAltOrMeta = false;
                var shouldHandleKey = keyPressIsAltOrMetaAlone && !(Utilities_1.isMac() || Utilities_1.isIOS());
                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 === 'off') {
                            _this._setOpenStateAndFocusOnClose(!isOpen, !!isOpen);
                        }
                        return;
                    default:
                        if (shouldHandleKey && isOpen) {
                            _this._setOpenStateAndFocusOnClose(!isOpen, true /* focusInputAfterClose */);
                        }
                        else {
                            if (_this.state.focusState === 'focusing' && _this.props.openOnKeyboardFocus) {
                                _this.setState({ isOpen: true });
                            }
                            if (_this.state.focusState !== 'focused') {
                                _this.setState({ focusState: 'focused' });
                            }
                        }
                        return;
                }
            };
            _this._onOptionMouseLeave = function () {
                if (_this._shouldIgnoreMouseEvent()) {
                    return;
                }
                // Ignore the event in persistMenu mode if the callout has
                // closed. This is to avoid clearing the visuals on item click.
                if (_this.props.persistMenu && !_this.state.isOpen) {
                    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 */);
                    _this.setState({ focusState: 'focused' });
                }
            };
            /**
             * Click handler for the autofill.
             */
            _this._onAutofillClick = function () {
                var _a = _this.props, disabled = _a.disabled, allowFreeform = _a.allowFreeform;
                if (allowFreeform && !disabled) {
                    _this.focus(_this.state.isOpen || _this._processingTouch);
                }
                else {
                    _this._onComboBoxClick();
                }
            };
            _this._onTouchStart = function () {
                if (_this._comboBoxWrapper.current && !('onpointerdown' in _this._comboBoxWrapper)) {
                    _this._handleTouchAndPointerEvent();
                }
            };
            _this._onPointerDown = function (ev) {
                if (ev.pointerType === 'touch') {
                    _this._handleTouchAndPointerEvent();
                    ev.preventDefault();
                    ev.stopImmediatePropagation();
                }
            };
            Utilities_1.initializeComponentRef(_this);
            _this._async = new Utilities_1.Async(_this);
            _this._events = new Utilities_1.EventGroup(_this);
            Utilities_1.warnMutuallyExclusive(COMPONENT_NAME, props, {
                defaultSelectedKey: 'selectedKey',
                text: 'defaultSelectedKey',
                selectedKey: 'value',
                dropdownWidth: 'useComboBoxAsMenuWidth',
            });
            _this._id = props.id || Utilities_1.getId('ComboBox');
            var selectedKeys = _this._buildDefaultSelectedKeys(props.defaultSelectedKey, props.selectedKey);
            _this._isScrollIdle = true;
            _this._processingTouch = false;
            _this._gotMouseMove = false;
            _this._processingClearPendingInfo = false;
            var initialSelectedIndices = _this._getSelectedIndices(props.options, selectedKeys);
            _this.state = {
                isOpen: false,
                selectedIndices: initialSelectedIndices,
                focusState: 'none',
                suggestedDisplayValue: undefined,
                currentOptions: _this.props.options,
                currentPendingValueValidIndex: -1,
                currentPendingValue: undefined,
                currentPendingValueValidIndexOnHover: HoverStatus.default,
            };
            return _this;
        }
        Object.defineProperty(ComboBox.prototype, "selectedOptions", {
            /**
             * All selected options
             */
            get: function () {
                var _a = this.state, currentOptions = _a.currentOptions, selectedIndices = _a.selectedIndices;
                return index_2.getAllSelectedOptions(currentOptions, selectedIndices);
            },
            enumerable: true,
            configurable: true
        });
        ComboBox.prototype.componentDidMount = function () {
            if (this._comboBoxWrapper.current && !this.props.disabled) {
                // 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.current, 'pointerdown', this._onPointerDown, true);
                }
            }
        };
        ComboBox.prototype.UNSAFE_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.options !== this.props.options) {
                var selectedKeys = this._buildSelectedKeys(newProps.selectedKey);
                var indices = this._getSelectedIndices(newProps.options, selectedKeys);
                this.setState({
                    selectedIndices: indices,
                    currentOptions: newProps.options,
                });
                if (newProps.selectedKey === null) {
                    this.setState({
                        suggestedDisplayValue: undefined,
                    });
                }
            }
        };
        ComboBox.prototype.componentDidUpdate = function (prevProps, prevState) {
            var _this = this;
            var _a = this.props, allowFreeform = _a.allowFreeform, text = _a.text, onMenuOpen = _a.onMenuOpen, onMenuDismissed = _a.onMenuDismissed;
            var _b = this.state, isOpen = _b.isOpen, 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 an action is taken that put focus in the ComboBox
            // and If we are open or we are just closed, shouldFocusAfterClose is set,
            // but we are not the activeElement set focus on the input
            if (this._hasFocus() &&
                (isOpen ||
                    (prevState.isOpen &&
                        !isOpen &&
                        this._focusInputAfterClose &&
                        this._autofill.current &&
                        document.activeElement !== this._autofill.current.inputElement))) {
                this.focus(undefined /*shouldOpenOnFocus*/, true /*useFocusAsync*/);
            }
            // 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) ||
                    (this._hasFocus() &&
                        ((!isOpen &&
                            !this.props.multiSelect &&
                            prevState.selectedIndices &&
                            selectedIndices &&
                            prevState.selectedIndices[0] !== selectedIndices[0]) ||
                            !allowFreeform ||
                            text !== prevProps.text)))) {
                this._onFocus();
            }
            this._notifyPendingValueChanged(prevState);
            if (isOpen && !prevState.isOpen && onMenuOpen) {
                onMenuOpen();
            }
            if (!isOpen && prevState.isOpen && onMenuDismissed) {
                onMenuDismissed();
            }
        };
        ComboBox.prototype.componentWillUnmount = function () {
            this._async.dispose();
            this._events.dispose();
        };
        // Primary Render
        ComboBox.prototype.render = function () {
            var _this = this;
            var id = this._id;
            var errorMessageId = id + '-error';
            var _a = this.props, className = _a.className, disabled = _a.disabled, required = _a.required, errorMessage = _a.errorMessage, _b = _a.onRenderContainer, onRenderContainer = _b === void 0 ? this._onRenderContainer : _b, _c = _a.onRenderLabel, onRenderLabel = _c === void 0 ? this._onRenderLabel : _c, _d = _a.onRenderList, onRenderList = _d === void 0 ? this._onRenderList : _d, _e = _a.onRenderItem, onRenderItem = _e === void 0 ? this._onRenderItem : _e, _f = _a.onRenderOption, onRenderOption = _f === void 0 ? this._onRenderOptionContent : _f, allowFreeform = _a.allowFreeform, customStyles = _a.styles, theme = _a.theme, keytipProps = _a.keytipProps, persistMenu = _a.persistMenu, multiSelect = _a.multiSelect;
            var _g = this.state, isOpen = _g.isOpen, suggestedDisplayValue = _g.suggestedDisplayValue;
            this._currentVisibleValue = this._getVisibleValue();
            // Single select is already accessible since the whole text is selected
            // when focus enters the input. Since multiselect appears to clear the input
            // it needs special accessible text
            var multiselectAccessibleText = multiSelect
                ? this._getMultiselectDisplayString(this.state.selectedIndices, this.state.currentOptions, suggestedDisplayValue)
                : undefined;
            var divProps = Utilities_1.getNativeProps(this.props, Utilities_1.divProperties, [
                'onChange',
                'value',
            ]);
            var hasE