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:
parent
5951cbbba3
commit
ca266f2839
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user