UNPKG

@fluentui/react

Version:

Reusable React components for building web experiences.

857 lines 49.2 kB
import { __assign, __extends, __rest, __spreadArrays } from "tslib"; import * as React from 'react'; import { ContextualMenuItemType, } from './ContextualMenu.types'; import { DirectionalHint } from '../../common/DirectionalHint'; import { FocusZone, FocusZoneDirection, FocusZoneTabbableElements } from '../../FocusZone'; import { divProperties, getNativeProps, shallowCompare, warnDeprecations, Async, EventGroup, assign, classNamesFunction, css, getFirstFocusable, getId, getLastFocusable, getRTL, KeyCodes, shouldWrapFocus, isIOS, isMac, initializeComponentRef, memoizeFunction, getPropsWithDefaults, getDocument, } from '../../Utilities'; import { hasSubmenu, getIsChecked, isItemDisabled } from '../../utilities/contextualMenu/index'; import { Callout } from '../../Callout'; import { ContextualMenuItem } from './ContextualMenuItem'; import { ContextualMenuSplitButton, ContextualMenuButton, ContextualMenuAnchor, } from './ContextualMenuItemWrapper/index'; import { concatStyleSetsWithProps } from '../../Styling'; import { getItemStyles } from './ContextualMenu.classNames'; import { useTarget, usePrevious, useMergedRefs } from '@fluentui/react-hooks'; import { useResponsiveMode, ResponsiveMode } from '../../ResponsiveMode'; import { MenuContext } from '../../utilities/MenuContext/index'; var getClassNames = classNamesFunction(); var getContextualMenuItemClassNames = classNamesFunction(); // The default ContextualMenu properties have no items and beak, the default submenu direction is right and top. var DEFAULT_PROPS = { items: [], shouldFocusOnMount: true, gapSpace: 0, directionalHint: DirectionalHint.bottomAutoEdge, beakWidth: 16, }; export function getSubmenuItems(item) { return item.subMenuProps ? item.subMenuProps.items : item.items; } /** * Returns true if a list of menu items can contain a checkbox */ export function canAnyMenuItemsCheck(items) { return items.some(function (item) { if (item.canCheck) { return true; } // If the item is a section, check if any of the items in the section can check. if (item.sectionProps && item.sectionProps.items.some(function (submenuItem) { return submenuItem.canCheck === true; })) { return true; } return false; }); } var NavigationIdleDelay = 250; /* ms */ var COMPONENT_NAME = 'ContextualMenu'; var _getMenuItemStylesFunction = memoizeFunction(function () { var styles = []; for (var _i = 0; _i < arguments.length; _i++) { styles[_i] = arguments[_i]; } return function (styleProps) { return concatStyleSetsWithProps.apply(void 0, __spreadArrays([styleProps, getItemStyles], styles)); }; }); function useVisibility(props, targetWindow) { var _a = props.hidden, hidden = _a === void 0 ? false : _a, onMenuDismissed = props.onMenuDismissed, onMenuOpened = props.onMenuOpened; var previousHidden = usePrevious(hidden); var onMenuOpenedRef = React.useRef(onMenuOpened); var onMenuClosedRef = React.useRef(onMenuDismissed); var propsRef = React.useRef(props); onMenuOpenedRef.current = onMenuOpened; onMenuClosedRef.current = onMenuDismissed; propsRef.current = props; React.useEffect(function () { var _a, _b; // Don't issue dismissed callbacks on initial mount if (hidden && previousHidden === false) { (_a = onMenuClosedRef.current) === null || _a === void 0 ? void 0 : _a.call(onMenuClosedRef, propsRef.current); } else if (!hidden && previousHidden !== false) { (_b = onMenuOpenedRef.current) === null || _b === void 0 ? void 0 : _b.call(onMenuOpenedRef, propsRef.current); } }, [hidden, previousHidden]); // Issue onDismissedCallback on unmount React.useEffect(function () { return function () { var _a; return (_a = onMenuClosedRef.current) === null || _a === void 0 ? void 0 : _a.call(onMenuClosedRef, propsRef.current); }; }, []); } function useSubMenuState(_a) { var hidden = _a.hidden; var _b = React.useState(), expandedMenuItemKey = _b[0], setExpandedMenuItemKey = _b[1]; var _c = React.useState(), submenuTarget = _c[0], setSubmenuTarget = _c[1]; /** True if the menu was expanded by mouse click OR hover (as opposed to by keyboard) */ var _d = React.useState(), expandedByMouseClick = _d[0], setExpandedByMouseClick = _d[1]; var closeSubMenu = React.useCallback(function () { setExpandedByMouseClick(undefined); setExpandedMenuItemKey(undefined); setSubmenuTarget(undefined); }, []); var openSubMenu = React.useCallback(function (_a, target, openedByMouseClick) { var submenuItemKey = _a.key; if (expandedMenuItemKey === submenuItemKey) { return; } target.focus(); setExpandedByMouseClick(openedByMouseClick); setExpandedMenuItemKey(submenuItemKey); setSubmenuTarget(target); }, [expandedMenuItemKey]); React.useEffect(function () { if (hidden) { closeSubMenu(); } }, [hidden, closeSubMenu]); return [expandedMenuItemKey, submenuTarget, expandedByMouseClick, openSubMenu, closeSubMenu]; } function useShouldUpdateFocusOnMouseMove(_a) { var delayUpdateFocusOnHover = _a.delayUpdateFocusOnHover, hidden = _a.hidden; var shouldUpdateFocusOnMouseEvent = React.useRef(!delayUpdateFocusOnHover); var gotMouseMove = React.useRef(false); React.useEffect(function () { shouldUpdateFocusOnMouseEvent.current = !delayUpdateFocusOnHover; gotMouseMove.current = hidden ? false : !delayUpdateFocusOnHover && gotMouseMove.current; }, [delayUpdateFocusOnHover, hidden]); var onMenuFocusCapture = React.useCallback(function () { if (delayUpdateFocusOnHover) { shouldUpdateFocusOnMouseEvent.current = true; } }, [delayUpdateFocusOnHover]); return [shouldUpdateFocusOnMouseEvent, gotMouseMove, onMenuFocusCapture]; } export var ContextualMenuBase = React.forwardRef(function (propsWithoutDefaults, forwardedRef) { var _a = getPropsWithDefaults(DEFAULT_PROPS, propsWithoutDefaults), ref = _a.ref, props = __rest(_a, ["ref"]); var rootRef = React.useRef(null); var hostElement = useMergedRefs(rootRef, forwardedRef); var _b = useTarget(props.target, hostElement), targetRef = _b[0], targetWindow = _b[1]; var _c = useSubMenuState(props), expandedMenuItemKey = _c[0], submenuTarget = _c[1], expandedByMouseClick = _c[2], openSubMenu = _c[3], closeSubMenu = _c[4]; var _d = useShouldUpdateFocusOnMouseMove(props), shouldUpdateFocusOnMouseEvent = _d[0], gotMouseMove = _d[1], onMenuFocusCapture = _d[2]; var responsiveMode = useResponsiveMode(hostElement); useVisibility(props, targetWindow); return (React.createElement(ContextualMenuInternal, __assign({}, props, { hoisted: { hostElement: hostElement, targetRef: targetRef, targetWindow: targetWindow, expandedMenuItemKey: expandedMenuItemKey, submenuTarget: submenuTarget, expandedByMouseClick: expandedByMouseClick, openSubMenu: openSubMenu, closeSubMenu: closeSubMenu, shouldUpdateFocusOnMouseEvent: shouldUpdateFocusOnMouseEvent, gotMouseMove: gotMouseMove, onMenuFocusCapture: onMenuFocusCapture, }, responsiveMode: responsiveMode }))); }); ContextualMenuBase.displayName = 'ContextualMenuBase'; var ContextualMenuInternal = /** @class */ (function (_super) { __extends(ContextualMenuInternal, _super); function ContextualMenuInternal(props) { var _this = _super.call(this, props) || this; _this._mounted = false; _this.dismiss = function (ev, dismissAll) { var onDismiss = _this.props.onDismiss; if (onDismiss) { onDismiss(ev, dismissAll); } }; _this._tryFocusPreviousActiveElement = function (options) { var _a, _b; if (options && options.documentContainsFocus && _this._previousActiveElement) { // Make sure that the focus method actually exists // In some cases the object might exist but not be a real element. // This is primarily for IE 11 and should be removed once IE 11 is no longer in use. (_b = (_a = _this._previousActiveElement).focus) === null || _b === void 0 ? void 0 : _b.call(_a); } }; _this._onRenderMenuList = function (menuListProps, defaultRender) { var indexCorrection = 0; var items = menuListProps.items, totalItemCount = menuListProps.totalItemCount, hasCheckmarks = menuListProps.hasCheckmarks, hasIcons = menuListProps.hasIcons; return (React.createElement("ul", { className: _this._classNames.list, onKeyDown: _this._onKeyDown, onKeyUp: _this._onKeyUp, role: 'presentation' }, items.map(function (item, index) { var menuItem = _this._renderMenuItem(item, index, indexCorrection, totalItemCount, hasCheckmarks, hasIcons); if (item.itemType !== ContextualMenuItemType.Divider && item.itemType !== ContextualMenuItemType.Header) { var indexIncrease = item.customOnRenderListLength ? item.customOnRenderListLength : 1; indexCorrection += indexIncrease; } return menuItem; }))); }; /** * !!!IMPORTANT!!! Avoid mutating `item: IContextualMenuItem` argument. It will * cause the menu items to always re-render because the component update is based on shallow comparison. */ _this._renderMenuItem = function (item, index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons) { var _a; var renderedItems = []; var iconProps = item.iconProps || { iconName: 'None' }; var getItemClassNames = item.getItemClassNames, // eslint-disable-line deprecation/deprecation itemProps = item.itemProps; var styles = itemProps ? itemProps.styles : undefined; var expandedMenuItemKey = _this.props.hoisted.expandedMenuItemKey; // We only send a dividerClassName when the item to be rendered is a divider. // For all other cases, the default divider style is used. var dividerClassName = item.itemType === ContextualMenuItemType.Divider ? item.className : undefined; var subMenuIconClassName = item.submenuIconProps ? item.submenuIconProps.className : ''; // eslint-disable-next-line deprecation/deprecation var itemClassNames; // IContextualMenuItem#getItemClassNames for backwards compatibility // otherwise uses mergeStyles for class names. if (getItemClassNames) { itemClassNames = getItemClassNames(_this.props.theme, isItemDisabled(item), expandedMenuItemKey === item.key, !!getIsChecked(item), !!item.href, iconProps.iconName !== 'None', item.className, dividerClassName, iconProps.className, subMenuIconClassName, item.primaryDisabled); } else { var itemStyleProps = { theme: _this.props.theme, disabled: isItemDisabled(item), expanded: expandedMenuItemKey === item.key, checked: !!getIsChecked(item), isAnchorLink: !!item.href, knownIcon: iconProps.iconName !== 'None', itemClassName: item.className, dividerClassName: dividerClassName, iconClassName: iconProps.className, subMenuClassName: subMenuIconClassName, primaryDisabled: item.primaryDisabled, }; // We need to generate default styles then override if styles are provided // since the ContextualMenu currently handles item classNames. itemClassNames = getContextualMenuItemClassNames(_getMenuItemStylesFunction((_a = _this._classNames.subComponentStyles) === null || _a === void 0 ? void 0 : _a.menuItem, styles), itemStyleProps); } // eslint-disable-next-line deprecation/deprecation if (item.text === '-' || item.name === '-') { item.itemType = ContextualMenuItemType.Divider; } switch (item.itemType) { case ContextualMenuItemType.Divider: renderedItems.push(_this._renderSeparator(index, itemClassNames)); break; case ContextualMenuItemType.Header: renderedItems.push(_this._renderSeparator(index, itemClassNames)); var headerItem = _this._renderHeaderMenuItem(item, itemClassNames, index, hasCheckmarks, hasIcons); renderedItems.push(_this._renderListItem(headerItem, item.key || index, itemClassNames, item.title)); break; case ContextualMenuItemType.Section: renderedItems.push(_this._renderSectionItem(item, itemClassNames, index, hasCheckmarks, hasIcons)); break; default: var menuItem = _this._renderNormalItem(item, itemClassNames, index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons); renderedItems.push(_this._renderListItem(menuItem, item.key || index, itemClassNames, item.title)); break; } // Since multiple nodes *could* be rendered, wrap them all in a fragment with this item's key. // This ensures the reconciler handles multi-item output per-node correctly and does not re-mount content. return React.createElement(React.Fragment, { key: item.key }, renderedItems); }; _this._defaultMenuItemRenderer = function (item) { var index = item.index, focusableElementIndex = item.focusableElementIndex, totalItemCount = item.totalItemCount, hasCheckmarks = item.hasCheckmarks, hasIcons = item.hasIcons; return _this._renderMenuItem(item, index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons); }; _this._onKeyDown = function (ev) { // Take note if we are processing an alt (option) or meta (command) keydown. // See comment in _shouldHandleKeyUp for reasoning. _this._lastKeyDownWasAltOrMeta = _this._isAltOrMeta(ev); // On Mac, pressing escape dismisses all levels of native context menus // eslint-disable-next-line deprecation/deprecation var dismissAllMenus = ev.which === KeyCodes.escape && (isMac() || isIOS()); return _this._keyHandler(ev, _this._shouldHandleKeyDown, dismissAllMenus); }; _this._shouldHandleKeyDown = function (ev) { return ( // eslint-disable-next-line deprecation/deprecation ev.which === KeyCodes.escape || _this._shouldCloseSubMenu(ev) || // eslint-disable-next-line deprecation/deprecation (ev.which === KeyCodes.up && (ev.altKey || ev.metaKey))); }; _this._onKeyUp = function (ev) { return _this._keyHandler(ev, _this._shouldHandleKeyUp, true /* dismissAllMenus */); }; /** * 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. */ _this._shouldHandleKeyUp = function (ev) { var keyPressIsAltOrMetaAlone = _this._lastKeyDownWasAltOrMeta && _this._isAltOrMeta(ev); _this._lastKeyDownWasAltOrMeta = false; return !!keyPressIsAltOrMetaAlone && !(isIOS() || isMac()); }; /** * Calls `shouldHandleKey` to determine whether the keyboard event should be handled; * if so, stops event propagation and dismisses menu(s). * @param ev - The keyboard event. * @param shouldHandleKey - Returns whether we should handle this keyboard event. * @param dismissAllMenus - If true, dismiss all menus. Otherwise, dismiss only the current menu. * Only does anything if `shouldHandleKey` returns true. * @returns Whether the event was handled. */ _this._keyHandler = function (ev, shouldHandleKey, dismissAllMenus) { var handled = false; if (shouldHandleKey(ev)) { _this.dismiss(ev, dismissAllMenus); ev.preventDefault(); ev.stopPropagation(); handled = true; } return handled; }; /** * Checks if the submenu should be closed */ _this._shouldCloseSubMenu = function (ev) { var submenuCloseKey = getRTL(_this.props.theme) ? KeyCodes.right : KeyCodes.left; // eslint-disable-next-line deprecation/deprecation if (ev.which !== submenuCloseKey || !_this.props.isSubMenu) { return false; } return (_this._adjustedFocusZoneProps.direction === FocusZoneDirection.vertical || (!!_this._adjustedFocusZoneProps.checkForNoWrap && !shouldWrapFocus(ev.target, 'data-no-horizontal-wrap'))); }; _this._onMenuKeyDown = function (ev) { // Mark as handled if onKeyDown returns true (for handling collapse cases) // or if we are attempting to expand a submenu var handled = _this._onKeyDown(ev); var hostElement = _this.props.hoisted.hostElement; if (handled || !hostElement.current) { return; } // If we have a modifier key being pressed, we do not want to move focus. // Otherwise, handle up and down keys. var hasModifier = !!(ev.altKey || ev.metaKey); // eslint-disable-next-line deprecation/deprecation var isUp = ev.which === KeyCodes.up; // eslint-disable-next-line deprecation/deprecation var isDown = ev.which === KeyCodes.down; if (!hasModifier && (isUp || isDown)) { var elementToFocus = isUp ? getLastFocusable(hostElement.current, hostElement.current.lastChild, true) : getFirstFocusable(hostElement.current, hostElement.current.firstChild, true); if (elementToFocus) { elementToFocus.focus(); ev.preventDefault(); ev.stopPropagation(); } } }; /** * Scroll handler for the callout to make sure the mouse events * for updating focus are not interacting during scroll */ _this._onScroll = function () { if (!_this._isScrollIdle && _this._scrollIdleTimeoutId !== undefined) { _this._async.clearTimeout(_this._scrollIdleTimeoutId); _this._scrollIdleTimeoutId = undefined; } else { _this._isScrollIdle = false; } _this._scrollIdleTimeoutId = _this._async.setTimeout(function () { _this._isScrollIdle = true; }, NavigationIdleDelay); }; _this._onItemMouseEnterBase = function (item, ev, target) { if (_this._shouldIgnoreMouseEvent()) { return; } _this._updateFocusOnMouseEvent(item, ev, target); }; _this._onItemMouseMoveBase = function (item, ev, target) { var targetElement = ev.currentTarget; var _a = _this.props.hoisted, shouldUpdateFocusOnMouseEvent = _a.shouldUpdateFocusOnMouseEvent, gotMouseMove = _a.gotMouseMove, targetWindow = _a.targetWindow; // Always do this check to make sure we record a mouseMove if needed (even if we are timed out) if (shouldUpdateFocusOnMouseEvent.current) { gotMouseMove.current = true; } else { return; } if (!_this._isScrollIdle || _this._enterTimerId !== undefined || targetElement === (targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.document.activeElement)) { return; } _this._updateFocusOnMouseEvent(item, ev, target); }; _this._onMouseItemLeave = function (item, ev) { var _a; var _b = _this.props.hoisted, expandedMenuItemKey = _b.expandedMenuItemKey, hostElement = _b.hostElement; if (_this._shouldIgnoreMouseEvent()) { return; } if (_this._enterTimerId !== undefined) { _this._async.clearTimeout(_this._enterTimerId); _this._enterTimerId = undefined; } if (expandedMenuItemKey !== undefined) { 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 (hostElement.current.setActive) { try { hostElement.current.setActive(); } catch (e) { /* no-op */ } } else { (_a = hostElement.current) === null || _a === void 0 ? void 0 : _a.focus(); } }; _this._onItemMouseDown = function (item, ev) { if (item.onMouseDown) { item.onMouseDown(item, ev); } }; _this._onItemClick = function (item, ev) { _this._onItemClickBase(item, ev, ev.currentTarget); }; _this._onItemClickBase = function (item, ev, target) { var items = getSubmenuItems(item); var _a = _this.props.hoisted, expandedMenuItemKey = _a.expandedMenuItemKey, openSubMenu = _a.openSubMenu; // Cancel a async menu item hover timeout action from being taken and instead // just trigger the click event instead. _this._cancelSubMenuTimer(); if (!hasSubmenu(item) && (!items || !items.length)) { // This is an item without a menu. Click it. _this._executeItemClick(item, ev); } else { if (item.key !== expandedMenuItemKey) { // This has a collapsed sub menu. Expand it. openSubMenu(item, target, // When Edge + Narrator are used together (regardless of if the button is in a form or not), pressing // "Enter" fires this method and not _onMenuKeyDown. Checking ev.nativeEvent.detail differentiates // between a real click event and a keypress event (detail should be the number of mouse clicks). // ...Plot twist! For a real click event in IE 11, detail is always 0 (Edge sets it properly to 1). // So we also check the pointerType property, which both Edge and IE set to "mouse" for real clicks // and "" for pressing "Enter" with Narrator on. ev.nativeEvent.detail !== 0 || ev.nativeEvent.pointerType === 'mouse'); } } ev.stopPropagation(); ev.preventDefault(); }; _this._onAnchorClick = function (item, ev) { _this._executeItemClick(item, ev); ev.stopPropagation(); }; _this._executeItemClick = function (item, ev) { if (item.disabled || item.isDisabled) { return; } var dismiss = false; if (item.onClick) { dismiss = !!item.onClick(ev, item); } else if (_this.props.onItemClick) { dismiss = !!_this.props.onItemClick(ev, item); } if (dismiss || !ev.defaultPrevented) { _this.dismiss(ev, true); } }; _this._onItemKeyDown = function (item, ev) { var openKey = getRTL(_this.props.theme) ? KeyCodes.left : KeyCodes.right; if (!item.disabled && // eslint-disable-next-line deprecation/deprecation (ev.which === openKey || ev.which === KeyCodes.enter || (ev.which === KeyCodes.down && (ev.altKey || ev.metaKey)))) { _this.props.hoisted.openSubMenu(item, ev.currentTarget, false); ev.preventDefault(); } }; // Cancel a async menu item hover timeout action from being taken and instead // do new upcoming behavior _this._cancelSubMenuTimer = function () { if (_this._enterTimerId !== undefined) { _this._async.clearTimeout(_this._enterTimerId); _this._enterTimerId = undefined; } }; /** * This function is called ASYNCHRONOUSLY, and so there is a chance it is called * after the component is unmounted. The _mounted property is added to prevent * from calling setState() after unmount. Do NOT copy this pattern in synchronous * code. */ _this._onSubMenuDismiss = function (ev, dismissAll) { if (dismissAll) { _this.dismiss(ev, dismissAll); } else if (_this._mounted) { _this.props.hoisted.closeSubMenu(); } }; _this._onPointerAndTouchEvent = function (ev) { _this._cancelSubMenuTimer(); }; _this._async = new Async(_this); _this._events = new EventGroup(_this); initializeComponentRef(_this); warnDeprecations(COMPONENT_NAME, props, { getMenuClassNames: 'styles', }); _this.state = { contextualMenuItems: undefined, subMenuId: getId('ContextualMenu'), }; _this._id = props.id || getId('ContextualMenu'); _this._isScrollIdle = true; return _this; } ContextualMenuInternal.prototype.shouldComponentUpdate = function (newProps, newState) { if (!newProps.shouldUpdateWhenHidden && this.props.hidden && newProps.hidden) { // Do not update when hidden. return false; } return !shallowCompare(this.props, newProps) || !shallowCompare(this.state, newState); }; ContextualMenuInternal.prototype.getSnapshotBeforeUpdate = function (prevProps) { var hoisted = this.props.hoisted; if (this._isHidden(prevProps) !== this._isHidden(this.props)) { if (this._isHidden(this.props)) { this._onMenuClosed(); } else { this._previousActiveElement = hoisted.targetWindow ? hoisted.targetWindow.document.activeElement : undefined; } } return null; }; // Invoked once, only on the client (not on the server), immediately after the initial rendering occurs. ContextualMenuInternal.prototype.componentDidMount = function () { var _a = this.props, hidden = _a.hidden, hoisted = _a.hoisted; if (!hidden) { this._previousActiveElement = hoisted.targetWindow ? hoisted.targetWindow.document.activeElement : undefined; } this._mounted = true; }; // Invoked immediately before a component is unmounted from the DOM. ContextualMenuInternal.prototype.componentWillUnmount = function () { if (this.props.onMenuDismissed) { this.props.onMenuDismissed(this.props); } this._events.dispose(); this._async.dispose(); this._mounted = false; }; ContextualMenuInternal.prototype.render = function () { var _this = this; var isBeakVisible = this.props.isBeakVisible; var _a = this.props, items = _a.items, labelElementId = _a.labelElementId, id = _a.id, className = _a.className, beakWidth = _a.beakWidth, directionalHint = _a.directionalHint, directionalHintForRTL = _a.directionalHintForRTL, alignTargetEdge = _a.alignTargetEdge, gapSpace = _a.gapSpace, coverTarget = _a.coverTarget, ariaLabel = _a.ariaLabel, doNotLayer = _a.doNotLayer, target = _a.target, bounds = _a.bounds, useTargetWidth = _a.useTargetWidth, useTargetAsMinWidth = _a.useTargetAsMinWidth, directionalHintFixed = _a.directionalHintFixed, shouldFocusOnMount = _a.shouldFocusOnMount, shouldFocusOnContainer = _a.shouldFocusOnContainer, title = _a.title, styles = _a.styles, theme = _a.theme, calloutProps = _a.calloutProps, _b = _a.onRenderSubMenu, onRenderSubMenu = _b === void 0 ? this._onRenderSubMenu : _b, _c = _a.onRenderMenuList, onRenderMenuList = _c === void 0 ? this._onRenderMenuList : _c, focusZoneProps = _a.focusZoneProps, // eslint-disable-next-line deprecation/deprecation getMenuClassNames = _a.getMenuClassNames, _d = _a.hoisted, expandedMenuItemKey = _d.expandedMenuItemKey, targetRef = _d.targetRef, onMenuFocusCapture = _d.onMenuFocusCapture, hostElement = _d.hostElement; this._classNames = getMenuClassNames ? getMenuClassNames(theme, className) : getClassNames(styles, { theme: theme, className: className, }); var hasIcons = itemsHaveIcons(items); function itemsHaveIcons(contextualMenuItems) { for (var _i = 0, contextualMenuItems_1 = contextualMenuItems; _i < contextualMenuItems_1.length; _i++) { var item = contextualMenuItems_1[_i]; if (item.iconProps) { return true; } if (item.itemType === ContextualMenuItemType.Section && item.sectionProps && itemsHaveIcons(item.sectionProps.items)) { return true; } } return false; } this._adjustedFocusZoneProps = __assign(__assign({}, focusZoneProps), { className: this._classNames.root, isCircularNavigation: true, handleTabKey: FocusZoneTabbableElements.all, direction: this._getFocusZoneDirection() }); var hasCheckmarks = canAnyMenuItemsCheck(items); var submenuProps = expandedMenuItemKey && this.props.hidden !== true ? this._getSubmenuProps() : null; isBeakVisible = isBeakVisible === undefined ? this.props.responsiveMode <= ResponsiveMode.medium : isBeakVisible; /** * When useTargetWidth is true, get the width of the target element and apply it for the context menu container */ var contextMenuStyle; var targetAsHtmlElement = targetRef.current; if ((useTargetWidth || useTargetAsMinWidth) && targetAsHtmlElement && targetAsHtmlElement.offsetWidth) { var targetBoundingRect = targetAsHtmlElement.getBoundingClientRect(); var targetWidth = targetBoundingRect.width - 2; /* Accounts for 1px border */ if (useTargetWidth) { contextMenuStyle = { width: targetWidth, }; } else if (useTargetAsMinWidth) { contextMenuStyle = { minWidth: targetWidth, }; } } // The menu should only return if items were provided, if no items were provided then it should not appear. if (items && items.length > 0) { var totalItemCount_1 = 0; for (var _i = 0, items_1 = items; _i < items_1.length; _i++) { var item = items_1[_i]; if (item.itemType !== ContextualMenuItemType.Divider && item.itemType !== ContextualMenuItemType.Header) { var itemCount = item.customOnRenderListLength ? item.customOnRenderListLength : 1; totalItemCount_1 += itemCount; } } var calloutStyles_1 = this._classNames.subComponentStyles ? this._classNames.subComponentStyles.callout : undefined; return (React.createElement(MenuContext.Consumer, null, function (menuContext) { return (React.createElement(Callout, __assign({ styles: calloutStyles_1, onRestoreFocus: _this._tryFocusPreviousActiveElement }, calloutProps, { target: target || menuContext.target, isBeakVisible: isBeakVisible, beakWidth: beakWidth, directionalHint: directionalHint, directionalHintForRTL: directionalHintForRTL, gapSpace: gapSpace, coverTarget: coverTarget, doNotLayer: doNotLayer, className: css('ms-ContextualMenu-Callout', calloutProps && calloutProps.className), setInitialFocus: shouldFocusOnMount, onDismiss: _this.props.onDismiss || menuContext.onDismiss, onScroll: _this._onScroll, bounds: bounds, directionalHintFixed: directionalHintFixed, alignTargetEdge: alignTargetEdge, hidden: _this.props.hidden || menuContext.hidden, ref: hostElement }), React.createElement("div", { style: contextMenuStyle, id: id, className: _this._classNames.container, tabIndex: shouldFocusOnContainer ? 0 : -1, onKeyDown: _this._onMenuKeyDown, onKeyUp: _this._onKeyUp, onFocusCapture: onMenuFocusCapture, "aria-label": ariaLabel, "aria-labelledby": labelElementId, role: 'menu' }, title && React.createElement("div", { className: _this._classNames.title }, " ", title, " "), items && items.length ? _this._renderFocusZone(onRenderMenuList({ ariaLabel: ariaLabel, items: items, totalItemCount: totalItemCount_1, hasCheckmarks: hasCheckmarks, hasIcons: hasIcons, defaultMenuItemRenderer: _this._defaultMenuItemRenderer, labelElementId: labelElementId, }, _this._onRenderMenuList)) : null, submenuProps && onRenderSubMenu(submenuProps, _this._onRenderSubMenu)))); })); } else { return null; } }; ContextualMenuInternal.prototype._onMenuClosed = function () { var _a, _b; (_a = this._tryFocusPreviousActiveElement) === null || _a === void 0 ? void 0 : _a.call(this, { originalElement: this._previousActiveElement, containsFocus: true, documentContainsFocus: ((_b = getDocument()) === null || _b === void 0 ? void 0 : _b.hasFocus()) || false, }); }; /** * Return whether the contextual menu is hidden. * Undefined value for hidden is equivalent to hidden being false. * @param props - Props for the component */ ContextualMenuInternal.prototype._isHidden = function (props) { return !!props.hidden; }; /** * Gets the focusZoneDirection by using the arrowDirection if specified, * the direction specified in the focusZoneProps, or defaults to FocusZoneDirection.vertical */ ContextualMenuInternal.prototype._getFocusZoneDirection = function () { var focusZoneProps = this.props.focusZoneProps; return focusZoneProps && focusZoneProps.direction !== undefined ? focusZoneProps.direction : FocusZoneDirection.vertical; }; ContextualMenuInternal.prototype._onRenderSubMenu = function (subMenuProps, defaultRender) { throw Error('ContextualMenuBase: onRenderSubMenu callback is null or undefined. ' + 'Please ensure to set `onRenderSubMenu` property either manually or with `styled` helper.'); }; ContextualMenuInternal.prototype._renderFocusZone = function (children) { var _a = this.props.focusZoneAs, ChildrenRenderer = _a === void 0 ? FocusZone : _a; return React.createElement(ChildrenRenderer, __assign({}, this._adjustedFocusZoneProps), children); }; ContextualMenuInternal.prototype._renderSectionItem = function (sectionItem, // eslint-disable-next-line deprecation/deprecation menuClassNames, index, hasCheckmarks, hasIcons) { var _this = this; var sectionProps = sectionItem.sectionProps; if (!sectionProps) { return; } var headerItem; var groupProps; if (sectionProps.title) { // Since title is a user-facing string, it needs to be stripped of whitespace in order to build a valid element ID var id = this._id + sectionProps.title.replace(/\s/g, ''); var headerContextualMenuItem = { key: "section-" + sectionProps.title + "-title", itemType: ContextualMenuItemType.Header, text: sectionProps.title, id: id, }; groupProps = { role: 'group', 'aria-labelledby': id, }; headerItem = this._renderHeaderMenuItem(headerContextualMenuItem, menuClassNames, index, hasCheckmarks, hasIcons); } if (sectionProps.items && sectionProps.items.length > 0) { return (React.createElement("li", { role: "presentation", key: sectionProps.key || sectionItem.key || "section-" + index }, React.createElement("div", __assign({}, groupProps), React.createElement("ul", { className: this._classNames.list, role: "presentation" }, sectionProps.topDivider && this._renderSeparator(index, menuClassNames, true, true), headerItem && this._renderListItem(headerItem, sectionItem.key || index, menuClassNames, sectionItem.title), sectionProps.items.map(function (contextualMenuItem, itemsIndex) { return _this._renderMenuItem(contextualMenuItem, itemsIndex, itemsIndex, sectionProps.items.length, hasCheckmarks, hasIcons); }), sectionProps.bottomDivider && this._renderSeparator(index, menuClassNames, false, true))))); } }; ContextualMenuInternal.prototype._renderListItem = function (content, key, classNames, // eslint-disable-line deprecation/deprecation title) { return (React.createElement("li", { role: "presentation", title: title, key: key, className: classNames.item }, content)); }; ContextualMenuInternal.prototype._renderSeparator = function (index, classNames, // eslint-disable-line deprecation/deprecation top, fromSection) { if (fromSection || index > 0) { return (React.createElement("li", { role: "separator", key: 'separator-' + index + (top === undefined ? '' : top ? '-top' : '-bottom'), className: classNames.divider, "aria-hidden": "true" })); } return null; }; ContextualMenuInternal.prototype._renderNormalItem = function (item, classNames, // eslint-disable-line deprecation/deprecation index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons) { if (item.onRender) { return item.onRender(__assign({ 'aria-posinset': focusableElementIndex + 1, 'aria-setsize': totalItemCount }, item), this.dismiss); } if (item.href) { return this._renderAnchorMenuItem(item, classNames, index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons); } if (item.split && hasSubmenu(item)) { return this._renderSplitButton(item, classNames, index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons); } return this._renderButtonItem(item, classNames, index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons); }; ContextualMenuInternal.prototype._renderHeaderMenuItem = function (item, // eslint-disable-next-line deprecation/deprecation classNames, index, hasCheckmarks, hasIcons) { var _a = this.props.contextualMenuItemAs, ChildrenRenderer = _a === void 0 ? ContextualMenuItem : _a; var itemProps = item.itemProps, id = item.id; var divHtmlProperties = itemProps && getNativeProps(itemProps, divProperties); return ( // eslint-disable-next-line deprecation/deprecation React.createElement("div", __assign({ id: id, className: this._classNames.header }, divHtmlProperties, { style: item.style }), React.createElement(ChildrenRenderer, __assign({ item: item, classNames: classNames, index: index, onCheckmarkClick: hasCheckmarks ? this._onItemClick : undefined, hasIcons: hasIcons }, itemProps)))); }; ContextualMenuInternal.prototype._renderAnchorMenuItem = function (item, // eslint-disable-next-line deprecation/deprecation classNames, index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons) { var _a = this.props, contextualMenuItemAs = _a.contextualMenuItemAs, _b = _a.hoisted, expandedMenuItemKey = _b.expandedMenuItemKey, openSubMenu = _b.openSubMenu; return (React.createElement(ContextualMenuAnchor, { item: item, classNames: classNames, index: index, focusableElementIndex: focusableElementIndex, totalItemCount: totalItemCount, hasCheckmarks: hasCheckmarks, hasIcons: hasIcons, contextualMenuItemAs: contextualMenuItemAs, onItemMouseEnter: this._onItemMouseEnterBase, onItemMouseLeave: this._onMouseItemLeave, onItemMouseMove: this._onItemMouseMoveBase, onItemMouseDown: this._onItemMouseDown, executeItemClick: this._executeItemClick, onItemClick: this._onAnchorClick, onItemKeyDown: this._onItemKeyDown, expandedMenuItemKey: expandedMenuItemKey, openSubMenu: openSubMenu, dismissSubMenu: this._onSubMenuDismiss, dismissMenu: this.dismiss })); }; ContextualMenuInternal.prototype._renderButtonItem = function (item, // eslint-disable-next-line deprecation/deprecation classNames, index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons) { var _a = this.props, contextualMenuItemAs = _a.contextualMenuItemAs, _b = _a.hoisted, expandedMenuItemKey = _b.expandedMenuItemKey, openSubMenu = _b.openSubMenu; return (React.createElement(ContextualMenuButton, { item: item, classNames: classNames, index: index, focusableElementIndex: focusableElementIndex, totalItemCount: totalItemCount, hasCheckmarks: hasCheckmarks, hasIcons: hasIcons, contextualMenuItemAs: contextualMenuItemAs, onItemMouseEnter: this._onItemMouseEnterBase, onItemMouseLeave: this._onMouseItemLeave, onItemMouseMove: this._onItemMouseMoveBase, onItemMouseDown: this._onItemMouseDown, executeItemClick: this._executeItemClick, onItemClick: this._onItemClick, onItemClickBase: this._onItemClickBase, onItemKeyDown: this._onItemKeyDown, expandedMenuItemKey: expandedMenuItemKey, openSubMenu: openSubMenu, dismissSubMenu: this._onSubMenuDismiss, dismissMenu: this.dismiss })); }; ContextualMenuInternal.prototype._renderSplitButton = function (item, // eslint-disable-next-line deprecation/deprecation classNames, index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons) { var _a = this.props, contextualMenuItemAs = _a.contextualMenuItemAs, _b = _a.hoisted, expandedMenuItemKey = _b.expandedMenuItemKey, openSubMenu = _b.openSubMenu; return (React.createElement(ContextualMenuSplitButton, { item: item, classNames: classNames, index: index, focusableElementIndex: focusableElementIndex, totalItemCount: totalItemCount, hasCheckmarks: hasCheckmarks, hasIcons: hasIcons, contextualMenuItemAs: contextualMenuItemAs, onItemMouseEnter: this._onItemMouseEnterBase, onItemMouseLeave: this._onMouseItemLeave, onItemMouseMove: this._onItemMouseMoveBase, onItemMouseDown: this._onItemMouseDown, executeItemClick: this._executeItemClick, onItemClick: this._onItemClick, onItemClickBase: this._onItemClickBase, onItemKeyDown: this._onItemKeyDown, openSubMenu: openSubMenu, dismissSubMenu: this._onSubMenuDismiss, dismissMenu: this.dismiss, expandedMenuItemKey: expandedMenuItemKey, onTap: this._onPointerAndTouchEvent })); }; /** * Returns true if the key for the event is alt (Mac option) or meta (Mac command). */ ContextualMenuInternal.prototype._isAltOrMeta = function (ev) { // eslint-disable-next-line deprecation/deprecation return ev.which === KeyCodes.alt || ev.key === 'Meta'; }; ContextualMenuInternal.prototype._shouldIgnoreMouseEvent = function () { return !this._isScrollIdle || !this.props.hoisted.gotMouseMove.current; }; /** * Handles updating focus when mouseEnter or mouseMove fire. * As part of updating focus, This function will also update * the expand/collapse state accordingly. */ ContextualMenuInternal.prototype._updateFocusOnMouseEvent = function (item, ev, target) { var _this = this; var targetElement = target ? target : ev.currentTarget; var _a = this.props, _b = _a.subMenuHoverDelay, timeoutDuration = _b === void 0 ? NavigationIdleDelay : _b, _c = _a.hoisted, expandedMenuItemKey = _c.expandedMenuItemKey, openSubMenu = _c.openSubMenu; if (item.key === expandedMenuItemKey) { return; } if (this._enterTimerId !== undefined) { this._async.clearTimeout(this._enterTimerId); this._enterTimerId = undefined; } // If the menu is not expanded we can update focus without any delay if (expandedMenuItemKey === undefined) { targetElement.focus(); } // Delay updating expanding/dismissing the submenu // and only set focus if we have not already done so if (hasSubmenu(item)) { ev.stopPropagation(); this._enterTimerId = this._async.setTimeout(function () { targetElement.focus(); openSubMenu(item, targetElement, true); _this._enterTimerId = undefined; }, timeoutDuration); } else { this._enterTimerId = this._async.setTimeout(function () { _this._onSubMenuDismiss(ev); targetElement.focus(); _this._enterTimerId = undefined; }, timeoutDuration); } }; ContextualMenuInternal.prototype._getSubmenuProps = function () { var _a = this.props.hoisted, submenuTarget = _a.submenuTarget, expandedMenuItemKey = _a.expandedMenuItemKey, expandedByMouseClick = _a.expandedByMouseClick; var item = this._findItemByKey(expandedMenuItemKey); var submenuProps = null; if (item) { submenuProps = { items: getSubmenuItems(item), target: submenuTarget, onDismiss: this._onSubMenuDismiss, isSubMenu: true, id: this.state.subMenuId, shouldFocusOnMount: true, shouldFocusOnContainer: expandedByMouseClick, directionalHint: getRTL(this.props.theme) ? DirectionalHint.leftTopEdge : DirectionalHint.rightTopEdge, className: this.props.className, gapSpace: 0, isBeakVisible: false, }; if (item.subMenuProps) { assign(submenuProps, item.subMenuProps); } } return submenuProps; }; ContextualMenuInternal.prototype._findItemByKey = function (key) { var items = this.props.items; return this._findItemByKeyFromItems(key, items); }; /** * Returns the item that matches a given key if any. * @param key - The key of the item to match * @param items - The items to look for the key */ ContextualMenuInternal.prototype._findItemByKeyFromItems = function (key, items) { for (var _i = 0, items_2 = items; _i < items_2.length; _i++) { var item = items_2[_i]; if (item.itemType === ContextualMenuItemType.Section && item.sectionProps) { var match = this._findItemByKeyFromItems(key, item.sectionProps.items); if (match) { return match; } } else if (item.key && item.key === key) { return item; } } }; return ContextualMenuInternal; }(React.Component)); //# sourceMappingURL=ContextualMenu.base.js.map