NODEDC_TASKMANAGER/plane-src/packages/ui/src/dropdowns/custom-search-select.tsx

230 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { Combobox } from "@headlessui/react";
import { Info } from "lucide-react";
import React, { useRef, useState } from "react";
import { createPortal } from "react-dom";
import { usePopper } from "react-popper";
import { useOutsideClickDetector } from "@plane/hooks";
import { CheckIcon, SearchIcon, ChevronDownIcon } from "@plane/propel/icons";
// plane imports
// local imports
import { Tooltip } from "@plane/propel/tooltip";
import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
import { cn } from "../utils";
import type { ICustomSearchSelectProps } from "./helper";
export function CustomSearchSelect(props: ICustomSearchSelectProps) {
const {
customButtonClassName = "",
buttonClassName = "",
className = "",
chevronClassName = "",
customButton,
placement,
disabled = false,
footerOption,
input = false,
label,
maxHeight = "md",
multiple = false,
noChevron = false,
onChange,
options,
onOpen,
onClose,
optionsClassName = "",
value,
tabIndex,
noResultsMessage = "Совпадений не найдено",
defaultOpen = false,
} = props;
const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [isOpen, setIsOpen] = useState(defaultOpen);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
});
const filteredOptions =
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
const comboboxProps: any = {
value,
onChange,
disabled,
};
if (multiple) comboboxProps.multiple = true;
const openDropdown = () => {
setIsOpen(true);
if (referenceElement) referenceElement.focus();
if (onOpen) onOpen();
};
const closeDropdown = () => {
setIsOpen(false);
onClose && onClose();
};
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
const toggleDropdown = () => {
if (isOpen) closeDropdown();
else openDropdown();
};
return (
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("relative flex-shrink-0 text-left", className)}
onKeyDown={handleKeyDown}
{...comboboxProps}
>
{({ open }: { open: boolean }) => {
if (open && onOpen) onOpen();
return (
<>
{customButton ? (
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={cn(
"flex w-full items-center justify-between gap-1 text-11",
{
"cursor-not-allowed text-secondary": disabled,
"cursor-pointer": !disabled,
},
customButtonClassName
)}
onClick={toggleDropdown}
>
{customButton}
</button>
</Combobox.Button>
) : (
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={cn(
"flex w-full items-center justify-between gap-1 rounded-full border-0 outline-none",
{
"px-3 py-2 text-13": input,
"px-2 py-1 text-11": !input,
"cursor-not-allowed text-secondary": disabled,
"cursor-pointer": !disabled,
},
buttonClassName
)}
onClick={toggleDropdown}
>
{label}
{!noChevron && !disabled && (
<ChevronDownIcon className={cn("h-3 w-3 flex-shrink-0", chevronClassName)} aria-hidden="true" />
)}
</button>
</Combobox.Button>
)}
{isOpen &&
createPortal(
<Combobox.Options data-prevent-outside-click static>
<div
className={cn(
"nodedc-dropdown-surface z-30 my-1 min-w-48 overflow-y-scroll whitespace-nowrap focus:outline-none",
optionsClassName
)}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="nodedc-dropdown-search mx-1">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
className="w-full bg-transparent py-1 text-11 text-secondary placeholder:text-placeholder focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Поиск"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div
className={cn("vertical-scrollbar mt-2 scrollbar-xs space-y-1 overflow-y-scroll px-1", {
"max-h-96": maxHeight === "2xl",
"max-h-80": maxHeight === "xl",
"max-h-60": maxHeight === "lg",
"max-h-48": maxHeight === "md",
"max-h-36": maxHeight === "rg",
"max-h-28": maxHeight === "sm",
})}
>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active }) =>
cn("nodedc-dropdown-option", {
"bg-white/6": active,
"cursor-not-allowed text-placeholder opacity-60": option.disabled,
})
}
onClick={() => {
if (!multiple) closeDropdown();
}}
disabled={option.disabled}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
{option.tooltip && (
<>
{typeof option.tooltip === "string" ? (
<Tooltip tooltipContent={option.tooltip}>
<Info className="h-3.5 w-3.5 flex-shrink-0 cursor-pointer text-secondary" />
</Tooltip>
) : (
option.tooltip
)}
</>
)}
</>
)}
</Combobox.Option>
))
) : (
<p className="px-1.5 py-1 text-placeholder italic">{noResultsMessage}</p>
)
) : (
<p className="px-1.5 py-1 text-placeholder italic">Загрузка...</p>
)}
</div>
{footerOption}
</div>
</Combobox.Options>,
document.body
)}
</>
);
}}
</Combobox>
);
}