'use client'; import * as React from 'react'; import { useControllableState, useEventCallback, useOnClickOutside, useOnScrollOutside, elementContains, useTimeout } from '@fluentui/react-utilities'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { usePositioning, resolvePositioningShorthand, mergeArrowOffset, usePositioningMouseTarget, usePositioningSlideDirection } from '@fluentui/react-positioning'; import { useFocusFinders, useActivateModal } from '@fluentui/react-tabster'; import { arrowHeights } from '../PopoverSurface/index'; import { popoverSurfaceBorderRadius } from './constants'; import { presenceMotionSlot } from '@fluentui/react-motion'; import { PopoverSurfaceMotion } from './PopoverSurfaceMotion'; /** * Create the state required to render Popover. * * The returned state can be modified with hooks such as usePopoverStyles, * before being passed to renderPopover_unstable. * * @param props - props from this instance of Popover */ export const usePopover_unstable = (props)=>{ const { appearance, size = 'medium' } = props; const positioning = resolvePositioningShorthand(props.positioning); const withArrow = props.withArrow && !positioning.coverTarget; const { targetDocument } = useFluent(); const handlePositionEnd = usePositioningSlideDirection({ targetDocument, onPositioningEnd: positioning.onPositioningEnd }); const state = usePopoverBase_unstable({ ...props, positioning: { ...positioning, onPositioningEnd: handlePositionEnd, // Update the offset with the arrow size only when it's available ...withArrow ? { offset: mergeArrowOffset(positioning.offset, arrowHeights[size]) } : {} } }); return { components: { surfaceMotion: PopoverSurfaceMotion }, appearance, size, ...state, surfaceMotion: presenceMotionSlot(props.surfaceMotion, { elementType: PopoverSurfaceMotion, defaultProps: { visible: state.open, appear: true, unmountOnExit: true } }) }; }; /** * Base hook that builds Popover state for behavior and structure only. * Does not add design-related defaults such as appearance or size. * Does not manage focus behavior, it's handled by `usePopoverFocusManagement_unstable`. * * @internal * @param props - props from this instance of Popover */ export const usePopoverBase_unstable = (props)=>{ const [contextTarget, setContextTarget] = usePositioningMouseTarget(); const initialState = { contextTarget, setContextTarget, ...props }; const children = React.Children.toArray(props.children); if (process.env.NODE_ENV !== 'production') { if (children.length === 0) { // eslint-disable-next-line no-console console.warn('Popover must contain at least one child'); } if (children.length > 2) { // eslint-disable-next-line no-console console.warn('Popover must contain at most two children'); } } let popoverTrigger = undefined; let popoverSurface = undefined; if (children.length === 2) { popoverTrigger = children[0]; popoverSurface = children[1]; } else if (children.length === 1) { popoverSurface = children[0]; } const [open, setOpenState] = useOpenState(initialState); const [setOpenTimeout, clearOpenTimeout] = useTimeout(); const setOpen = useEventCallback((e, shouldOpen)=>{ clearOpenTimeout(); if (!(e instanceof Event) && e.persist) { // < React 17 still uses pooled synthetic events e.persist(); } if (e.type === 'mouseleave') { var _props_mouseLeaveDelay; // FIXME leaking Node timeout type // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore setOpenTimeout(()=>{ setOpenState(e, shouldOpen); }, (_props_mouseLeaveDelay = props.mouseLeaveDelay) !== null && _props_mouseLeaveDelay !== void 0 ? _props_mouseLeaveDelay : 500); } else { setOpenState(e, shouldOpen); } }); const toggleOpen = React.useCallback((e)=>{ setOpen(e, !open); }, [ setOpen, open ]); const positioningRefs = usePopoverRefs(initialState); const { targetDocument } = useFluent(); var _props_closeOnIframeFocus; useOnClickOutside({ contains: elementContains, element: targetDocument, callback: (ev)=>setOpen(ev, false), refs: [ positioningRefs.triggerRef, positioningRefs.contentRef ], disabled: !open, disabledFocusOnIframe: !((_props_closeOnIframeFocus = props.closeOnIframeFocus) !== null && _props_closeOnIframeFocus !== void 0 ? _props_closeOnIframeFocus : true) }); // only close on scroll for context, or when closeOnScroll is specified const closeOnScroll = initialState.openOnContext || initialState.closeOnScroll; useOnScrollOutside({ contains: elementContains, element: targetDocument, callback: (ev)=>setOpen(ev, false), refs: [ positioningRefs.triggerRef, positioningRefs.contentRef ], disabled: !open || !closeOnScroll }); const { findFirstFocusable } = useFocusFinders(); const activateModal = useActivateModal(); React.useEffect(()=>{ if (props.unstable_disableAutoFocus) { return; } const contentElement = positioningRefs.contentRef.current; if (open && contentElement) { var _contentElement_getAttribute; const shouldFocusContainer = !isNaN((_contentElement_getAttribute = contentElement.getAttribute('tabIndex')) !== null && _contentElement_getAttribute !== void 0 ? _contentElement_getAttribute : undefined); const firstFocusable = shouldFocusContainer ? contentElement : findFirstFocusable(contentElement); firstFocusable === null || firstFocusable === void 0 ? void 0 : firstFocusable.focus(); if (shouldFocusContainer) { // Modal activation happens automatically when something inside the modal is focused programmatically. // When the container is focused, we need to activate the modal manually. activateModal(contentElement); } } }, [ findFirstFocusable, activateModal, open, positioningRefs.contentRef, props.unstable_disableAutoFocus ]); var _props_inertTrapFocus, _props_inline; return { ...initialState, ...positioningRefs, // eslint-disable-next-line @typescript-eslint/no-deprecated inertTrapFocus: (_props_inertTrapFocus = props.inertTrapFocus) !== null && _props_inertTrapFocus !== void 0 ? _props_inertTrapFocus : props.legacyTrapFocus === undefined ? false : !props.legacyTrapFocus, popoverTrigger, popoverSurface, open, setOpen, toggleOpen, setContextTarget, contextTarget, inline: (_props_inline = props.inline) !== null && _props_inline !== void 0 ? _props_inline : false }; }; /** * Creates and manages the Popover open state */ function useOpenState(state) { 'use no memo'; const onOpenChange = useEventCallback((e, data)=>{ var _state_onOpenChange; return (_state_onOpenChange = state.onOpenChange) === null || _state_onOpenChange === void 0 ? void 0 : _state_onOpenChange.call(state, e, data); }); const [open, setOpenState] = useControllableState({ state: state.open, defaultState: state.defaultOpen, initialState: false }); state.open = open !== undefined ? open : state.open; const setContextTarget = state.setContextTarget; const setOpen = React.useCallback((e, shouldOpen)=>{ if (shouldOpen && e.type === 'contextmenu') { setContextTarget(e); } if (!shouldOpen) { setContextTarget(undefined); } setOpenState(shouldOpen); onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(e, { open: shouldOpen }); }, [ setOpenState, onOpenChange, setContextTarget ]); return [ open, setOpen ]; } /** * Creates and sets the necessary trigger, target and content refs used by Popover */ function usePopoverRefs(state) { 'use no memo'; const positioningOptions = { position: 'above', align: 'center', arrowPadding: 2 * popoverSurfaceBorderRadius, target: state.openOnContext ? state.contextTarget : undefined, ...resolvePositioningShorthand(state.positioning) }; // no reason to render arrow when covering the target if (positioningOptions.coverTarget) { state.withArrow = false; } const { targetRef: triggerRef, containerRef: contentRef, arrowRef } = usePositioning(positioningOptions); return { triggerRef, contentRef, arrowRef }; }