Skip to content

Commit 39156a7

Browse files
feat: enable typing to open and search select dropdowns
- Add keydown handler to select trigger that opens popover when typing printable characters - Automatically focus search input when popover opens - Set initial search query to the typed character - Clear search query when popover closes - Maintains normal keyboard navigation (Enter, Space, Arrow keys, etc.)
1 parent 7788aa7 commit 39156a7

File tree

1 file changed

+41
-2
lines changed

1 file changed

+41
-2
lines changed

packages/components/src/ui/select.tsx

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,26 @@ export function Select<T extends React.Key = string>({
8484
const triggerRef = useRef<HTMLButtonElement>(null);
8585
const popoverRef = useRef<HTMLDivElement>(null);
8686
const selectedItemRef = useRef<HTMLElement>(null);
87+
const searchInputRef = useRef<HTMLInputElement>(null);
8788
const [searchQuery, setSearchQuery] = useState('');
8889
// No need for JavaScript width measurement - Radix provides --radix-popover-trigger-width CSS variable
8990

9091
// When opening, ensure the currently selected option is the active item for keyboard nav
92+
// and focus the search input if searchable
9193
useEffect(() => {
92-
if (!popoverState.isOpen) return;
94+
if (!popoverState.isOpen) {
95+
// Clear search query when closing
96+
setSearchQuery('');
97+
return;
98+
}
9399
requestAnimationFrame(() => {
100+
if (searchable && searchInputRef.current) {
101+
searchInputRef.current.focus();
102+
}
94103
const selectedEl = selectedItemRef.current as HTMLElement | null;
95104
if (selectedEl) selectedEl.scrollIntoView({ block: 'center' });
96105
});
97-
}, [popoverState.isOpen]);
106+
}, [popoverState.isOpen, searchable]);
98107

99108
const selectedOption = options.find((o) => o.value === value);
100109

@@ -116,6 +125,34 @@ export function Select<T extends React.Key = string>({
116125
const ChevronIcon = components?.ChevronIcon || DefaultChevronIcon;
117126
const SearchInput = components?.SearchInput || DefaultSearchInput;
118127

128+
// Handle keydown on trigger to open popover and start typing
129+
const handleTriggerKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
130+
// Allow normal keyboard navigation (Enter, Space, Arrow keys, etc.)
131+
if (
132+
e.key === 'Enter' ||
133+
e.key === ' ' ||
134+
e.key === 'ArrowDown' ||
135+
e.key === 'ArrowUp' ||
136+
e.key === 'Escape' ||
137+
e.key === 'Tab'
138+
) {
139+
return;
140+
}
141+
142+
// If it's a printable character and searchable is enabled, open the popover and start typing
143+
if (searchable && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
144+
e.preventDefault();
145+
if (!popoverState.isOpen) {
146+
popoverState.open();
147+
}
148+
// Set the initial search query
149+
setSearchQuery(e.key);
150+
}
151+
152+
// Call the original onKeyDown if provided
153+
buttonProps.onKeyDown?.(e);
154+
};
155+
119156
return (
120157
<Popover open={popoverState.isOpen} onOpenChange={popoverState.setOpen}>
121158
<PopoverTrigger asChild>
@@ -131,6 +168,7 @@ export function Select<T extends React.Key = string>({
131168
aria-haspopup="listbox"
132169
aria-expanded={popoverState.isOpen}
133170
aria-controls={listboxId}
171+
onKeyDown={handleTriggerKeyDown}
134172
{...buttonProps}
135173
>
136174
{value != null && value !== '' ? (selectedOption?.label ?? String(value)) : placeholder}
@@ -159,6 +197,7 @@ export function Select<T extends React.Key = string>({
159197
{searchable && (
160198
<div className="px-1.5 pb-1.5 pt-1.5">
161199
<SearchInput
200+
ref={searchInputRef}
162201
placeholder="Search..."
163202
value={searchQuery}
164203
onValueChange={(v: string) => {

0 commit comments

Comments
 (0)