waveterm/frontend/app/element/popover.tsx
Evan Simkowitz 04c4f0a203
Create and rename files and dirs in dirpreview (#1156)
New context menu options are available in the directory preview to
create and rename files and directories

It's missing three pieces of functionality, none of which are a
regression:
- Editing or creating an entry does not update the focused index. Focus
index right now is pretty dumb, it doesn't factor in the column sorting
so if you change that, the selected item will change to whatever is now
at that index. We should update this so we use the actual file name to
determine which element to focus and let the table determine which index
to then highlight given the current sorting algo
- Open in native preview should not be an option on remote connections
with the exception of WSL, where it should resolve the file in the
Windows filesystem, rather than the WSL one
- We should catch CRUD errors in the dir preview and display a popup
2024-12-02 22:23:44 -08:00

189 lines
5.6 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(
({ children, className, placement = "bottom-start", offset = 3, onDismiss, middleware }: PopoverProps) => {
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 <div className={clsx("popover", className)}>{renderChildren}</div>;
}
);
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 };