2023-03-16 10:57:18 +00:00
|
|
|
import { Fragment, useState } from "react";
|
2023-03-18 06:04:29 +00:00
|
|
|
|
|
|
|
// headless ui
|
|
|
|
import { Menu, Transition } from "@headlessui/react";
|
|
|
|
// icons
|
2023-06-26 08:54:52 +00:00
|
|
|
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
2023-03-18 06:04:29 +00:00
|
|
|
import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid";
|
2023-03-16 10:57:18 +00:00
|
|
|
|
|
|
|
type MultiLevelDropdownProps = {
|
|
|
|
label: string;
|
|
|
|
options: {
|
|
|
|
id: string;
|
|
|
|
label: string;
|
|
|
|
value: any;
|
|
|
|
selected?: boolean;
|
|
|
|
children?: {
|
|
|
|
id: string;
|
2023-03-16 12:44:07 +00:00
|
|
|
label: string | JSX.Element;
|
2023-03-16 10:57:18 +00:00
|
|
|
value: any;
|
|
|
|
selected?: boolean;
|
2023-07-04 17:43:07 +00:00
|
|
|
element?: JSX.Element;
|
2023-03-16 10:57:18 +00:00
|
|
|
}[];
|
|
|
|
}[];
|
|
|
|
onSelect: (value: any) => void;
|
|
|
|
direction?: "left" | "right";
|
2023-03-23 17:55:08 +00:00
|
|
|
height?: "sm" | "md" | "rg" | "lg";
|
2023-03-16 10:57:18 +00:00
|
|
|
};
|
|
|
|
|
2023-03-16 12:44:07 +00:00
|
|
|
export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
|
|
|
label,
|
|
|
|
options,
|
|
|
|
onSelect,
|
|
|
|
direction = "right",
|
2023-03-23 17:55:08 +00:00
|
|
|
height = "md",
|
2023-03-16 12:44:07 +00:00
|
|
|
}) => {
|
2023-03-16 10:57:18 +00:00
|
|
|
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
|
|
|
|
|
|
|
|
return (
|
2023-07-04 17:43:07 +00:00
|
|
|
<>
|
|
|
|
<Menu as="div" className="relative z-10 inline-block text-left">
|
|
|
|
{({ open }) => (
|
|
|
|
<>
|
|
|
|
<div>
|
|
|
|
<Menu.Button
|
|
|
|
onClick={() => setOpenChildFor(null)}
|
|
|
|
className={`group flex items-center justify-between gap-2 rounded-md border border-brand-base px-3 py-1.5 text-xs shadow-sm duration-300 focus:outline-none ${
|
|
|
|
open ? "bg-brand-surface-1 text-brand-base" : "text-brand-secondary"
|
|
|
|
}`}
|
|
|
|
>
|
|
|
|
{label}
|
|
|
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
|
|
|
</Menu.Button>
|
|
|
|
</div>
|
|
|
|
<Transition
|
|
|
|
as={Fragment}
|
|
|
|
enter="transition ease-out duration-100"
|
|
|
|
enterFrom="transform opacity-0 scale-95"
|
|
|
|
enterTo="transform opacity-100 scale-100"
|
|
|
|
leave="transition ease-in duration-75"
|
|
|
|
leaveFrom="transform opacity-100 scale-100"
|
|
|
|
leaveTo="transform opacity-0 scale-95"
|
2023-03-16 10:57:18 +00:00
|
|
|
>
|
2023-07-04 17:43:07 +00:00
|
|
|
<Menu.Items
|
|
|
|
static
|
|
|
|
className="absolute right-0 z-10 mt-1 w-36 origin-top-right select-none rounded-md bg-brand-surface-1 text-xs shadow-lg focus:outline-none"
|
|
|
|
>
|
|
|
|
{options.map((option) => (
|
|
|
|
<div className="relative p-1" key={option.id}>
|
|
|
|
<Menu.Item
|
|
|
|
as="button"
|
|
|
|
onClick={(e: any) => {
|
|
|
|
if (option.children) {
|
|
|
|
e.stopPropagation();
|
|
|
|
e.preventDefault();
|
2023-03-22 11:28:32 +00:00
|
|
|
|
2023-07-04 17:43:07 +00:00
|
|
|
if (openChildFor === option.id) setOpenChildFor(null);
|
|
|
|
else setOpenChildFor(option.id);
|
|
|
|
} else {
|
|
|
|
onSelect(option.value);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
className="w-full"
|
2023-03-16 10:57:18 +00:00
|
|
|
>
|
2023-07-04 17:43:07 +00:00
|
|
|
{({ active }) => (
|
|
|
|
<>
|
|
|
|
<div
|
2023-03-29 12:48:45 +00:00
|
|
|
className={`${
|
2023-07-04 17:43:07 +00:00
|
|
|
active || option.selected ? "bg-brand-surface-2" : ""
|
|
|
|
} flex items-center gap-1 rounded px-1 py-1.5 text-brand-secondary ${
|
|
|
|
direction === "right" ? "justify-between" : ""
|
|
|
|
}`}
|
2023-03-16 12:44:07 +00:00
|
|
|
>
|
2023-07-04 17:43:07 +00:00
|
|
|
{direction === "left" && option.children && (
|
|
|
|
<ChevronLeftIcon className="h-4 w-4" aria-hidden="true" />
|
|
|
|
)}
|
|
|
|
<span>{option.label}</span>
|
|
|
|
{direction === "right" && option.children && (
|
|
|
|
<ChevronRightIcon className="h-4 w-4" aria-hidden="true" />
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</Menu.Item>
|
|
|
|
{option.children && option.id === openChildFor && (
|
|
|
|
<div
|
|
|
|
className={`absolute top-0 w-36 origin-top-right select-none overflow-y-scroll rounded-md bg-brand-surface-1 shadow-lg focus:outline-none ${
|
|
|
|
direction === "left"
|
|
|
|
? "right-full -translate-x-1"
|
|
|
|
: "left-full translate-x-1"
|
|
|
|
} ${
|
|
|
|
height === "sm"
|
|
|
|
? "max-h-28"
|
|
|
|
: height === "md"
|
|
|
|
? "max-h-44"
|
|
|
|
: height === "rg"
|
|
|
|
? "max-h-56"
|
|
|
|
: height === "lg"
|
|
|
|
? "max-h-80"
|
|
|
|
: ""
|
|
|
|
}`}
|
|
|
|
>
|
|
|
|
<div className="space-y-1 p-1">
|
|
|
|
{option.children.map((child) => {
|
|
|
|
if (child.element) return child.element;
|
|
|
|
else
|
|
|
|
return (
|
|
|
|
<button
|
|
|
|
key={child.id}
|
|
|
|
type="button"
|
|
|
|
onClick={() => onSelect(child.value)}
|
|
|
|
className={`${
|
|
|
|
child.selected ? "bg-brand-surface-2" : ""
|
|
|
|
} flex w-full items-center justify-between break-words rounded px-1 py-1.5 text-left text-brand-secondary hover:bg-brand-surface-2`}
|
|
|
|
>
|
|
|
|
{child.label}{" "}
|
|
|
|
<CheckIcon
|
|
|
|
className={`h-3.5 w-3.5 opacity-0 ${
|
|
|
|
child.selected ? "opacity-100" : ""
|
|
|
|
}`}
|
|
|
|
/>
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
2023-03-16 12:44:07 +00:00
|
|
|
</div>
|
2023-07-04 17:43:07 +00:00
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
</Menu.Items>
|
|
|
|
</Transition>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</Menu>
|
|
|
|
</>
|
2023-03-16 10:57:18 +00:00
|
|
|
);
|
|
|
|
};
|