in packages/react/src/components/ContextualMenu/ContextualMenu.base.tsx [494:690]
function useMouseHandlers(
props: IContextualMenuProps,
isScrollIdle: React.MutableRefObject<boolean>,
subMenuEntryTimer: React.RefObject<number | undefined>,
targetWindow: Window | undefined,
shouldUpdateFocusOnMouseEvent: React.MutableRefObject<boolean>,
gotMouseMove: React.MutableRefObject<boolean>,
expandedMenuItemKey: string | undefined,
hostElement: React.RefObject<HTMLDivElement>,
startSubmenuTimer: (onTimerExpired: () => void) => void,
cancelSubMenuTimer: () => void,
openSubMenu: (submenuItemKey: IContextualMenuItem, target: HTMLElement, openedByMouseClick?: boolean) => void,
onSubMenuDismiss: (ev?: any, dismissAll?: boolean) => void,
dismiss: (ev?: any, dismissAll?: boolean) => void,
) {
const { target: menuTarget } = props;
const onItemMouseEnterBase = (item: any, ev: React.MouseEvent<HTMLElement>, target?: HTMLElement): void => {
if (shouldIgnoreMouseEvent()) {
return;
}
updateFocusOnMouseEvent(item, ev, target);
};
const onItemMouseMoveBase = (item: any, ev: React.MouseEvent<HTMLElement>, target: HTMLElement): void => {
const targetElement = ev.currentTarget as HTMLElement;
// 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?.document.activeElement as HTMLElement)
) {
return;
}
updateFocusOnMouseEvent(item, ev, target);
};
const shouldIgnoreMouseEvent = (): boolean => {
return !isScrollIdle.current || !gotMouseMove.current;
};
const onMouseItemLeave = (item: any, ev: React.MouseEvent<HTMLElement>): void => {
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 as any).setActive) {
try {
(hostElement.current as any).setActive();
} catch (e) {
/* no-op */
}
} else {
hostElement.current?.focus();
}
};
/**
* Handles updating focus when mouseEnter or mouseMove fire.
* As part of updating focus, This function will also update
* the expand/collapse state accordingly.
*/
const updateFocusOnMouseEvent = (
item: IContextualMenuItem,
ev: React.MouseEvent<HTMLElement>,
target?: HTMLElement,
) => {
const targetElement = target ? target : (ev.currentTarget as HTMLElement);
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(() => {
targetElement.focus();
openSubMenu(item, targetElement, true);
});
} else {
startSubmenuTimer(() => {
onSubMenuDismiss(ev);
targetElement.focus();
});
}
};
const onItemClick = (
item: IContextualMenuItem,
ev: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
): void => {
onItemClickBase(item, ev, ev.currentTarget as HTMLElement);
};
const onItemClickBase = (
item: IContextualMenuItem,
ev: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
target: HTMLElement,
): void => {
const 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.
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 as PointerEvent).pointerType === 'mouse',
);
}
}
ev.stopPropagation();
ev.preventDefault();
};
const onAnchorClick = (item: IContextualMenuItem, ev: React.MouseEvent<HTMLElement>) => {
executeItemClick(item, ev);
ev.stopPropagation();
};
const executeItemClick = (
item: IContextualMenuItem,
ev: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
): void => {
if (item.disabled || item.isDisabled) {
return;
}
if (item.preferMenuTargetAsEventTarget) {
overrideTarget(ev, menuTarget);
}
let 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,
] as const;
}