From ca266f2839b16a4c6306e5ebef59889bb0e5fecf Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sat, 25 Apr 2026 09:59:14 -0400 Subject: [PATCH] Fix autocomplete dropdown clipped by overflow container; use fixed positioning Co-Authored-By: Claude Sonnet 4.6 --- ui/src/pages/Mappings.jsx | 11 ++++++++--- ui/src/pages/Records.jsx | 20 ++++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/ui/src/pages/Mappings.jsx b/ui/src/pages/Mappings.jsx index 1c5156d..1cc2d26 100644 --- a/ui/src/pages/Mappings.jsx +++ b/ui/src/pages/Mappings.jsx @@ -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 && (
{filtered.map((s, i) => (
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 (
{ 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 && ( -
+ {open && filtered.length > 0 && dropPos && ( +
{filtered.map((s, i) => (
{ e.preventDefault(); select(s) }}>{s}