import { FloatingFocusManager, FloatingOverlay, FloatingPortal, Placement, arrow, autoUpdate, flip, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useMergeRefs, useRole, } from "@floating-ui/react"; import * as React from "react"; import "./Popover.css"; import { css } from "@emotion/css"; interface PopoverOptions { initialOpen?: boolean; placement?: Placement; modal?: boolean; open?: boolean; offset?: number; onOpenChange?: (open: boolean) => void; autoFocus?: boolean; backdrop?: boolean | "self"; root?: HTMLElement; } export function usePopover({ initialOpen = false, placement = "bottom", modal = false, open: controlledOpen, offset: popoverOffset = 5, onOpenChange: setControlledOpen, autoFocus = false, backdrop = true, root, }: PopoverOptions = {}) { const arrowRef = React.useRef(null); const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen); const [labelId, setLabelId] = React.useState(); const [descriptionId, setDescriptionId] = React.useState(); // Determine whether the popover is open const open = controlledOpen ?? uncontrolledOpen; const setOpen = setControlledOpen ?? setUncontrolledOpen; // Floating UI setup const data = useFloating({ placement, open, onOpenChange: setOpen, whileElementsMounted: autoUpdate, middleware: [ offset(popoverOffset), flip({ fallbackAxisSideDirection: "end", padding: 8, }), shift({ padding: 5 }), arrow({ element: arrowRef }), ], }); const context = data.context; // Add interactions (click, dismiss, role) const click = useClick(context, { enabled: controlledOpen == null, // Only enable if not controlled }); const dismiss = useDismiss(context); const role = useRole(context); // Combine all interactions const interactions = useInteractions([click, dismiss, role]); // Return memoized popover properties return React.useMemo( () => ({ open, setOpen, ...interactions, ...data, arrowRef, modal, labelId, descriptionId, setLabelId, setDescriptionId, backdrop, autoFocus, root, }), [ open, setOpen, interactions, data, modal, labelId, descriptionId, backdrop, autoFocus, root, ] ); } function mapPlacementSideToCSSProperty(placement: Placement) { const staticPosition = placement.split("-")[0]; const staticSide = { top: "bottom", right: "left", bottom: "top", left: "right", }[staticPosition]; return staticSide; } function PopoverArrow() { const context = usePopoverContext(); const { x: arrowX, y: arrowY } = context.middlewareData.arrow || { x: 0, y: 0, }; const staticSide = mapPlacementSideToCSSProperty(context.placement) as string; return (
); } type ContextType = | (ReturnType & { setLabelId: React.Dispatch>; setDescriptionId: React.Dispatch< React.SetStateAction >; }) | null; const PopoverContext = React.createContext(null); export const usePopoverContext = () => { const context = React.useContext(PopoverContext); if (context == null) { throw new Error("Popover components must be wrapped in "); } return context; }; export function Popover({ children, content, modal = false, className, classNameTrigger, arrow, ...restOptions }: { root?: HTMLElement; className?: string; classNameTrigger?: string; children: React.ReactNode; content?: React.ReactNode; arrow?: boolean; } & PopoverOptions) { const popover = usePopover({ modal, ...restOptions }); let _content = content; if (!content) _content =
; return ( { popover.setOpen(!popover.open); } : undefined } > {[children]} {_content} {(typeof arrow === "undefined" || arrow) && } ); } interface PopoverTriggerProps { children: React.ReactNode; asChild?: boolean; } export const PopoverTrigger = React.forwardRef< HTMLElement, React.HTMLProps & PopoverTriggerProps >(function PopoverTrigger({ children, asChild = false, ...props }, propRef) { const context = usePopoverContext(); // Gabungkan refs dari popover context dan ref prop const ref = useMergeRefs([context.refs.setReference, propRef]); // `asChild` memungkinkan elemen anak digunakan sebagai anchor if (asChild && React.isValidElement(children)) { return React.cloneElement(children, { ref, ...context.getReferenceProps({ ...props, ...children.props, "data-state": context.open ? "open" : "closed", }), } as any); } return (
{children}
); }); export const PopoverContent = React.forwardRef< HTMLDivElement, React.HTMLProps >(function PopoverContent(props, propRef) { const { context: floatingContext, ...context } = usePopoverContext(); const ref = useMergeRefs([context.refs.setFloating, propRef]); if (!floatingContext.open) return null; const _content = (
{props.children}
); const content = context.autoFocus ? ( {_content} ) : ( _content ); return ( {context.backdrop ? ( {content} ) : ( content )} ); }); export const PopoverHeading = React.forwardRef< HTMLHeadingElement, React.HTMLProps >(function PopoverHeading({ children, ...props }, ref) { const { setLabelId } = usePopoverContext(); const id = useId(); // Only sets `aria-labelledby` on the Popover root element // if this component is mounted inside it. React.useLayoutEffect(() => { setLabelId(id); return () => setLabelId(undefined); }, [id, setLabelId]); return (

{children}

); }); export const PopoverDescription = React.forwardRef< HTMLParagraphElement, React.HTMLProps >(function PopoverDescription({ children, ...props }, ref) { const { setDescriptionId } = usePopoverContext(); const id = useId(); // Only sets `aria-describedby` on the Popover root element // if this component is mounted inside it. React.useLayoutEffect(() => { setDescriptionId(id); return () => setDescriptionId(undefined); }, [id, setDescriptionId]); return (

{children}

); }); export const PopoverClose = React.forwardRef< HTMLButtonElement, React.ButtonHTMLAttributes >(function PopoverClose(props, ref) { const { setOpen } = usePopoverContext(); return (