rallly/components/dropdown.tsx
2022-05-09 08:21:53 +01:00

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;