plane/web/components/custom-attributes/attribute-display/select.tsx

151 lines
5.7 KiB
TypeScript
Raw Normal View History

2023-09-15 10:54:20 +00:00
import React, { useRef, useState } from "react";
// headless ui
import { Combobox, Transition } from "@headlessui/react";
2023-09-15 10:54:20 +00:00
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// icons
2023-09-15 10:54:20 +00:00
import { Check, Search } from "lucide-react";
// types
import { Props } from "./types";
2023-09-15 10:54:20 +00:00
export const CustomSelectAttribute: React.FC<
Props &
(
| { multiple?: false; value: string | undefined }
| { multiple?: true; value: string[] | undefined }
)
> = (props) => {
const { attributeDetails, multiple = false, onChange, value } = props;
const [isOpen, setIsOpen] = useState(false);
const [query, setQuery] = useState("");
2023-09-15 10:54:20 +00:00
const dropdownButtonRef = useRef<HTMLButtonElement>(null);
const dropdownOptionsRef = useRef<HTMLUListElement>(null);
const selectedOption =
attributeDetails.children.find((option) => option.id === value) ??
attributeDetails.children.find((option) => option.is_default);
const options = attributeDetails.children.filter((option) =>
option.display_name.toLowerCase().includes(query.toLowerCase())
);
2023-09-15 10:54:20 +00:00
const handleOnOpen = () => {
const dropdownButton = dropdownButtonRef.current;
const dropdownOptions = dropdownOptionsRef.current;
if (!dropdownButton || !dropdownOptions) return;
const dropdownWidth = dropdownOptions.clientWidth;
const dropdownHeight = dropdownOptions.clientHeight;
const dropdownBtnX = dropdownButton.getBoundingClientRect().x;
const dropdownBtnY = dropdownButton.getBoundingClientRect().y;
const dropdownBtnHeight = dropdownButton.clientHeight;
let top = dropdownBtnY + dropdownBtnHeight;
if (dropdownBtnY + dropdownHeight > window.innerHeight)
top = dropdownBtnY - dropdownHeight - 10;
else top = top + 4;
let left = dropdownBtnX;
if (dropdownBtnX + dropdownWidth > window.innerWidth) left = dropdownBtnX - dropdownWidth;
dropdownOptions.style.top = `${Math.round(top)}px`;
dropdownOptions.style.left = `${Math.round(left)}px`;
};
useOutsideClickDetector(dropdownOptionsRef, () => {
if (isOpen) setIsOpen(false);
});
const comboboxProps: any = {
value,
onChange,
};
if (multiple) comboboxProps.multiple = true;
return (
2023-09-15 10:54:20 +00:00
<Combobox as="div" className="flex-shrink-0 text-left" {...comboboxProps}>
{({ open }: { open: boolean }) => {
if (open) handleOnOpen();
return (
<>
<Combobox.Button
ref={dropdownButtonRef}
className={`flex items-center text-xs rounded px-2.5 py-0.5 truncate w-min max-w-full text-left ${
selectedOption ? "" : "bg-custom-background-80"
}`}
style={{
backgroundColor: `${selectedOption?.color}40`,
}}
>
{Array.isArray(value)
? value.length > 0
? value.map((v) => options.find((o) => o.id === v)?.display_name).join(", ")
: `Select ${attributeDetails.display_name}`
: value}
2023-09-15 10:54:20 +00:00
</Combobox.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<div className="fixed z-10 top-0 left-0 h-full w-full cursor-auto">
<Combobox.Options
ref={dropdownOptionsRef}
className="fixed z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap"
2023-09-15 10:54:20 +00:00
>
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] border-custom-border-200 bg-custom-background-90 px-2 mb-1">
<Search className="text-custom-text-400" size={12} strokeWidth={1.5} />
<Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Type to search..."
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className="mt-1 overflow-y-auto">
{(options ?? []).map((option) => (
<Combobox.Option
key={option.id}
value={option.id}
className={({ active }) =>
`flex items-center justify-between gap-1 cursor-pointer select-none truncate rounded px-1 py-1.5 w-full ${
active ? "bg-custom-background-80" : ""
}`
}
>
{({ selected }) => (
<>
<span
className="px-1 rounded-sm truncate"
style={{ backgroundColor: `${option.color}40` }}
>
{option.display_name}
</span>
{selected && <Check size={14} strokeWidth={1.5} />}
</>
)}
</Combobox.Option>
))}
</div>
</Combobox.Options>
</div>
2023-09-15 10:54:20 +00:00
</Transition>
</>
);
}}
</Combobox>
);
};