Skip to content

Commit 51cf8b8

Browse files
authored
Merge pull request #164 from lambda-curry/codegen-bot/type-to-open-select-f85f09
2 parents 42da88b + 6921b25 commit 51cf8b8

File tree

1 file changed

+50
-3
lines changed

1 file changed

+50
-3
lines changed

packages/components/src/ui/select.tsx

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export interface SelectProps<T extends React.Key = string>
5757

5858
// Default search input built on top of CommandInput. Supports cmdk props at runtime.
5959
const DefaultSearchInput = forwardRef<HTMLInputElement, React.ComponentPropsWithoutRef<typeof CommandInput>>(
60-
(props, _ref) => <CommandInput {...props} />,
60+
(props, ref) => <CommandInput {...props} />,
6161
);
6262
DefaultSearchInput.displayName = 'SelectSearchInput';
6363

@@ -84,17 +84,34 @@ 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) {
101+
// Query for the input element since CommandInput uses asChild and ref forwarding is complex
102+
const inputElement = popoverRef.current?.querySelector<HTMLInputElement>('input[type="text"]');
103+
if (inputElement) {
104+
inputElement.focus();
105+
const selectionEnd = inputElement.value.length;
106+
if (selectionEnd > 0) {
107+
inputElement.setSelectionRange(selectionEnd, selectionEnd);
108+
}
109+
}
110+
}
94111
const selectedEl = selectedItemRef.current as HTMLElement | null;
95112
if (selectedEl) selectedEl.scrollIntoView({ block: 'center' });
96113
});
97-
}, [popoverState.isOpen]);
114+
}, [popoverState.isOpen, searchable]);
98115

99116
const selectedOption = options.find((o) => o.value === value);
100117

@@ -116,6 +133,34 @@ export function Select<T extends React.Key = string>({
116133
const ChevronIcon = components?.ChevronIcon || DefaultChevronIcon;
117134
const SearchInput = components?.SearchInput || DefaultSearchInput;
118135

136+
// Handle keydown on trigger to open popover and start typing
137+
const handleTriggerKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
138+
// Allow normal keyboard navigation (Enter, Space, Arrow keys, etc.)
139+
if (
140+
e.key === 'Enter' ||
141+
e.key === ' ' ||
142+
e.key === 'ArrowDown' ||
143+
e.key === 'ArrowUp' ||
144+
e.key === 'Escape' ||
145+
e.key === 'Tab'
146+
) {
147+
return;
148+
}
149+
150+
// If it's a printable character and searchable is enabled, open the popover and start typing
151+
if (searchable && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
152+
e.preventDefault();
153+
if (!popoverState.isOpen) {
154+
popoverState.open();
155+
}
156+
// Set the initial search query
157+
setSearchQuery(e.key);
158+
}
159+
160+
// Call the original onKeyDown if provided
161+
buttonProps.onKeyDown?.(e);
162+
};
163+
119164
return (
120165
<Popover open={popoverState.isOpen} onOpenChange={popoverState.setOpen}>
121166
<PopoverTrigger asChild>
@@ -131,6 +176,7 @@ export function Select<T extends React.Key = string>({
131176
aria-haspopup="listbox"
132177
aria-expanded={popoverState.isOpen}
133178
aria-controls={listboxId}
179+
onKeyDown={handleTriggerKeyDown}
134180
{...buttonProps}
135181
>
136182
{value != null && value !== '' ? (selectedOption?.label ?? String(value)) : placeholder}
@@ -159,6 +205,7 @@ export function Select<T extends React.Key = string>({
159205
{searchable && (
160206
<div className="px-1.5 pb-1.5 pt-1.5">
161207
<SearchInput
208+
ref={searchInputRef}
162209
placeholder="Search..."
163210
value={searchQuery}
164211
onValueChange={(v: string) => {

0 commit comments

Comments
 (0)