11import { Combobox } from "@headlessui/react" ;
22import { Check } from "lucide-react" ;
3- import React , { useRef , useState } from "react" ;
3+ import React , { createContext , useCallback , useContext , useRef , useState } from "react" ;
44import { createPortal } from "react-dom" ;
55import { usePopper } from "react-popper" ;
66import { useOutsideClickDetector } from "@plane/hooks" ;
@@ -13,6 +13,9 @@ import { cn } from "../utils";
1313// types
1414import type { ICustomSelectItemProps , ICustomSelectProps } from "./helper" ;
1515
16+ // Context to share the close handler with option components
17+ const DropdownContext = createContext < ( ) => void > ( ( ) => { } ) ;
18+
1619function 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
136145function 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