fix(ui): stop Preferences redrawing with the main window; unclip the dropdowns
TWO FAULTS, ONE SYMPTOM — 'it refreshes ten times a second and the buttons cannot be pressed'. Preferences is a child of the main view, so every cluster spot, CAT push and decode re-rendered the entire panel. On a busy evening that is several times a second, and the panel is large enough that the rebuild outlasts the gap between them: buttons missed their clicks because the element under the pointer was replaced between the press and the release. It is memoised now, and the callbacks App hands it hold their identity — without that the memo compares unequal every time and buys nothing. The dropdown menu was an absolutely-positioned child, so it was clipped by whichever scrolling or overflow-hidden box it sat in: the satellite list showed one entry of eight. It is portalled to the body now, positioned from the field's rectangle, re-measured while open, and opens upward when the field is near the bottom of the screen — which is exactly where these fields tend to be.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { Input } from './input';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -36,6 +37,10 @@ export function Combobox({
|
||||
// otherwise "open" onto COM7 alone.
|
||||
const [browse, setBrowse] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
// Where the portalled menu goes. Measured from the field itself, and
|
||||
// re-measured while it is open, so scrolling the panel underneath does not
|
||||
// leave the list floating over the wrong row.
|
||||
const [menuPos, setMenuPos] = useState({ top: 0, left: 0, width: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
function onDoc(e: MouseEvent) {
|
||||
@@ -45,6 +50,28 @@ export function Combobox({
|
||||
return () => document.removeEventListener('mousedown', onDoc);
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!open) return;
|
||||
const place = () => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
// Opens UPWARD when there is not enough room below — which is exactly
|
||||
// where these fields tend to sit, at the bottom of a panel.
|
||||
const height = 240;
|
||||
const below = window.innerHeight - r.bottom;
|
||||
const top = below < height + 8 ? Math.max(4, r.top - height - 4) : r.bottom + 4;
|
||||
setMenuPos({ top, left: r.left, width: r.width });
|
||||
};
|
||||
place();
|
||||
window.addEventListener('scroll', place, true);
|
||||
window.addEventListener('resize', place);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', place, true);
|
||||
window.removeEventListener('resize', place);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const filtered = !open ? []
|
||||
: browse ? options.slice(0, 60)
|
||||
: options.filter((o) => o.toLowerCase().includes(query.toLowerCase())).slice(0, 60);
|
||||
@@ -121,8 +148,16 @@ export function Combobox({
|
||||
<ChevronDown className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{open && filtered.length > 0 && (
|
||||
<div className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border border-border bg-card shadow-lg text-xs">
|
||||
{/* THE MENU IS PORTALLED to the body, and positioned from the field's own
|
||||
rectangle. As an absolutely-positioned child it was clipped by whichever
|
||||
scrolling or overflow-hidden box it happened to sit in: in the details
|
||||
panel it was cut off after the first row, and a list showing one entry
|
||||
of eight is worse than no list at all. */}
|
||||
{open && filtered.length > 0 && createPortal(
|
||||
<div
|
||||
style={{ position: 'fixed', top: menuPos.top, left: menuPos.left, width: menuPos.width }}
|
||||
className="z-[100] max-h-60 overflow-auto rounded-md border border-border bg-card shadow-lg text-xs"
|
||||
>
|
||||
{filtered.map((o) => (
|
||||
<button
|
||||
key={o}
|
||||
@@ -134,7 +169,8 @@ export function Combobox({
|
||||
{o}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user