Fix SearchableSelect — only open on focus, close properly on blur/select
Dropdown was opening automatically on render and not closing when clicking elsewhere. Now opens only on focus/click, closes on blur, selection, Enter, Escape, and Tab. Selected value persists in the input after selection.
This commit is contained in:
@@ -41,7 +41,7 @@ const OPTION_HIGHLIGHT = {
|
|||||||
export default function SearchableSelect({ value, options, onChange, onClose, placeholder, autoFocus }) {
|
export default function SearchableSelect({ value, options, onChange, onClose, placeholder, autoFocus }) {
|
||||||
const [filter, setFilter] = useState(value || '');
|
const [filter, setFilter] = useState(value || '');
|
||||||
const [highlightIdx, setHighlightIdx] = useState(-1);
|
const [highlightIdx, setHighlightIdx] = useState(-1);
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const listRef = useRef(null);
|
const listRef = useRef(null);
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ export default function SearchableSelect({ value, options, onChange, onClose, pl
|
|||||||
if (autoFocus && inputRef.current) {
|
if (autoFocus && inputRef.current) {
|
||||||
inputRef.current.focus();
|
inputRef.current.focus();
|
||||||
inputRef.current.select();
|
inputRef.current.select();
|
||||||
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
}, [autoFocus]);
|
}, [autoFocus]);
|
||||||
|
|
||||||
@@ -79,25 +80,31 @@ export default function SearchableSelect({ value, options, onChange, onClose, pl
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (highlightIdx >= 0 && filteredOptions[highlightIdx]) {
|
if (highlightIdx >= 0 && filteredOptions[highlightIdx]) {
|
||||||
onChange(filteredOptions[highlightIdx]);
|
onChange(filteredOptions[highlightIdx]);
|
||||||
|
setFilter(filteredOptions[highlightIdx]);
|
||||||
} else if (filter.trim()) {
|
} else if (filter.trim()) {
|
||||||
onChange(filter.trim());
|
onChange(filter.trim());
|
||||||
}
|
}
|
||||||
|
setIsOpen(false);
|
||||||
if (onClose) onClose();
|
if (onClose) onClose();
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
|
setIsOpen(false);
|
||||||
if (onClose) onClose();
|
if (onClose) onClose();
|
||||||
} else if (e.key === 'Tab') {
|
} else if (e.key === 'Tab') {
|
||||||
// Accept current value on tab
|
|
||||||
if (highlightIdx >= 0 && filteredOptions[highlightIdx]) {
|
if (highlightIdx >= 0 && filteredOptions[highlightIdx]) {
|
||||||
onChange(filteredOptions[highlightIdx]);
|
onChange(filteredOptions[highlightIdx]);
|
||||||
|
setFilter(filteredOptions[highlightIdx]);
|
||||||
} else if (filter.trim()) {
|
} else if (filter.trim()) {
|
||||||
onChange(filter.trim());
|
onChange(filter.trim());
|
||||||
}
|
}
|
||||||
|
setIsOpen(false);
|
||||||
if (onClose) onClose();
|
if (onClose) onClose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (opt) => {
|
const handleSelect = (opt) => {
|
||||||
onChange(opt);
|
onChange(opt);
|
||||||
|
setFilter(opt);
|
||||||
|
setIsOpen(false);
|
||||||
if (onClose) onClose();
|
if (onClose) onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,16 +113,18 @@ export default function SearchableSelect({ value, options, onChange, onClose, pl
|
|||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
style={{
|
style={{
|
||||||
background: '#0F172A', border: '1px solid #7C3AED', borderRadius: isOpen ? '0.375rem 0.375rem 0 0' : '0.375rem',
|
background: '#0F172A', border: isOpen ? '1px solid #7C3AED' : '1px solid #334155', borderRadius: isOpen ? '0.375rem 0.375rem 0 0' : '0.375rem',
|
||||||
color: '#E2E8F0', padding: '0.3rem 0.5rem', fontSize: '0.7rem', width: '100%',
|
color: '#E2E8F0', padding: '0.3rem 0.5rem', fontSize: '0.7rem', width: '100%',
|
||||||
fontFamily: "'JetBrains Mono', monospace", outline: 'none', boxSizing: 'border-box',
|
fontFamily: "'JetBrains Mono', monospace", outline: 'none', boxSizing: 'border-box',
|
||||||
}}
|
}}
|
||||||
value={filter}
|
value={filter}
|
||||||
onChange={e => { setFilter(e.target.value); setHighlightIdx(-1); setIsOpen(true); }}
|
onChange={e => { setFilter(e.target.value); setHighlightIdx(-1); setIsOpen(true); }}
|
||||||
|
onFocus={() => setIsOpen(true)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
// Delay close so click on option can fire
|
// Delay close so click on option can fire
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
setIsOpen(false);
|
||||||
if (filter.trim() && filter.trim() !== value) {
|
if (filter.trim() && filter.trim() !== value) {
|
||||||
onChange(filter.trim());
|
onChange(filter.trim());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user