import { clsx } from 'clsx'; import { CSSProperties, HTMLAttributes, MutableRefObject, PropsWithChildren, SyntheticEvent, forwardRef, useContext, useEffect, useState, } from 'react'; import { usePopper } from 'react-popper'; import { Position, PositionBottom, PositionLeft, PositionRight, PositionTop } from '..'; import Dimmer from '../../dimmer'; import { OverlayIdContext } from '../../provider/overlay/OverlayIdProvider'; const POPOVER_OFFSET = [0, 16]; // By default the flip positioning explores only the opposite alternative. So if left is passed and there's no enough space // the right one gets chosen. If there's no space on both sides popover goes back to the initially chosen one left. // This mapping forces popover to try the four available positions before going back to the initial chosen one. const fallbackPlacements = { [Position.TOP]: [Position.BOTTOM, Position.RIGHT, Position.LEFT], [Position.BOTTOM]: [Position.TOP, Position.RIGHT, Position.LEFT], [Position.LEFT]: [Position.RIGHT, Position.TOP, Position.BOTTOM], [Position.RIGHT]: [Position.LEFT, Position.TOP, Position.BOTTOM], }; export type PanelProps = PropsWithChildren<{ arrow?: boolean; flip?: boolean; altAxis?: boolean; open?: boolean; onClose?: (event: Event | SyntheticEvent) => void; position?: PositionBottom | PositionLeft | PositionRight | PositionTop; anchorRef: MutableRefObject; anchorWidth?: boolean; considerHeight?: boolean; }> & HTMLAttributes; const Panel = forwardRef(function Panel( { arrow = false, flip = true, altAxis = false, children, open = false, onClose, position = Position.BOTTOM, anchorRef, anchorWidth = false, considerHeight = false, ...rest }: PanelProps, reference, ) { const [arrowElement, setArrowElement] = useState(null); const [popperElement, setPopperElement] = useState(null); const modifiers = []; if (altAxis) { modifiers.push({ // https://popper.js.org/docs/v2/modifiers/prevent-overflow name: 'preventOverflow', options: { altAxis: true, tether: false, }, }); } if (arrow) { modifiers.push({ name: 'arrow', options: { element: arrowElement, options: { padding: 8, // 8px from the edges of the popper }, }, }); // This lets you displace a popper element from its reference element. modifiers.push({ name: 'offset', options: { offset: POPOVER_OFFSET } }); } if (flip && fallbackPlacements[position]) { modifiers.push({ name: 'flip', options: { fallbackPlacements: fallbackPlacements[position], }, }); } const { styles, attributes, forceUpdate } = usePopper(anchorRef.current, popperElement, { placement: position, modifiers, }); // If the trigger is not visible when the position is calculated, it will be incorrect. Because this can happen repeatedly (on resize for example), // it is most simple just to always position before opening useEffect(() => { if (open && forceUpdate) { forceUpdate(); } }, [open]); const contentStyle: CSSProperties = { ...(anchorWidth ? { width: anchorRef.current?.clientWidth } : undefined), }; const overlayId = useContext(OverlayIdContext); return (