129 lines
5.9 KiB
JavaScript
129 lines
5.9 KiB
JavaScript
'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
|
|
};
|
|
};
|