mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
190 lines
5.7 KiB
TypeScript
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 };
|