1+ import { omit , isEqual } from "lodash-es" ;
12import { useState , useEffect } from "react" ;
2- import { Combobox as HeadlessCombobox } from "@headlessui/react" ;
3+ import {
4+ Combobox as HeadlessCombobox ,
5+ ComboboxButton as HeadlessComboboxButton ,
6+ ComboboxInput as HeadlessComboboxInput ,
7+ ComboboxOption as HeadlessComboboxOption ,
8+ ComboboxOptions as HeadlessComboboxOptions ,
9+ Label ,
10+ } from "@headlessui/react" ;
311import { ChevronDownIcon , MagnifyingGlassIcon } from "@radix-ui/react-icons" ;
412import { cn } from "@ui/cn" ;
5- import { isEqual } from "lodash-es" ;
613import fuzzy from "fuzzy" ;
714import { Button , ButtonProps } from "@ui/Button" ;
815import { createPortal } from "react-dom" ;
916import { usePopper } from "react-popper" ;
17+ import { Tooltip } from "./Tooltip" ;
1018
1119const { test } = fuzzy ;
1220
@@ -156,65 +164,84 @@ export function Combobox<T>({
156164
157165 return (
158166 < >
159- < HeadlessCombobox . Label
167+ < Label
160168 hidden = { labelHidden }
161169 className = "text-left text-sm text-content-primary"
162170 >
163171 { label }
164- </ HeadlessCombobox . Label >
172+ </ Label >
165173 < div className = { cn ( "relative" , className ) } >
166174 < div
167175 ref = { setReferenceElement }
168176 className = { cn ( "relative flex w-60 items-center" , buttonClasses ) }
169177 >
170- < HeadlessCombobox . Button
171- as = { Button }
172- variant = "unstyled"
173- data-testid = { `combobox-button-${ label } ` }
174- className = { cn (
175- "group flex w-full items-center gap-1" ,
176- "relative truncate rounded-md text-left text-content-primary disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-background-secondary" ,
177- "border bg-background-secondary text-sm focus-visible:z-10 focus-visible:border-border-selected focus-visible:outline-hidden" ,
178- "hover:bg-background-tertiary" ,
179- "cursor-pointer" ,
180- open && "z-10 border-border-selected" ,
181- size === "sm" && "px-1.5 py-1 text-xs" ,
182- size === "md" && "p-1.5" ,
183- innerButtonClasses ,
184- ) }
185- { ...buttonProps }
178+ < Tooltip
179+ tip = { buttonProps ?. tip }
180+ side = { buttonProps ?. tipSide }
181+ disableHoverableContent = {
182+ buttonProps ?. tipDisableHoverableContent
183+ }
184+ asChild
186185 >
187- { icon }
188- < div className = "truncate" >
189- { ! ! Option && ! ! selectedOptionData ? (
190- < Option
191- inButton
192- label = { selectedOptionData . label }
193- value = { selectedOptionData . value }
194- disabled = { selectedOptionData . disabled }
195- />
196- ) : (
197- selectedOptionData ?. label || (
186+ < HeadlessComboboxButton
187+ as = { Button }
188+ variant = "unstyled"
189+ data-testid = { `combobox-button-${ label } ` }
190+ className = { cn (
191+ "group flex w-full items-center gap-1" ,
192+ "relative truncate rounded-md text-left text-content-primary disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-background-secondary" ,
193+ "border bg-background-secondary text-sm focus-visible:z-10 focus-visible:border-border-selected focus-visible:outline-hidden" ,
194+ "hover:bg-background-tertiary" ,
195+ "cursor-pointer" ,
196+ open && "z-10 border-border-selected" ,
197+ size === "sm" && "px-1.5 py-1 text-xs" ,
198+ size === "md" && "p-1.5" ,
199+ innerButtonClasses ,
200+ ) }
201+ {
202+ // <HeadlessComboboxButton as={Button} tip="…" /> causes a state update loop since Headless UI 2.0
203+ // (presumably because Headless UI and the tooltip both want to update the ref).
204+ // To circumvent this issue, we place the <Tooltip /> component as a parent of <HeadlessComboboxButton />
205+ ...omit (
206+ buttonProps ,
207+ "tip" ,
208+ "tipSide" ,
209+ "tipDisableHoverableContent" ,
210+ )
211+ }
212+ >
213+ { icon }
214+ < div className = "truncate" >
215+ { ! ! Option && ! ! selectedOptionData ? (
216+ < Option
217+ inButton
218+ label = { selectedOptionData . label }
219+ value = { selectedOptionData . value }
220+ disabled = { selectedOptionData . disabled }
221+ />
222+ ) : (
223+ selectedOptionData ?. label || (
224+ < span className = "text-content-tertiary" >
225+ { selectedOption && unknownLabel ( selectedOption ) }
226+ </ span >
227+ )
228+ ) }
229+ { ! selectedOptionData && (
198230 < span className = "text-content-tertiary" >
199- { selectedOption && unknownLabel ( selectedOption ) }
231+ { placeholder }
200232 </ span >
201- )
202- ) }
203- { ! selectedOptionData && (
204- < span className = "text-content-tertiary" >
205- { placeholder }
206- </ span >
207- ) }
208- </ div >
209- { size === "md" && (
210- < ChevronDownIcon
211- className = { cn (
212- "ml-auto size-4 text-content-primary transition-all" ,
213- open && "rotate-180" ,
214233 ) }
215- />
216- ) }
217- </ HeadlessCombobox . Button >
234+ </ div >
235+ { size === "md" && (
236+ < ChevronDownIcon
237+ className = { cn (
238+ "ml-auto size-4 text-content-primary transition-all" ,
239+ open && "rotate-180" ,
240+ ) }
241+ />
242+ ) }
243+ </ HeadlessComboboxButton >
244+ </ Tooltip >
218245 </ div >
219246 { open &&
220247 createPortal (
@@ -227,7 +254,8 @@ export function Combobox<T>({
227254 { ...attributes . popper }
228255 className = "z-50"
229256 >
230- < HeadlessCombobox . Options
257+ < HeadlessComboboxOptions
258+ modal = { false }
231259 static
232260 className = { cn (
233261 "mt-1 scrollbar max-h-[14.75rem] overflow-auto rounded-md border bg-background-secondary pb-1 text-xs shadow-sm" ,
@@ -243,7 +271,7 @@ export function Combobox<T>({
243271 { ! disableSearch && (
244272 < div className = "sticky top-0 z-10 flex w-full items-center gap-2 border-b bg-background-secondary px-3 pt-1" >
245273 < MagnifyingGlassIcon className = "text-content-secondary" />
246- < HeadlessCombobox . Input
274+ < HeadlessComboboxInput
247275 onChange = { ( event ) => setQuery ( event . target . value ) }
248276 value = { query }
249277 autoFocus
@@ -256,14 +284,14 @@ export function Combobox<T>({
256284 </ div >
257285 ) }
258286 { displayedOptions . map ( ( option , idx ) => (
259- < HeadlessCombobox . Option
287+ < HeadlessComboboxOption
260288 key = { idx }
261289 value = { option . value }
262290 disabled = { option . disabled }
263- className = { ( { active } ) =>
291+ className = { ( { focus } ) =>
264292 cn (
265293 "relative w-fit min-w-full cursor-pointer px-3 py-1.5 text-content-primary select-none" ,
266- active && "bg-background-tertiary" ,
294+ focus && "bg-background-tertiary" ,
267295 option . disabled &&
268296 "cursor-not-allowed text-content-secondary opacity-75" ,
269297 )
@@ -287,7 +315,7 @@ export function Combobox<T>({
287315 ) }
288316 </ span >
289317 ) }
290- </ HeadlessCombobox . Option >
318+ </ HeadlessComboboxOption >
291319 ) ) }
292320
293321 { hasMoreThanMaxOptions && (
@@ -301,16 +329,16 @@ export function Combobox<T>({
301329 { allowCustomValue &&
302330 query . length > 0 &&
303331 ! filtered . some ( ( x ) => x . value === query ) && (
304- < HeadlessCombobox . Option
332+ < HeadlessComboboxOption
305333 value = { query }
306- className = { ( { active } ) =>
334+ className = { ( { focus } ) =>
307335 `text-content-primary relative cursor-pointer w-60 select-none py-1 px-3 text-xs ${
308- active ? "bg-background-tertiary" : ""
336+ focus ? "bg-background-tertiary" : ""
309337 } `
310338 }
311339 >
312340 Unknown option: "{ query } "
313- </ HeadlessCombobox . Option >
341+ </ HeadlessComboboxOption >
314342 ) }
315343
316344 { filtered . length === 0 && ! allowCustomValue && (
@@ -319,7 +347,7 @@ export function Combobox<T>({
319347 </ div >
320348 ) }
321349 </ div >
322- </ HeadlessCombobox . Options >
350+ </ HeadlessComboboxOptions >
323351 </ div > ,
324352 document . body ,
325353 ) }
0 commit comments