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 }) {
|
function AutocompleteInput({ value, onChange, onEnter, suggestions = [], className, placeholder }) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [highlighted, setHighlighted] = useState(0)
|
const [highlighted, setHighlighted] = useState(0)
|
||||||
|
const [dropPos, setDropPos] = useState(null)
|
||||||
const inputRef = useRef()
|
const inputRef = useRef()
|
||||||
const listRef = useRef()
|
const listRef = useRef()
|
||||||
|
|
||||||
@ -12,6 +13,10 @@ function AutocompleteInput({ value, onChange, onEnter, suggestions = [], classNa
|
|||||||
: suggestions
|
: suggestions
|
||||||
|
|
||||||
function openList() {
|
function openList() {
|
||||||
|
if (inputRef.current) {
|
||||||
|
const r = inputRef.current.getBoundingClientRect()
|
||||||
|
setDropPos({ top: r.bottom + 2, left: r.left, minWidth: r.width })
|
||||||
|
}
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
setHighlighted(0)
|
setHighlighted(0)
|
||||||
}
|
}
|
||||||
@ -42,7 +47,6 @@ function AutocompleteInput({ value, onChange, onEnter, suggestions = [], classNa
|
|||||||
if (e.key === 'Enter') onEnter?.()
|
if (e.key === 'Enter') onEnter?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll highlighted item into view
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !listRef.current) return
|
if (!open || !listRef.current) return
|
||||||
const item = listRef.current.children[highlighted]
|
const item = listRef.current.children[highlighted]
|
||||||
@ -60,10 +64,11 @@ function AutocompleteInput({ value, onChange, onEnter, suggestions = [], classNa
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={e => { if (!listRef.current?.contains(e.relatedTarget)) setOpen(false) }}
|
onBlur={e => { if (!listRef.current?.contains(e.relatedTarget)) setOpen(false) }}
|
||||||
/>
|
/>
|
||||||
{open && filtered.length > 0 && (
|
{open && filtered.length > 0 && dropPos && (
|
||||||
<div
|
<div
|
||||||
ref={listRef}
|
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) => (
|
{filtered.map((s, i) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -4,16 +4,26 @@ import { api } from '../api'
|
|||||||
function AutocompleteInput({ value, onChange, onEnter, onFocus, suggestions = [], className, placeholder }) {
|
function AutocompleteInput({ value, onChange, onEnter, onFocus, suggestions = [], className, placeholder }) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [highlighted, setHighlighted] = useState(0)
|
const [highlighted, setHighlighted] = useState(0)
|
||||||
|
const [dropPos, setDropPos] = useState(null)
|
||||||
const inputRef = useRef()
|
const inputRef = useRef()
|
||||||
const listRef = useRef()
|
const listRef = useRef()
|
||||||
const filtered = value
|
const filtered = value
|
||||||
? suggestions.filter(s => s.toLowerCase().includes(value.toLowerCase()))
|
? suggestions.filter(s => s.toLowerCase().includes(value.toLowerCase()))
|
||||||
: suggestions
|
: 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 select(val) { onChange(val); setOpen(false); inputRef.current?.focus() }
|
||||||
|
|
||||||
function handleKeyDown(e) {
|
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 (open && filtered.length > 0) {
|
||||||
if (e.key === 'Tab') { e.preventDefault(); setHighlighted(h => (h + 1) % filtered.length); return }
|
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 }
|
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 (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input ref={inputRef} className={className} value={value} placeholder={placeholder}
|
<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}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={e => { if (!listRef.current?.contains(e.relatedTarget)) setOpen(false) }}
|
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-40 overflow-y-auto min-w-full">
|
<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) => (
|
{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'}`}
|
<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>
|
onMouseDown={e => { e.preventDefault(); select(s) }}>{s}</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user