UNPKG

@fluentui/react

Version:

Reusable React components for building web experiences.

794 lines 113 kB
define(["require", "exports", "tslib", "react", "../../Autofill", "../../Utilities", "../../Callout", "../../Checkbox", "./ComboBox.styles", "./ComboBox.classNames", "../../Label", "../../SelectableOption", "../../Button", "@fluentui/react-hooks", "@fluentui/utilities", "@fluentui/react-window-provider", "../../utilities/dom"], function (require, exports, tslib_1, React, Autofill_1, Utilities_1, Callout_1, Checkbox_1, ComboBox_styles_1, ComboBox_classNames_1, Label_1, SelectableOption_1, Button_1, react_hooks_1, utilities_1, react_window_provider_1, dom_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ComboBox = void 0; 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 component that is used to wrap all ComboBox options. * This is used to customize when we want to re-render components, * so we don't re-render every option every time render is executed. */ var ComboBoxOptionWrapper = React.memo(function (_a) { var render = _a.render; return render(); }, function (_a, _b) { var oldRender = _a.render, oldProps = tslib_1.__rest(_a, ["render"]); var newRender = _b.render, newProps = tslib_1.__rest(_b, ["render"]); // The render function will always be different, so we ignore that prop return (0, Utilities_1.shallowCompare)(oldProps, newProps); }); var COMPONENT_NAME = 'ComboBox'; var DEFAULT_PROPS = { options: [], allowFreeform: false, allowParentArrowNavigation: false, autoComplete: 'on', buttonIconProps: { iconName: 'ChevronDown' }, }; function useOptionsState(_a) { var options = _a.options, defaultSelectedKey = _a.defaultSelectedKey, selectedKey = _a.selectedKey; /** The currently selected indices */ var _b = React.useState(function () { return getSelectedIndices(options, buildDefaultSelectedKeys(defaultSelectedKey, selectedKey)); }), selectedIndices = _b[0], setSelectedIndices = _b[1]; /** The options currently available for the callout */ var _c = React.useState(options), currentOptions = _c[0], setCurrentOptions = _c[1]; /** This value is used for the autocomplete hint value */ var _d = React.useState(), suggestedDisplayValue = _d[0], setSuggestedDisplayValue = _d[1]; React.useEffect(function () { if (selectedKey !== undefined) { var selectedKeys = buildSelectedKeys(selectedKey); var indices = getSelectedIndices(options, selectedKeys); setSelectedIndices(indices); } setCurrentOptions(options); }, [options, selectedKey]); React.useEffect(function () { if (selectedKey === null) { setSuggestedDisplayValue(undefined); } }, [selectedKey]); return [ selectedIndices, setSelectedIndices, currentOptions, setCurrentOptions, suggestedDisplayValue, setSuggestedDisplayValue, ]; } exports.ComboBox = React.forwardRef(function (propsWithoutDefaults, forwardedRef) { var _a = (0, Utilities_1.getPropsWithDefaults)(DEFAULT_PROPS, propsWithoutDefaults), ref = _a.ref, props = tslib_1.__rest(_a, ["ref"]); var rootRef = React.useRef(null); var mergedRootRef = (0, react_hooks_1.useMergedRefs)(rootRef, forwardedRef); var _b = useOptionsState(props), selectedIndices = _b[0], setSelectedIndices = _b[1], currentOptions = _b[2], setCurrentOptions = _b[3], suggestedDisplayValue = _b[4], setSuggestedDisplayValue = _b[5]; return (React.createElement(ComboBoxInternal, tslib_1.__assign({}, props, { hoisted: { mergedRootRef: mergedRootRef, rootRef: rootRef, selectedIndices: selectedIndices, setSelectedIndices: setSelectedIndices, currentOptions: currentOptions, setCurrentOptions: setCurrentOptions, suggestedDisplayValue: suggestedDisplayValue, setSuggestedDisplayValue: setSuggestedDisplayValue, } }))); }); exports.ComboBox.displayName = COMPONENT_NAME; /** * Depth-first search to find the first descendant element where the match function returns true. * @param element - element to start searching at * @param match - the function that determines if the element is a match * @returns the matched element or null no match was found */ function findFirstDescendant(element, match) { var children = (0, utilities_1.getChildren)(element); // For loop is used because forEach cannot be stopped. for (var index = 0; index < children.length; index++) { var child = children[index]; if (match(child)) { return child; } var candidate = findFirstDescendant(child, match); if (candidate) { return candidate; } } return null; } var ComboBoxInternal = /** @class */ (function (_super) { tslib_1.__extends(ComboBoxInternal, _super); function ComboBoxInternal(props) { var _this = _super.call(this, props) || this; /** The input aspect of the combo box */ _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(); // props to prevent dismiss on scroll/resize immediately after opening _this._overrideScrollDismiss = false; /** * {@inheritdoc} */ _this.focus = function (shouldOpenOnFocus, useFocusAsync) { if (_this.props.disabled) { return; } if (_this._autofill.current) { if (useFocusAsync) { (0, Utilities_1.focusAsync)(_this._autofill.current); } else { _this._autofill.current.focus(); } if (shouldOpenOnFocus) { _this.setState({ isOpen: true, }); } } // Programmatically 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 input 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; } return normalizeToString(_this._currentVisibleValue); }; _this._renderComboBoxWrapper = function (multiselectAccessibleText, errorMessageId) { var _a = _this.props, label = _a.label, disabled = _a.disabled, ariaLabel = _a.ariaLabel, _b = _a.ariaDescribedBy, ariaDescribedBy = _b === void 0 ? _this.props['aria-describedby'] : _b, required = _a.required, errorMessage = _a.errorMessage, buttonIconProps = _a.buttonIconProps, isButtonAriaHidden = _a.isButtonAriaHidden, title = _a.title, placeholderProp = _a.placeholder, tabIndex = _a.tabIndex, autofill = _a.autofill, iconButtonProps = _a.iconButtonProps, suggestedDisplayValue = _a.hoisted.suggestedDisplayValue; var _c = _this.state, ariaActiveDescendantValue = _c.ariaActiveDescendantValue, isOpen = _c.isOpen; // If the combo box 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 combo box should behave like a picker. var placeholder = _this._hasFocus() && _this.props.multiSelect && multiselectAccessibleText ? multiselectAccessibleText : placeholderProp; var labelledBy = [_this.props['aria-labelledby'], label && _this._id + '-label'].join(' ').trim(); var labelProps = { 'aria-labelledby': labelledBy ? labelledBy : undefined, 'aria-label': ariaLabel && !label ? ariaLabel : undefined, }; var hasErrorMessage = errorMessage && errorMessage.length > 0 ? true : false; return (React.createElement("div", { "data-ktp-target": true, ref: _this._comboBoxWrapper, id: _this._id + 'wrapper', className: _this._classNames.root, "aria-owns": isOpen ? _this._id + '-list' : undefined }, React.createElement(Autofill_1.Autofill, tslib_1.__assign({ "data-ktp-execute-target": true, "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 }, labelProps, { "aria-describedby": errorMessage !== undefined ? (0, Utilities_1.mergeAriaAttributeValues)(ariaDescribedBy, errorMessageId) : ariaDescribedBy, "aria-activedescendant": ariaActiveDescendantValue, "aria-required": required, "aria-disabled": disabled, "aria-invalid": hasErrorMessage, "aria-controls": isOpen ? _this._id + '-list' : undefined, spellCheck: false, defaultVisibleValue: _this._currentVisibleValue, suggestedDisplayValue: suggestedDisplayValue, // eslint-disable-next-line @typescript-eslint/no-deprecated updateValueInWillReceiveProps: _this._onUpdateValueInAutofillWillReceiveProps, shouldSelectFullInputValueInComponentDidUpdate: _this._onShouldSelectFullInputValueInAutofillComponentDidUpdate, title: title, preventValueSelection: !_this._hasFocus(), placeholder: placeholder, tabIndex: disabled ? -1 : tabIndex }, autofill)), React.createElement(Button_1.IconButton, tslib_1.__assign({ className: 'ms-ComboBox-CaretDown-button', styles: _this._getCaretButtonStyles(), role: isButtonAriaHidden ? 'presentation' : undefined, "aria-hidden": isButtonAriaHidden }, (!isButtonAriaHidden ? labelProps : undefined), { "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.props.hoisted.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, allowFreeInput = _a.allowFreeInput, autoComplete = _a.autoComplete, _b = _a.hoisted, suggestedDisplayValue = _b.suggestedDisplayValue, selectedIndices = _b.selectedIndices, currentOptions = _b.currentOptions; var _c = _this.state, currentPendingValueValidIndex = _c.currentPendingValueValidIndex, currentPendingValue = _c.currentPendingValue, isOpen = _c.isOpen; var currentPendingIndexValid = 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 || 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 || allowFreeInput) { // If we are allowing freeform/free input 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 normalizeToString(currentPendingValue); } else if (!_this.state.isOpen && currentPendingValue) { return indexWithinBounds(currentOptions, index) ? currentPendingValue : normalizeToString(suggestedDisplayValue); } else { return indexWithinBounds(currentOptions, index) ? getPreviewText(currentOptions[index]) : 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; } if (_this.props.onInputValueChange) { _this.props.onInputValueChange(updatedValue); } _this.props.allowFreeform || _this.props.allowFreeInput ? _this._processInputChangeWithFreeform(updatedValue) : _this._processInputChangeWithoutFreeform(updatedValue); }; /** * Focus (and select) the content of the input * and set the focused state */ _this._onFocus = function () { var _a, _b; (_b = (_a = _this._autofill.current) === null || _a === void 0 ? void 0 : _a.inputElement) === null || _b === void 0 ? void 0 : _b.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_1 = _this.props.onResolveOptions(tslib_1.__spreadArray([], _this.props.hoisted.currentOptions, true)); // 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_1)) { _this.props.hoisted.setCurrentOptions(newOptions_1); } else if (newOptions_1 && newOptions_1.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 _this._currentPromise = newOptions_1; newOptions_1.then(function (newOptionsFromPromise) { if (newOptions_1 === _this._currentPromise) { _this.props.hoisted.setCurrentOptions(newOptionsFromPromise); } }); } } }; /** * OnBlur handler. Set the focused state to false * and submit any pending value */ // eslint-disable-next-line @typescript-eslint/no-deprecated _this._onBlur = function (event) { var _a, _b; var doc = (0, dom_1.getDocumentEx)(_this.context); // Do nothing if the blur is coming from something // inside the comboBox root or the comboBox menu since // it we are not really blurring 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 = doc === null || doc === void 0 ? void 0 : doc.activeElement; } if (relatedTarget) { var isBlurFromComboBoxTitle = (_a = _this.props.hoisted.rootRef.current) === null || _a === void 0 ? void 0 : _a.contains(relatedTarget); var isBlurFromComboBoxMenu = (_b = _this._comboBoxMenu.current) === null || _b === void 0 ? void 0 : _b.contains(relatedTarget); var isBlurFromComboBoxMenuAncestor = _this._comboBoxMenu.current && (0, Utilities_1.findElementRecursive)(_this._comboBoxMenu.current, function (element) { return element === relatedTarget; }, doc); if (isBlurFromComboBoxTitle || isBlurFromComboBoxMenu || isBlurFromComboBoxMenuAncestor) { if (isBlurFromComboBoxMenuAncestor && _this._hasFocus() && (!_this.props.multiSelect || _this.props.allowFreeform)) { _this._submitPendingValue(event); } 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, defaultRender) { 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: Callout_1.DirectionalHint.bottomLeftEdge, directionalHintFixed: false }, calloutProps, { onLayerMounted: _this._onLayerMounted, className: (0, Utilities_1.css)(_this._classNames.callout, calloutProps === null || calloutProps === void 0 ? void 0 : calloutProps.className), 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, // eslint-disable-next-line @typescript-eslint/no-deprecated shouldRestoreFocus: shouldRestoreFocus, // eslint-disable-next-line react/jsx-no-bind preventDismissOnEvent: function (ev) { return _this._preventDismissOnScrollOrResize(ev); } }), onRenderUpperContent(_this.props, _this._onRenderUpperContent), React.createElement("div", { className: _this._classNames.optionsContainerWrapper, ref: _this._comboBoxMenu }, onRenderList === null || onRenderList === void 0 ? void 0 : onRenderList(tslib_1.__assign(tslib_1.__assign({}, props), { id: id }), _this._onRenderList)), onRenderLowerContent(_this.props, _this._onRenderLowerContent))); }; _this._onLayerMounted = function () { _this._onCalloutLayerMounted(); // need to call this again here to get the correct scroll parent dimensions // when the callout is first opened _this._async.setTimeout(function () { _this._scrollIntoView(); }, 0); 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 _a = props.onRenderItem, onRenderItem = _a === void 0 ? _this._onRenderItem : _a, label = props.label, ariaLabel = props.ariaLabel, multiSelect = props.multiSelect; var queue = { items: [] }; var renderedList = []; var emptyQueue = function () { var newGroup = queue.id ? [ React.createElement("div", { role: "group", key: queue.id, "aria-labelledby": queue.id }, queue.items), ] : queue.items; renderedList = tslib_1.__spreadArray(tslib_1.__spreadArray([], renderedList, true), newGroup, true); // Flush items and id queue = { items: [] }; }; var placeRenderedOptionIntoQueue = function (item, index) { /* Case Header empty queue if it's not already empty ensure unique ID for header and set queue ID push header into queue Case Divider push divider into queue if not first item empty queue if not already empty Default push item into queue */ switch (item.itemType) { case SelectableOption_1.SelectableOptionMenuItemType.Header: queue.items.length > 0 && emptyQueue(); var id_1 = _this._id + item.key; queue.items.push(onRenderItem(tslib_1.__assign(tslib_1.__assign({ id: id_1 }, item), { index: index }), _this._onRenderItem)); queue.id = id_1; break; case SelectableOption_1.SelectableOptionMenuItemType.Divider: index > 0 && queue.items.push(onRenderItem(tslib_1.__assign(tslib_1.__assign({}, item), { index: index }), _this._onRenderItem)); queue.items.length > 0 && emptyQueue(); break; default: queue.items.push(onRenderItem(tslib_1.__assign(tslib_1.__assign({}, item), { index: index }), _this._onRenderItem)); } }; // Place options into the queue. Queue will be emptied anytime a Header or Divider is encountered props.options.forEach(function (item, index) { placeRenderedOptionIntoQueue(item, index); }); // Push remaining items into all renderedList queue.items.length > 0 && emptyQueue(); var id = _this._id; return (React.createElement("div", { id: id + '-list', className: _this._classNames.optionsContainer, "aria-labelledby": label && id + '-label', "aria-label": ariaLabel && !label ? ariaLabel : undefined, "aria-multiselectable": multiSelect ? 'true' : undefined, role: "listbox" }, renderedList)); }; // Render items _this._onRenderItem = function (item) { switch (item.itemType) { case SelectableOption_1.SelectableOptionMenuItemType.Divider: return _this._renderSeparator(item); case SelectableOption_1.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; var _b = _this.props.onRenderOption, onRenderOption = _b === void 0 ? _this._onRenderOptionContent : _b; var id = (_a = item.id) !== null && _a !== void 0 ? _a : _this._id + '-list' + item.index; var isSelected = _this._isOptionSelected(item.index); var isChecked = _this._isOptionChecked(item.index); var isIndeterminate = _this._isOptionIndeterminate(item.index); var optionStyles = _this._getCurrentOptionStyles(item); var optionClassNames = (0, ComboBox_classNames_1.getComboBoxOptionClassNames)(optionStyles); var title = item.title; var getOptionComponent = function () { return !_this.props.multiSelect ? (React.createElement(Button_1.CommandButton, { id: id, 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, ariaLabel: item.ariaLabel, ariaLabelledBy: item.ariaLabel ? undefined : id + '-label', key: item.key, styles: optionStyles, className: 'ms-ComboBox-option', onChange: _this._onItemClick(item), label: item.text, checked: isChecked, indeterminate: isIndeterminate, title: title, disabled: item.disabled, // eslint-disable-next-line react/jsx-no-bind onRenderLabel: _this._renderCheckboxLabel.bind(_this, tslib_1.__assign(tslib_1.__assign({}, item), { id: id + '-label' })), inputProps: tslib_1.__assign({ // aria-selected should only be applied to checked items, not hovered items 'aria-selected': isChecked ? 'true' : 'false', role: 'option' }, { 'data-index': item.index, 'data-is-focusable': true, }) })); }; return (React.createElement(ComboBoxOptionWrapper, { key: item.key, index: item.index, disabled: item.disabled, isSelected: isSelected, isChecked: isChecked, isIndeterminate: isIndeterminate, 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 () { var _a; if (!_this._isScrollIdle && _this._scrollIdleTimeoutId !== undefined) { _this._async.clearTimeout(_this._scrollIdleTimeoutId); _this._scrollIdleTimeoutId = undefined; } else { _this._isScrollIdle = false; } if ((_a = _this.props.calloutProps) === null || _a === void 0 ? void 0 : _a.onScroll) { _this.props.calloutProps.onScroll(); } _this._scrollIdleTimeoutId = _this._async.setTimeout(function () { _this._isScrollIdle = true; }, ScrollIdleDelay); }; _this._onRenderOptionContent = function (item) { var optionClassNames = (0, ComboBox_classNames_1.getComboBoxOptionClassNames)(_this._getCurrentOptionStyles(item)); return React.createElement("span", { className: optionClassNames.optionText }, item.text); }; /* * Render content of a multiselect item label. * Text within the label is aria-hidden, to prevent duplicate input/label exposure */ _this._onRenderMultiselectOptionContent = function (item) { var optionClassNames = (0, ComboBox_classNames_1.getComboBoxOptionClassNames)(_this._getCurrentOptionStyles(item)); return (React.createElement("span", { id: item.id, "aria-hidden": "true", 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, allowFreeInput = _a.allowFreeInput, allowParentArrowNavigation = _a.allowParentArrowNavigation, autoComplete = _a.autoComplete, currentOptions = _a.hoisted.currentOptions; var _b = _this.state, isOpen = _b.isOpen, currentPendingValueValidIndexOnHover = _b.currentPendingValueValidIndexOnHover; // Take note if we are processing an alt (option) or meta (command) keydown. // See comment in _onInputKeyUp for reasoning. _this._lastKeyDownWasAltOrMeta = isAltOrMeta(ev); if (disabled) { _this._handleInputWhenDisabled(ev); return; } var index = _this._getPendingSelectedIndex(false /* includeCurrentPendingValue */); // eslint-disable-next-line @typescript-eslint/no-deprecated 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 combo box is open, flip the open state if (isOpen) { _this._setOpenStateAndFocusOnClose(!isOpen, false /* focusInputAfterClose */); } // Allow TAB to propagate 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.props.hoisted.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; } // do not scroll page ev.preventDefault(); // Go to the previous option _this._setPendingInfoFromIndexAndDirection(index, SearchDirection.backward); break; case Utilities_1.KeyCodes.down: // Expand the combo box 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; } // do not scroll page ev.preventDefault(); // Got to the next option _this._setPendingInfoFromIndexAndDirection(index, SearchDirection.forward); } break; case Utilities_1.KeyCodes.home: case Utilities_1.KeyCodes.end: if (allowFreeform || allowFreeInput) { 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 // eslint-disable-next-line @typescript-eslint/no-deprecated 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 && !allowFreeInput && autoComplete === 'off') { break; } default: /* eslint-enable no-fallthrough */ // are we processing a function key? if so bail out // eslint-disable-next-line @typescript-eslint/no-deprecated 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 // eslint-disable-next-line @typescript-eslint/no-deprecated if (ev.keyCode === Utilities_1.KeyCodes.alt || ev.key === 'Meta' /* && isOpen */) { return; } // eslint-disable-next-line @typescript-eslint/no-deprecated if (allowParentArrowNavigation && (ev.keyCode === Utilities_1.KeyCodes.left || ev.keyCode === Utilities_1.KeyCodes.right)) { return; } // If we are not allowing freeform or free input and // allowing autoComplete, handle the input here if (!allowFreeform && !allowFreeInput && 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, allowFreeInput = _a.allowFreeInput, 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 && isAltOrMeta(ev); _this._lastKeyDownWasAltOrMeta = false; var shouldHandleKey = keyPressIsAltOrMetaAlone && !((0, Utilities_1.isMac)() || (0, Utilities_1.isIOS)()); if (disabled) { _this._handleInputWhenDisabled(ev); return; } // eslint-disable-next-line @typescript-eslint/no-deprecated switch (ev.which) { case Utilities_1.KeyCodes.space: // If we are not allowing freeform or free input, and autoComplete is off // make space expand/collapse the combo box // and allow the event to propagate if (!allowFreeform && !allowFreeInput && 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({ currentPendingValueValidIndex