import React, { useCallback, useMemo, useRef } from 'react'; import { arrow, useFloating, UseFloatingOptions, UseFloatingReturn, } from '@floating-ui/react-dom'; /*-- Types --*/ type ArrowOptions = Parameters[0]; type Attributes = { [key: string]: string }; export type UsePopoverOptions = UseFloatingOptions & { arrow?: boolean | Omit; styles?: { floating?: React.CSSProperties; arrow?: React.CSSProperties; }; }; export type UsePopoverReturn = Omit< UseFloatingReturn, 'refs' | 'floatingStyles' > & { refs: UseFloatingReturn['refs'] & { arrow: React.MutableRefObject; setArrow: (node: HTMLElement | null) => void; }; styles: { floating: React.CSSProperties; arrow: React.CSSProperties; }; attributes: { floating: Attributes; }; }; /*-- Main --*/ /** * Extends `useFloating`: * - Adds logic and styles for arrow * - Creates styles and attributes for floating */ export function usePopover({ arrow: arrowOptions, styles: userStyles = {}, middleware = [], ...floatingProps }: UsePopoverOptions): UsePopoverReturn { const arrowRef = useRef(null); arrowOptions = arrowOptions === true ? {} : arrowOptions; const floatingData = useFloating({ ...floatingProps, middleware: [ ...middleware, arrowOptions && arrow({ ...arrowOptions, element: arrowRef }), ], }); const { placement, update: updateFloating, middlewareData, refs: baseRefs, } = floatingData; const { floatingStyles, ...exposedFloatingData } = floatingData; const arrowCallbackRef: React.RefCallback = useCallback( node => { arrowRef.current = node; updateFloating(); }, [updateFloating], ); const refs = useMemo( () => ({ ...baseRefs, arrow: arrowRef, setArrow: arrowCallbackRef, }), [arrowCallbackRef, baseRefs], ); const floatingAttributes = useMemo( () => ({ 'data-popover-placement': placement, }), [placement], ); const attributes = useMemo( () => ({ floating: floatingAttributes, }), [floatingAttributes], ); const arrowStyles = useMemo(() => { if (!middlewareData.arrow) { return {}; } const { x: arrowX, y: arrowY } = middlewareData.arrow; return { ...userStyles.arrow, ...getCoordinatesStyles(arrowX, arrowY) }; }, [middlewareData.arrow, userStyles.arrow]); const styles = useMemo( () => ({ floating: floatingStyles, arrow: arrowStyles, }), [arrowStyles, floatingStyles], ); return { ...exposedFloatingData, refs, styles, attributes, }; } /*-- Utils --*/ function getCoordinatesStyles(x: unknown, y: unknown) { const xNumber = typeof x === 'number' ? x : 0; const yNumber = typeof y === 'number' ? y : 0; return { top: typeof y === 'number' ? '0px' : '', left: typeof x === 'number' ? '0px' : '', transform: `translate(${xNumber}px, ${yNumber}px)`, }; }