UNPKG

@fluentui/react

Version:

Reusable React components for building web experiences.

853 lines (852 loc) 52.5 kB
define(["require", "exports", "tslib", "react", "./ContextualMenu.types", "../../common/DirectionalHint", "../../FocusZone", "../../Utilities", "../../utilities/contextualMenu/index", "../../Callout", "./ContextualMenuItem", "./ContextualMenuItemWrapper/index", "../../Styling", "./ContextualMenu.classNames", "@fluentui/react-hooks", "../../ResponsiveMode", "../../utilities/MenuContext/index"], function (require, exports, tslib_1, React, ContextualMenu_types_1, DirectionalHint_1, FocusZone_1, Utilities_1, index_1, Callout_1, ContextualMenuItem_1, index_2, Styling_1, ContextualMenu_classNames_1, react_hooks_1, ResponsiveMode_1, index_3) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ContextualMenuBase = exports.canAnyMenuItemsCheck = exports.getSubmenuItems = void 0; var getClassNames = (0, Utilities_1.classNamesFunction)(); var getContextualMenuItemClassNames = (0, Utilities_1.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_1.DirectionalHint.bottomAutoEdge, beakWidth: 16, }; /* return number of menu items, excluding headers and dividers */ function getItemCount(items) { var totalItemCount = 0; for (var _i = 0, items_1 = items; _i < items_1.length; _i++) { var item = items_1[_i]; if (item.itemType !== ContextualMenu_types_1.ContextualMenuItemType.Divider && item.itemType !== ContextualMenu_types_1.ContextualMenuItemType.Header) { var itemCount = item.customOnRenderListLength ? item.customOnRenderListLength : 1; totalItemCount += itemCount; } } return totalItemCount; } function getSubmenuItems(item, options) { var target = options === null || options === void 0 ? void 0 : options.target; // eslint-disable-next-line deprecation/deprecation var items = item.subMenuProps ? item.subMenuProps.items : item.items; if (items) { var overrideItems = []; for (var _i = 0, items_2 = items; _i < items_2.length; _i++) { var subItem = items_2[_i]; if (subItem.preferMenuTargetAsEventTarget) { // For sub-items which need an overridden target, intercept `onClick` var onClick = subItem.onClick, contextItem = tslib_1.__rest(subItem, ["onClick"]); overrideItems.push(tslib_1.__assign(tslib_1.__assign({}, contextItem), { onClick: getOnClickWithOverrideTarget(onClick, target) })); } else { overrideItems.push(subItem); } } return overrideItems; } } exports.getSubmenuItems = getSubmenuItems; /** * Returns true if a list of menu items can contain a checkbox */ 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; }); } exports.canAnyMenuItemsCheck = canAnyMenuItemsCheck; var NavigationIdleDelay = 250; /* ms */ var COMPONENT_NAME = 'ContextualMenu'; var _getMenuItemStylesFunction = (0, Utilities_1.memoizeFunction)(function () { var styles = []; for (var _i = 0; _i < arguments.length; _i++) { styles[_i] = arguments[_i]; } return function (styleProps) { return Styling_1.concatStyleSetsWithProps.apply(void 0, tslib_1.__spreadArray([styleProps, ContextualMenu_classNames_1.getItemStyles], styles, false)); }; }); //#region Custom hooks function useVisibility(props, targetWindow) { var _a = props.hidden, hidden = _a === void 0 ? false : _a, onMenuDismissed = props.onMenuDismissed, onMenuOpened = props.onMenuOpened; var previousHidden = (0, react_hooks_1.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, dismiss) { var hidden = _a.hidden, items = _a.items, theme = _a.theme, className = _a.className, id = _a.id, menuTarget = _a.target; 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(), shouldFocusOnContainer = _d[0], setShouldFocusOnContainer = _d[1]; var subMenuId = (0, react_hooks_1.useId)(COMPONENT_NAME, id); var closeSubMenu = React.useCallback(function () { setShouldFocusOnContainer(undefined); setExpandedMenuItemKey(undefined); setSubmenuTarget(undefined); }, []); var openSubMenu = React.useCallback(function (_a, target, focusContainer) { var submenuItemKey = _a.key; if (expandedMenuItemKey === submenuItemKey) { return; } target.focus(); setShouldFocusOnContainer(focusContainer); setExpandedMenuItemKey(submenuItemKey); setSubmenuTarget(target); }, [expandedMenuItemKey]); React.useEffect(function () { if (hidden) { closeSubMenu(); } }, [hidden, closeSubMenu]); var onSubMenuDismiss = useOnSubmenuDismiss(dismiss, closeSubMenu); var getSubmenuProps = function () { var item = findItemByKeyFromItems(expandedMenuItemKey, items); var submenuProps = null; if (item) { submenuProps = { items: getSubmenuItems(item, { target: menuTarget }), target: submenuTarget, onDismiss: onSubMenuDismiss, isSubMenu: true, id: subMenuId, shouldFocusOnMount: true, shouldFocusOnContainer: shouldFocusOnContainer, directionalHint: (0, Utilities_1.getRTL)(theme) ? DirectionalHint_1.DirectionalHint.leftTopEdge : DirectionalHint_1.DirectionalHint.rightTopEdge, className: className, gapSpace: 0, isBeakVisible: false, }; if (item.subMenuProps) { (0, Utilities_1.assign)(submenuProps, item.subMenuProps); } if (item.preferMenuTargetAsEventTarget) { var onItemClick = item.onItemClick; submenuProps.onItemClick = getOnClickWithOverrideTarget(onItemClick, menuTarget); } } return submenuProps; }; return [expandedMenuItemKey, openSubMenu, getSubmenuProps, onSubMenuDismiss]; } 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 = false; } }, [delayUpdateFocusOnHover]); return [shouldUpdateFocusOnMouseEvent, gotMouseMove, onMenuFocusCapture]; } function usePreviousActiveElement(_a, targetWindow, hostElement) { var hidden = _a.hidden, onRestoreFocus = _a.onRestoreFocus; var previousActiveElement = React.useRef(); var tryFocusPreviousActiveElement = React.useCallback(function (options) { var _a, _b; if (onRestoreFocus) { onRestoreFocus(options); } else if (options === null || options === void 0 ? void 0 : options.documentContainsFocus) { // 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 = previousActiveElement.current) === null || _a === void 0 ? void 0 : _a.focus) === null || _b === void 0 ? void 0 : _b.call(_a); } }, [onRestoreFocus]); (0, react_hooks_1.useIsomorphicLayoutEffect)(function () { var _a, _b; if (!hidden) { var newElement = targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.document.activeElement; if (!((_a = hostElement.current) === null || _a === void 0 ? void 0 : _a.contains(newElement)) && newElement.tagName !== 'BODY') { previousActiveElement.current = newElement; } } else if (previousActiveElement.current) { tryFocusPreviousActiveElement({ originalElement: previousActiveElement.current, containsFocus: true, documentContainsFocus: ((_b = (0, Utilities_1.getDocument)()) === null || _b === void 0 ? void 0 : _b.hasFocus()) || false, }); previousActiveElement.current = undefined; } }, [hidden, targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.document.activeElement, tryFocusPreviousActiveElement, hostElement]); return [tryFocusPreviousActiveElement]; } function useKeyHandlers(_a, dismiss, hostElement, openSubMenu) { var theme = _a.theme, isSubMenu = _a.isSubMenu, _b = _a.focusZoneProps, _c = _b === void 0 ? {} : _b, checkForNoWrap = _c.checkForNoWrap, _d = _c.direction, focusZoneDirection = _d === void 0 ? FocusZone_1.FocusZoneDirection.vertical : _d; /** True if the most recent keydown event was for alt (option) or meta (command). */ var lastKeyDownWasAltOrMeta = React.useRef(); /** * 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. */ var keyHandler = function (ev, shouldHandleKey, dismissAllMenus) { var handled = false; if (shouldHandleKey(ev)) { dismiss(ev, dismissAllMenus); ev.preventDefault(); ev.stopPropagation(); handled = true; } return handled; }; /** * Checks if the submenu should be closed */ var shouldCloseSubMenu = function (ev) { var submenuCloseKey = (0, Utilities_1.getRTL)(theme) ? Utilities_1.KeyCodes.right : Utilities_1.KeyCodes.left; // eslint-disable-next-line deprecation/deprecation if (ev.which !== submenuCloseKey || !isSubMenu) { return false; } return !!(focusZoneDirection === FocusZone_1.FocusZoneDirection.vertical || (checkForNoWrap && !(0, Utilities_1.shouldWrapFocus)(ev.target, 'data-no-horizontal-wrap'))); }; var shouldHandleKeyDown = function (ev) { return ( // eslint-disable-next-line deprecation/deprecation ev.which === Utilities_1.KeyCodes.escape || shouldCloseSubMenu(ev) || // eslint-disable-next-line deprecation/deprecation (ev.which === Utilities_1.KeyCodes.up && (ev.altKey || ev.metaKey))); }; var onKeyDown = function (ev) { // Take note if we are processing an alt (option) or meta (command) keydown. // See comment in shouldHandleKeyUp for reasoning. lastKeyDownWasAltOrMeta.current = isAltOrMeta(ev); // On Mac, pressing escape dismisses all levels of native context menus // eslint-disable-next-line deprecation/deprecation var dismissAllMenus = ev.which === Utilities_1.KeyCodes.escape && ((0, Utilities_1.isMac)() || (0, Utilities_1.isIOS)()); return keyHandler(ev, shouldHandleKeyDown, 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. */ var shouldHandleKeyUp = function (ev) { var keyPressIsAltOrMetaAlone = lastKeyDownWasAltOrMeta.current && isAltOrMeta(ev); lastKeyDownWasAltOrMeta.current = false; return !!keyPressIsAltOrMetaAlone && !((0, Utilities_1.isIOS)() || (0, Utilities_1.isMac)()); }; var onKeyUp = function (ev) { return keyHandler(ev, shouldHandleKeyUp, true /* dismissAllMenus */); }; var 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 = onKeyDown(ev); 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 === Utilities_1.KeyCodes.up; // eslint-disable-next-line deprecation/deprecation var isDown = ev.which === Utilities_1.KeyCodes.down; if (!hasModifier && (isUp || isDown)) { var elementToFocus = isUp ? (0, Utilities_1.getLastFocusable)(hostElement.current, hostElement.current.lastChild, true) : (0, Utilities_1.getFirstFocusable)(hostElement.current, hostElement.current.firstChild, true); if (elementToFocus) { elementToFocus.focus(); ev.preventDefault(); ev.stopPropagation(); } } }; var onItemKeyDown = function (item, ev) { var openKey = (0, Utilities_1.getRTL)(theme) ? Utilities_1.KeyCodes.left : Utilities_1.KeyCodes.right; if (!item.disabled && // eslint-disable-next-line deprecation/deprecation (ev.which === openKey || ev.which === Utilities_1.KeyCodes.enter || (ev.which === Utilities_1.KeyCodes.down && (ev.altKey || ev.metaKey)))) { openSubMenu(item, ev.currentTarget); ev.preventDefault(); } }; return [onKeyDown, onKeyUp, onMenuKeyDown, onItemKeyDown]; } function useScrollHandler(asyncTracker) { var isScrollIdle = React.useRef(true); var scrollIdleTimeoutId = React.useRef(); /** * Scroll handler for the callout to make sure the mouse events * for updating focus are not interacting during scroll */ var onScroll = function () { if (!isScrollIdle.current && scrollIdleTimeoutId.current !== undefined) { asyncTracker.clearTimeout(scrollIdleTimeoutId.current); scrollIdleTimeoutId.current = undefined; } else { isScrollIdle.current = false; } scrollIdleTimeoutId.current = asyncTracker.setTimeout(function () { isScrollIdle.current = true; }, NavigationIdleDelay); }; return [onScroll, isScrollIdle]; } function useOnSubmenuDismiss(dismiss, closeSubMenu) { var isMountedRef = React.useRef(false); React.useEffect(function () { isMountedRef.current = true; return function () { isMountedRef.current = false; }; }, []); /** * This function is called ASYNCHRONOUSLY, and so there is a chance it is called * after the component is unmounted. The isMountedRef is added to prevent * from calling setState() after unmount. Do NOT copy this pattern in synchronous * code. */ var onSubMenuDismiss = function (ev, dismissAll) { if (dismissAll) { dismiss(ev, dismissAll); } else if (isMountedRef.current) { closeSubMenu(); } }; return onSubMenuDismiss; } function useSubmenuEnterTimer(_a, asyncTracker) { var _b = _a.subMenuHoverDelay, subMenuHoverDelay = _b === void 0 ? NavigationIdleDelay : _b; var enterTimerRef = React.useRef(undefined); var cancelSubMenuTimer = function () { if (enterTimerRef.current !== undefined) { asyncTracker.clearTimeout(enterTimerRef.current); enterTimerRef.current = undefined; } }; var startSubmenuTimer = function (onTimerExpired) { enterTimerRef.current = asyncTracker.setTimeout(function () { onTimerExpired(); cancelSubMenuTimer(); }, subMenuHoverDelay); }; return [cancelSubMenuTimer, startSubmenuTimer, enterTimerRef]; } function useMouseHandlers(props, isScrollIdle, subMenuEntryTimer, targetWindow, shouldUpdateFocusOnMouseEvent, gotMouseMove, expandedMenuItemKey, hostElement, startSubmenuTimer, cancelSubMenuTimer, openSubMenu, onSubMenuDismiss, dismiss) { var menuTarget = props.target; var onItemMouseEnterBase = function (item, ev, target) { if (shouldUpdateFocusOnMouseEvent.current) { gotMouseMove.current = true; } if (shouldIgnoreMouseEvent()) { return; } updateFocusOnMouseEvent(item, ev, target); }; var onItemMouseMoveBase = function (item, ev, target) { var targetElement = ev.currentTarget; // 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 (!isScrollIdle.current || subMenuEntryTimer.current !== undefined || targetElement === (targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.document.activeElement)) { return; } updateFocusOnMouseEvent(item, ev, target); }; var shouldIgnoreMouseEvent = function () { return !isScrollIdle.current || !gotMouseMove.current; }; var onMouseItemLeave = function (item, ev) { var _a; if (shouldIgnoreMouseEvent()) { return; } cancelSubMenuTimer(); 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(); } }; /** * Handles updating focus when mouseEnter or mouseMove fire. * As part of updating focus, This function will also update * the expand/collapse state accordingly. */ var updateFocusOnMouseEvent = function (item, ev, target) { var targetElement = target ? target : ev.currentTarget; if (item.key === expandedMenuItemKey) { return; } cancelSubMenuTimer(); // 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 ((0, index_1.hasSubmenu)(item)) { ev.stopPropagation(); startSubmenuTimer(function () { targetElement.focus(); openSubMenu(item, targetElement, true); }); } else { startSubmenuTimer(function () { onSubMenuDismiss(ev); targetElement.focus(); }); } }; var onItemClick = function (item, ev) { onItemClickBase(item, ev, ev.currentTarget); }; var onItemClickBase = function (item, ev, target) { var items = getSubmenuItems(item, { target: menuTarget }); // Cancel an async menu item hover timeout action from being taken and instead // just trigger the click event instead. cancelSubMenuTimer(); if (!(0, index_1.hasSubmenu)(item) && (!items || !items.length)) { // This is an item without a menu. Click it. executeItemClick(item, ev); } else { if (item.key !== expandedMenuItemKey) { // This has a collapsed sub menu. Expand it. // focus on the container by default when the menu is opened with a click event // this differentiates from a keyboard interaction triggering the click event var shouldFocusOnContainer = typeof props.shouldFocusOnContainer === 'boolean' ? props.shouldFocusOnContainer : ev.nativeEvent.pointerType === 'mouse'; openSubMenu(item, target, shouldFocusOnContainer); } } ev.stopPropagation(); ev.preventDefault(); }; var onAnchorClick = function (item, ev) { executeItemClick(item, ev); ev.stopPropagation(); }; var executeItemClick = function (item, ev) { if (item.disabled || item.isDisabled) { return; } if (item.preferMenuTargetAsEventTarget) { overrideTarget(ev, menuTarget); } var shouldDismiss = false; if (item.onClick) { shouldDismiss = !!item.onClick(ev, item); } else if (props.onItemClick) { shouldDismiss = !!props.onItemClick(ev, item); } if (shouldDismiss || !ev.defaultPrevented) { dismiss(ev, true); } }; return [ onItemMouseEnterBase, onItemMouseMoveBase, onMouseItemLeave, onItemClick, onAnchorClick, executeItemClick, onItemClickBase, ]; } //#endregion exports.ContextualMenuBase = React.memo(React.forwardRef(function (propsWithoutDefaults, forwardedRef) { var _a; var _b = (0, Utilities_1.getPropsWithDefaults)(DEFAULT_PROPS, propsWithoutDefaults), ref = _b.ref, props = tslib_1.__rest(_b, ["ref"]); var hostElement = React.useRef(null); var asyncTracker = (0, react_hooks_1.useAsync)(); var menuId = (0, react_hooks_1.useId)(COMPONENT_NAME, props.id); (0, react_hooks_1.useWarnings)({ name: COMPONENT_NAME, props: props, deprecations: { getMenuClassNames: 'styles', }, }); var dismiss = function (ev, dismissAll) { var _a; return (_a = props.onDismiss) === null || _a === void 0 ? void 0 : _a.call(props, ev, dismissAll); }; var _c = (0, react_hooks_1.useTarget)(props.target, hostElement), targetRef = _c[0], targetWindow = _c[1]; var tryFocusPreviousActiveElement = usePreviousActiveElement(props, targetWindow, hostElement)[0]; var _d = useSubMenuState(props, dismiss), expandedMenuItemKey = _d[0], openSubMenu = _d[1], getSubmenuProps = _d[2], onSubMenuDismiss = _d[3]; var _e = useShouldUpdateFocusOnMouseMove(props), shouldUpdateFocusOnMouseEvent = _e[0], gotMouseMove = _e[1], onMenuFocusCapture = _e[2]; var _f = useScrollHandler(asyncTracker), onScroll = _f[0], isScrollIdle = _f[1]; var _g = useSubmenuEnterTimer(props, asyncTracker), cancelSubMenuTimer = _g[0], startSubmenuTimer = _g[1], subMenuEntryTimer = _g[2]; var responsiveMode = (0, ResponsiveMode_1.useResponsiveMode)(hostElement, props.responsiveMode); useVisibility(props, targetWindow); var _h = useKeyHandlers(props, dismiss, hostElement, openSubMenu), onKeyDown = _h[0], onKeyUp = _h[1], onMenuKeyDown = _h[2], onItemKeyDown = _h[3]; var _j = useMouseHandlers(props, isScrollIdle, subMenuEntryTimer, targetWindow, shouldUpdateFocusOnMouseEvent, gotMouseMove, expandedMenuItemKey, hostElement, startSubmenuTimer, cancelSubMenuTimer, openSubMenu, onSubMenuDismiss, dismiss), onItemMouseEnterBase = _j[0], onItemMouseMoveBase = _j[1], onMouseItemLeave = _j[2], onItemClick = _j[3], onAnchorClick = _j[4], executeItemClick = _j[5], onItemClickBase = _j[6]; //#region Render helpers var onDefaultRenderMenuList = function (menuListProps, // eslint-disable-next-line deprecation/deprecation menuClassNames, defaultRender) { var indexCorrection = 0; var items = menuListProps.items, totalItemCount = menuListProps.totalItemCount, hasCheckmarks = menuListProps.hasCheckmarks, hasIcons = menuListProps.hasIcons; return (React.createElement("ul", { className: menuClassNames.list, onKeyDown: onKeyDown, onKeyUp: onKeyUp, role: 'presentation' }, items.map(function (item, index) { var menuItem = renderMenuItem(item, index, indexCorrection, totalItemCount, hasCheckmarks, hasIcons, menuClassNames); if (item.itemType !== ContextualMenu_types_1.ContextualMenuItemType.Divider && item.itemType !== ContextualMenu_types_1.ContextualMenuItemType.Header) { var indexIncrease = item.customOnRenderListLength ? item.customOnRenderListLength : 1; indexCorrection += indexIncrease; } return menuItem; }))); }; var renderFocusZone = function (children, adjustedFocusZoneProps) { var _a = props.focusZoneAs, ChildrenRenderer = _a === void 0 ? FocusZone_1.FocusZone : _a; return React.createElement(ChildrenRenderer, tslib_1.__assign({}, adjustedFocusZoneProps), children); }; /** * !!!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. */ var renderMenuItem = function (item, index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons, // eslint-disable-next-line deprecation/deprecation menuClassNames) { 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; // 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 === ContextualMenu_types_1.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(props.theme, (0, index_1.isItemDisabled)(item), expandedMenuItemKey === item.key, !!(0, index_1.getIsChecked)(item), !!item.href, iconProps.iconName !== 'None', item.className, dividerClassName, iconProps.className, subMenuIconClassName, item.primaryDisabled); } else { var itemStyleProps = { theme: props.theme, disabled: (0, index_1.isItemDisabled)(item), expanded: expandedMenuItemKey === item.key, checked: !!(0, index_1.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 = menuClassNames.subComponentStyles) === null || _a === void 0 ? void 0 : _a.menuItem, styles), itemStyleProps); } // eslint-disable-next-line deprecation/deprecation if (item.text === '-' || item.name === '-') { item.itemType = ContextualMenu_types_1.ContextualMenuItemType.Divider; } switch (item.itemType) { case ContextualMenu_types_1.ContextualMenuItemType.Divider: renderedItems.push(renderSeparator(index, itemClassNames)); break; case ContextualMenu_types_1.ContextualMenuItemType.Header: renderedItems.push(renderSeparator(index, itemClassNames)); var headerItem = renderHeaderMenuItem(item, itemClassNames, menuClassNames, index, hasCheckmarks, hasIcons); renderedItems.push(renderListItem(headerItem, item.key || index, itemClassNames, item.title)); break; case ContextualMenu_types_1.ContextualMenuItemType.Section: renderedItems.push(renderSectionItem(item, itemClassNames, menuClassNames, index, hasCheckmarks, hasIcons)); break; default: var defaultRenderNormalItem = function () { return renderNormalItem(item, itemClassNames, index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons); }; var menuItem = props.onRenderContextualMenuItem ? props.onRenderContextualMenuItem(item, defaultRenderNormalItem) : defaultRenderNormalItem(); renderedItems.push(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); }; var defaultMenuItemRenderer = function (item, // eslint-disable-next-line deprecation/deprecation menuClassNames) { var index = item.index, focusableElementIndex = item.focusableElementIndex, totalItemCount = item.totalItemCount, hasCheckmarks = item.hasCheckmarks, hasIcons = item.hasIcons; return renderMenuItem(item, index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons, menuClassNames); }; var renderSectionItem = function (sectionItem, // eslint-disable-next-line deprecation/deprecation itemClassNames, // eslint-disable-next-line deprecation/deprecation menuClassNames, index, hasCheckmarks, hasIcons) { var sectionProps = sectionItem.sectionProps; if (!sectionProps) { return; } var headerItem; var groupProps; if (sectionProps.title) { var headerContextualMenuItem = undefined; var ariaLabelledby = ''; if (typeof sectionProps.title === 'string') { // Since title is a user-facing string, it needs to be stripped // of whitespace in order to build a valid element ID var id_1 = menuId + sectionProps.title.replace(/\s/g, ''); headerContextualMenuItem = { key: "section-".concat(sectionProps.title, "-title"), itemType: ContextualMenu_types_1.ContextualMenuItemType.Header, text: sectionProps.title, id: id_1, }; ariaLabelledby = id_1; } else { var id_2 = sectionProps.title.id || menuId + sectionProps.title.key.replace(/\s/g, ''); headerContextualMenuItem = tslib_1.__assign(tslib_1.__assign({}, sectionProps.title), { id: id_2 }); ariaLabelledby = id_2; } if (headerContextualMenuItem) { groupProps = { role: 'group', 'aria-labelledby': ariaLabelledby, }; headerItem = renderHeaderMenuItem(headerContextualMenuItem, itemClassNames, menuClassNames, index, hasCheckmarks, hasIcons); } } if (sectionProps.items && sectionProps.items.length > 0) { var correctedIndex_1 = 0; return (React.createElement("li", { role: "presentation", key: sectionProps.key || sectionItem.key || "section-".concat(index) }, React.createElement("div", tslib_1.__assign({}, groupProps), React.createElement("ul", { className: menuClassNames.list, role: "presentation" }, sectionProps.topDivider && renderSeparator(index, itemClassNames, true, true), headerItem && renderListItem(headerItem, sectionItem.key || index, itemClassNames, sectionItem.title), sectionProps.items.map(function (contextualMenuItem, itemsIndex) { var menuItem = renderMenuItem(contextualMenuItem, itemsIndex, correctedIndex_1, getItemCount(sectionProps.items), hasCheckmarks, hasIcons, menuClassNames); if (contextualMenuItem.itemType !== ContextualMenu_types_1.ContextualMenuItemType.Divider && contextualMenuItem.itemType !== ContextualMenu_types_1.ContextualMenuItemType.Header) { var indexIncrease = contextualMenuItem.customOnRenderListLength ? contextualMenuItem.customOnRenderListLength : 1; correctedIndex_1 += indexIncrease; } return menuItem; }), sectionProps.bottomDivider && renderSeparator(index, itemClassNames, false, true))))); } }; var 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)); }; var 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; }; var renderNormalItem = function (item, classNames, // eslint-disable-line deprecation/deprecation index, focusableElementIndex, totalItemCount, hasCheckmarks, hasIcons) { if (item.onRender) { return item.onRender(tslib_1.__assign({ 'aria-posinset': focusableElementIndex + 1, 'aria-setsize': totalItemCount }, item), dismiss); } var contextualMenuItemAs = props.contextualMenuItemAs; var commonProps = { item: item, classNames: classNames, index: index, focusableElementIndex: focusableElementIndex, totalItemCount: totalItemCount, hasCheckmarks: hasCheckmarks, hasIcons: hasIcons, contextualMenuItemAs: contextualMenuItemAs, onItemMouseEnter: onItemMouseEnterBase, onItemMouseLeave: onMouseItemLeave, onItemMouseMove: onItemMouseMoveBase, onItemMouseDown: onItemMouseDown, executeItemClick: executeItemClick, onItemKeyDown: onItemKeyDown, expandedMenuItemKey: expandedMenuItemKey, openSubMenu: openSubMenu, dismissSubMenu: onSubMenuDismiss, dismissMenu: dismiss, }; if (item.href) { var ContextualMenuAnchorAs = index_2.ContextualMenuAnchor; if (item.contextualMenuItemWrapperAs) { ContextualMenuAnchorAs = (0, Utilities_1.composeComponentAs)(item.contextualMenuItemWrapperAs, ContextualMenuAnchorAs); } return React.createElement(ContextualMenuAnchorAs, tslib_1.__assign({}, commonProps, { onItemClick: onAnchorClick })); } if (item.split && (0, index_1.hasSubmenu)(item)) { var ContextualMenuSplitButtonAs = index_2.ContextualMenuSplitButton; if (item.contextualMenuItemWrapperAs) { ContextualMenuSplitButtonAs = (0, Utilities_1.composeComponentAs)(item.contextualMenuItemWrapperAs, ContextualMenuSplitButtonAs); } return (React.createElement(ContextualMenuSplitButtonAs, tslib_1.__assign({}, commonProps, { onItemClick: onItemClick, onItemClickBase: onItemClickBase, onTap: cancelSubMenuTimer }))); } var ContextualMenuButtonAs = index_2.ContextualMenuButton; if (item.contextualMenuItemWrapperAs) { ContextualMenuButtonAs = (0, Utilities_1.composeComponentAs)(item.contextualMenuItemWrapperAs, ContextualMenuButtonAs); } return React.createElement(ContextualMenuButtonAs, tslib_1.__assign({}, commonProps, { onItemClick: onItemClick, onItemClickBase: onItemClickBase })); }; var renderHeaderMenuItem = function (item, // eslint-disable-next-line deprecation/deprecation itemClassNames, // eslint-disable-next-line deprecation/deprecation menuClassNames, index, hasCheckmarks, hasIcons) { var ChildrenRenderer = ContextualMenuItem_1.ContextualMenuItem; if (item.contextualMenuItemAs) { ChildrenRenderer = (0, Utilities_1.composeComponentAs)(item.contextualMenuItemAs, ChildrenRenderer); } if (props.contextualMenuItemAs) { ChildrenRenderer = (0, Utilities_1.composeComponentAs)(props.contextualMenuItemAs, ChildrenRenderer); } var itemProps = item.itemProps, id = item.id; var divHtmlProperties = itemProps && (0, Utilities_1.getNativeProps)(itemProps, Utilities_1.divProperties); return ( // eslint-disable-next-line deprecation/deprecation React.createElement("div", tslib_1.__assign({ id: id, className: menuClassNames.header }, divHtmlProperties, { style: item.style }), React.createElement(ChildrenRenderer, tslib_1.__assign({ item: item, classNames: itemClassNames, index: index, onCheckmarkClick: hasCheckmarks ? onItemClick : undefined, hasIcons: hasIcons }, itemProps)))); }; //#endregion //#region Main render var isBeakVisible = props.isBeakVisible; var items = props.items, labelElementId = props.labelElementId, id = props.id, className = props.className, beakWidth = props.beakWidth, directionalHint = props.directionalHint, directionalHintForRTL = props.directionalHintForRTL, alignTargetEdge = props.alignTargetEdge, gapSpace = props.gapSpace, coverTarget = props.coverTarget, ariaLabel = props.ariaLabel, doNotLayer = props.doNotLayer, target = props.target, bounds = props.bounds, useTargetWidth = props.useTargetWidth, useTargetAsMinWidth = props.useTargetAsMinWidth, directionalHintFixed = props.directionalHintFixed, shouldFocusOnMount = props.shouldFocusOnMount, shouldFocusOnContainer = props.shouldFocusOnContainer, title = props.title, styles = props.styles, theme = props.theme, calloutProps = props.calloutProps, _k = props.onRenderSubMenu, onRenderSubMenu = _k === void 0 ? onDefaultRenderSubMenu : _k, _l = props.onRenderMenuList, onRenderMenuList = _l === void 0 ? function (menuListProps, defaultRender) { return onDefaultRenderMenuList(menuListProps, classNames, defaultRender); } : _l, focusZoneProps = props.focusZoneProps, // eslint-disable-next-line deprecation/deprecation getMenuClassNames = props.getMenuClassNames; var 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 === ContextualMenu_types_1.ContextualMenuItemType.Section && item.sectionProps && itemsHaveIcons(item.sectionProps.items)) { return true; } } return false; } var adjustedFocusZoneProps = tslib_1.__assign(tslib_1.__assign({ direction: FocusZone_1.FocusZoneDirection.vertical, handleTabKey: FocusZone_1.FocusZoneTabbableElements.all, isCircularNavigation: true, 'data-tabster': '{"uncontrolled": {}, "focusable": { "excludeFromMover": true }}' }, focusZoneProps), { className: (0, Utilities_1.css)(classNames.root, (_a = props.focusZoneProps) === null || _a === void 0 ? void 0 : _a.className) }); var hasCheckmarks = canAnyMenuItemsCheck(items); var submenuProps = expandedMenuItemKey && props.hidden !== true ? getSubmenuProps() : null; isBeakVisible = isBeakVisible === undefined ? responsiveMode <= ResponsiveMode_1.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 = getItemCount(items); var calloutStyles_1 = classNames.subComponentStyles ? classNames.subComponentStyles.callout : undefined; return (React.createElement(index_3.MenuContext.Consumer, null, function (menuContext) { return (React.createElement(Callout_1.Callout, tslib_1.__assign({ styles: calloutStyles_1, onRestoreFocus: tryFocusPreviousActiveElement }, calloutProps, { target: target || menuContext.target, isBeakVisible: isBeakVisible, beakWidth: beakWidth, directionalHint: directionalHint, directionalHintForRTL: directionalHintForRTL, gapSpace: gapSpace, coverTarget: coverTarget, doNotLayer: doNotLayer, className: (0, Utilities_1.css)('ms-ContextualMenu-Callout', calloutProps && calloutProps.className), setInitialFocus: shouldFocusOnMount, onDismiss: props.onDismiss || menuContext.onDismiss, onScroll: onScroll, bounds: bounds, directionalHintFixed: directionalHintFixed, alignTargetEdge: alignTargetEdge, hidden: props.hidden || menuContext.hidden, ref: forwardedRef }), React.createElement("div", { style: contextMenuStyle, ref: hostElement, id: id, className: classNames.container, tabIndex: shouldFocusOnContainer ? 0 : -1, onKeyDown: onMenuKeyDown, onKeyUp: onKeyUp, onFocusCapture: onMenuFocusCapture, "aria-label": ariaLabel, "aria-labelledby": labelElementId, role: 'menu' }, title && React.createElement("div", { className: classNames.title }, " ", title, " "), items && items.length ? renderFocusZone(onRenderMenuList({ ariaLabel: ariaLabel, items: items, totalItemCount: totalItemCount_1, hasCheckmarks: hasCheckmarks, hasIcons: hasIcons, defaultMenuItemRenderer: function (item) { return defaultMenuItemRenderer(item, classNames); }, labelElementId: labelElementId, }, function (menuListProps, defaultRender) { return onDefaultRenderMenuList(menuListProps, classNames, defaultRender); }), adjustedFocusZoneProps) : null, submenuProps && onRenderSubMenu(submenuProps, onDefaultRenderSubMenu)), React.createElement(Utilities_1.FocusRects, null))); })); }