UNPKG

office-ui-fabric-react

Version:

Reusable React components for building experiences for Microsoft 365.

815 lines • 96.8 kB
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