import React, { useEffect, useMemo } from 'react'; import { autoUpdate, offset, shift, UseFloatingOptions, ShiftOptions as BaseShiftOptions, DetectOverflowOptions, } from '@floating-ui/react-dom'; import { usePopover, UsePopoverReturn } from '../utils/hooks/use-popover'; import { createContextSelector } from '../utils/react'; /*-- Context --*/ export type DropdownMenuContextValues = Pick< UsePopoverReturn, 'refs' | 'attributes' | 'styles' > & { open: boolean; keepMounted: boolean; withArrow: boolean; }; const [DropdownMenuContext, useDropdownMenuContext] = createContextSelector( { open: false, keepMounted: false, withArrow: false, attributes: { floating: {}, }, styles: { arrow: {}, floating: {}, }, refs: { arrow: { current: null }, floating: { current: null }, reference: { current: null }, setArrow: () => {}, setFloating: () => {}, setReference: () => {}, }, }, { name: 'DropdownMenuContext' }, ); /*-- Provider --*/ type ShiftOptions = Partial; export interface DropdownMenuProviderProps { /** A distance between reference (target) and floating (popper) elements. */ offset?: number; /** A shift of floating element from a reference center. */ skidding?: number; placement?: UseFloatingOptions['placement']; /** * Specifies whether to use [shift()](https://floating-ui.com/docs/shift) middleware * or not. `shift()` moves the floating element along the specified axes in order to * keep it in view. */ shift?: boolean | ShiftOptions; withArrow?: boolean; open?: boolean; /** Specifies whether to unmount floating (popper) element or not when closed. */ keepMounted?: boolean; children: React.ReactNode; } function DropdownMenuProvider({ placement: initialPlacement, withArrow = false, offset: mainAxis = 8, skidding: alignmentAxis = withArrow ? -12 : 0, shift: userShift = true, children, open = false, keepMounted = false, }: DropdownMenuProviderProps) { const shiftOptions = { padding: 8, ...(userShift as ShiftOptions), }; const { update: updateFloating, styles, attributes, refs, } = usePopover({ placement: initialPlacement, arrow: withArrow ? { padding: 12 } : false, // Order of middlewares matters middleware: [ offset({ mainAxis, alignmentAxis }), userShift && shift(shiftOptions), ], }); useEffect(() => { if (!open || !refs.reference.current || !refs.floating.current) { return; } return autoUpdate( refs.reference.current, refs.floating.current, updateFloating, { ancestorScroll: false, layoutShift: false }, ); }, [open, refs.reference, refs.floating, updateFloating]); const contextValue = useMemo( () => ({ open, keepMounted, withArrow, styles, attributes, refs, }), [open, keepMounted, withArrow, styles, attributes, refs], ); return ( {children} ); } export { DropdownMenuProvider, useDropdownMenuContext };