waveterm/frontend/app/element/popover.tsx
2024-12-04 22:01:59 +08:00

190 lines
5.7 KiB
TypeScript

// 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<PopoverButtonProps, JSXElementConstructor<PopoverButtonProps>> => {
return element.type === PopoverButton;
};
const isPopoverContent = (
element: ReactElement
): element is ReactElement<PopoverContentProps, JSXElementConstructor<PopoverContentProps>> => {
return element.type === PopoverContent;
};
const Popover = memo(
forwardRef<HTMLDivElement, PopoverProps>(
({ children, className, placement = "bottom-start", offset = 3, onDismiss }, ref) => {
const [isOpen, setIsOpen] = useState(false);
const handleOpenChange = (open: boolean) => {
setIsOpen(open);
if (!open && onDismiss) {
onDismiss();
}
};
const { refs, floatingStyles, context } = useFloating({
placement,
open: isOpen,
onOpenChange: handleOpenChange,
middleware: [offsetMiddleware(offset)],
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 (
<div ref={ref} className={clsx("popover", className)}>
{renderChildren}
</div>
);
}
)
);
Popover.displayName = "Popover";
interface PopoverButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
isActive?: boolean;
children: React.ReactNode;
getReferenceProps?: () => any;
as?: keyof JSX.IntrinsicElements | React.ComponentType<any>;
}
const PopoverButton = forwardRef<HTMLButtonElement | HTMLDivElement, PopoverButtonProps>(
(
{
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 (
<Button
ref={ref}
className={clsx("popover-button", className, { "is-active": isActive })}
{...props} // Spread the rest of the props
{...restReferenceProps} // Spread referenceProps without onClick
onClick={combinedOnClick} // Assign combined onClick after spreading
>
{children}
</Button>
);
}
);
interface PopoverContentProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
getFloatingProps?: () => any;
}
const PopoverContent = forwardRef<HTMLDivElement, PopoverContentProps>(
({ children, className, getFloatingProps, style, ...props }, ref) => {
return (
<FloatingPortal>
<div
ref={ref}
className={clsx("popover-content", className)}
style={style}
{...getFloatingProps?.()}
{...props}
>
{children}
</div>
</FloatingPortal>
);
}
);
Popover.displayName = "Popover";
PopoverButton.displayName = "PopoverButton";
PopoverContent.displayName = "PopoverContent";
export { Popover, PopoverButton, PopoverContent };
export type { PopoverButtonProps, PopoverContentProps };