Fix autocomplete dropdown clipped by overflow container; use fixed positioning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-04-25 09:59:14 -04:00
parent 5951cbbba3
commit ca266f2839
2 changed files with 24 additions and 7 deletions

View File

@ -4,6 +4,7 @@ import { api, authHeaders } from '../api'
function AutocompleteInput({ value, onChange, onEnter, suggestions = [], className, placeholder }) {
const [open, setOpen] = useState(false)
const [highlighted, setHighlighted] = useState(0)
const [dropPos, setDropPos] = useState(null)
const inputRef = useRef()
const listRef = useRef()
@ -12,6 +13,10 @@ function AutocompleteInput({ value, onChange, onEnter, suggestions = [], classNa
: suggestions
function openList() {
if (inputRef.current) {
const r = inputRef.current.getBoundingClientRect()
setDropPos({ top: r.bottom + 2, left: r.left, minWidth: r.width })
}
setOpen(true)
setHighlighted(0)
}
@ -42,7 +47,6 @@ function AutocompleteInput({ value, onChange, onEnter, suggestions = [], classNa
if (e.key === 'Enter') onEnter?.()
}
// Scroll highlighted item into view
useEffect(() => {
if (!open || !listRef.current) return
const item = listRef.current.children[highlighted]
@ -60,10 +64,11 @@ function AutocompleteInput({ value, onChange, onEnter, suggestions = [], classNa
onKeyDown={handleKeyDown}
onBlur={e => { if (!listRef.current?.contains(e.relatedTarget)) setOpen(false) }}
/>
{open && filtered.length > 0 && (
{open && filtered.length > 0 && dropPos && (
<div
ref={listRef}
className="absolute z-50 left-0 top-full mt-0.5 bg-white border border-gray-200 rounded shadow-lg max-h-48 overflow-y-auto min-w-full"
style={{ position: 'fixed', top: dropPos.top, left: dropPos.left, minWidth: dropPos.minWidth, zIndex: 9999 }}
className="bg-white border border-gray-200 rounded shadow-lg max-h-48 overflow-y-auto"
>
{filtered.map((s, i) => (
<div

View File

@ -4,16 +4,26 @@ import { api } from '../api'
function AutocompleteInput({ value, onChange, onEnter, onFocus, suggestions = [], className, placeholder }) {
const [open, setOpen] = useState(false)
const [highlighted, setHighlighted] = useState(0)
const [dropPos, setDropPos] = useState(null)
const inputRef = useRef()
const listRef = useRef()
const filtered = value
? suggestions.filter(s => s.toLowerCase().includes(value.toLowerCase()))
: suggestions
function openList() {
if (inputRef.current) {
const r = inputRef.current.getBoundingClientRect()
setDropPos({ top: r.bottom + 2, left: r.left, minWidth: r.width })
}
setOpen(true)
setHighlighted(0)
}
function select(val) { onChange(val); setOpen(false); inputRef.current?.focus() }
function handleKeyDown(e) {
if (e.altKey && e.key === 'ArrowDown') { e.preventDefault(); setOpen(true); setHighlighted(0); return }
if (e.altKey && e.key === 'ArrowDown') { e.preventDefault(); openList(); return }
if (open && filtered.length > 0) {
if (e.key === 'Tab') { e.preventDefault(); setHighlighted(h => (h + 1) % filtered.length); return }
if (e.key === 'ArrowDown') { e.preventDefault(); setHighlighted(h => Math.min(h + 1, filtered.length - 1)); return }
@ -32,13 +42,15 @@ function AutocompleteInput({ value, onChange, onEnter, onFocus, suggestions = []
return (
<div className="relative">
<input ref={inputRef} className={className} value={value} placeholder={placeholder}
onChange={e => { onChange(e.target.value); if (e.target.value) setOpen(true) }}
onChange={e => { onChange(e.target.value); if (e.target.value) openList() }}
onKeyDown={handleKeyDown}
onFocus={onFocus}
onBlur={e => { if (!listRef.current?.contains(e.relatedTarget)) setOpen(false) }}
/>
{open && filtered.length > 0 && (
<div ref={listRef} className="absolute z-50 left-0 top-full mt-0.5 bg-white border border-gray-200 rounded shadow-lg max-h-40 overflow-y-auto min-w-full">
{open && filtered.length > 0 && dropPos && (
<div ref={listRef}
style={{ position: 'fixed', top: dropPos.top, left: dropPos.left, minWidth: dropPos.minWidth, zIndex: 9999 }}
className="bg-white border border-gray-200 rounded shadow-lg max-h-40 overflow-y-auto">
{filtered.map((s, i) => (
<div key={s} className={`px-2 py-1 text-xs cursor-pointer whitespace-nowrap ${i === highlighted ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'}`}
onMouseDown={e => { e.preventDefault(); select(s) }}>{s}</div>