917 lines
48 KiB
JavaScript
917 lines
48 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.DropdownBase = void 0;
|
|
var tslib_1 = require("tslib");
|
|
var React = require("react");
|
|
var Utilities_1 = require("../../Utilities");
|
|
var Callout_1 = require("../../Callout");
|
|
var Button_1 = require("../../Button");
|
|
var Dropdown_types_1 = require("./Dropdown.types");
|
|
var DropdownSizePosCache_1 = require("./utilities/DropdownSizePosCache");
|
|
var FocusZone_1 = require("../../FocusZone");
|
|
var Icon_1 = require("../../Icon");
|
|
var Label_1 = require("../../Label");
|
|
var Panel_1 = require("../../Panel");
|
|
var ResponsiveMode_1 = require("../../ResponsiveMode");
|
|
var SelectableOption_1 = require("../../SelectableOption");
|
|
// import and use V7 Checkbox to ensure no breaking changes.
|
|
var Checkbox_1 = require("../../Checkbox");
|
|
var utilities_1 = require("@fluentui/utilities");
|
|
var react_hooks_1 = require("@fluentui/react-hooks");
|
|
var react_window_provider_1 = require("@fluentui/react-window-provider");
|
|
var dom_1 = require("../../utilities/dom");
|
|
var COMPONENT_NAME = 'Dropdown';
|
|
var getClassNames = (0, Utilities_1.classNamesFunction)();
|
|
var DEFAULT_PROPS = {
|
|
options: [],
|
|
};
|
|
function useSelectedItemsState(_a) {
|
|
var defaultSelectedKeys = _a.defaultSelectedKeys, selectedKeys = _a.selectedKeys, defaultSelectedKey = _a.defaultSelectedKey, selectedKey = _a.selectedKey, options = _a.options, multiSelect = _a.multiSelect;
|
|
var oldOptions = (0, react_hooks_1.usePrevious)(options);
|
|
var _b = React.useState([]), selectedIndices = _b[0], setSelectedIndices = _b[1];
|
|
// In controlled component usage where selectedKey is provided, update the selectedIndex
|
|
// state if the key or options change.
|
|
var selectedKeyPropToUse;
|
|
// this does a shallow compare (assumes options are pure), for the purposes of determining whether
|
|
// defaultSelectedKey/defaultSelectedKeys are respected.
|
|
var didOptionsChange = options !== oldOptions;
|
|
if (multiSelect) {
|
|
if (didOptionsChange && defaultSelectedKeys !== undefined) {
|
|
selectedKeyPropToUse = defaultSelectedKeys;
|
|
}
|
|
else {
|
|
selectedKeyPropToUse = selectedKeys;
|
|
}
|
|
}
|
|
else {
|
|
if (didOptionsChange && defaultSelectedKey !== undefined) {
|
|
selectedKeyPropToUse = defaultSelectedKey;
|
|
}
|
|
else {
|
|
selectedKeyPropToUse = selectedKey;
|
|
}
|
|
}
|
|
var oldSelectedKeyProp = (0, react_hooks_1.usePrevious)(selectedKeyPropToUse);
|
|
React.useEffect(function () {
|
|
/** Get all selected indexes for multi-select mode */
|
|
var getSelectedIndexes = function () {
|
|
if (selectedKeyPropToUse === undefined) {
|
|
if (multiSelect) {
|
|
return getAllSelectedIndices();
|
|
}
|
|
var selectedIndex = getSelectedIndex(null);
|
|
return selectedIndex !== -1 ? [selectedIndex] : [];
|
|
}
|
|
else if (!Array.isArray(selectedKeyPropToUse)) {
|
|
var selectedIndex = getSelectedIndex(selectedKeyPropToUse);
|
|
return selectedIndex !== -1 ? [selectedIndex] : [];
|
|
}
|
|
var returnValue = [];
|
|
for (var _i = 0, selectedKeyPropToUse_1 = selectedKeyPropToUse; _i < selectedKeyPropToUse_1.length; _i++) {
|
|
var key = selectedKeyPropToUse_1[_i];
|
|
var selectedIndex = getSelectedIndex(key);
|
|
selectedIndex !== -1 && returnValue.push(selectedIndex);
|
|
}
|
|
return returnValue;
|
|
};
|
|
var getAllSelectedIndices = function () {
|
|
return options
|
|
.map(function (option, index) { return (option.selected ? index : -1); })
|
|
.filter(function (index) { return index !== -1; });
|
|
};
|
|
var getSelectedIndex = function (searchKey) {
|
|
return (0, Utilities_1.findIndex)(options, function (option) {
|
|
// eslint-disable-next-line eqeqeq
|
|
if (searchKey != null) {
|
|
return option.key === searchKey;
|
|
}
|
|
else {
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
return !!option.selected || !!option.isSelected;
|
|
}
|
|
});
|
|
};
|
|
if ((selectedKeyPropToUse !== undefined || !oldOptions) &&
|
|
(selectedKeyPropToUse !== oldSelectedKeyProp || didOptionsChange)) {
|
|
setSelectedIndices(getSelectedIndexes());
|
|
}
|
|
}, [didOptionsChange, multiSelect, oldOptions, oldSelectedKeyProp, options, selectedKeyPropToUse]);
|
|
return [selectedIndices, setSelectedIndices];
|
|
}
|
|
exports.DropdownBase = React.forwardRef(function (propsWithoutDefaults, forwardedRef) {
|
|
var props = (0, utilities_1.getPropsWithDefaults)(DEFAULT_PROPS, propsWithoutDefaults);
|
|
var rootRef = React.useRef(null);
|
|
var mergedRootRef = (0, react_hooks_1.useMergedRefs)(forwardedRef, rootRef);
|
|
var responsiveMode = (0, ResponsiveMode_1.useResponsiveMode)(rootRef, props.responsiveMode);
|
|
var _a = useSelectedItemsState(props), selectedIndices = _a[0], setSelectedIndices = _a[1];
|
|
return (React.createElement(DropdownInternal, tslib_1.__assign({}, props, { responsiveMode: responsiveMode, hoisted: { rootRef: mergedRootRef, selectedIndices: selectedIndices, setSelectedIndices: setSelectedIndices } })));
|
|
});
|
|
exports.DropdownBase.displayName = 'DropdownBase';
|
|
var DropdownInternal = /** @class */ (function (_super) {
|
|
tslib_1.__extends(DropdownInternal, _super);
|
|
function DropdownInternal(props) {
|
|
var _this = _super.call(this, props) || this;
|
|
_this._host = React.createRef();
|
|
_this._focusZone = React.createRef();
|
|
_this._dropDown = React.createRef();
|
|
_this._scrollIdleDelay = 250 /* ms */;
|
|
_this._sizePosCache = new DropdownSizePosCache_1.DropdownSizePosCache();
|
|
_this._requestAnimationFrame = (0, Utilities_1.safeRequestAnimationFrame)(_this);
|
|
/**
|
|
* Close menu callout if it is open
|
|
*/
|
|
_this.dismissMenu = function () {
|
|
var isOpen = _this.state.isOpen;
|
|
isOpen && _this.setState({ isOpen: false });
|
|
};
|
|
_this._onChange = function (event, options, index, checked, multiSelect) {
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
var _a = _this.props, onChange = _a.onChange, onChanged = _a.onChanged;
|
|
if (onChange || onChanged) {
|
|
// for single-select, option passed in will always be selected.
|
|
// for multi-select, flip the checked value
|
|
var changedOpt = multiSelect ? tslib_1.__assign(tslib_1.__assign({}, options[index]), { selected: !checked }) : options[index];
|
|
onChange && onChange(tslib_1.__assign(tslib_1.__assign({}, event), { target: _this._dropDown.current }), changedOpt, index);
|
|
onChanged && onChanged(changedOpt, index);
|
|
}
|
|
};
|
|
/** Get either props.placeholder (new name) or props.placeHolder (old name) */
|
|
_this._getPlaceholder = function () {
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
return _this.props.placeholder || _this.props.placeHolder;
|
|
};
|
|
/** Get text in dropdown input as a string */
|
|
_this._getTitle = function (items, _unused) {
|
|
var _a = _this.props.multiSelectDelimiter, multiSelectDelimiter = _a === void 0 ? ', ' : _a;
|
|
return items.map(function (i) { return i.text; }).join(multiSelectDelimiter);
|
|
};
|
|
/** Render text in dropdown input */
|
|
_this._onRenderTitle = function (items) {
|
|
return React.createElement(React.Fragment, null, _this._getTitle(items));
|
|
};
|
|
/** Render placeholder text in dropdown input */
|
|
_this._onRenderPlaceholder = function (props) {
|
|
if (!_this._getPlaceholder()) {
|
|
return null;
|
|
}
|
|
return React.createElement(React.Fragment, null, _this._getPlaceholder());
|
|
};
|
|
/** Render Callout or Panel container and pass in list */
|
|
_this._onRenderContainer = function (props) {
|
|
var calloutProps = props.calloutProps, panelProps = props.panelProps;
|
|
var _a = _this.props, responsiveMode = _a.responsiveMode, dropdownWidth = _a.dropdownWidth;
|
|
var isSmall = responsiveMode <= ResponsiveMode_1.ResponsiveMode.medium;
|
|
var focusTrapZoneProps = { firstFocusableTarget: "#".concat(_this._listId, "1") };
|
|
var panelStyles = _this._classNames.subComponentStyles
|
|
? _this._classNames.subComponentStyles.panel
|
|
: undefined;
|
|
var calloutWidth = undefined;
|
|
var calloutMinWidth = undefined;
|
|
if (dropdownWidth === 'auto') {
|
|
calloutMinWidth = _this._dropDown.current ? _this._dropDown.current.clientWidth : 0;
|
|
}
|
|
else {
|
|
calloutWidth = dropdownWidth || (_this._dropDown.current ? _this._dropDown.current.clientWidth : 0);
|
|
}
|
|
return isSmall ? (React.createElement(Panel_1.Panel, tslib_1.__assign({ closeButtonAriaLabel: "Close", focusTrapZoneProps: focusTrapZoneProps, hasCloseButton: true, isOpen: true, isLightDismiss: true, onDismiss: _this._onDismiss, styles: panelStyles }, panelProps), _this._renderFocusableList(props))) : (React.createElement(Callout_1.Callout, tslib_1.__assign({ isBeakVisible: false, gapSpace: 0, doNotLayer: false, directionalHintFixed: false, directionalHint: Callout_1.DirectionalHint.bottomLeftEdge, calloutWidth: calloutWidth, calloutMinWidth: calloutMinWidth }, calloutProps, { className: _this._classNames.callout, target: _this._dropDown.current, onDismiss: _this._onDismiss, onScroll: _this._onScroll, onPositioned: _this._onPositioned }), _this._renderFocusableList(props)));
|
|
};
|
|
/** Render Caret Down Icon */
|
|
_this._onRenderCaretDown = function (props) {
|
|
return React.createElement(Icon_1.Icon, { className: _this._classNames.caretDown, iconName: "ChevronDown", "aria-hidden": true });
|
|
};
|
|
/** Render List of items */
|
|
_this._onRenderList = function (props) {
|
|
var _a = props.onRenderItem, onRenderItem = _a === void 0 ? _this._onRenderItem : _a;
|
|
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 = _this._id + item.key;
|
|
queue.items.push(onRenderItem(tslib_1.__assign(tslib_1.__assign({ id: id }, item), { index: index }), _this._onRenderItem));
|
|
queue.id = id;
|
|
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();
|
|
return React.createElement(React.Fragment, null, renderedList);
|
|
};
|
|
_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);
|
|
}
|
|
};
|
|
_this._renderOption = function (item) {
|
|
var _a;
|
|
var _b = _this.props, _c = _b.onRenderOption, onRenderOption = _c === void 0 ? _this._onRenderOption : _c, _d = _b.hoisted.selectedIndices, selectedIndices = _d === void 0 ? [] : _d;
|
|
var isItemSelected = item.index !== undefined && selectedIndices ? selectedIndices.indexOf(item.index) > -1 : false;
|
|
// select the right className based on the combination of selected/disabled
|
|
var itemClassName = item.hidden // predicate: item hidden
|
|
? _this._classNames.dropdownItemHidden
|
|
: isItemSelected && item.disabled === true // predicate: both selected and disabled
|
|
? _this._classNames.dropdownItemSelectedAndDisabled
|
|
: isItemSelected // predicate: selected only
|
|
? _this._classNames.dropdownItemSelected
|
|
: item.disabled === true // predicate: disabled only
|
|
? _this._classNames.dropdownItemDisabled
|
|
: _this._classNames.dropdownItem;
|
|
var title = item.title;
|
|
// define the id and label id (for multiselect checkboxes)
|
|
var id = _this._listId + item.index;
|
|
var labelId = (_a = item.id) !== null && _a !== void 0 ? _a : id + '-label';
|
|
var multiSelectItemStyles = _this._classNames.subComponentStyles
|
|
? _this._classNames.subComponentStyles.multiSelectItem
|
|
: undefined;
|
|
return !_this.props.multiSelect ? (React.createElement(Button_1.CommandButton, { id: id, key: item.key, "data-index": item.index, "data-is-focusable": !item.disabled, disabled: item.disabled, className: itemClassName, onClick: _this._onItemClick(item),
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onMouseEnter: _this._onItemMouseEnter.bind(_this, item),
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onMouseLeave: _this._onMouseItemLeave.bind(_this, item),
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onMouseMove: _this._onItemMouseMove.bind(_this, item), role: "option", "aria-selected": isItemSelected ? 'true' : 'false', ariaLabel: item.ariaLabel, title: title, "aria-posinset": _this._sizePosCache.positionInSet(item.index), "aria-setsize": _this._sizePosCache.optionSetSize }, onRenderOption(item, _this._onRenderOption))) : (React.createElement(Checkbox_1.Checkbox, { id: id, key: item.key, disabled: item.disabled, onChange: _this._onItemClick(item), inputProps: tslib_1.__assign({ 'aria-selected': isItemSelected, onMouseEnter: _this._onItemMouseEnter.bind(_this, item), onMouseLeave: _this._onMouseItemLeave.bind(_this, item), onMouseMove: _this._onItemMouseMove.bind(_this, item), role: 'option' }, {
|
|
'data-index': item.index,
|
|
'data-is-focusable': !(item.disabled || item.hidden),
|
|
}), label: item.text, title: title,
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onRenderLabel: _this._onRenderItemLabel.bind(_this, tslib_1.__assign(tslib_1.__assign({}, item), { id: labelId })), className: (0, Utilities_1.css)(itemClassName, 'is-multi-select'), checked: isItemSelected, styles: multiSelectItemStyles, ariaPositionInSet: !item.hidden ? _this._sizePosCache.positionInSet(item.index) : undefined, ariaSetSize: !item.hidden ? _this._sizePosCache.optionSetSize : undefined, ariaLabel: item.ariaLabel, ariaLabelledBy: item.ariaLabel ? undefined : labelId }));
|
|
};
|
|
/** Render content of item (i.e. text/icon inside of button) */
|
|
_this._onRenderOption = function (item) {
|
|
return React.createElement("span", { className: _this._classNames.dropdownOptionText }, item.text);
|
|
};
|
|
/*
|
|
* Render content of a multiselect item label.
|
|
* Text within the label is aria-hidden, to prevent duplicate input/label exposure
|
|
*/
|
|
_this._onRenderMultiselectOption = function (item) {
|
|
return (React.createElement("span", { id: item.id, "aria-hidden": "true", className: _this._classNames.dropdownOptionText }, item.text));
|
|
};
|
|
/** Render custom label for multiselect checkbox items */
|
|
_this._onRenderItemLabel = function (item) {
|
|
var _a = _this.props.onRenderOption, onRenderOption = _a === void 0 ? _this._onRenderMultiselectOption : _a;
|
|
return onRenderOption(item, _this._onRenderMultiselectOption);
|
|
};
|
|
_this._onPositioned = function (positions) {
|
|
if (_this._focusZone.current) {
|
|
// Focusing an element can trigger a reflow. Making this wait until there is an animation
|
|
// frame can improve perf significantly.
|
|
_this._requestAnimationFrame(function () {
|
|
var selectedIndices = _this.props.hoisted.selectedIndices;
|
|
if (_this._focusZone.current) {
|
|
if (!_this._hasBeenPositioned &&
|
|
selectedIndices &&
|
|
selectedIndices[0] &&
|
|
!_this.props.options[selectedIndices[0]].disabled) {
|
|
var element = (0, Utilities_1.getDocument)().getElementById("".concat(_this._id, "-list").concat(selectedIndices[0]));
|
|
if (element) {
|
|
_this._focusZone.current.focusElement(element);
|
|
}
|
|
_this._hasBeenPositioned = true;
|
|
}
|
|
else {
|
|
_this._focusZone.current.focus();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if (!_this.state.calloutRenderEdge || _this.state.calloutRenderEdge !== positions.targetEdge) {
|
|
_this.setState({
|
|
calloutRenderEdge: positions.targetEdge,
|
|
});
|
|
}
|
|
};
|
|
_this._onItemClick = function (item) {
|
|
return function (event) {
|
|
if (!item.disabled) {
|
|
_this.setSelectedIndex(event, item.index);
|
|
if (!_this.props.multiSelect) {
|
|
// only close the callout when it's in single-select mode
|
|
_this.setState({
|
|
isOpen: false,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
};
|
|
/**
|
|
* Scroll handler for the callout to make sure the mouse events
|
|
* for updating focus are not interacting during scroll
|
|
*/
|
|
_this._onScroll = function () {
|
|
var win = (0, dom_1.getWindowEx)(_this.context); // can only be called on the client
|
|
if (!_this._isScrollIdle && _this._scrollIdleTimeoutId !== undefined) {
|
|
win.clearTimeout(_this._scrollIdleTimeoutId);
|
|
_this._scrollIdleTimeoutId = undefined;
|
|
}
|
|
else {
|
|
_this._isScrollIdle = false;
|
|
}
|
|
_this._scrollIdleTimeoutId = win.setTimeout(function () {
|
|
_this._isScrollIdle = true;
|
|
}, _this._scrollIdleDelay);
|
|
};
|
|
_this._onMouseItemLeave = function (item, ev) {
|
|
if (_this._shouldIgnoreMouseEvent()) {
|
|
return;
|
|
}
|
|
/**
|
|
* IE11 focus() method forces parents to scroll to top of element.
|
|
* Edge and IE expose a setActive() function for focusable divs that
|
|
* sets the page focus but does not scroll the parent element.
|
|
*/
|
|
if (_this._host.current) {
|
|
if (_this._host.current.setActive) {
|
|
try {
|
|
_this._host.current.setActive();
|
|
}
|
|
catch (e) {
|
|
/* no-op */
|
|
}
|
|
}
|
|
else {
|
|
_this._host.current.focus();
|
|
}
|
|
}
|
|
};
|
|
_this._onDismiss = function () {
|
|
_this.setState({ isOpen: false });
|
|
};
|
|
_this._onDropdownBlur = function (ev) {
|
|
// If Dropdown disabled do not proceed with this logic.
|
|
var disabled = _this._isDisabled();
|
|
if (disabled) {
|
|
return;
|
|
}
|
|
if (_this.state.isOpen) {
|
|
// Do not call onBlur or update focus state when the callout is opened
|
|
return;
|
|
}
|
|
_this.setState({ hasFocus: false });
|
|
if (_this.props.onBlur) {
|
|
_this.props.onBlur(ev);
|
|
}
|
|
};
|
|
_this._onDropdownKeyDown = function (ev) {
|
|
// If Dropdown disabled do not process any keyboard events.
|
|
var disabled = _this._isDisabled();
|
|
if (disabled) {
|
|
return;
|
|
}
|
|
// Take note if we are processing an alt (option) or meta (command) keydown.
|
|
// See comment in _shouldHandleKeyUp for reasoning.
|
|
_this._lastKeyDownWasAltOrMeta = _this._isAltOrMeta(ev);
|
|
if (_this.props.onKeyDown) {
|
|
_this.props.onKeyDown(ev);
|
|
if (ev.defaultPrevented) {
|
|
return;
|
|
}
|
|
}
|
|
var newIndex;
|
|
var selectedIndex = _this.props.hoisted.selectedIndices.length ? _this.props.hoisted.selectedIndices[0] : -1;
|
|
var containsExpandCollapseModifier = ev.altKey || ev.metaKey;
|
|
var isOpen = _this.state.isOpen;
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
switch (ev.which) {
|
|
case Utilities_1.KeyCodes.enter:
|
|
_this.setState({
|
|
isOpen: !isOpen,
|
|
});
|
|
break;
|
|
case Utilities_1.KeyCodes.escape:
|
|
if (!isOpen) {
|
|
return;
|
|
}
|
|
_this.setState({
|
|
isOpen: false,
|
|
});
|
|
break;
|
|
case Utilities_1.KeyCodes.up:
|
|
if (containsExpandCollapseModifier) {
|
|
if (isOpen) {
|
|
_this.setState({ isOpen: false });
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
if (_this.props.multiSelect) {
|
|
_this.setState({ isOpen: true });
|
|
}
|
|
else if (!_this._isDisabled()) {
|
|
newIndex = _this._moveIndex(ev, -1, selectedIndex - 1, selectedIndex);
|
|
}
|
|
break;
|
|
case Utilities_1.KeyCodes.down:
|
|
if (containsExpandCollapseModifier) {
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
}
|
|
if ((containsExpandCollapseModifier && !isOpen) || _this.props.multiSelect) {
|
|
_this.setState({ isOpen: true });
|
|
}
|
|
else if (!_this._isDisabled()) {
|
|
newIndex = _this._moveIndex(ev, 1, selectedIndex + 1, selectedIndex);
|
|
}
|
|
break;
|
|
case Utilities_1.KeyCodes.home:
|
|
if (!_this.props.multiSelect) {
|
|
newIndex = _this._moveIndex(ev, 1, 0, selectedIndex);
|
|
}
|
|
break;
|
|
case Utilities_1.KeyCodes.end:
|
|
if (!_this.props.multiSelect) {
|
|
newIndex = _this._moveIndex(ev, -1, _this.props.options.length - 1, selectedIndex);
|
|
}
|
|
break;
|
|
case Utilities_1.KeyCodes.space:
|
|
// event handled in _onDropdownKeyUp
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
if (newIndex !== selectedIndex) {
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
}
|
|
};
|
|
_this._onDropdownKeyUp = function (ev) {
|
|
// If Dropdown disabled do not process any keyboard events.
|
|
var disabled = _this._isDisabled();
|
|
if (disabled) {
|
|
return;
|
|
}
|
|
var shouldHandleKey = _this._shouldHandleKeyUp(ev);
|
|
var isOpen = _this.state.isOpen;
|
|
if (_this.props.onKeyUp) {
|
|
_this.props.onKeyUp(ev);
|
|
if (ev.defaultPrevented) {
|
|
return;
|
|
}
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
switch (ev.which) {
|
|
case Utilities_1.KeyCodes.space:
|
|
_this.setState({
|
|
isOpen: !isOpen,
|
|
});
|
|
break;
|
|
default:
|
|
if (shouldHandleKey && isOpen) {
|
|
_this.setState({ isOpen: false });
|
|
}
|
|
return;
|
|
}
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
};
|
|
_this._onZoneKeyDown = function (ev) {
|
|
var _a, _b;
|
|
var elementToFocus;
|
|
// Take note if we are processing an alt (option) or meta (command) keydown.
|
|
// See comment in _shouldHandleKeyUp for reasoning.
|
|
_this._lastKeyDownWasAltOrMeta = _this._isAltOrMeta(ev);
|
|
var containsExpandCollapseModifier = ev.altKey || ev.metaKey;
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
switch (ev.which) {
|
|
case Utilities_1.KeyCodes.up:
|
|
if (containsExpandCollapseModifier) {
|
|
_this.setState({ isOpen: false });
|
|
}
|
|
else {
|
|
if (_this._host.current) {
|
|
elementToFocus = (0, Utilities_1.getLastFocusable)(_this._host.current, _this._host.current.lastChild, true);
|
|
}
|
|
}
|
|
break;
|
|
// All directional keystrokes should be canceled when the zone is rendered.
|
|
// This avoids the body scroll from reacting and thus dismissing the dropdown.
|
|
case Utilities_1.KeyCodes.home:
|
|
case Utilities_1.KeyCodes.end:
|
|
case Utilities_1.KeyCodes.pageUp:
|
|
case Utilities_1.KeyCodes.pageDown:
|
|
break;
|
|
case Utilities_1.KeyCodes.down:
|
|
if (!containsExpandCollapseModifier && _this._host.current) {
|
|
elementToFocus = (0, Utilities_1.getFirstFocusable)(_this._host.current, _this._host.current.firstChild, true);
|
|
}
|
|
break;
|
|
case Utilities_1.KeyCodes.escape:
|
|
_this.setState({ isOpen: false });
|
|
break;
|
|
case Utilities_1.KeyCodes.tab:
|
|
_this.setState({ isOpen: false });
|
|
var document_1 = (0, Utilities_1.getDocument)();
|
|
if (document_1) {
|
|
if (ev.shiftKey) {
|
|
(_a = (0, utilities_1.getPreviousElement)(document_1.body, _this._dropDown.current, false, false, true, true)) === null || _a === void 0 ? void 0 : _a.focus();
|
|
}
|
|
else {
|
|
(_b = (0, utilities_1.getNextElement)(document_1.body, _this._dropDown.current, false, false, true, true)) === null || _b === void 0 ? void 0 : _b.focus();
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
if (elementToFocus) {
|
|
elementToFocus.focus();
|
|
}
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
};
|
|
_this._onZoneKeyUp = function (ev) {
|
|
var shouldHandleKey = _this._shouldHandleKeyUp(ev);
|
|
if (shouldHandleKey && _this.state.isOpen) {
|
|
_this.setState({ isOpen: false });
|
|
ev.preventDefault();
|
|
}
|
|
};
|
|
_this._onDropdownClick = function (ev) {
|
|
if (_this.props.onClick) {
|
|
_this.props.onClick(ev);
|
|
if (ev.defaultPrevented) {
|
|
return;
|
|
}
|
|
}
|
|
var isOpen = _this.state.isOpen;
|
|
var disabled = _this._isDisabled();
|
|
if (!disabled && !_this._shouldOpenOnFocus()) {
|
|
_this.setState({
|
|
isOpen: !isOpen,
|
|
});
|
|
}
|
|
_this._isFocusedByClick = false; // reset
|
|
};
|
|
_this._onDropdownMouseDown = function () {
|
|
_this._isFocusedByClick = true;
|
|
};
|
|
_this._onFocus = function (ev) {
|
|
var disabled = _this._isDisabled();
|
|
if (!disabled) {
|
|
if (_this.props.onFocus) {
|
|
_this.props.onFocus(ev);
|
|
}
|
|
var state = { hasFocus: true };
|
|
if (_this._shouldOpenOnFocus()) {
|
|
state.isOpen = true;
|
|
}
|
|
_this.setState(state);
|
|
}
|
|
};
|
|
/**
|
|
* Because the isDisabled prop is deprecated, we have had to repeat this logic all over the place.
|
|
* This helper method avoids all the repetition.
|
|
*/
|
|
_this._isDisabled = function () {
|
|
var disabled = _this.props.disabled;
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
var isDisabled = _this.props.isDisabled;
|
|
// Remove this deprecation workaround at 1.0.0
|
|
if (disabled === undefined) {
|
|
disabled = isDisabled;
|
|
}
|
|
return disabled;
|
|
};
|
|
_this._onRenderLabel = function (props) {
|
|
var label = props.label, required = props.required, disabled = props.disabled;
|
|
var labelStyles = _this._classNames.subComponentStyles
|
|
? _this._classNames.subComponentStyles.label
|
|
: undefined;
|
|
return label ? (React.createElement(Label_1.Label, { className: _this._classNames.label, id: _this._labelId, required: required, styles: labelStyles, disabled: disabled }, label)) : null;
|
|
};
|
|
(0, Utilities_1.initializeComponentRef)(_this);
|
|
var multiSelect = props.multiSelect, selectedKey = props.selectedKey, selectedKeys = props.selectedKeys, defaultSelectedKey = props.defaultSelectedKey, defaultSelectedKeys = props.defaultSelectedKeys, options = props.options;
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
(0, Utilities_1.warnDeprecations)(COMPONENT_NAME, props, {
|
|
isDisabled: 'disabled',
|
|
onChanged: 'onChange',
|
|
placeHolder: 'placeholder',
|
|
onRenderPlaceHolder: 'onRenderPlaceholder',
|
|
});
|
|
(0, Utilities_1.warnMutuallyExclusive)(COMPONENT_NAME, props, {
|
|
defaultSelectedKey: 'selectedKey',
|
|
defaultSelectedKeys: 'selectedKeys',
|
|
selectedKeys: 'selectedKey',
|
|
});
|
|
if (multiSelect) {
|
|
var warnMultiSelect = function (prop) {
|
|
return (0, Utilities_1.warn)("Dropdown property '".concat(prop, "' cannot be used when 'multiSelect' is true. Use '").concat(prop, "s' instead."));
|
|
};
|
|
if (selectedKey !== undefined) {
|
|
warnMultiSelect('selectedKey');
|
|
}
|
|
if (defaultSelectedKey !== undefined) {
|
|
warnMultiSelect('defaultSelectedKey');
|
|
}
|
|
}
|
|
else {
|
|
var warnNotMultiSelect = function (prop) {
|
|
return (0, Utilities_1.warn)("Dropdown property '".concat(prop, "s' cannot be used when 'multiSelect' is false/unset. Use '").concat(prop, "' instead."));
|
|
};
|
|
if (selectedKeys !== undefined) {
|
|
warnNotMultiSelect('selectedKey');
|
|
}
|
|
if (defaultSelectedKeys !== undefined) {
|
|
warnNotMultiSelect('defaultSelectedKey');
|
|
}
|
|
}
|
|
}
|
|
_this._id = props.id || (0, Utilities_1.getId)('Dropdown');
|
|
_this._labelId = _this._id + '-label';
|
|
_this._listId = _this._id + '-list';
|
|
_this._optionId = _this._id + '-option';
|
|
_this._isScrollIdle = true;
|
|
_this._hasBeenPositioned = false;
|
|
_this._sizePosCache.updateOptions(options);
|
|
_this.state = {
|
|
isOpen: false,
|
|
hasFocus: false,
|
|
calloutRenderEdge: undefined,
|
|
};
|
|
return _this;
|
|
}
|
|
Object.defineProperty(DropdownInternal.prototype, "selectedOptions", {
|
|
/**
|
|
* All selected options
|
|
*/
|
|
get: function () {
|
|
var _a = this.props, options = _a.options, selectedIndices = _a.hoisted.selectedIndices;
|
|
return (0, SelectableOption_1.getAllSelectedOptions)(options, selectedIndices);
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
DropdownInternal.prototype.componentWillUnmount = function () {
|
|
clearTimeout(this._scrollIdleTimeoutId);
|
|
};
|
|
DropdownInternal.prototype.componentDidUpdate = function (prevProps, prevState) {
|
|
if (prevState.isOpen === true && this.state.isOpen === false) {
|
|
this._gotMouseMove = false;
|
|
this._hasBeenPositioned = false;
|
|
if (this.props.onDismiss) {
|
|
this.props.onDismiss();
|
|
}
|
|
}
|
|
};
|
|
DropdownInternal.prototype.render = function () {
|
|
var id = this._id;
|
|
var props = this.props;
|
|
var className = props.className, label = props.label, options = props.options, ariaLabel = props.ariaLabel, required = props.required, errorMessage = props.errorMessage, propStyles = props.styles, theme = props.theme, panelProps = props.panelProps, calloutProps = props.calloutProps, _a = props.onRenderTitle, onRenderTitle = _a === void 0 ? this._getTitle : _a, _b = props.onRenderContainer, onRenderContainer = _b === void 0 ? this._onRenderContainer : _b, _c = props.onRenderCaretDown, onRenderCaretDown = _c === void 0 ? this._onRenderCaretDown : _c, _d = props.onRenderLabel, onRenderLabel = _d === void 0 ? this._onRenderLabel : _d, _e = props.onRenderItem, onRenderItem = _e === void 0 ? this._onRenderItem : _e, selectedIndices = props.hoisted.selectedIndices, responsiveMode = props.responsiveMode;
|
|
var _f = this.state, isOpen = _f.isOpen, calloutRenderEdge = _f.calloutRenderEdge, hasFocus = _f.hasFocus;
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
var onRenderPlaceholder = props.onRenderPlaceholder || props.onRenderPlaceHolder || this._getPlaceholder;
|
|
// If our cached options are out of date update our cache
|
|
if (options !== this._sizePosCache.cachedOptions) {
|
|
this._sizePosCache.updateOptions(options);
|
|
}
|
|
var selectedOptions = (0, SelectableOption_1.getAllSelectedOptions)(options, selectedIndices);
|
|
var divProps = (0, Utilities_1.getNativeProps)(props, Utilities_1.divProperties);
|
|
var disabled = this._isDisabled();
|
|
var isSmall = responsiveMode <= ResponsiveMode_1.ResponsiveMode.medium;
|
|
var errorMessageId = id + '-errorMessage';
|
|
this._classNames = getClassNames(propStyles, {
|
|
theme: theme,
|
|
className: className,
|
|
hasError: !!(errorMessage && errorMessage.length > 0),
|
|
hasLabel: !!label,
|
|
isOpen: isOpen,
|
|
required: required,
|
|
disabled: disabled,
|
|
isRenderingPlaceholder: !selectedOptions.length,
|
|
panelClassName: panelProps ? panelProps.className : undefined,
|
|
calloutClassName: calloutProps ? calloutProps.className : undefined,
|
|
calloutRenderEdge: calloutRenderEdge,
|
|
});
|
|
var hasErrorMessage = !!errorMessage && errorMessage.length > 0;
|
|
return (React.createElement("div", { className: this._classNames.root, ref: this.props.hoisted.rootRef, "aria-owns": isOpen && !isSmall ? this._listId : undefined },
|
|
onRenderLabel(this.props, this._onRenderLabel),
|
|
React.createElement("div", tslib_1.__assign({ "data-is-focusable": !disabled, "data-ktp-target": true, ref: this._dropDown, id: id, tabIndex: disabled ? -1 : 0, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": isOpen ? 'true' : 'false', "aria-label": ariaLabel, "aria-labelledby": label && !ariaLabel ? (0, Utilities_1.mergeAriaAttributeValues)(this._labelId, this._optionId) : undefined, "aria-describedby": hasErrorMessage ? this._id + '-errorMessage' : undefined, "aria-required": required, "aria-disabled": disabled, "aria-invalid": hasErrorMessage, "aria-controls": isOpen && !isSmall ? this._listId : undefined }, divProps, { className: this._classNames.dropdown, onBlur: this._onDropdownBlur, onKeyDown: this._onDropdownKeyDown, onKeyUp: this._onDropdownKeyUp, onClick: this._onDropdownClick, onMouseDown: this._onDropdownMouseDown, onFocus: this._onFocus }),
|
|
React.createElement("span", { id: this._optionId, className: this._classNames.title, "aria-live": hasFocus ? 'polite' : undefined, "aria-atomic": hasFocus ? true : undefined },
|
|
// If option is selected render title, otherwise render the placeholder text
|
|
selectedOptions.length
|
|
? onRenderTitle(selectedOptions, this._onRenderTitle)
|
|
: onRenderPlaceholder(props, this._onRenderPlaceholder)),
|
|
React.createElement("span", { className: this._classNames.caretDownWrapper }, onRenderCaretDown(props, this._onRenderCaretDown))),
|
|
isOpen &&
|
|
onRenderContainer(tslib_1.__assign(tslib_1.__assign({}, props), { onDismiss: this._onDismiss, onRenderItem: onRenderItem }), this._onRenderContainer),
|
|
hasErrorMessage && (React.createElement("div", { role: "alert", id: errorMessageId, className: this._classNames.errorMessage }, errorMessage))));
|
|
};
|
|
DropdownInternal.prototype.focus = function (shouldOpenOnFocus) {
|
|
if (this._dropDown.current) {
|
|
this._dropDown.current.focus();
|
|
if (shouldOpenOnFocus) {
|
|
this.setState({
|
|
isOpen: true,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
DropdownInternal.prototype.setSelectedIndex = function (event, index) {
|
|
var _a = this.props, options = _a.options, selectedKey = _a.selectedKey, selectedKeys = _a.selectedKeys, multiSelect = _a.multiSelect, notifyOnReselect = _a.notifyOnReselect, _b = _a.hoisted.selectedIndices, selectedIndices = _b === void 0 ? [] : _b;
|
|
var checked = selectedIndices ? selectedIndices.indexOf(index) > -1 : false;
|
|
var newIndexes = [];
|
|
index = Math.max(0, Math.min(options.length - 1, index));
|
|
// If this is a controlled component then no state change should take place.
|
|
if (selectedKey !== undefined || selectedKeys !== undefined) {
|
|
this._onChange(event, options, index, checked, multiSelect);
|
|
return;
|
|
}
|
|
if (!multiSelect && !notifyOnReselect && index === selectedIndices[0]) {
|
|
return;
|
|
}
|
|
else if (multiSelect) {
|
|
newIndexes = selectedIndices ? this._copyArray(selectedIndices) : [];
|
|
if (checked) {
|
|
var position = newIndexes.indexOf(index);
|
|
if (position > -1) {
|
|
// unchecked the current one
|
|
newIndexes.splice(position, 1);
|
|
}
|
|
}
|
|
else {
|
|
// add the new selected index into the existing one
|
|
newIndexes.push(index);
|
|
}
|
|
}
|
|
else {
|
|
// Set the selected option if this is an uncontrolled component
|
|
newIndexes = [index];
|
|
}
|
|
event.persist();
|
|
// Call onChange after state is updated
|
|
this.props.hoisted.setSelectedIndices(newIndexes);
|
|
this._onChange(event, options, index, checked, multiSelect);
|
|
};
|
|
DropdownInternal.prototype._copyArray = function (array) {
|
|
var newArray = [];
|
|
for (var _i = 0, array_1 = array; _i < array_1.length; _i++) {
|
|
var element = array_1[_i];
|
|
newArray.push(element);
|
|
}
|
|
return newArray;
|
|
};
|
|
/**
|
|
* Finds the next valid Dropdown option and sets the selected index to it.
|
|
* @param stepValue - Value of how many items the function should traverse. Should be -1 or 1.
|
|
* @param index - Index of where the search should start
|
|
* @param selectedIndex - The selectedIndex Dropdown's state
|
|
* @returns The next valid dropdown option's index
|
|
*/
|
|
DropdownInternal.prototype._moveIndex = function (event, stepValue, index, selectedIndex) {
|
|
var options = this.props.options;
|
|
// Return selectedIndex if nothing has changed or options is empty
|
|
if (selectedIndex === index || options.length === 0) {
|
|
return selectedIndex;
|
|
}
|
|
// If the user is pressing the up or down key we want to make
|
|
// sure that the dropdown cycles through the options without
|
|
// causing the screen to scroll. In _onDropdownKeyDown
|
|
// at the very end is a check to see if newIndex !== selectedIndex.
|
|
// If the index is less than 0 and we set it back to 0, then
|
|
// newIndex will equal selectedIndex and not stop the action
|
|
// of the key press happening and vice versa for indexes greater
|
|
// than or equal to the options length.
|
|
if (index >= options.length) {
|
|
index = 0;
|
|
}
|
|
else if (index < 0) {
|
|
index = options.length - 1;
|
|
}
|
|
var stepCounter = 0;
|
|
// If current index is a header or divider, or disabled, increment by step
|
|
while (options[index].itemType === Dropdown_types_1.DropdownMenuItemType.Header ||
|
|
options[index].itemType === Dropdown_types_1.DropdownMenuItemType.Divider ||
|
|
options[index].disabled) {
|
|
// If stepCounter exceeds length of options, then return selectedIndex (-1)
|
|
if (stepCounter >= options.length) {
|
|
return selectedIndex;
|
|
}
|
|
// If index + stepValue is out of bounds, wrap around
|
|
if (index + stepValue < 0) {
|
|
index = options.length;
|
|
}
|
|
else if (index + stepValue >= options.length) {
|
|
index = -1;
|
|
}
|
|
index = index + stepValue;
|
|
stepCounter++;
|
|
}
|
|
this.setSelectedIndex(event, index);
|
|
return index;
|
|
};
|
|
/** Wrap item list in a FocusZone */
|
|
DropdownInternal.prototype._renderFocusableList = function (props) {
|
|
var _a = props.onRenderList, onRenderList = _a === void 0 ? this._onRenderList : _a, label = props.label, ariaLabel = props.ariaLabel, multiSelect = props.multiSelect;
|
|
return (React.createElement("div", { className: this._classNames.dropdownItemsWrapper, onKeyDown: this._onZoneKeyDown, onKeyUp: this._onZoneKeyUp, ref: this._host, tabIndex: 0 },
|
|
React.createElement(FocusZone_1.FocusZone, { ref: this._focusZone, direction: FocusZone_1.FocusZoneDirection.vertical, id: this._listId, className: this._classNames.dropdownItems, role: "listbox", "aria-label": ariaLabel, "aria-labelledby": label && !ariaLabel ? this._labelId : undefined, "aria-multiselectable": multiSelect }, onRenderList(props, this._onRenderList))));
|
|
};
|
|
DropdownInternal.prototype._renderSeparator = function (item) {
|
|
var index = item.index, key = item.key;
|
|
var separatorClassName = item.hidden ? this._classNames.dropdownDividerHidden : this._classNames.dropdownDivider;
|
|
if (index > 0) {
|
|
return React.createElement("div", { role: "presentation", key: key, className: separatorClassName });
|
|
}
|
|
return null;
|
|
};
|
|
DropdownInternal.prototype._renderHeader = function (item) {
|
|
var _a = this.props.onRenderOption, onRenderOption = _a === void 0 ? this._onRenderOption : _a;
|
|
var key = item.key, id = item.id;
|
|
var headerClassName = item.hidden
|
|
? this._classNames.dropdownItemHeaderHidden
|
|
: this._classNames.dropdownItemHeader;
|
|
return (React.createElement("div", { id: id, key: key, className: headerClassName }, onRenderOption(item, this._onRenderOption)));
|
|
};
|
|
DropdownInternal.prototype._onItemMouseEnter = function (item, ev) {
|
|
if (this._shouldIgnoreMouseEvent()) {
|
|
return;
|
|
}
|
|
var targetElement = ev.currentTarget;
|
|
targetElement.focus();
|
|
};
|
|
DropdownInternal.prototype._onItemMouseMove = function (item, ev) {
|
|
var doc = (0, dom_1.getDocumentEx)(this.context); // can only be called on the client
|
|
var targetElement = ev.currentTarget;
|
|
this._gotMouseMove = true;
|
|
if (!this._isScrollIdle || doc.activeElement === targetElement) {
|
|
return;
|
|
}
|
|
targetElement.focus();
|
|
};
|
|
DropdownInternal.prototype._shouldIgnoreMouseEvent = function () {
|
|
return !this._isScrollIdle || !this._gotMouseMove;
|
|
};
|
|
/**
|
|
* Returns true if the key for the event is alt (Mac option) or meta (Mac command).
|
|
*/
|
|
DropdownInternal.prototype._isAltOrMeta = function (ev) {
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
return ev.which === Utilities_1.KeyCodes.alt || ev.key === 'Meta';
|
|
};
|
|
/**
|
|
* 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.
|
|
*/
|
|
DropdownInternal.prototype._shouldHandleKeyUp = function (ev) {
|
|
var keyPressIsAltOrMetaAlone = this._lastKeyDownWasAltOrMeta && this._isAltOrMeta(ev);
|
|
this._lastKeyDownWasAltOrMeta = false;
|
|
return !!keyPressIsAltOrMetaAlone && !((0, Utilities_1.isMac)() || (0, Utilities_1.isIOS)());
|
|
};
|
|
/**
|
|
* Returns true if dropdown should set to open on focus.
|
|
* Otherwise, isOpen state should be toggled on click
|
|
*/
|
|
DropdownInternal.prototype._shouldOpenOnFocus = function () {
|
|
var hasFocus = this.state.hasFocus;
|
|
var openOnKeyboardFocus = this.props.openOnKeyboardFocus;
|
|
return !this._isFocusedByClick && openOnKeyboardFocus === true && !hasFocus;
|
|
};
|
|
DropdownInternal.defaultProps = {
|
|
options: [],
|
|
};
|
|
DropdownInternal.contextType = react_window_provider_1.WindowContext;
|
|
return DropdownInternal;
|
|
}(React.Component));
|
|
//# sourceMappingURL=Dropdown.base.js.map
|