'use client'; import { useARIAButtonProps } from '@fluentui/react-aria'; import { ArrowRight, ArrowLeft, Escape, ArrowDown } from '@fluentui/keyboard-keys'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { useFocusFinders } from '@fluentui/react-tabster'; import { applyTriggerPropsToChildren, getTriggerChild, getReactElementRef, isHTMLElement, mergeCallbacks, useEventCallback, useMergedRefs } from '@fluentui/react-utilities'; import * as React from 'react'; import { useMenuContext_unstable } from '../../contexts/menuContext'; import { useMenuListContext_unstable } from '../../contexts/menuListContext'; import { useIsSubmenu, useOnMenuSafeZoneTimeout } from '../../utils'; function noop() { // does nothing } /** * Create the state required to render MenuTrigger. * Clones the only child component and adds necessary event handling behaviours to open a popup menu * * @param props - props from this instance of MenuTrigger */ export const useMenuTrigger_unstable = (props)=>{ const { children, disableButtonEnhancement = false } = props; const triggerRef = useMenuContext_unstable((context)=>context.triggerRef); const menuPopoverRef = useMenuContext_unstable((context)=>context.menuPopoverRef); const setOpen = useMenuContext_unstable((context)=>context.setOpen); const open = useMenuContext_unstable((context)=>context.open); const triggerId = useMenuContext_unstable((context)=>context.triggerId); const openOnHover = useMenuContext_unstable((context)=>context.openOnHover); const openOnContext = useMenuContext_unstable((context)=>context.openOnContext); const isSubmenu = useIsSubmenu(); const shouldOpenOnArrowRight = useMenuListContext_unstable((ctx)=>{ var _ctx_shouldOpenOnArrowRight; return (_ctx_shouldOpenOnArrowRight = ctx.shouldOpenOnArrowRight) !== null && _ctx_shouldOpenOnArrowRight !== void 0 ? _ctx_shouldOpenOnArrowRight : true; }); const { findFirstFocusable } = useFocusFinders(); const focusFirst = React.useCallback(()=>{ const firstFocusable = findFirstFocusable(menuPopoverRef.current); firstFocusable === null || firstFocusable === void 0 ? void 0 : firstFocusable.focus(); }, [ findFirstFocusable, menuPopoverRef ]); const openedWithKeyboardRef = React.useRef(false); const openedViaSafeZoneRef = React.useRef(false); const hasMouseMovedRef = React.useRef(false); const { dir } = useFluent(); const OpenArrowKey = dir === 'ltr' ? ArrowRight : ArrowLeft; const child = getTriggerChild(children); // Heads up! // // Handles an edge case where mouse movement over the menu trigger didn't happen as safe zone blocked pointer events, // but the cursor is already over the menu trigger. const safeZoneHandlerRef = useOnMenuSafeZoneTimeout(useEventCallback(()=>{ if (isSubmenu) { openedViaSafeZoneRef.current = true; } })); const onContextMenu = (event)=>{ if (isTargetDisabled(event) || event.isDefaultPrevented()) { return; } if (openOnContext) { event.preventDefault(); setOpen(event, { open: true, keyboard: false, type: 'menuTriggerContextMenu', event }); } }; const onClick = (event)=>{ if (isTargetDisabled(event)) { return; } if (!openOnContext) { setOpen(event, { open: !open, keyboard: openedWithKeyboardRef.current, type: 'menuTriggerClick', event }); openedWithKeyboardRef.current = false; } }; const onKeyDown = (event)=>{ if (isTargetDisabled(event) || event.isDefaultPrevented()) { return; } const key = event.key; if (!openOnContext && (isSubmenu && shouldOpenOnArrowRight && key === OpenArrowKey || !isSubmenu && key === ArrowDown)) { setOpen(event, { open: true, keyboard: true, type: 'menuTriggerKeyDown', event }); } if (key === Escape && !isSubmenu) { setOpen(event, { open: false, keyboard: true, type: 'menuTriggerKeyDown', event }); } // if menu is already open, can't rely on effects to focus if (open && key === OpenArrowKey && isSubmenu && shouldOpenOnArrowRight) { focusFirst(); } }; const onMouseOver = (event)=>{ if (isTargetDisabled(event)) { return; } if (openOnHover) { if (hasMouseMovedRef.current) { setOpen(event, { open: true, keyboard: false, type: 'menuTriggerMouseEnter', event }); } else if (openedViaSafeZoneRef.current) { setOpen(event, { open: true, keyboard: false, ignoreHoverDelay: true, type: 'menuTriggerMouseEnter', event }); openedViaSafeZoneRef.current = false; } } }; // Opening a menu when a mouse hasn't moved and just entering the trigger is a bad a11y experience // First time open the mouse using mousemove and then continue with mouseenter // Only use once to determine that the user is using the mouse since it is an expensive event to handle const onMouseMove = (event)=>{ if (isTargetDisabled(event)) { return; } if (openOnHover && !hasMouseMovedRef.current) { setOpen(event, { open: true, keyboard: false, type: 'menuTriggerMouseMove', event }); hasMouseMovedRef.current = true; } }; const onMouseLeave = (event)=>{ if (isTargetDisabled(event)) { return; } if (openOnHover) { setOpen(event, { open: false, keyboard: false, type: 'menuTriggerMouseLeave', event }); } }; var _child_props_onMouseEnter; const contextMenuProps = { id: triggerId, ...child === null || child === void 0 ? void 0 : child.props, ref: useMergedRefs(triggerRef, getReactElementRef(child), safeZoneHandlerRef), onMouseEnter: useEventCallback((_child_props_onMouseEnter = child === null || child === void 0 ? void 0 : child.props.onMouseEnter) !== null && _child_props_onMouseEnter !== void 0 ? _child_props_onMouseEnter : noop), onMouseLeave: useEventCallback(mergeCallbacks(child === null || child === void 0 ? void 0 : child.props.onMouseLeave, onMouseLeave)), onContextMenu: useEventCallback(mergeCallbacks(child === null || child === void 0 ? void 0 : child.props.onContextMenu, onContextMenu)), onMouseMove: useEventCallback(mergeCallbacks(child === null || child === void 0 ? void 0 : child.props.onMouseMove, onMouseMove)), onMouseOver: useEventCallback(mergeCallbacks(child === null || child === void 0 ? void 0 : child.props.onMouseOver, onMouseOver)) }; const triggerChildProps = { 'aria-haspopup': 'menu', 'aria-expanded': !open && !isSubmenu ? undefined : open, ...contextMenuProps, onClick: useEventCallback(mergeCallbacks(child === null || child === void 0 ? void 0 : child.props.onClick, onClick)), onKeyDown: useEventCallback(mergeCallbacks(child === null || child === void 0 ? void 0 : child.props.onKeyDown, onKeyDown)) }; const ariaButtonTriggerChildProps = useARIAButtonProps((child === null || child === void 0 ? void 0 : child.type) === 'button' || (child === null || child === void 0 ? void 0 : child.type) === 'a' ? child.type : 'div', triggerChildProps); return { isSubmenu, children: applyTriggerPropsToChildren(children, openOnContext ? contextMenuProps : disableButtonEnhancement ? triggerChildProps : ariaButtonTriggerChildProps) }; }; const isTargetDisabled = (event)=>{ const isDisabled = (el)=>el.hasAttribute('disabled') || el.hasAttribute('aria-disabled') && el.getAttribute('aria-disabled') === 'true'; if (isHTMLElement(event.target) && isDisabled(event.target)) { return true; } return isHTMLElement(event.currentTarget) && isDisabled(event.currentTarget); };