[PAI-963] feat: enhance CustomSelect component with context for dropdown management (#8202)

* feat: enhance CustomSelect component with context for dropdown management

* refactor: streamline CustomSelect component structure and improve dropdown options rendering
This commit is contained in:
pratapalakshmi 2025-12-09 20:57:15 +05:30 committed by GitHub
parent af939fca41
commit 7caa1bb482
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,6 +1,6 @@
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
import { Check } from "lucide-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 { createPortal } from "react-dom";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { useOutsideClickDetector } from "@plane/hooks"; import { useOutsideClickDetector } from "@plane/hooks";
@ -13,6 +13,9 @@ import { cn } from "../utils";
// types // types
import type { ICustomSelectItemProps, ICustomSelectProps } from "./helper"; import type { ICustomSelectItemProps, ICustomSelectProps } from "./helper";
// Context to share the close handler with option components
const DropdownContext = createContext<() => void>(() => {});
function CustomSelect(props: ICustomSelectProps) { function CustomSelect(props: ICustomSelectProps) {
const { const {
customButtonClassName = "", customButtonClassName = "",
@ -42,99 +45,112 @@ function CustomSelect(props: ICustomSelectProps) {
placement: placement ?? "bottom-start", placement: placement ?? "bottom-start",
}); });
const openDropdown = () => { const openDropdown = useCallback(() => {
setIsOpen(true); setIsOpen(true);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; }, [referenceElement]);
const closeDropdown = () => setIsOpen(false);
const closeDropdown = useCallback(() => setIsOpen(false), []);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown); useOutsideClickDetector(dropdownRef, closeDropdown);
const toggleDropdown = () => { const toggleDropdown = useCallback(() => {
if (isOpen) closeDropdown(); if (isOpen) closeDropdown();
else openDropdown(); else openDropdown();
}; }, [closeDropdown, isOpen, openDropdown]);
return ( return (
<Combobox <DropdownContext.Provider value={closeDropdown}>
as="div" <Combobox
ref={dropdownRef} as="div"
tabIndex={tabIndex} ref={dropdownRef}
value={value} tabIndex={tabIndex}
onChange={onChange} value={value}
className={cn("relative flex-shrink-0 text-left", className)} onChange={(val) => {
onKeyDown={handleKeyDown} onChange?.(val);
disabled={disabled} closeDropdown();
> }}
<> className={cn("relative flex-shrink-0 text-left", className)}
{customButton ? ( onKeyDown={handleKeyDown}
<Combobox.Button as={React.Fragment}> disabled={disabled}
<button >
ref={setReferenceElement} <>
type="button" {customButton ? (
className={`flex items-center justify-between gap-1 text-xs ${ <Combobox.Button as={React.Fragment}>
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80" <button
} ${customButtonClassName}`} ref={setReferenceElement}
onClick={toggleDropdown} type="button"
> className={`flex items-center justify-between gap-1 text-xs rounded ${
{customButton} disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
</button> } ${customButtonClassName}`}
</Combobox.Button> onClick={toggleDropdown}
) : (
<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}
>
<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} {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(
"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}
>
<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>
</div> </Combobox.Options>,
</Combobox.Options>, document.body
document.body )}
)} </Combobox>
</Combobox> </DropdownContext.Provider>
); );
} }
function Option(props: ICustomSelectItemProps) { function Option(props: ICustomSelectItemProps) {
const { children, value, className } = props; 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 ( return (
<Combobox.Option <Combobox.Option
value={value} value={value}
@ -149,10 +165,10 @@ function Option(props: ICustomSelectItemProps) {
} }
> >
{({ selected }) => ( {({ selected }) => (
<> <div onMouseDown={handleMouseDown} className="flex items-center justify-between gap-2 w-full">
{children} {children}
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />} {selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</> </div>
)} )}
</Combobox.Option> </Combobox.Option>
); );