11'use client' ;
22
3- import { useCallback , useEffect , useRef , useState } from 'react' ;
3+ import { Select } from '@base-ui/ react/select ' ;
44
5- import { cn } from './cn ' ;
5+ import { Button } from './Button ' ;
66
77type Option = {
88 value : string ;
@@ -18,107 +18,54 @@ type FilterSelectProps = {
1818} ;
1919
2020export function FilterSelect ( { value, onChange, ariaLabel, options, minDropdownWidth } : FilterSelectProps ) {
21- const [ open , setOpen ] = useState ( false ) ;
22- const ref = useRef < HTMLDivElement > ( null ) ;
23- const sizerRef = useRef < HTMLSpanElement > ( null ) ;
24- const [ dropdownW , setDropdownW ] = useState < number | undefined > ( ) ;
25- const selected = options . find ( ( o ) => o . value === value ) ;
26-
27- useEffect ( ( ) => {
28- if ( ! sizerRef . current ) return ;
29- const spans = sizerRef . current . children ;
30- let max = 0 ;
31- for ( let i = 0 ; i < spans . length ; i ++ ) {
32- max = Math . max ( max , ( spans [ i ] as HTMLElement ) . offsetWidth ) ;
33- }
34- setDropdownW ( max + 24 ) ;
35- } , [ options ] ) ;
36-
37- const toggle = useCallback ( ( ) => setOpen ( ( o ) => ! o ) , [ ] ) ;
38-
39- const handleSelect = useCallback (
40- ( v : string ) => {
41- onChange ( v ) ;
42- setOpen ( false ) ;
43- } ,
44- [ onChange ] ,
45- ) ;
46-
47- useEffect ( ( ) => {
48- if ( ! open ) return ;
49- const onDown = ( e : MouseEvent ) => {
50- if ( ref . current && ! ref . current . contains ( e . target as Node ) ) setOpen ( false ) ;
51- } ;
52- const onKey = ( e : KeyboardEvent ) => {
53- if ( e . key === 'Escape' ) setOpen ( false ) ;
54- } ;
55- document . addEventListener ( 'mousedown' , onDown ) ;
56- document . addEventListener ( 'keydown' , onKey ) ;
57- return ( ) => {
58- document . removeEventListener ( 'mousedown' , onDown ) ;
59- document . removeEventListener ( 'keydown' , onKey ) ;
60- } ;
61- } , [ open ] ) ;
62-
6321 return (
64- < div ref = { ref } className = "relative inline-flex" >
65- < button
66- type = "button"
67- onClick = { toggle }
68- aria-label = { ariaLabel }
69- aria-expanded = { open }
70- aria-haspopup = "listbox"
71- className = "flex h-9 items-center gap-1.5 rounded-full border border-bds-gray-10 bg-background px-3 text-[14px] text-foreground outline-none transition-colors hover:bg-bds-gray-5"
72- >
73- < span className = "whitespace-nowrap" >
74- { selected ?. label ?? value }
75- </ span >
76- < svg
77- aria-hidden = "true"
78- width = "16"
79- height = "16"
80- viewBox = "0 0 16 16"
81- fill = "none"
82- stroke = "currentColor"
83- strokeWidth = "1.5"
84- strokeLinecap = "round"
85- strokeLinejoin = "round"
86- className = { cn ( 'shrink-0 text-bds-gray-40 transition-transform duration-150' , open && 'rotate-180' ) }
87- >
88- < path d = "M4 6L8 10L12 6" />
89- </ svg >
90- </ button >
91-
92- < span ref = { sizerRef } aria-hidden className = "pointer-events-none invisible fixed left-0 top-0 flex flex-col whitespace-nowrap text-[14px]" >
93- { options . map ( ( o ) => (
94- < span key = { o . value } > { o . label } </ span >
95- ) ) }
96- </ span >
97-
98- { open && (
99- < div
100- role = "listbox"
101- aria-label = { ariaLabel }
102- style = { { minWidth : Math . max ( dropdownW ?? 0 , minDropdownWidth ?? 0 ) || undefined } }
103- className = "absolute left-0 top-full z-50 mt-1 max-h-64 min-w-full overflow-y-auto rounded-xl border border-bds-gray-10 bg-background py-1 shadow-lg"
22+ < Select . Root
23+ value = { value }
24+ onValueChange = { ( next ) => {
25+ if ( next != null ) onChange ( next ) ;
26+ } }
27+ items = { options }
28+ >
29+ < Select . Trigger aria-label = { ariaLabel } render = { < Button variant = "outline" size = "sm" /> } >
30+ < Select . Value className = "whitespace-nowrap" />
31+ < Select . Icon
32+ className = "shrink-0 text-bds-gray-40 transition-transform duration-150 group-data-[popup-open]:rotate-180"
10433 >
105- { options . map ( ( option ) => (
106- < button
107- key = { option . value }
108- type = "button"
109- role = "option"
110- aria-selected = { option . value === value }
111- onClick = { ( ) => handleSelect ( option . value ) }
112- className = { cn (
113- 'flex w-full items-center px-3 py-2 text-left text-[14px] transition-colors hover:bg-bds-gray-5' ,
114- option . value === value ? 'text-foreground' : 'text-bds-gray-60' ,
115- ) }
116- >
117- { option . label }
118- </ button >
119- ) ) }
120- </ div >
121- ) }
122- </ div >
34+ < svg
35+ aria-hidden = "true"
36+ width = "16"
37+ height = "16"
38+ viewBox = "0 0 16 16"
39+ fill = "none"
40+ stroke = "currentColor"
41+ strokeWidth = "1.5"
42+ strokeLinecap = "round"
43+ strokeLinejoin = "round"
44+ >
45+ < path d = "M4 6L8 10L12 6" />
46+ </ svg >
47+ </ Select . Icon >
48+ </ Select . Trigger >
49+ < Select . Portal >
50+ < Select . Positioner className = "z-50 outline-none" align = "start" sideOffset = { 4 } alignItemWithTrigger = { false } >
51+ < Select . Popup
52+ style = { minDropdownWidth ? { minWidth : `max(var(--anchor-width), ${ minDropdownWidth } px)` } : undefined }
53+ className = "min-w-[var(--anchor-width)] origin-[var(--transform-origin)] overflow-hidden rounded-xl border border-bds-gray-10 bg-background py-1 shadow-lg outline-none [transform:scale(1)] transition-[opacity,transform] duration-150 ease-out data-[ending-style]:opacity-0 data-[ending-style]:[transform:scale(0.97)] data-[starting-style]:opacity-0 data-[starting-style]:[transform:scale(0.97)]"
54+ >
55+ < Select . List className = "max-h-64 overflow-y-auto" >
56+ { options . map ( ( option ) => (
57+ < Select . Item
58+ key = { option . value }
59+ value = { option . value }
60+ className = "flex w-full cursor-pointer items-center px-3 py-2 text-left text-[14px] text-bds-gray-60 outline-none data-[highlighted]:bg-bds-gray-5 data-[selected]:text-foreground"
61+ >
62+ < Select . ItemText > { option . label } </ Select . ItemText >
63+ </ Select . Item >
64+ ) ) }
65+ </ Select . List >
66+ </ Select . Popup >
67+ </ Select . Positioner >
68+ </ Select . Portal >
69+ </ Select . Root >
12370 ) ;
12471}
0 commit comments