mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-05 05:06:04 +02:00
119 lines
3.2 KiB
TypeScript
119 lines
3.2 KiB
TypeScript
import {
|
|
autoUpdate,
|
|
flip,
|
|
FloatingPortal,
|
|
offset,
|
|
Placement,
|
|
useFloating,
|
|
} from "@floating-ui/react-dom-interactions";
|
|
import { Menu } from "@headlessui/react";
|
|
import clsx from "clsx";
|
|
import { motion } from "framer-motion";
|
|
import * as React from "react";
|
|
import { transformOriginByPlacement } from "utils/constants";
|
|
import { stopPropagation } from "utils/stop-propagation";
|
|
|
|
const MotionMenuItems = motion(Menu.Items);
|
|
|
|
export interface DropdownProps {
|
|
trigger?: React.ReactNode;
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
placement?: Placement;
|
|
}
|
|
|
|
const Dropdown: React.VoidFunctionComponent<DropdownProps> = ({
|
|
children,
|
|
className,
|
|
trigger,
|
|
placement: preferredPlacement,
|
|
}) => {
|
|
const { reference, floating, x, y, strategy, placement, refs, update } =
|
|
useFloating({
|
|
strategy: "fixed",
|
|
placement: preferredPlacement,
|
|
middleware: [offset(5), flip()],
|
|
});
|
|
|
|
const animationOrigin = transformOriginByPlacement[placement];
|
|
|
|
React.useEffect(() => {
|
|
if (!refs.reference.current || !refs.floating.current) {
|
|
return;
|
|
}
|
|
// Only call this when the floating element is rendered
|
|
return autoUpdate(refs.reference.current, refs.floating.current, update);
|
|
}, [refs.reference, refs.floating, update]);
|
|
|
|
return (
|
|
<Menu>
|
|
{({ open }) => (
|
|
<>
|
|
<Menu.Button as="div" className={className} ref={reference}>
|
|
{trigger}
|
|
</Menu.Button>
|
|
<FloatingPortal>
|
|
{open ? (
|
|
<MotionMenuItems
|
|
transition={{ duration: 0.1 }}
|
|
initial={{ opacity: 0, scale: 0.9, y: -10 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.9, y: -10 }}
|
|
className={clsx(
|
|
"z-50 divide-gray-100 rounded-md bg-white p-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none",
|
|
animationOrigin,
|
|
)}
|
|
onMouseDown={stopPropagation}
|
|
ref={floating}
|
|
style={{
|
|
position: strategy,
|
|
left: x ?? "",
|
|
top: y ?? "",
|
|
}}
|
|
>
|
|
{children}
|
|
</MotionMenuItems>
|
|
) : null}
|
|
</FloatingPortal>
|
|
</>
|
|
)}
|
|
</Menu>
|
|
);
|
|
};
|
|
|
|
export const DropdownItem: React.VoidFunctionComponent<{
|
|
icon?: React.ComponentType<{ className?: string }>;
|
|
label?: React.ReactNode;
|
|
disabled?: boolean;
|
|
onClick?: () => void;
|
|
}> = ({ icon: Icon, label, onClick, disabled }) => {
|
|
return (
|
|
<Menu.Item disabled={disabled}>
|
|
{({ active }) => (
|
|
<button
|
|
onClick={onClick}
|
|
className={clsx(
|
|
"group flex w-full items-center rounded py-2 pl-2 pr-4",
|
|
{
|
|
"bg-indigo-500 text-white": active,
|
|
"text-gray-700": !active,
|
|
"opacity-50": disabled,
|
|
},
|
|
)}
|
|
>
|
|
{Icon && (
|
|
<Icon
|
|
className={clsx("mr-2 h-5 w-5", {
|
|
"text-white": active,
|
|
"text-indigo-500": !disabled,
|
|
})}
|
|
/>
|
|
)}
|
|
{label}
|
|
</button>
|
|
)}
|
|
</Menu.Item>
|
|
);
|
|
};
|
|
|
|
export default Dropdown;
|