|
1 | 1 | import { Popover } from '@radix-ui/react-popover'; |
| 2 | +import * as PopoverPrimitive from '@radix-ui/react-popover'; |
2 | 3 | import { Check as DefaultCheckIcon, ChevronDown as DefaultChevronIcon } from 'lucide-react'; |
3 | 4 | import * as React from 'react'; |
4 | 5 | import { useOverlayTriggerState } from 'react-stately'; |
5 | | -import { PopoverContent, PopoverTrigger } from './popover'; |
| 6 | +import { PopoverTrigger } from './popover'; |
6 | 7 | import { cn } from './utils'; |
7 | 8 |
|
8 | 9 | export interface SelectOption { |
@@ -52,11 +53,7 @@ export function Select({ |
52 | 53 | const triggerRef = React.useRef<HTMLButtonElement>(null); |
53 | 54 | const popoverRef = React.useRef<HTMLDivElement>(null); |
54 | 55 | const selectedItemRef = React.useRef<HTMLButtonElement>(null); |
55 | | - const [menuWidth, setMenuWidth] = React.useState<number | undefined>(undefined); |
56 | | - |
57 | | - React.useEffect(() => { |
58 | | - if (triggerRef.current) setMenuWidth(triggerRef.current.offsetWidth); |
59 | | - }, []); |
| 56 | + // No need for JavaScript width measurement - Radix provides --radix-popover-trigger-width CSS variable |
60 | 57 |
|
61 | 58 | // Scroll to selected item when dropdown opens |
62 | 59 | React.useEffect(() => { |
@@ -130,15 +127,27 @@ export function Select({ |
130 | 127 | <ChevronIcon className="w-4 h-4 opacity-50" /> |
131 | 128 | </Trigger> |
132 | 129 | </PopoverTrigger> |
133 | | - <PopoverContent |
134 | | - ref={popoverRef} |
135 | | - className={cn('z-50 p-0 shadow-md border-0', contentClassName)} |
136 | | - // biome-ignore lint/a11y/useSemanticElements: using <div> for PopoverContent to ensure keyboard accessibility and focus management |
137 | | - role="listbox" |
138 | | - id={listboxId} |
139 | | - style={{ width: menuWidth ? `${menuWidth}px` : undefined }} |
140 | | - > |
141 | | - <div className="bg-white p-1.5 rounded-md focus:outline-none sm:text-sm"> |
| 130 | + <PopoverPrimitive.Portal> |
| 131 | + <PopoverPrimitive.Content |
| 132 | + ref={popoverRef} |
| 133 | + align="start" |
| 134 | + sideOffset={4} |
| 135 | + className={cn( |
| 136 | + 'z-50 rounded-md border bg-popover text-popover-foreground shadow-md outline-none', |
| 137 | + 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', |
| 138 | + 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95', |
| 139 | + 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2', |
| 140 | + 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', |
| 141 | + 'p-0 shadow-md border-0', |
| 142 | + contentClassName |
| 143 | + )} |
| 144 | + // biome-ignore lint/a11y/useSemanticElements: using <div> for PopoverContent to ensure keyboard accessibility and focus management |
| 145 | + role="listbox" |
| 146 | + id={listboxId} |
| 147 | + style={{ width: 'var(--radix-popover-trigger-width)' }} |
| 148 | + data-slot="popover-content" |
| 149 | + > |
| 150 | + <div className="bg-white p-1.5 rounded-md focus:outline-none sm:text-sm w-full"> |
142 | 151 | <div className="px-1.5 pb-1.5"> |
143 | 152 | <SearchInput |
144 | 153 | type="text" |
@@ -168,7 +177,7 @@ export function Select({ |
168 | 177 | className="w-full h-9 rounded-md bg-white px-2 text-sm leading-none focus:ring-0 focus:outline-none border-0" |
169 | 178 | /> |
170 | 179 | </div> |
171 | | - <ul className="max-h-[200px] overflow-y-auto rounded-md"> |
| 180 | + <ul className="max-h-[200px] overflow-y-auto rounded-md w-full"> |
172 | 181 | {filtered.length === 0 && <li className="px-3 py-2 text-sm text-gray-500">No results.</li>} |
173 | 182 | {filtered.map((option) => { |
174 | 183 | const isSelected = option.value === value; |
@@ -208,7 +217,8 @@ export function Select({ |
208 | 217 | })} |
209 | 218 | </ul> |
210 | 219 | </div> |
211 | | - </PopoverContent> |
| 220 | + </PopoverPrimitive.Content> |
| 221 | + </PopoverPrimitive.Portal> |
212 | 222 | </Popover> |
213 | 223 | ); |
214 | 224 | } |
0 commit comments