@fluentui/react
Version:
Reusable React components for building web experiences.
927 lines • 47.7 kB
JavaScript
import { __assign, __rest, __spreadArray } 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, assign, classNamesFunction, css, getFirstFocusable, getLastFocusable, getRTL, KeyCodes, shouldWrapFocus, isIOS, isMac, memoizeFunction, getPropsWithDefaults, getDocument, FocusRects, composeComponentAs, } 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, useAsync, useWarnings, useId, useIsomorphicLayoutEffect, } 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,
};
/* 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 !== ContextualMenuItemType.Divider && item.itemType !== ContextualMenuItemType.Header) {
var itemCount = item.customOnRenderListLength ? item.customOnRenderListLength : 1;
totalItemCount += itemCount;
}
}
return totalItemCount;
}
export 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 = __rest(subItem, ["onClick"]);
overrideItems.push(__assign(__assign({}, contextItem), { onClick: getOnClickWithOverrideTarget(onClick, target) }));
}
else {
overrideItems.push(subItem);
}
}
return overrideItems;
}
}
/**
* 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, __spreadArray([styleProps, 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 = 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 = 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: getRTL(theme) ? DirectionalHint.leftTopEdge : DirectionalHint.rightTopEdge,
className: className,
gapSpace: 0,
isBeakVisible: false,
};
if (item.subMenuProps) {
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]);
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 = 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 ? 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 = getRTL(theme) ? KeyCodes.right : KeyCodes.left;
// eslint-disable-next-line deprecation/deprecation
if (ev.which !== submenuCloseKey || !isSubMenu) {
return false;
}
return !!(focusZoneDirection === FocusZoneDirection.vertical ||
(checkForNoWrap && !shouldWrapFocus(ev.target, 'data-no-horizontal-wrap')));
};
var shouldHandleKeyDown = function (ev) {
return (
// eslint-disable-next-line deprecation/deprecation
ev.which === KeyCodes.escape ||
shouldCloseSubMenu(ev) ||
// eslint-disable-next-line deprecation/deprecation
(ev.which === 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 === KeyCodes.escape && (isMac() || 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 && !(isIOS() || 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 === 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();
}
}
};
var onItemKeyDown = function (item, ev) {
var openKey = getRTL(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)))) {
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 (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 (!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
export var ContextualMenuBase = React.memo(React.forwardRef(function (propsWithoutDefaults, forwardedRef) {
var _a;
var _b = getPropsWithDefaults(DEFAULT_PROPS, propsWithoutDefaults), ref = _b.ref, props = __rest(_b, ["ref"]);
var hostElement = React.useRef(null);
var asyncTracker = useAsync();
var menuId = useId(COMPONENT_NAME, props.id);
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 = 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 = 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 !== ContextualMenuItemType.Divider && item.itemType !== 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 : _a;
return React.createElement(ChildrenRenderer, __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 === 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, isItemDisabled(item), expandedMenuItemKey === item.key, !!getIsChecked(item), !!item.href, iconProps.iconName !== 'None', item.className, dividerClassName, iconProps.className, subMenuIconClassName, item.primaryDisabled);
}
else {
var itemStyleProps = {
theme: 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 = 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 = ContextualMenuItemType.Divider;
}
switch (item.itemType) {
case ContextualMenuItemType.Divider:
renderedItems.push(renderSeparator(index, itemClassNames));
break;
case 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 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: 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 = __assign(__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", __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 !== ContextualMenuItemType.Divider &&
contextualMenuItem.itemType !== 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(__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 = ContextualMenuAnchor;
if (item.contextualMenuItemWrapperAs) {
ContextualMenuAnchorAs = composeComponentAs(item.contextualMenuItemWrapperAs, ContextualMenuAnchorAs);
}
return React.createElement(ContextualMenuAnchorAs, __assign({}, commonProps, { onItemClick: onAnchorClick }));
}
if (item.split && hasSubmenu(item)) {
var ContextualMenuSplitButtonAs = ContextualMenuSplitButton;
if (item.contextualMenuItemWrapperAs) {
ContextualMenuSplitButtonAs = composeComponentAs(item.contextualMenuItemWrapperAs, ContextualMenuSplitButtonAs);
}
return (React.createElement(ContextualMenuSplitButtonAs, __assign({}, commonProps, { onItemClick: onItemClick, onItemClickBase: onItemClickBase, onTap: cancelSubMenuTimer })));
}
var ContextualMenuButtonAs = ContextualMenuButton;
if (item.contextualMenuItemWrapperAs) {
ContextualMenuButtonAs = composeComponentAs(item.contextualMenuItemWrapperAs, ContextualMenuButtonAs);
}
return React.createElement(ContextualMenuButtonAs, __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;
if (item.contextualMenuItemAs) {
ChildrenRenderer = composeComponentAs(item.contextualMenuItemAs, ChildrenRenderer);
}
if (props.contextualMenuItemAs) {
ChildrenRenderer = composeComponentAs(props.contextualMenuItemAs, ChildrenRenderer);
}
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: menuClassNames.header }, divHtmlProperties, { style: item.style }),
React.createElement(ChildrenRenderer, __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 === ContextualMenuItemType.Section &&
item.sectionProps &&
itemsHaveIcons(item.sectionProps.items)) {
return true;
}
}
return false;
}
var adjustedFocusZoneProps = __assign(__assign({ direction: FocusZoneDirection.vertical, handleTabKey: FocusZoneTabbableElements.all, isCircularNavigation: true, 'data-tabster': '{"uncontrolled": {}, "focusable": { "excludeFromMover": true }}' }, focusZoneProps), { className: 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.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(MenuContext.Consumer, null, function (menuContext) { return (React.createElement(Callout, __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: 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(FocusRects, null))); }));
}
else {
return null;
}
//#endregion
}), function (prevProps, newProps) {
if (!newProps.shouldUpdateWhenHidden && prevProps.hidden && newProps.hidden) {
// Do not update when hidden.
return true;
}
return shallowCompare(prevProps, newProps);
});
ContextualMenuBase.displayName = 'ContextualMenuBase';
/**
* Returns true if the key for the event is alt (Mac option) or meta (Mac command).
*/
function isAltOrMeta(ev) {
// eslint-disable-next-line deprecation/deprecation
return ev.which === KeyCodes.alt || ev.key === 'Meta';
}
function onItemMouseDown(item, ev) {
var _a;
(_a = item.onMouseDown) === null || _a === void 0 ? void 0 : _a.call(item, item, ev);
}
function onDefaultRenderSubMenu(subMenuProps, defaultRender) {
throw Error('ContextualMenuBase: onRenderSubMenu callback is null or undefined. ' +
'Please ensure to set `onRenderSubMenu` property either manually or with `styled` helper.');
}
/**
* 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
*/
function findItemByKeyFromItems(key, items) {
for (var _i = 0, items_3 = items; _i < items_3.length; _i++) {
var item = items_3[_i];
if (item.itemType === ContextualMenuItemType.Section && item.sectionProps) {
var match = findItemByKeyFromItems(key, item.sectionProps.items);
if (match) {
return match;
}
}
else if (item.key && item.key === key) {
return item;
}
}
}
function getOnClickWithOverrideTarget(onClick, target) {
return onClick
? function (ev, item) {
overrideTarget(ev, target);
return onClick(ev, item);
}
: onClick;
}
function overrideTarget(ev, target) {
if (ev && target) {
ev.persist();
if (target instanceof Event) {
ev.target = target.target;
}
else if (target instanceof Element) {
ev.target = target;
}
}
}
//# sourceMappingURL=ContextualMenu.base.js.map