'use client'; import { ArrowLeft, Tab, ArrowRight, Escape } from '@fluentui/keyboard-keys'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { useMotionForwardedRef } from '@fluentui/react-motion'; import { useRestoreFocusSource } from '@fluentui/react-tabster'; import { getIntrinsicElementProps, useEventCallback, useMergedRefs, slot, useTimeout } from '@fluentui/react-utilities'; import * as React from 'react'; import { useMenuContext_unstable } from '../../contexts/menuContext'; import { useMenuListContext_unstable } from '../../contexts/menuListContext'; import { dispatchMenuEnterEvent, useIsSubmenu } from '../../utils/index'; /** * Create the state required to render MenuPopover. * * The returned state can be modified with hooks such as useMenuPopoverStyles_unstable, * before being passed to renderMenuPopover_unstable. * * @param props - props from this instance of MenuPopover * @param ref - reference to root HTMLElement of MenuPopover */ export const useMenuPopover_unstable = (props, ref)=>{ 'use no memo'; const safeZone = useMenuContext_unstable((context)=>context.safeZone); const popoverRef = useMenuContext_unstable((context)=>context.menuPopoverRef); const setOpen = useMenuContext_unstable((context)=>context.setOpen); const open = useMenuContext_unstable((context)=>context.open); const openOnHover = useMenuContext_unstable((context)=>context.openOnHover); const triggerRef = useMenuContext_unstable((context)=>context.triggerRef); const isSubmenu = useIsSubmenu(); const shouldCloseOnArrowLeft = useMenuListContext_unstable((ctx)=>{ var _ctx_shouldCloseOnArrowLeft; return (_ctx_shouldCloseOnArrowLeft = ctx.shouldCloseOnArrowLeft) !== null && _ctx_shouldCloseOnArrowLeft !== void 0 ? _ctx_shouldCloseOnArrowLeft : true; }); const canDispatchCustomEventRef = React.useRef(true); const restoreFocusSourceAttributes = useRestoreFocusSource(); const [setThrottleTimeout, clearThrottleTimeout] = useTimeout(); const { dir } = useFluent(); const CloseArrowKey = dir === 'ltr' ? ArrowLeft : ArrowRight; // use DOM listener since react events propagate up the react tree // no need to do `contains` logic as menus are all positioned in different portals const mouseOverListenerCallbackRef = React.useCallback((node)=>{ if (node) { // Dispatches the custom menu mouse enter event with throttling // Needs to trigger on mouseover to support keyboard + mouse together // i.e. keyboard opens submenus while cursor is still on the parent node.addEventListener('mouseover', (e)=>{ if (canDispatchCustomEventRef.current) { canDispatchCustomEventRef.current = false; dispatchMenuEnterEvent(popoverRef.current, e); setThrottleTimeout(()=>{ canDispatchCustomEventRef.current = true; }, 250); } }); } }, [ popoverRef, setThrottleTimeout ]); React.useEffect(()=>{ return ()=>clearThrottleTimeout(); }, [ clearThrottleTimeout ]); var _useMenuContext_unstable; const inline = (_useMenuContext_unstable = useMenuContext_unstable((context)=>context.inline)) !== null && _useMenuContext_unstable !== void 0 ? _useMenuContext_unstable : false; const mountNode = useMenuContext_unstable((context)=>context.mountNode); const rootProps = slot.always(getIntrinsicElementProps('div', { role: 'presentation', ...restoreFocusSourceAttributes, ...props, // FIXME: // `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement` // but since it would be a breaking change to fix it, we are casting ref to it's proper type ref: useMergedRefs(ref, popoverRef, mouseOverListenerCallbackRef, useMotionForwardedRef()) }), { elementType: 'div' }); const { onMouseEnter: onMouseEnterOriginal, onKeyDown: onKeyDownOriginal } = rootProps; rootProps.onMouseEnter = useEventCallback((event)=>{ if (openOnHover || isSubmenu) { setOpen(event, { open: true, keyboard: false, type: 'menuPopoverMouseEnter', event }); } onMouseEnterOriginal === null || onMouseEnterOriginal === void 0 ? void 0 : onMouseEnterOriginal(event); }); rootProps.onKeyDown = useEventCallback((event)=>{ const key = event.key; if (key === Escape || isSubmenu && shouldCloseOnArrowLeft && key === CloseArrowKey) { var _popoverRef_current; if (open && ((_popoverRef_current = popoverRef.current) === null || _popoverRef_current === void 0 ? void 0 : _popoverRef_current.contains(event.target)) && !event.isDefaultPrevented()) { setOpen(event, { open: false, keyboard: true, type: 'menuPopoverKeyDown', event }); // stop propagation to avoid conflicting with other elements that listen for `Escape` // e,g: Dialog, Popover, Menu and Tooltip event.preventDefault(); } } if (key === Tab) { setOpen(event, { open: false, keyboard: true, type: 'menuPopoverKeyDown', event }); if (!isSubmenu) { var _triggerRef_current; (_triggerRef_current = triggerRef.current) === null || _triggerRef_current === void 0 ? void 0 : _triggerRef_current.focus(); } } onKeyDownOriginal === null || onKeyDownOriginal === void 0 ? void 0 : onKeyDownOriginal(event); }); return { inline, mountNode, safeZone, components: { root: 'div' }, root: rootProps }; };