1887 lines
111 KiB
JavaScript
1887 lines
111 KiB
JavaScript
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({
|
|
currentPendingValueValidIndexOnHover: HoverStatus.clearAll,
|
|
});
|
|
};
|
|
/**
|
|
* Click handler for the button of the combo box and the input when not allowing freeform.
|
|
* This toggles the expand/collapse state of the combo box (if enabled).
|
|
*/
|
|
_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();
|
|
}
|
|
};
|
|
(0, Utilities_1.initializeComponentRef)(_this);
|
|
_this._async = new Utilities_1.Async(_this);
|
|
_this._events = new Utilities_1.EventGroup(_this);
|
|
(0, Utilities_1.warnMutuallyExclusive)(COMPONENT_NAME, props, {
|
|
defaultSelectedKey: 'selectedKey',
|
|
text: 'defaultSelectedKey',
|
|
selectedKey: 'value',
|
|
dropdownWidth: 'useComboBoxAsMenuWidth',
|
|
ariaLabel: 'label',
|
|
});
|
|
_this._id = props.id || (0, Utilities_1.getId)('ComboBox');
|
|
_this._isScrollIdle = true;
|
|
_this._processingTouch = false;
|
|
_this._gotMouseMove = false;
|
|
_this._processingClearPendingInfo = false;
|
|
_this.state = {
|
|
isOpen: false,
|
|
focusState: 'none',
|
|
currentPendingValueValidIndex: -1,
|
|
currentPendingValue: undefined,
|
|
currentPendingValueValidIndexOnHover: HoverStatus.default,
|
|
};
|
|
return _this;
|
|
}
|
|
Object.defineProperty(ComboBoxInternal.prototype, "selectedOptions", {
|
|
/**
|
|
* All selected options
|
|
*/
|
|
get: function () {
|
|
var _a = this.props.hoisted, currentOptions = _a.currentOptions, selectedIndices = _a.selectedIndices;
|
|
return (0, SelectableOption_1.getAllSelectedOptions)(currentOptions, selectedIndices);
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
ComboBoxInternal.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);
|
|
}
|
|
}
|
|
};
|
|
ComboBoxInternal.prototype.componentDidUpdate = function (prevProps, prevState) {
|
|
var _this = this;
|
|
var _a, _b, _c;
|
|
var _d = this.props, allowFreeform = _d.allowFreeform, allowFreeInput = _d.allowFreeInput, text = _d.text, onMenuOpen = _d.onMenuOpen, onMenuDismissed = _d.onMenuDismissed, _e = _d.hoisted, currentOptions = _e.currentOptions, selectedIndices = _e.selectedIndices;
|
|
var _f = this.state, currentPendingValue = _f.currentPendingValue, currentPendingValueValidIndex = _f.currentPendingValueValidIndex, isOpen = _f.isOpen;
|
|
// 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);
|
|
}
|
|
var doc = (0, dom_1.getDocumentEx)(this.context);
|
|
// 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 &&
|
|
(doc === null || doc === void 0 ? void 0 : doc.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 free input OR
|
|
// the value changed
|
|
// we need to set selection
|
|
if (this._focusInputAfterClose &&
|
|
((prevState.isOpen && !isOpen) ||
|
|
(this._hasFocus() &&
|
|
((!isOpen &&
|
|
!this.props.multiSelect &&
|
|
prevProps.hoisted.selectedIndices &&
|
|
selectedIndices &&
|
|
prevProps.hoisted.selectedIndices[0] !== selectedIndices[0]) ||
|
|
(!allowFreeform && !allowFreeInput) ||
|
|
text !== prevProps.text)))) {
|
|
this._onFocus();
|
|
}
|
|
this._notifyPendingValueChanged(prevState);
|
|
if (isOpen && !prevState.isOpen) {
|
|
// handle dismiss buffer after suggestions are opened
|
|
this._overrideScrollDismiss = true;
|
|
this._async.clearTimeout(this._overrideScrollDimissTimeout);
|
|
this._overrideScrollDimissTimeout = this._async.setTimeout(function () {
|
|
_this._overrideScrollDismiss = false;
|
|
}, 100);
|
|
onMenuOpen === null || onMenuOpen === void 0 ? void 0 : onMenuOpen();
|
|
}
|
|
if (!isOpen && prevState.isOpen && onMenuDismissed) {
|
|
onMenuDismissed();
|
|
}
|
|
var newCurrentPendingValueValidIndex = currentPendingValueValidIndex;
|
|
var options = currentOptions.map(function (item, index) { return (tslib_1.__assign(tslib_1.__assign({}, item), { index: index })); });
|
|
// If currentOptions differs from the previous currentOptions we need to update the currentPendingValueValidIndex
|
|
// otherwise, it will be out of sync with the currentOptions. This can happen when the options are filtered.
|
|
if (!(0, Utilities_1.shallowCompare)(prevProps.hoisted.currentOptions, currentOptions) && currentPendingValue) {
|
|
newCurrentPendingValueValidIndex =
|
|
this.props.allowFreeform || this.props.allowFreeInput
|
|
? this._processInputChangeWithFreeform(currentPendingValue)
|
|
: this._updateAutocompleteIndexWithoutFreeform(currentPendingValue);
|
|
}
|
|
var descendantText = undefined;
|
|
if (isOpen && this._hasFocus() && newCurrentPendingValueValidIndex !== -1) {
|
|
descendantText =
|
|
(_a = options[newCurrentPendingValueValidIndex].id) !== null && _a !== void 0 ? _a : this._id + '-list' + newCurrentPendingValueValidIndex;
|
|
}
|
|
else if (isOpen && selectedIndices.length) {
|
|
descendantText = (_c = (_b = options[selectedIndices[0]]) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : this._id + '-list' + selectedIndices[0];
|
|
}
|
|
if (descendantText !== this.state.ariaActiveDescendantValue) {
|
|
this.setState({
|
|
ariaActiveDescendantValue: descendantText,
|
|
});
|
|
}
|
|
};
|
|
ComboBoxInternal.prototype.componentWillUnmount = function () {
|
|
this._async.dispose();
|
|
this._events.dispose();
|
|
};
|
|
// Primary Render
|
|
ComboBoxInternal.prototype.render = function () {
|
|
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, persistMenu = _a.persistMenu, multiSelect = _a.multiSelect, _g = _a.hoisted, suggestedDisplayValue = _g.suggestedDisplayValue, selectedIndices = _g.selectedIndices, currentOptions = _g.currentOptions;
|
|
var isOpen = this.state.isOpen;
|
|
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(selectedIndices, currentOptions, suggestedDisplayValue)
|
|
: undefined;
|
|
var divProps = (0, Utilities_1.getNativeProps)(this.props, Utilities_1.divProperties, [
|
|
'onChange',
|
|
'value',
|
|
'aria-describedby',
|
|
'aria-labelledby',
|
|
]);
|
|
var hasErrorMessage = errorMessage && errorMessage.length > 0 ? true : false;
|
|
this._classNames = this.props.getClassNames
|
|
? this.props.getClassNames(theme, !!isOpen, !!disabled, !!required, !!this._hasFocus(), !!allowFreeform, !!hasErrorMessage, className)
|
|
: (0, ComboBox_classNames_1.getClassNames)((0, ComboBox_styles_1.getStyles)(theme, customStyles), className, !!isOpen, !!disabled, !!required, !!this._hasFocus(), !!allowFreeform, !!hasErrorMessage);
|
|
var comboBoxWrapper = this._renderComboBoxWrapper(multiselectAccessibleText, errorMessageId);
|
|
return (React.createElement("div", tslib_1.__assign({}, divProps, { ref: this.props.hoisted.mergedRootRef, className: this._classNames.container }),
|
|
onRenderLabel({ props: this.props, multiselectAccessibleText: multiselectAccessibleText }, this._onRenderLabel),
|
|
comboBoxWrapper,
|
|
(persistMenu || isOpen) &&
|
|
onRenderContainer(tslib_1.__assign(tslib_1.__assign({}, this.props), { onRenderList: onRenderList, onRenderItem: onRenderItem, onRenderOption: onRenderOption, options: currentOptions.map(function (item, index) { return (tslib_1.__assign(tslib_1.__assign({}, item), { index: index })); }), onDismiss: this._onDismiss }), this._onRenderContainer),
|
|
hasErrorMessage && (React.createElement("div", { role: "alert", id: errorMessageId, className: this._classNames.errorMessage }, errorMessage))));
|
|
};
|
|
ComboBoxInternal.prototype._getPendingString = function (currentPendingValue, currentOptions, index) {
|
|
return currentPendingValue !== null && currentPendingValue !== undefined
|
|
? currentPendingValue
|
|
: indexWithinBounds(currentOptions, index)
|
|
? getPreviewText(currentOptions[index])
|
|
: '';
|
|
};
|
|
/**
|
|
* Returns a string that concatenates all of the selected values
|
|
* for multiselect combo box.
|
|
*/
|
|
ComboBoxInternal.prototype._getMultiselectDisplayString = function (selectedIndices, currentOptions, suggestedDisplayValue) {
|
|
var displayValues = [];
|
|
for (var idx = 0; selectedIndices && idx < selectedIndices.length; idx++) {
|
|
var index = selectedIndices[idx];
|
|
if (currentOptions[index].itemType !== SelectableOption_1.SelectableOptionMenuItemType.SelectAll) {
|
|
displayValues.push(indexWithinBounds(currentOptions, index)
|
|
? currentOptions[index].text
|
|
: normalizeToString(suggestedDisplayValue));
|
|
}
|
|
}
|
|
var _a = this.props.multiSelectDelimiter, multiSelectDelimiter = _a === void 0 ? ', ' : _a;
|
|
return displayValues.join(multiSelectDelimiter);
|
|
};
|
|
/**
|
|
* Do not dismiss if the window resizes or scrolls within 100ms of opening
|
|
* This prevents the Android issue where pickers immediately dismiss on open, because the keyboard appears
|
|
* @param ev - the event triggering the dismiss check
|
|
* @returns a boolean indicating whether the callout dismissal should be prevented
|
|
*/
|
|
ComboBoxInternal.prototype._preventDismissOnScrollOrResize = function (ev) {
|
|
// default to passed-in preventDismiss
|
|
var calloutProps = this.props.calloutProps;
|
|
if (calloutProps === null || calloutProps === void 0 ? void 0 : calloutProps.preventDismissOnEvent) {
|
|
return calloutProps.preventDismissOnEvent(ev);
|
|
}
|
|
if (this._overrideScrollDismiss && (ev.type === 'scroll' || ev.type === 'resize')) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
/**
|
|
* Process the new input's new value when the combo box allows freeform entry
|
|
* @param updatedValue - the input's newly changed value
|
|
* @returns the index of the matched option, -1 if no match was found
|
|
*/
|
|
ComboBoxInternal.prototype._processInputChangeWithFreeform = function (updatedValue) {
|
|
var _this = this;
|
|
var currentOptions = this.props.hoisted.currentOptions;
|
|
var newCurrentPendingValueValidIndex = -1;
|
|
// if the new value is empty, see if we have an exact match and then set the pending info
|
|
if (updatedValue === '') {
|
|
var items = currentOptions
|
|
.map(function (item, index) { return (tslib_1.__assign(tslib_1.__assign({}, item), { index: index })); })
|
|
.filter(function (option) { return isNormalOption(option) && !option.disabled && getPreviewText(option) === updatedValue; });
|
|
// if we found a match remember the index
|
|
if (items.length === 1) {
|
|
newCurrentPendingValueValidIndex = items[0].index;
|
|
}
|
|
this._setPendingInfo(updatedValue, newCurrentPendingValueValidIndex, updatedValue);
|
|
return newCurrentPendingValueValidIndex;
|
|
}
|
|
// Remember the original value and then make the value lowercase for comparison
|
|
var originalUpdatedValue = updatedValue;
|
|
// Make the value lowercase for comparison if caseSensitive is false
|
|
updatedValue = this._adjustForCaseSensitivity(updatedValue);
|
|
var newSuggestedDisplayValue = '';
|
|
// If autoComplete is on, attempt to find a match from the available options
|
|
if (this.props.autoComplete === 'on') {
|
|
// If autoComplete is on, attempt to find a match where the text of an option starts with the updated value
|
|
var items = currentOptions
|
|
.map(function (item, index) { return (tslib_1.__assign(tslib_1.__assign({}, item), { index: index })); })
|
|
.filter(function (option) {
|
|
return isNormalOption(option) &&
|
|
!option.disabled &&
|
|
_this._adjustForCaseSensitivity(getPreviewText(option)).indexOf(updatedValue) === 0;
|
|
});
|
|
if (items.length > 0) {
|
|
// use ariaLabel as the value when the option is set
|
|
var text = getPreviewText(items[0]);
|
|
// If the user typed out the complete option text, we don't need any suggested display text anymore
|
|
newSuggestedDisplayValue = this._adjustForCaseSensitivity(text) !== updatedValue ? text : '';
|
|
// remember the index of the match we found
|
|
newCurrentPendingValueValidIndex = items[0].index;
|
|
}
|
|
}
|
|
else {
|
|
// If autoComplete is off, attempt to find a match only when the value is exactly equal to the text of an option
|
|
var items = currentOptions
|
|
.map(function (item, index) { return (tslib_1.__assign(tslib_1.__assign({}, item), { index: index })); })
|
|
.filter(function (option) {
|
|
return isNormalOption(option) &&
|
|
!option.disabled &&
|
|
_this._adjustForCaseSensitivity(getPreviewText(option)) === updatedValue;
|
|
});
|
|
// if we found a match remember the index
|
|
if (items.length === 1) {
|
|
newCurrentPendingValueValidIndex = items[0].index;
|
|
}
|
|
}
|
|
// Set the updated state
|
|
this._setPendingInfo(originalUpdatedValue, newCurrentPendingValueValidIndex, newSuggestedDisplayValue);
|
|
return newCurrentPendingValueValidIndex;
|
|
};
|
|
/**
|
|
* Process the new input's new value when the combo box does not allow freeform entry
|
|
* @param updatedValue - the input's newly changed value
|
|
* @returns the index of the matched option
|
|
*/
|
|
ComboBoxInternal.prototype._processInputChangeWithoutFreeform = function (updatedValue) {
|
|
var _this = this;
|
|
var _a = this.state, currentPendingValue = _a.currentPendingValue, currentPendingValueValidIndex = _a.currentPendingValueValidIndex;
|
|
if (this.props.autoComplete === 'on') {
|
|
// If autoComplete is on while allow freeform is off,
|
|
// we will remember the key press and build up a string to attempt to match
|
|
// as long as characters are typed within a the timeout span of each other,
|
|
// otherwise we will clear the string and start building a new one on the next keypress.
|
|
// Also, only do this processing if we have a non-empty value
|
|
if (updatedValue !== '') {
|
|
// If we have a pending autocomplete clearing task,
|
|
// we know that the user is typing with key press happening
|
|
// within the timeout of each other so remove the clearing task
|
|
// and continue building the pending value with the updated value
|
|
if (this._autoCompleteTimeout) {
|
|
this._async.clearTimeout(this._autoCompleteTimeout);
|
|
this._autoCompleteTimeout = undefined;
|
|
updatedValue = normalizeToString(currentPendingValue) + updatedValue;
|
|
}
|
|
var matchingIndex = this._updateAutocompleteIndexWithoutFreeform(updatedValue);
|
|
// Schedule a timeout to clear the pending value after the timeout span
|
|
this._autoCompleteTimeout = this._async.setTimeout(function () {
|
|
_this._autoCompleteTimeout = undefined;
|
|
}, ReadOnlyPendingAutoCompleteTimeout);
|
|
return matchingIndex;
|
|
}
|
|
}
|
|
// If we get here, autoComplete is off.
|
|
// Remember we are not allowing freeform, so at this point, if we have a pending valid value index
|
|
// use that; otherwise use the selectedIndex
|
|
var index = currentPendingValueValidIndex >= 0 ? currentPendingValueValidIndex : this._getFirstSelectedIndex();
|
|
// Since we are not allowing freeform, we need to
|
|
// set both the pending and suggested values/index
|
|
// to allow us to select all content in the input to
|
|
// give the illusion that we are readonly (e.g. freeform off)
|
|
this._setPendingInfoFromIndex(index);
|
|
return index;
|
|
};
|
|
ComboBoxInternal.prototype._updateAutocompleteIndexWithoutFreeform = function (updatedValue) {
|
|
var _this = this;
|
|
var currentOptions = this.props.hoisted.currentOptions;
|
|
var originalUpdatedValue = updatedValue;
|
|
updatedValue = this._adjustForCaseSensitivity(updatedValue);
|
|
// If autoComplete is on, attempt to find a match where the text of an option starts with the updated value
|
|
var items = currentOptions
|
|
.map(function (item, i) { return (tslib_1.__assign(tslib_1.__assign({}, item), { index: i })); })
|
|
.filter(function (option) {
|
|
return isNormalOption(option) &&
|
|
!option.disabled &&
|
|
_this._adjustForCaseSensitivity(option.text).indexOf(updatedValue) === 0;
|
|
});
|
|
// If we found a match, update the state
|
|
if (items.length > 0) {
|
|
this._setPendingInfo(originalUpdatedValue, items[0].index, getPreviewText(items[0]));
|
|
return items[0].index;
|
|
}
|
|
return -1;
|
|
};
|
|
ComboBoxInternal.prototype._getFirstSelectedIndex = function () {
|
|
var selectedIndices = this.props.hoisted.selectedIndices;
|
|
return (selectedIndices === null || selectedIndices === void 0 ? void 0 : selectedIndices.length) ? selectedIndices[0] : -1;
|
|
};
|
|
/**
|
|
* Walk along the options starting at the index, stepping by the delta (positive or negative)
|
|
* looking for the next valid selectable index (e.g. skipping headings and dividers)
|
|
* @param index - the index to get the next selectable index from
|
|
* @param delta - optional delta to step by when finding the next index, defaults to 0
|
|
* @returns - the next valid selectable index. If the new index is outside of the bounds,
|
|
* it will snap to the edge of the options array. If delta == 0 and the given index is not selectable
|
|
*/
|
|
ComboBoxInternal.prototype._getNextSelectableIndex = function (index, searchDirection) {
|
|
var currentOptions = this.props.hoisted.currentOptions;
|
|
var newIndex = index + searchDirection;
|
|
newIndex = Math.max(0, Math.min(currentOptions.length - 1, newIndex));
|
|
if (!indexWithinBounds(currentOptions, newIndex)) {
|
|
return -1;
|
|
}
|
|
var option = currentOptions[newIndex];
|
|
if (!isSelectableOption(option) || option.hidden === true) {
|
|
// Should we continue looking for an index to select?
|
|
if (searchDirection !== SearchDirection.none &&
|
|
((newIndex > 0 && searchDirection < SearchDirection.none) ||
|
|
(newIndex >= 0 && newIndex < currentOptions.length && searchDirection > SearchDirection.none))) {
|
|
newIndex = this._getNextSelectableIndex(newIndex, searchDirection);
|
|
}
|
|
else {
|
|
// If we cannot perform a useful search just return the index we were given
|
|
return index;
|
|
}
|
|
}
|
|
// We have the next valid selectable index, return it
|
|
return newIndex;
|
|
};
|
|
/**
|
|
* Set the selected index. Note, this is
|
|
* the "real" selected index, not the pending selected index
|
|
* @param index - the index to set (or the index to set from if a search direction is provided)
|
|
* @param searchDirection - the direction to search along the options from the given index
|
|
*/
|
|
ComboBoxInternal.prototype._setSelectedIndex = function (index, submitPendingValueEvent, searchDirection) {
|
|
if (searchDirection === void 0) { searchDirection = SearchDirection.none; }
|
|
var _a = this.props, onChange = _a.onChange, onPendingValueChanged = _a.onPendingValueChanged, _b = _a.hoisted, initialIndices = _b.selectedIndices, currentOptions = _b.currentOptions;
|
|
// Clone currentOptions and selectedIndices so we don't mutate state
|
|
var selectedIndices = initialIndices ? initialIndices.slice() : [];
|
|
var changedOptions = currentOptions.slice();
|
|
// Find the next selectable index, if searchDirection is none
|
|
// we will get our starting index back
|
|
index = this._getNextSelectableIndex(index, searchDirection);
|
|
if (!indexWithinBounds(currentOptions, index)) {
|
|
return;
|
|
}
|
|
// Are we at a new index? If so, update the state, otherwise
|
|
// there is nothing to do
|
|
if (this.props.multiSelect ||
|
|
selectedIndices.length < 1 ||
|
|
(selectedIndices.length === 1 && selectedIndices[0] !== index)) {
|
|
var option = tslib_1.__assign({}, currentOptions[index]);
|
|
// if option doesn't existing, or option is disabled, we noop
|
|
if (!option || option.disabled) {
|
|
return;
|
|
}
|
|
if (this.props.multiSelect) {
|
|
// Setting the initial state of option.selected in Multi-select combo box by checking the
|
|
// selectedIndices array and overriding the undefined issue
|
|
option.selected = option.selected !== undefined ? !option.selected : selectedIndices.indexOf(index) < 0;
|
|
// handle changing all options if SelectAll is changed
|
|
if (option.itemType === SelectableOption_1.SelectableOptionMenuItemType.SelectAll) {
|
|
selectedIndices = [];
|
|
// if select all is set to checked, push all selectable option indices
|
|
if (option.selected) {
|
|
currentOptions.forEach(function (currentOption, i) {
|
|
if (!currentOption.disabled && isSelectableOption(currentOption)) {
|
|
selectedIndices.push(i);
|
|
changedOptions[i] = tslib_1.__assign(tslib_1.__assign({}, currentOption), { selected: true });
|
|
}
|
|
});
|
|
}
|
|
// otherwise un-check all options
|
|
else {
|
|
changedOptions = currentOptions.map(function (currentOption) { return (tslib_1.__assign(tslib_1.__assign({}, currentOption), { selected: false })); });
|
|
}
|
|
}
|
|
// otherwise update the individual option
|
|
else {
|
|
if (option.selected && selectedIndices.indexOf(index) < 0) {
|
|
selectedIndices.push(index);
|
|
}
|
|
else if (!option.selected && selectedIndices.indexOf(index) >= 0) {
|
|
selectedIndices = selectedIndices.filter(function (value) { return value !== index; });
|
|
}
|
|
changedOptions[index] = option;
|
|
// If SelectAll exists and another option was toggled, update the SelectAll option's state
|
|
var selectAllOption = changedOptions.filter(function (o) { return o.itemType === SelectableOption_1.SelectableOptionMenuItemType.SelectAll; })[0];
|
|
if (selectAllOption) {
|
|
var selectAllState = this._isSelectAllChecked(selectedIndices);
|
|
var selectAllIndex_1 = changedOptions.indexOf(selectAllOption);
|
|
if (selectAllState) {
|
|
selectedIndices.push(selectAllIndex_1);
|
|
changedOptions[selectAllIndex_1] = tslib_1.__assign(tslib_1.__assign({}, selectAllOption), { selected: true });
|
|
}
|
|
else {
|
|
selectedIndices = selectedIndices.filter(function (value) { return value !== selectAllIndex_1; });
|
|
changedOptions[selectAllIndex_1] = tslib_1.__assign(tslib_1.__assign({}, selectAllOption), { selected: false });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
selectedIndices[0] = index;
|
|
}
|
|
submitPendingValueEvent.persist();
|
|
// Only setState if combo box is uncontrolled.
|
|
if (this.props.selectedKey || this.props.selectedKey === null) {
|
|
// If combo box value is changed, revert preview first
|
|
if (this._hasPendingValue && onPendingValueChanged) {
|
|
onPendingValueChanged();
|
|
this._hasPendingValue = false;
|
|
}
|
|
}
|
|
else {
|
|
this.props.hoisted.setSelectedIndices(selectedIndices);
|
|
this.props.hoisted.setCurrentOptions(changedOptions);
|
|
// If ComboBox value is changed, revert preview first
|
|
if (this._hasPendingValue && onPendingValueChanged) {
|
|
onPendingValueChanged();
|
|
this._hasPendingValue = false;
|
|
}
|
|
}
|
|
// Call onChange after state is updated
|
|
if (onChange) {
|
|
onChange(submitPendingValueEvent, option, index, getPreviewText(option));
|
|
}
|
|
}
|
|
if (this.props.multiSelect && this.state.isOpen) {
|
|
return;
|
|
}
|
|
// clear all of the pending info
|
|
this._clearPendingInfo();
|
|
};
|
|
/**
|
|
* Submit a pending value if there is one
|
|
*/
|
|
ComboBoxInternal.prototype._submitPendingValue = function (submitPendingValueEvent) {
|
|
var _a;
|
|
var _b = this.props, onChange = _b.onChange, allowFreeform = _b.allowFreeform, autoComplete = _b.autoComplete, multiSelect = _b.multiSelect, hoisted = _b.hoisted;
|
|
var currentOptions = hoisted.currentOptions;
|
|
var _c = this.state, currentPendingValue = _c.currentPendingValue, currentPendingValueValidIndex = _c.currentPendingValueValidIndex, currentPendingValueValidIndexOnHover = _c.currentPendingValueValidIndexOnHover;
|
|
var selectedIndices = this.props.hoisted.selectedIndices;
|
|
// Do not submit any pending value if we
|
|
// have already initiated clearing the pending info
|
|
if (this._processingClearPendingInfo) {
|
|
return;
|
|
}
|
|
// If we allow freeform we need to handle that
|
|
if (allowFreeform) {
|
|
// if currentPendingValue is null or undefined the user did not submit anything
|
|
// (not even empty because we would have stored that as the pending value)
|
|
if (currentPendingValue === null || currentPendingValue === undefined) {
|
|
// if a user did not type anything they may just hovered over an item
|
|
if (currentPendingValueValidIndexOnHover >= 0) {
|
|
this._setSelectedIndex(currentPendingValueValidIndexOnHover, submitPendingValueEvent);
|
|
this._clearPendingInfo();
|
|
}
|
|
return;
|
|
}
|
|
// Check to see if the user typed an exact match
|
|
if (indexWithinBounds(currentOptions, currentPendingValueValidIndex)) {
|
|
var pendingOptionText = this._adjustForCaseSensitivity(getPreviewText(currentOptions[currentPendingValueValidIndex]));
|
|
var autofill = this._autofill.current;
|
|
// By exact match, that means: our pending value is the same as the pending option text OR
|
|
// the pending option starts with the pending value and we have an "autoComplete" selection
|
|
// where the total length is equal to pending option length OR
|
|
// the live value in the underlying input matches the pending option; update the state
|
|
var adjustedCurrentPendingValue = this._adjustForCaseSensitivity(currentPendingValue);
|
|
if (adjustedCurrentPendingValue === pendingOptionText ||
|
|
(autoComplete &&
|
|
pendingOptionText.indexOf(adjustedCurrentPendingValue) === 0 &&
|
|
(autofill === null || autofill === void 0 ? void 0 : autofill.isValueSelected) &&
|
|
currentPendingValue.length + (autofill.selectionEnd - autofill.selectionStart) ===
|
|
pendingOptionText.length) ||
|
|
(((_a = autofill === null || autofill === void 0 ? void 0 : autofill.inputElement) === null || _a === void 0 ? void 0 : _a.value) !== undefined &&
|
|
this._adjustForCaseSensitivity(autofill.inputElement.value) === pendingOptionText)) {
|
|
this._setSelectedIndex(currentPendingValueValidIndex, submitPendingValueEvent);
|
|
if (multiSelect && this.state.isOpen) {
|
|
return;
|
|
}
|
|
this._clearPendingInfo();
|
|
return;
|
|
}
|
|
}
|
|
if (onChange) {
|
|
if (onChange) {
|
|
// trigger onChange to clear value
|
|
onChange(submitPendingValueEvent, undefined, undefined, currentPendingValue);
|
|
}
|
|
}
|
|
else {
|
|
// If we are not controlled, create a new selected option
|
|
var newOption = {
|
|
key: currentPendingValue || (0, Utilities_1.getId)(),
|
|
text: normalizeToString(currentPendingValue),
|
|
};
|
|
// If it's multiselect, set selected state to true
|
|
if (multiSelect) {
|
|
newOption.selected = true;
|
|
}
|
|
var newOptions = currentOptions.concat([newOption]);
|
|
if (selectedIndices) {
|
|
if (!multiSelect) {
|
|
selectedIndices = [];
|
|
}
|
|
selectedIndices.push(newOptions.length - 1);
|
|
}
|
|
hoisted.setCurrentOptions(newOptions);
|
|
hoisted.setSelectedIndices(selectedIndices);
|
|
}
|
|
}
|
|
else if (currentPendingValueValidIndex >= 0) {
|
|
// Since we are not allowing freeform, we must have a matching
|
|
// to be able to update state
|
|
this._setSelectedIndex(currentPendingValueValidIndex, submitPendingValueEvent);
|
|
}
|
|
else if (currentPendingValueValidIndexOnHover >= 0) {
|
|
// If all else failed and we were hovering over an item, select it
|
|
this._setSelectedIndex(currentPendingValueValidIndexOnHover, submitPendingValueEvent);
|
|
}
|
|
// Finally, clear the pending info
|
|
this._clearPendingInfo();
|
|
};
|
|
ComboBoxInternal.prototype._onCalloutLayerMounted = function () {
|
|
// In persistMenu mode _onLayerMounted is only called once for the lifetime
|
|
// of the component. Any functionality required for callout "on mount" can
|
|
// go here so that we can also call it again during callout dismissal to reset
|
|
// object state.
|
|
this._gotMouseMove = false;
|
|
};
|
|
// Render separator
|
|
ComboBoxInternal.prototype._renderSeparator = function (item) {
|
|
var index = item.index, key = item.key;
|
|
if (index && index > 0) {
|
|
return React.createElement("div", { role: "presentation", key: key, className: this._classNames.divider });
|
|
}
|
|
return null;
|
|
};
|
|
ComboBoxInternal.prototype._renderHeader = function (item) {
|
|
var _a = this.props.onRenderOption, onRenderOption = _a === void 0 ? this._onRenderOptionContent : _a;
|
|
return (React.createElement("div", { id: item.id, key: item.key, className: this._classNames.header }, onRenderOption(item, this._onRenderOptionContent)));
|
|
};
|
|
ComboBoxInternal.prototype._renderCheckboxLabel = function (item) {
|
|
var _a = this.props.onRenderOption, onRenderOption = _a === void 0 ? this._onRenderMultiselectOptionContent : _a;
|
|
return onRenderOption(item, this._onRenderMultiselectOptionContent);
|
|
};
|
|
/**
|
|
* If we are coming from a mouseOut:
|
|
* there is no visible selected option.
|
|
*
|
|
* Else if We are hovering over an item:
|
|
* that gets the selected look.
|
|
*
|
|
* Else:
|
|
* Use the current valid pending index if it exists OR
|
|
* we do not have a valid index and we currently have a pending input value,
|
|
* otherwise use the selected index
|
|
* */
|
|
ComboBoxInternal.prototype._isOptionHighlighted = function (index) {
|
|
var currentPendingValueValidIndexOnHover = this.state.currentPendingValueValidIndexOnHover;
|
|
// If the hover state is set to clearAll, don't show a selected index.
|
|
// Note, this happens when the user moused out of the menu items
|
|
if (currentPendingValueValidIndexOnHover === HoverStatus.clearAll) {
|
|
return false;
|
|
}
|
|
return currentPendingValueValidIndexOnHover >= 0
|
|
? currentPendingValueValidIndexOnHover === index
|
|
: this._isOptionSelected(index);
|
|
};
|
|
ComboBoxInternal.prototype._isOptionSelected = function (index) {
|
|
return this._getPendingSelectedIndex(true /* includePendingValue */) === index;
|
|
};
|
|
ComboBoxInternal.prototype._isOptionChecked = function (index) {
|
|
if (this.props.multiSelect && index !== undefined && this.props.hoisted.selectedIndices) {
|
|
var idxOfSelectedIndex = -1;
|
|
idxOfSelectedIndex = this.props.hoisted.selectedIndices.indexOf(index);
|
|
return idxOfSelectedIndex >= 0;
|
|
}
|
|
return false;
|
|
};
|
|
ComboBoxInternal.prototype._isOptionIndeterminate = function (index) {
|
|
var _a = this.props, multiSelect = _a.multiSelect, hoisted = _a.hoisted;
|
|
if (multiSelect && index !== undefined && hoisted.selectedIndices && hoisted.currentOptions) {
|
|
var option = hoisted.currentOptions[index];
|
|
if (option && option.itemType === SelectableOption_1.SelectableOptionMenuItemType.SelectAll) {
|
|
return hoisted.selectedIndices.length > 0 && !this._isSelectAllChecked();
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
ComboBoxInternal.prototype._isSelectAllChecked = function (testIndices) {
|
|
var _a = this.props, multiSelect = _a.multiSelect, hoisted = _a.hoisted;
|
|
var selectAllOption = hoisted.currentOptions.find(function (option) { return option.itemType === SelectableOption_1.SelectableOptionMenuItemType.SelectAll; });
|
|
var selectedIndices = testIndices || hoisted.selectedIndices;
|
|
if (!multiSelect || !selectedIndices || !selectAllOption) {
|
|
return false;
|
|
}
|
|
// start by not including the select all option itself
|
|
var selectAllIndex = hoisted.currentOptions.indexOf(selectAllOption);
|
|
var compareSelectedIndices = selectedIndices.filter(function (value) { return value !== selectAllIndex; });
|
|
// get array of selectable options, excluding disabled options, headers, and dividers
|
|
var selectableOptions = hoisted.currentOptions.filter(function (option) {
|
|
return !option.disabled && option.itemType !== SelectableOption_1.SelectableOptionMenuItemType.SelectAll && isSelectableOption(option);
|
|
});
|
|
return compareSelectedIndices.length === selectableOptions.length;
|
|
};
|
|
/**
|
|
* Gets the pending selected index taking into account valueValidIndex and selectedIndex
|
|
* @param includeCurrentPendingValue - Should we include the currentPendingValue when
|
|
* finding the index
|
|
*/
|
|
ComboBoxInternal.prototype._getPendingSelectedIndex = function (includeCurrentPendingValue) {
|
|
var _a = this.state, currentPendingValueValidIndex = _a.currentPendingValueValidIndex, currentPendingValue = _a.currentPendingValue;
|
|
return currentPendingValueValidIndex >= 0 ||
|
|
(includeCurrentPendingValue && currentPendingValue !== null && currentPendingValue !== undefined)
|
|
? currentPendingValueValidIndex
|
|
: this.props.multiSelect
|
|
? -1
|
|
: this._getFirstSelectedIndex();
|
|
};
|
|
/**
|
|
* Scroll the selected element into view
|
|
*/
|
|
ComboBoxInternal.prototype._scrollIntoView = function () {
|
|
var _a = this.props, onScrollToItem = _a.onScrollToItem, scrollSelectedToTop = _a.scrollSelectedToTop;
|
|
var currentPendingSelectedIndex = this._getPendingSelectedIndex(true);
|
|
if (onScrollToItem) {
|
|
// Use the custom scroll handler
|
|
onScrollToItem(currentPendingSelectedIndex >= 0 ? currentPendingSelectedIndex : this._getFirstSelectedIndex());
|
|
return;
|
|
}
|
|
var scrollToElement = this._selectedElement.current;
|
|
// in multi-select there are multiple selected elements, so we use the pending select index
|
|
// to locate the option to scroll to.
|
|
if (this.props.multiSelect && this._comboBoxMenu.current) {
|
|
scrollToElement = findFirstDescendant(this._comboBoxMenu.current, function (element) {
|
|
var _a;
|
|
return ((_a = element.dataset) === null || _a === void 0 ? void 0 : _a.index) === currentPendingSelectedIndex.toString();
|
|
});
|
|
}
|
|
if (scrollToElement && scrollToElement.offsetParent) {
|
|
var alignToTop = true;
|
|
// We are using refs, scroll the ref into view
|
|
if (this._comboBoxMenu.current && this._comboBoxMenu.current.offsetParent) {
|
|
var scrollableParent = this._comboBoxMenu.current.offsetParent;
|
|
var selectedElement = scrollToElement.offsetParent;
|
|
var _b = selectedElement, offsetHeight = _b.offsetHeight, offsetTop = _b.offsetTop;
|
|
var _c = scrollableParent, parentOffsetHeight = _c.offsetHeight, scrollTop = _c.scrollTop;
|
|
var isAbove = offsetTop < scrollTop;
|
|
var isBelow = offsetTop + offsetHeight > scrollTop + parentOffsetHeight;
|
|
if (isAbove || scrollSelectedToTop) {
|
|
alignToTop = false;
|
|
scrollableParent.scrollTo(0, offsetTop);
|
|
}
|
|
else if (isBelow) {
|
|
scrollableParent.scrollTo(0, offsetTop - parentOffsetHeight + offsetHeight);
|
|
}
|
|
}
|
|
// if _comboboxMenu doesn't exist, fall back to scrollIntoView
|
|
else {
|
|
scrollToElement.offsetParent.scrollIntoView(alignToTop);
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* Click handler for the menu items
|
|
* to select the item and also close the menu
|
|
* @param index - the index of the item that was clicked
|
|
*/
|
|
ComboBoxInternal.prototype._onItemClick = function (item) {
|
|
var _this = this;
|
|
var onItemClick = this.props.onItemClick;
|
|
var index = item.index;
|
|
return function (ev) {
|
|
// only close the callout when it's in single-select mode
|
|
if (!_this.props.multiSelect) {
|
|
// ensure that focus returns to the input, not the button
|
|
_this._autofill.current && _this._autofill.current.focus();
|
|
_this.setState({
|
|
isOpen: false,
|
|
});
|
|
}
|
|
// Continue processing the click only after
|
|
// performing menu close / control focus(inner working)
|
|
onItemClick && onItemClick(ev, item, index);
|
|
_this._setSelectedIndex(index, ev);
|
|
};
|
|
};
|
|
/**
|
|
* Reset the selected index by clearing the
|
|
* input (of any pending text), clearing the pending state,
|
|
* and setting the suggested display value to the last
|
|
* selected state text
|
|
*/
|
|
ComboBoxInternal.prototype._resetSelectedIndex = function () {
|
|
var currentOptions = this.props.hoisted.currentOptions;
|
|
this._clearPendingInfo();
|
|
var selectedIndex = this._getFirstSelectedIndex();
|
|
if (selectedIndex > 0 && selectedIndex < currentOptions.length) {
|
|
this.props.hoisted.setSuggestedDisplayValue(currentOptions[selectedIndex].text);
|
|
}
|
|
else if (this.props.text) {
|
|
// If we had a value initially, restore it
|
|
this.props.hoisted.setSuggestedDisplayValue(this.props.text);
|
|
}
|
|
};
|
|
/**
|
|
* Clears the pending info state
|
|
*/
|
|
ComboBoxInternal.prototype._clearPendingInfo = function () {
|
|
this._processingClearPendingInfo = true;
|
|
this.props.hoisted.setSuggestedDisplayValue(undefined);
|
|
this.setState({
|
|
currentPendingValue: undefined,
|
|
currentPendingValueValidIndex: -1,
|
|
currentPendingValueValidIndexOnHover: HoverStatus.default,
|
|
}, this._onAfterClearPendingInfo);
|
|
};
|
|
/**
|
|
* Set the pending info
|
|
* @param currentPendingValue - new pending value to set
|
|
* @param currentPendingValueValidIndex - new pending value index to set
|
|
* @param suggestedDisplayValue - new suggest display value to set
|
|
*/
|
|
ComboBoxInternal.prototype._setPendingInfo = function (currentPendingValue, currentPendingValueValidIndex, suggestedDisplayValue) {
|
|
if (currentPendingValueValidIndex === void 0) { currentPendingValueValidIndex = -1; }
|
|
if (this._processingClearPendingInfo) {
|
|
return;
|
|
}
|
|
this.props.hoisted.setSuggestedDisplayValue(suggestedDisplayValue);
|
|
this.setState({
|
|
currentPendingValue: normalizeToString(currentPendingValue),
|
|
currentPendingValueValidIndex: currentPendingValueValidIndex,
|
|
currentPendingValueValidIndexOnHover: HoverStatus.default,
|
|
});
|
|
};
|
|
/**
|
|
* Set the pending info from the given index
|
|
* @param index - the index to set the pending info from
|
|
*/
|
|
ComboBoxInternal.prototype._setPendingInfoFromIndex = function (index) {
|
|
var currentOptions = this.props.hoisted.currentOptions;
|
|
if (index >= 0 && index < currentOptions.length) {
|
|
var option = currentOptions[index];
|
|
this._setPendingInfo(getPreviewText(option), index, getPreviewText(option));
|
|
}
|
|
else {
|
|
this._clearPendingInfo();
|
|
}
|
|
};
|
|
/**
|
|
* Sets the pending info for the combo box
|
|
* @param index - the index to search from
|
|
* @param searchDirection - the direction to search
|
|
*/
|
|
ComboBoxInternal.prototype._setPendingInfoFromIndexAndDirection = function (index, searchDirection) {
|
|
var currentOptions = this.props.hoisted.currentOptions;
|
|
// update index to allow content to wrap
|
|
if (searchDirection === SearchDirection.forward && index >= currentOptions.length - 1) {
|
|
index = -1;
|
|
}
|
|
else if (searchDirection === SearchDirection.backward && index <= 0) {
|
|
index = currentOptions.length;
|
|
}
|
|
// get the next "valid" index
|
|
var indexUpdate = this._getNextSelectableIndex(index, searchDirection);
|
|
// if the two indices are equal we didn't move and
|
|
// we should attempt to get get the first/last "valid" index to use
|
|
// (Note, this takes care of the potential cases where the first/last
|
|
// item is not focusable), otherwise use the updated index
|
|
if (index === indexUpdate) {
|
|
if (searchDirection === SearchDirection.forward) {
|
|
index = this._getNextSelectableIndex(-1, searchDirection);
|
|
}
|
|
else if (searchDirection === SearchDirection.backward) {
|
|
index = this._getNextSelectableIndex(currentOptions.length, searchDirection);
|
|
}
|
|
}
|
|
else {
|
|
index = indexUpdate;
|
|
}
|
|
if (indexWithinBounds(currentOptions, index)) {
|
|
this._setPendingInfoFromIndex(index);
|
|
}
|
|
};
|
|
ComboBoxInternal.prototype._notifyPendingValueChanged = function (prevState) {
|
|
var onPendingValueChanged = this.props.onPendingValueChanged;
|
|
if (!onPendingValueChanged) {
|
|
return;
|
|
}
|
|
var currentOptions = this.props.hoisted.currentOptions;
|
|
var _a = this.state, currentPendingValue = _a.currentPendingValue, currentPendingValueValidIndex = _a.currentPendingValueValidIndex, currentPendingValueValidIndexOnHover = _a.currentPendingValueValidIndexOnHover;
|
|
var newPendingIndex = undefined;
|
|
var newPendingValue = undefined;
|
|
if (currentPendingValueValidIndexOnHover !== prevState.currentPendingValueValidIndexOnHover &&
|
|
indexWithinBounds(currentOptions, currentPendingValueValidIndexOnHover)) {
|
|
// Set new pending index if hover index was changed
|
|
newPendingIndex = currentPendingValueValidIndexOnHover;
|
|
}
|
|
else if (currentPendingValueValidIndex !== prevState.currentPendingValueValidIndex &&
|
|
indexWithinBounds(currentOptions, currentPendingValueValidIndex)) {
|
|
// Set new pending index if currentPendingValueValidIndex was changed
|
|
newPendingIndex = currentPendingValueValidIndex;
|
|
}
|
|
else if (currentPendingValue !== prevState.currentPendingValue) {
|
|
// Set pendingValue in the case it was changed and no index was changed
|
|
newPendingValue = currentPendingValue;
|
|
}
|
|
// Notify when there is a new pending index/value. Also, if there is a pending value, it needs to send undefined.
|
|
if (newPendingIndex !== undefined || newPendingValue !== undefined || this._hasPendingValue) {
|
|
onPendingValueChanged(newPendingIndex !== undefined ? currentOptions[newPendingIndex] : undefined, newPendingIndex, newPendingValue);
|
|
this._hasPendingValue = newPendingIndex !== undefined || newPendingValue !== undefined;
|
|
}
|
|
};
|
|
/**
|
|
* Sets the isOpen state and updates focusInputAfterClose
|
|
*/
|
|
ComboBoxInternal.prototype._setOpenStateAndFocusOnClose = function (isOpen, focusInputAfterClose) {
|
|
this._focusInputAfterClose = focusInputAfterClose;
|
|
this.setState({ isOpen: isOpen });
|
|
};
|
|
ComboBoxInternal.prototype._onOptionMouseEnter = function (index) {
|
|
if (this._shouldIgnoreMouseEvent()) {
|
|
return;
|
|
}
|
|
this.setState({
|
|
currentPendingValueValidIndexOnHover: index,
|
|
});
|
|
};
|
|
ComboBoxInternal.prototype._onOptionMouseMove = function (index) {
|
|
this._gotMouseMove = true;
|
|
if (!this._isScrollIdle || this.state.currentPendingValueValidIndexOnHover === index) {
|
|
return;
|
|
}
|
|
this.setState({
|
|
currentPendingValueValidIndexOnHover: index,
|
|
});
|
|
};
|
|
ComboBoxInternal.prototype._shouldIgnoreMouseEvent = function () {
|
|
return !this._isScrollIdle || !this._gotMouseMove;
|
|
};
|
|
/**
|
|
* Handle dismissing the menu and eating the required key event when disabled
|
|
* @param ev - the keyboard event that was fired
|
|
*/
|
|
ComboBoxInternal.prototype._handleInputWhenDisabled = function (ev) {
|
|
// If we are disabled, close the menu (if needed)
|
|
// and eat all keystrokes other than TAB or ESC
|
|
if (this.props.disabled) {
|
|
if (this.state.isOpen) {
|
|
this.setState({ isOpen: false });
|
|
}
|
|
// When disabled stop propagation and prevent default
|
|
// of the event unless we have a tab, escape, or function key
|
|
if (ev !== null &&
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
ev.which !== Utilities_1.KeyCodes.tab &&
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
ev.which !== Utilities_1.KeyCodes.escape &&
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
(ev.which < 112 /* F1 */ || ev.which > 123) /* F12 */) {
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
}
|
|
}
|
|
};
|
|
ComboBoxInternal.prototype._handleTouchAndPointerEvent = function () {
|
|
var _this = this;
|
|
// If we already have an existing timeout from a previous touch and pointer event
|
|
// cancel that timeout so we can set a nwe one.
|
|
if (this._lastTouchTimeoutId !== undefined) {
|
|
this._async.clearTimeout(this._lastTouchTimeoutId);
|
|
this._lastTouchTimeoutId = undefined;
|
|
}
|
|
this._processingTouch = true;
|
|
this._lastTouchTimeoutId = this._async.setTimeout(function () {
|
|
_this._processingTouch = false;
|
|
_this._lastTouchTimeoutId = undefined;
|
|
}, TouchIdleDelay);
|
|
};
|
|
/**
|
|
* Get the styles for the current option.
|
|
* @param item - Item props for the current option
|
|
*/
|
|
ComboBoxInternal.prototype._getCaretButtonStyles = function () {
|
|
var customCaretDownButtonStyles = this.props.caretDownButtonStyles;
|
|
return (0, ComboBox_styles_1.getCaretDownButtonStyles)(this.props.theme, customCaretDownButtonStyles);
|
|
};
|
|
/**
|
|
* Get the styles for the current option.
|
|
* @param item - Item props for the current option
|
|
*/
|
|
ComboBoxInternal.prototype._getCurrentOptionStyles = function (item) {
|
|
var _a;
|
|
var customStylesForAllOptions = this.props.comboBoxOptionStyles;
|
|
var customStylesForCurrentOption = item.styles;
|
|
var optionStyles = (0, ComboBox_styles_1.getOptionStyles)(this.props.theme, customStylesForAllOptions, customStylesForCurrentOption, this._isPendingOption(item), item.hidden, this._isOptionHighlighted(item.index));
|
|
// TODO: fix this for multi-window scenarios
|
|
optionStyles.__shadowConfig__ = (_a = this.props.styles) === null || _a === void 0 ? void 0 : _a.__shadowConfig__;
|
|
return optionStyles;
|
|
};
|
|
/**
|
|
* Get the aria autocomplete value for the combo box
|
|
* @returns 'inline' if auto-complete automatically dynamic, 'both' if we have a list of possible values to pick from
|
|
* and can dynamically populate input, and 'list' if auto-complete is not enabled as selection is the only option.
|
|
* Ideally, this should be 'none' if auto-complete is not enabled, but there is a known bug in Edge
|
|
* where the callout may appear over the combo box if this attribute is set to 'none'
|
|
*/
|
|
ComboBoxInternal.prototype._getAriaAutoCompleteValue = function () {
|
|
var autoComplete = !this.props.disabled && this.props.autoComplete === 'on';
|
|
return autoComplete ? (this.props.allowFreeform ? 'inline' : 'both') : 'list';
|
|
};
|
|
ComboBoxInternal.prototype._isPendingOption = function (item) {
|
|
return item && item.index === this.state.currentPendingValueValidIndex;
|
|
};
|
|
/**
|
|
* Returns true if the component has some kind of focus. If it's either focusing or if it's focused
|
|
*/
|
|
ComboBoxInternal.prototype._hasFocus = function () {
|
|
return this.state.focusState !== 'none';
|
|
};
|
|
ComboBoxInternal.prototype._adjustForCaseSensitivity = function (text) {
|
|
return this.props.caseSensitive ? text : text.toLowerCase();
|
|
};
|
|
ComboBoxInternal.contextType = react_window_provider_1.WindowContext;
|
|
ComboBoxInternal = tslib_1.__decorate([
|
|
(0, Utilities_1.customizable)('ComboBox', ['theme', 'styles'], true)
|
|
], ComboBoxInternal);
|
|
return ComboBoxInternal;
|
|
}(React.Component));
|
|
/**
|
|
* Get the indices of the options that are marked as selected
|
|
* @param options - the combo box options
|
|
* @param selectedKeys - the known selected keys to find
|
|
* @returns - an array of the indices of the selected options, empty array if nothing is selected
|
|
*/
|
|
function getSelectedIndices(options, selectedKeys) {
|
|
if (!options || !selectedKeys) {
|
|
return [];
|
|
}
|
|
var selectedIndices = {};
|
|
options.forEach(function (option, index) {
|
|
if (option.selected) {
|
|
selectedIndices[index] = true;
|
|
}
|
|
});
|
|
var _loop_1 = function (selectedKey) {
|
|
var index = (0, Utilities_1.findIndex)(options, function (option) { return option.key === selectedKey; });
|
|
if (index > -1) {
|
|
selectedIndices[index] = true;
|
|
}
|
|
};
|
|
for (var _i = 0, selectedKeys_1 = selectedKeys; _i < selectedKeys_1.length; _i++) {
|
|
var selectedKey = selectedKeys_1[_i];
|
|
_loop_1(selectedKey);
|
|
}
|
|
return Object.keys(selectedIndices).map(Number).sort();
|
|
}
|
|
/**
|
|
* Given default selected key(s) and selected key(s), return the selected keys(s).
|
|
* When default selected key(s) are available, they take precedence and return them instead of selected key(s).
|
|
*
|
|
* @returns No matter what specific types the input parameters are, always return an array of
|
|
* either strings or numbers instead of primitive type. This normalization makes caller's logic easier.
|
|
*/
|
|
function buildDefaultSelectedKeys(defaultSelectedKey, selectedKey) {
|
|
var selectedKeys = buildSelectedKeys(defaultSelectedKey);
|
|
if (selectedKeys.length) {
|
|
return selectedKeys;
|
|
}
|
|
return buildSelectedKeys(selectedKey);
|
|
}
|
|
function buildSelectedKeys(selectedKey) {
|
|
if (selectedKey === undefined) {
|
|
return [];
|
|
}
|
|
// need to cast here so typescript does not complain
|
|
return (selectedKey instanceof Array ? selectedKey : [selectedKey]);
|
|
}
|
|
function normalizeToString(value) {
|
|
return value || '';
|
|
}
|
|
/**
|
|
* Is the index within the bounds of the array?
|
|
* @param options - options to check if the index is valid for
|
|
* @param index - the index to check
|
|
* @returns - true if the index is valid for the given options, false otherwise
|
|
*/
|
|
function indexWithinBounds(options, index) {
|
|
return !!options && index >= 0 && index < options.length;
|
|
}
|
|
/** Whether this is a normal option, not a header or divider or select all. */
|
|
function isNormalOption(option) {
|
|
return (option.itemType !== SelectableOption_1.SelectableOptionMenuItemType.Header &&
|
|
option.itemType !== SelectableOption_1.SelectableOptionMenuItemType.Divider &&
|
|
option.itemType !== SelectableOption_1.SelectableOptionMenuItemType.SelectAll);
|
|
}
|
|
/** Whether this is a selectable option, not a header or divider. */
|
|
function isSelectableOption(option) {
|
|
return (option.itemType !== SelectableOption_1.SelectableOptionMenuItemType.Header && option.itemType !== SelectableOption_1.SelectableOptionMenuItemType.Divider);
|
|
}
|
|
/**
|
|
* For scenarios where the option's `text` prop contains embedded styles, we use the option's
|
|
* `ariaLabel` value as the text in the input and for autocomplete matching. We know to use this
|
|
* when the `useAriaLabelAsText` prop is set to true.
|
|
*/
|
|
function getPreviewText(item) {
|
|
return item.useAriaLabelAsText && item.ariaLabel ? item.ariaLabel : item.text;
|
|
}
|
|
/**
|
|
* Returns true if the key for the event is alt (Mac option) or meta (Mac command).
|
|
*/
|
|
function isAltOrMeta(ev) {
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
return ev.which === Utilities_1.KeyCodes.alt || ev.key === 'Meta';
|
|
}
|
|
});
|
|
//# sourceMappingURL=ComboBox.js.map
|