11import * as PopoverPrimitive from '@radix-ui/react-popover' ;
22import { Popover } from '@radix-ui/react-popover' ;
33import { Check as DefaultCheckIcon , ChevronDown as DefaultChevronIcon } from 'lucide-react' ;
4- import * as React from 'react' ;
54import { useOverlayTriggerState } from 'react-stately' ;
65import { PopoverTrigger } from './popover' ;
76import { cn } from './utils' ;
87import { Command , CommandEmpty , CommandGroup , CommandInput , CommandItem , CommandList } from './command' ;
8+ import {
9+ forwardRef ,
10+ type Ref ,
11+ useEffect ,
12+ type ButtonHTMLAttributes ,
13+ type ComponentType ,
14+ type InputHTMLAttributes ,
15+ type RefAttributes ,
16+ useId ,
17+ useRef ,
18+ } from 'react' ;
919export interface SelectOption {
1020 label : string ;
1121 value : string ;
1222}
1323
1424export interface SelectUIComponents {
15- Trigger ?: React . ComponentType < React . ButtonHTMLAttributes < HTMLButtonElement > & React . RefAttributes < HTMLButtonElement > > ;
16- Item ?: React . ComponentType <
17- React . ButtonHTMLAttributes < HTMLButtonElement > & { selected ?: boolean } & React . RefAttributes < HTMLButtonElement >
18- > ;
19- SearchInput ?: React . ComponentType <
20- React . InputHTMLAttributes < HTMLInputElement > & React . RefAttributes < HTMLInputElement >
25+ Trigger ?: ComponentType < ButtonHTMLAttributes < HTMLButtonElement > & RefAttributes < HTMLButtonElement > > ;
26+ Item ?: ComponentType <
27+ ButtonHTMLAttributes < HTMLButtonElement > & { selected ?: boolean } & RefAttributes < HTMLButtonElement >
2128 > ;
29+ SearchInput ?: ComponentType < InputHTMLAttributes < HTMLInputElement > & React . RefAttributes < HTMLInputElement > > ;
2230 CheckIcon ?: React . ComponentType < React . SVGProps < SVGSVGElement > > ;
2331 ChevronIcon ?: React . ComponentType < React . SVGProps < SVGSVGElement > > ;
2432}
@@ -48,34 +56,35 @@ export function Select({
4856 ...buttonProps
4957} : SelectProps ) {
5058 const popoverState = useOverlayTriggerState ( { } ) ;
51- const listboxId = React . useId ( ) ;
52- const triggerRef = React . useRef < HTMLButtonElement > ( null ) ;
53- const popoverRef = React . useRef < HTMLDivElement > ( null ) ;
54- const selectedItemRef = React . useRef < HTMLElement > ( null ) ;
59+ const listboxId = useId ( ) ;
60+ const triggerRef = useRef < HTMLButtonElement > ( null ) ;
61+ const popoverRef = useRef < HTMLDivElement > ( null ) ;
62+ const selectedItemRef = useRef < HTMLElement > ( null ) ;
5563 // No need for JavaScript width measurement - Radix provides --radix-popover-trigger-width CSS variable
5664
57- // Scroll to selected item when dropdown opens (cmdk also helps with focus/scroll)
58- React . useEffect ( ( ) => {
65+ // When opening, ensure the currently selected option is the active item for keyboard nav
66+ useEffect ( ( ) => {
5967 if ( ! popoverState . isOpen ) return ;
6068 requestAnimationFrame ( ( ) => {
61- selectedItemRef . current ?. scrollIntoView ( { block : 'nearest' } ) ;
69+ const selectedEl = selectedItemRef . current as HTMLElement | null ;
70+ if ( selectedEl ) selectedEl . scrollIntoView ( { block : 'center' } ) ;
6271 } ) ;
6372 } , [ popoverState . isOpen ] ) ;
6473
6574 const selectedOption = options . find ( ( o ) => o . value === value ) ;
6675
6776 const Trigger =
6877 components ?. Trigger ||
69- React . forwardRef < HTMLButtonElement , React . ButtonHTMLAttributes < HTMLButtonElement > > ( ( props , ref ) => (
78+ forwardRef < HTMLButtonElement , ButtonHTMLAttributes < HTMLButtonElement > > ( ( props , ref ) => (
7079 < button ref = { ref } type = "button" { ...props } />
7180 ) ) ;
7281 Trigger . displayName = Trigger . displayName || 'SelectTrigger' ;
7382
7483 const Item =
7584 components ?. Item ||
76- React . forwardRef < HTMLButtonElement , React . ButtonHTMLAttributes < HTMLButtonElement > & { selected ?: boolean } > (
77- ( props , ref ) => < button ref = { ref } type = "button" { ...props } /> ,
78- ) ;
85+ forwardRef < HTMLButtonElement , ButtonHTMLAttributes < HTMLButtonElement > & { selected ?: boolean } > ( ( props , ref ) => (
86+ < button ref = { ref } type = "button" { ...props } />
87+ ) ) ;
7988 Item . displayName = Item . displayName || 'SelectItem' ;
8089
8190 const CheckIcon = components ?. CheckIcon || DefaultCheckIcon ;
@@ -113,7 +122,7 @@ export function Select({
113122 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95' ,
114123 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2' ,
115124 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2' ,
116- 'p-0 shadow-md border-0 min-w-[8rem] ' ,
125+ 'p-0 shadow-md border-0 min-w-2xs ' ,
117126 contentClassName ,
118127 ) }
119128 style = { { width : 'var(--radix-popover-trigger-width)' } }
@@ -145,11 +154,11 @@ export function Select({
145154 popoverState . close ( ) ;
146155 } }
147156 value = { option . label }
148- aria-selected = { isSelected }
149157 id = { `${ listboxId } -option-${ index } ` }
150158 { ...commonProps }
151159 className = { cn ( itemClassName ) }
152- ref = { isSelected ? selectedItemRef : undefined }
160+ // Attach ref to CommandItem (even with asChild) so we can focus the selected item on open
161+ ref = { isSelected ? ( selectedItemRef as unknown as Ref < HTMLDivElement > ) : undefined }
153162 asChild
154163 >
155164 < CustomItem selected = { isSelected } >
@@ -170,7 +179,6 @@ export function Select({
170179 popoverState . close ( ) ;
171180 } }
172181 value = { option . label }
173- aria-selected = { isSelected }
174182 id = { `${ listboxId } -option-${ index } ` }
175183 { ...commonProps }
176184 className = { cn (
@@ -179,7 +187,8 @@ export function Select({
179187 isSelected ? 'bg-gray-100' : 'hover:bg-gray-100' ,
180188 itemClassName ,
181189 ) }
182- ref = { isSelected ? selectedItemRef : undefined }
190+ // Ensure we can programmatically focus the selected item when opening
191+ ref = { isSelected ? ( selectedItemRef as unknown as Ref < HTMLDivElement > ) : undefined }
183192 >
184193 { isSelected && < CheckIcon className = "h-4 w-4 flex-shrink-0" /> }
185194 < span className = { cn ( 'block truncate' , ! isSelected && 'ml-6' , isSelected && 'font-semibold' ) } >
0 commit comments