Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 95 additions & 79 deletions packages/ui/src/dropdowns/custom-select.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Combobox } from "@headlessui/react";
import { Check } from "lucide-react";
import React, { useRef, useState } from "react";
import React, { createContext, useCallback, useContext, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { usePopper } from "react-popper";
import { useOutsideClickDetector } from "@plane/hooks";
Expand All @@ -13,6 +13,9 @@ import { cn } from "../utils";
// types
import type { ICustomSelectItemProps, ICustomSelectProps } from "./helper";

// Context to share the close handler with option components
const DropdownContext = createContext<() => void>(() => {});

function CustomSelect(props: ICustomSelectProps) {
const {
customButtonClassName = "",
Expand Down Expand Up @@ -42,99 +45,112 @@ function CustomSelect(props: ICustomSelectProps) {
placement: placement ?? "bottom-start",
});

const openDropdown = () => {
const openDropdown = useCallback(() => {
setIsOpen(true);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
}, [referenceElement]);

const closeDropdown = useCallback(() => setIsOpen(false), []);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);

const toggleDropdown = () => {
const toggleDropdown = useCallback(() => {
if (isOpen) closeDropdown();
else openDropdown();
};
}, [closeDropdown, isOpen, openDropdown]);

return (
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
value={value}
onChange={onChange}
className={cn("relative flex-shrink-0 text-left", className)}
onKeyDown={handleKeyDown}
disabled={disabled}
>
<>
{customButton ? (
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex items-center justify-between gap-1 text-xs ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${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 border-[0.5px] border-custom-border-300",
{
"px-3 py-2 text-sm": input,
"px-2 py-1 text-xs": !input,
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer hover:bg-custom-background-80": !disabled,
},
buttonClassName
)}
onClick={toggleDropdown}
>
{label}
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
)}
</>
{isOpen &&
createPortal(
<Combobox.Options data-prevent-outside-click static>
<div
className={cn(
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-48 whitespace-nowrap z-30",
optionsClassName
)}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<DropdownContext.Provider value={closeDropdown}>
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
value={value}
onChange={(val) => {
onChange?.(val);
closeDropdown();
}}
className={cn("relative flex-shrink-0 text-left", className)}
onKeyDown={handleKeyDown}
disabled={disabled}
>
<>
{customButton ? (
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex items-center justify-between gap-1 text-xs rounded ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${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 border-[0.5px] border-custom-border-300",
{
"px-3 py-2 text-sm": input,
"px-2 py-1 text-xs": !input,
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer hover:bg-custom-background-80": !disabled,
},
buttonClassName
)}
onClick={toggleDropdown}
>
{label}
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
)}
</>
{isOpen &&
createPortal(
<Combobox.Options data-prevent-outside-click>
<div
className={cn("space-y-1 overflow-y-scroll", {
"max-h-60": maxHeight === "lg",
"max-h-48": maxHeight === "md",
"max-h-36": maxHeight === "rg",
"max-h-28": maxHeight === "sm",
})}
className={cn(
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-48 whitespace-nowrap z-30",
optionsClassName
)}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
{children}
<div
className={cn("space-y-1 overflow-y-scroll", {
"max-h-60": maxHeight === "lg",
"max-h-48": maxHeight === "md",
"max-h-36": maxHeight === "rg",
"max-h-28": maxHeight === "sm",
})}
>
{children}
</div>
</div>
</div>
</Combobox.Options>,
document.body
)}
</Combobox>
</Combobox.Options>,
document.body
)}
</Combobox>
</DropdownContext.Provider>
);
}

function Option(props: ICustomSelectItemProps) {
const { children, value, className } = props;
const closeDropdown = useContext(DropdownContext);

const handleMouseDown = useCallback(() => {
// Close dropdown for both new and already-selected options.
requestAnimationFrame(() => closeDropdown());
}, [closeDropdown]);

return (
<Combobox.Option
value={value}
Expand All @@ -149,10 +165,10 @@ function Option(props: ICustomSelectItemProps) {
}
>
{({ selected }) => (
<>
<div onMouseDown={handleMouseDown} className="flex items-center justify-between gap-2 w-full">
{children}
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
</div>
)}
</Combobox.Option>
);
Expand Down
Loading