// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { Button } from "@/element/button"; import { autoUpdate, FloatingPortal, Middleware, offset as offsetMiddleware, useClick, useDismiss, useFloating, useInteractions, type OffsetOptions, type Placement, } from "@floating-ui/react"; import clsx from "clsx"; import { Children, cloneElement, forwardRef, isValidElement, JSXElementConstructor, memo, ReactElement, ReactNode, useState, } from "react"; import "./popover.scss"; interface PopoverProps { children: ReactNode; className?: string; placement?: Placement; offset?: OffsetOptions; onDismiss?: () => void; middleware?: Middleware[]; } const isPopoverButton = ( element: ReactElement ): element is ReactElement> => { return element.type === PopoverButton; }; const isPopoverContent = ( element: ReactElement ): element is ReactElement> => { return element.type === PopoverContent; }; const Popover = memo( forwardRef( ({ children, className, placement = "bottom-start", offset = 3, onDismiss, middleware }, ref) => { const [isOpen, setIsOpen] = useState(false); const handleOpenChange = (open: boolean) => { setIsOpen(open); if (!open && onDismiss) { onDismiss(); } }; if (offset === undefined) { offset = 3; } middleware ??= []; middleware.push(offsetMiddleware(offset)); const { refs, floatingStyles, context } = useFloating({ placement, open: isOpen, onOpenChange: handleOpenChange, middleware: middleware, whileElementsMounted: autoUpdate, }); const click = useClick(context); const dismiss = useDismiss(context); const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]); const renderChildren = Children.map(children, (child) => { if (isValidElement(child)) { if (isPopoverButton(child)) { return cloneElement(child as any, { isActive: isOpen, ref: refs.setReference, getReferenceProps, // Do not overwrite onClick }); } if (isPopoverContent(child)) { return isOpen ? cloneElement(child as any, { ref: refs.setFloating, style: floatingStyles, getFloatingProps, }) : null; } } return child; }); return (
{renderChildren}
); } ) ); Popover.displayName = "Popover"; interface PopoverButtonProps extends React.ButtonHTMLAttributes { isActive?: boolean; children: React.ReactNode; getReferenceProps?: () => any; as?: keyof JSX.IntrinsicElements | React.ComponentType; } const PopoverButton = forwardRef( ( { isActive, children, onClick: userOnClick, // Destructured from props getReferenceProps, className, as: Component = "button", ...props // The rest of the props, without onClick }, ref ) => { const referenceProps = getReferenceProps?.() || {}; const popoverOnClick = referenceProps.onClick; // Remove onClick from referenceProps to prevent it from overwriting our combinedOnClick const { onClick: refOnClick, ...restReferenceProps } = referenceProps; const combinedOnClick = (event: React.MouseEvent) => { if (userOnClick) { userOnClick(event as any); // Our custom onClick logic } if (popoverOnClick) { popoverOnClick(event); // Popover's onClick logic } }; return ( ); } ); interface PopoverContentProps extends React.HTMLAttributes { children: React.ReactNode; getFloatingProps?: () => any; } const PopoverContent = forwardRef( ({ children, className, getFloatingProps, style, ...props }, ref) => { return (
{children}
); } ); Popover.displayName = "Popover"; PopoverButton.displayName = "PopoverButton"; PopoverContent.displayName = "PopoverContent"; export { Popover, PopoverButton, PopoverContent }; export type { PopoverButtonProps, PopoverContentProps };