import React, { cloneElement, isValidElement, useMemo, useState, type CSSProperties, type HTMLProps, type JSX, type ReactElement, type ReactNode, } from 'react'; import { autoUpdate } from '@floating-ui/dom'; import type { Placement } from '@floating-ui/react'; import { flip, FloatingFocusManager, FloatingNode, FloatingPortal, offset, shift, useClick, useDismiss, useFloating, useFloatingNodeId, useId, useInteractions, useRole, } from '@floating-ui/react'; import type { FlexRenderable } from '@wener/reaction'; import { flexRender, mergeRefs, useControllable } from '@wener/reaction'; import { clsx } from 'clsx'; import type { UseFloatingInteractionsOptions } from './useFloatingInteractions'; import { useFloatingInteractions } from './useFloatingInteractions'; export interface PopoverProps extends UseFloatingInteractionsOptions { placement?: Placement; children: JSX.Element; modal?: boolean; open?: boolean; onOpenChange?: (v: boolean) => void; content?: FlexRenderable; offset?: number; shift?: Parameters[0]; portal?: boolean; className?: string; style?: CSSProperties; } export interface PopoverContentProps { close: () => void; labelId: string; descriptionId: string; } export const Popover = ({ children, placement, content, portal, className, style, click = true, dismiss = true, hover = false, role = 'dialog', focus, modal, offset: _offset = 5, shift: _shift, ...props }: PopoverProps) => { const [open, setOpen] = useControllable(props.open, props.onOpenChange, false); const { x, y, refs, strategy, context } = useFloating({ open, onOpenChange: setOpen, middleware: [offset(_offset), flip(), shift(_shift)], placement, whileElementsMounted: autoUpdate, }); const id = useId(); const labelId = `${id}-label`; const descriptionId = `${id}-description`; const { getReferenceProps, getFloatingProps } = useFloatingInteractions(context, { click, dismiss, hover, focus, role, }); // Preserve the consumer's ref const ref = useMemo(() => mergeRefs(refs.setReference, (children as any).ref), [refs.setReference, children]); const pop = open && (
{!open ? null : flexRender(content, { close: () => { setOpen(false); }, labelId, descriptionId, })}
); return ( <> {cloneElement(children, getReferenceProps({ ref, ...children.props }))} {open && !portal && pop} {open && portal && pop} ); }; interface Props { render: (data: { close: () => void; labelId: string; descriptionId: string }) => ReactNode; placement?: Placement; modal?: boolean; children?: ReactElement; bubbles?: boolean; } function PopoverComponent({ children, render, placement, modal = true, bubbles = true }: Props) { const [open, setOpen] = useState(false); const nodeId = useFloatingNodeId(); const { floatingStyles, refs, context } = useFloating({ nodeId, open, placement, onOpenChange: setOpen, middleware: [offset(10), flip(), shift()], whileElementsMounted: autoUpdate, }); const id = useId(); const labelId = `${id}-label`; const descriptionId = `${id}-description`; const { getReferenceProps, getFloatingProps } = useInteractions([ useClick(context), useRole(context), useDismiss(context, { bubbles, }), ]); return ( {isValidElement(children) && cloneElement( children, getReferenceProps({ ref: refs.setReference, 'data-open': open ? '' : undefined, } as HTMLProps), )} {open && (
{render({ labelId, descriptionId, close: () => setOpen(false), })}
)}
); } /* export function Popover(props: Props) { const parentId = useFloatingParentNodeId(); // This is a root, so we wrap it with the tree if (parentId === null) { return ( ); } return ; } */