function useMouseHandlers()

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;
}