Skip to content

Commit 7caa1bb

Browse files
[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
1 parent af939fc commit 7caa1bb

File tree

1 file changed

+95
-79
lines changed

1 file changed

+95
-79
lines changed

packages/ui/src/dropdowns/custom-select.tsx

Lines changed: 95 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Combobox } from "@headlessui/react";
22
import { Check } from "lucide-react";
3-
import React, { useRef, useState } from "react";
3+
import React, { createContext, useCallback, useContext, useRef, useState } from "react";
44
import { createPortal } from "react-dom";
55
import { usePopper } from "react-popper";
66
import { useOutsideClickDetector } from "@plane/hooks";
@@ -13,6 +13,9 @@ import { cn } from "../utils";
1313
// types
1414
import type { ICustomSelectItemProps, ICustomSelectProps } from "./helper";
1515

16+
// Context to share the close handler with option components
17+
const DropdownContext = createContext<() => void>(() => {});
18+
1619
function CustomSelect(props: ICustomSelectProps) {
1720
const {
1821
customButtonClassName = "",
@@ -42,99 +45,112 @@ function CustomSelect(props: ICustomSelectProps) {
4245
placement: placement ?? "bottom-start",
4346
});
4447

45-
const openDropdown = () => {
48+
const openDropdown = useCallback(() => {
4649
setIsOpen(true);
4750
if (referenceElement) referenceElement.focus();
48-
};
49-
const closeDropdown = () => setIsOpen(false);
51+
}, [referenceElement]);
52+
53+
const closeDropdown = useCallback(() => setIsOpen(false), []);
5054
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
5155
useOutsideClickDetector(dropdownRef, closeDropdown);
5256

53-
const toggleDropdown = () => {
57+
const toggleDropdown = useCallback(() => {
5458
if (isOpen) closeDropdown();
5559
else openDropdown();
56-
};
60+
}, [closeDropdown, isOpen, openDropdown]);
5761

5862
return (
59-
<Combobox
60-
as="div"
61-
ref={dropdownRef}
62-
tabIndex={tabIndex}
63-
value={value}
64-
onChange={onChange}
65-
className={cn("relative flex-shrink-0 text-left", className)}
66-
onKeyDown={handleKeyDown}
67-
disabled={disabled}
68-
>
69-
<>
70-
{customButton ? (
71-
<Combobox.Button as={React.Fragment}>
72-
<button
73-
ref={setReferenceElement}
74-
type="button"
75-
className={`flex items-center justify-between gap-1 text-xs ${
76-
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
77-
} ${customButtonClassName}`}
78-
onClick={toggleDropdown}
79-
>
80-
{customButton}
81-
</button>
82-
</Combobox.Button>
83-
) : (
84-
<Combobox.Button as={React.Fragment}>
85-
<button
86-
ref={setReferenceElement}
87-
type="button"
88-
className={cn(
89-
"flex w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300",
90-
{
91-
"px-3 py-2 text-sm": input,
92-
"px-2 py-1 text-xs": !input,
93-
"cursor-not-allowed text-custom-text-200": disabled,
94-
"cursor-pointer hover:bg-custom-background-80": !disabled,
95-
},
96-
buttonClassName
97-
)}
98-
onClick={toggleDropdown}
99-
>
100-
{label}
101-
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
102-
</button>
103-
</Combobox.Button>
104-
)}
105-
</>
106-
{isOpen &&
107-
createPortal(
108-
<Combobox.Options data-prevent-outside-click static>
109-
<div
110-
className={cn(
111-
"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",
112-
optionsClassName
113-
)}
114-
ref={setPopperElement}
115-
style={styles.popper}
116-
{...attributes.popper}
117-
>
63+
<DropdownContext.Provider value={closeDropdown}>
64+
<Combobox
65+
as="div"
66+
ref={dropdownRef}
67+
tabIndex={tabIndex}
68+
value={value}
69+
onChange={(val) => {
70+
onChange?.(val);
71+
closeDropdown();
72+
}}
73+
className={cn("relative flex-shrink-0 text-left", className)}
74+
onKeyDown={handleKeyDown}
75+
disabled={disabled}
76+
>
77+
<>
78+
{customButton ? (
79+
<Combobox.Button as={React.Fragment}>
80+
<button
81+
ref={setReferenceElement}
82+
type="button"
83+
className={`flex items-center justify-between gap-1 text-xs rounded ${
84+
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
85+
} ${customButtonClassName}`}
86+
onClick={toggleDropdown}
87+
>
88+
{customButton}
89+
</button>
90+
</Combobox.Button>
91+
) : (
92+
<Combobox.Button as={React.Fragment}>
93+
<button
94+
ref={setReferenceElement}
95+
type="button"
96+
className={cn(
97+
"flex w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300",
98+
{
99+
"px-3 py-2 text-sm": input,
100+
"px-2 py-1 text-xs": !input,
101+
"cursor-not-allowed text-custom-text-200": disabled,
102+
"cursor-pointer hover:bg-custom-background-80": !disabled,
103+
},
104+
buttonClassName
105+
)}
106+
onClick={toggleDropdown}
107+
>
108+
{label}
109+
{!noChevron && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
110+
</button>
111+
</Combobox.Button>
112+
)}
113+
</>
114+
{isOpen &&
115+
createPortal(
116+
<Combobox.Options data-prevent-outside-click>
118117
<div
119-
className={cn("space-y-1 overflow-y-scroll", {
120-
"max-h-60": maxHeight === "lg",
121-
"max-h-48": maxHeight === "md",
122-
"max-h-36": maxHeight === "rg",
123-
"max-h-28": maxHeight === "sm",
124-
})}
118+
className={cn(
119+
"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",
120+
optionsClassName
121+
)}
122+
ref={setPopperElement}
123+
style={styles.popper}
124+
{...attributes.popper}
125125
>
126-
{children}
126+
<div
127+
className={cn("space-y-1 overflow-y-scroll", {
128+
"max-h-60": maxHeight === "lg",
129+
"max-h-48": maxHeight === "md",
130+
"max-h-36": maxHeight === "rg",
131+
"max-h-28": maxHeight === "sm",
132+
})}
133+
>
134+
{children}
135+
</div>
127136
</div>
128-
</div>
129-
</Combobox.Options>,
130-
document.body
131-
)}
132-
</Combobox>
137+
</Combobox.Options>,
138+
document.body
139+
)}
140+
</Combobox>
141+
</DropdownContext.Provider>
133142
);
134143
}
135144

136145
function Option(props: ICustomSelectItemProps) {
137146
const { children, value, className } = props;
147+
const closeDropdown = useContext(DropdownContext);
148+
149+
const handleMouseDown = useCallback(() => {
150+
// Close dropdown for both new and already-selected options.
151+
requestAnimationFrame(() => closeDropdown());
152+
}, [closeDropdown]);
153+
138154
return (
139155
<Combobox.Option
140156
value={value}
@@ -149,10 +165,10 @@ function Option(props: ICustomSelectItemProps) {
149165
}
150166
>
151167
{({ selected }) => (
152-
<>
168+
<div onMouseDown={handleMouseDown} className="flex items-center justify-between gap-2 w-full">
153169
{children}
154170
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
155-
</>
171+
</div>
156172
)}
157173
</Combobox.Option>
158174
);

0 commit comments

Comments
 (0)