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:
2026-08-25 20:04:07 +02:00
parent b53c56d508
commit e8dfa0eaaf
4 changed files with 101 additions and 27 deletions
+16 -2
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { memo, useEffect, useMemo, useRef, useState } from 'react';
import {
ArrowDown, ArrowUp, ArrowLeft, ArrowRight, Copy, Plus, Star, StarOff, Trash2,
ChevronDown, ChevronRight,
@@ -1524,7 +1524,19 @@ function brandOfBackend(backend: string, kenwoodLink?: string): { brand: string;
}
}
export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable, icomAvailable, yaesuAvailable, onEditQSO }: Props) {
// SETTINGS DOES NOT RE-RENDER WITH THE MAIN WINDOW.
//
// It is a child of App, and App re-renders on every cluster spot, every CAT
// status push, every decode — several times a second on a busy evening. Each of
// those re-rendered this entire panel, which is large enough that the rebuild
// takes longer than the gap between them: the page looked like it was
// refreshing ten times a second and buttons stopped responding, because the
// element under the pointer was replaced between the press and the release.
//
// memo() cuts that off. It only works if the props hold still, which is why
// App passes callbacks that do not change identity on every render — see the
// useCallback wrappers there.
function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable, icomAvailable, yaesuAvailable, onEditQSO }: Props) {
const { t } = useI18n();
const [selected, setSelected] = useState<SectionId>((initialSection as SectionId) || 'station');
const [loading, setLoading] = useState(true);
@@ -7716,6 +7728,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
);
}
export const SettingsModal = memo(SettingsModalImpl);
// PortInput — a TCP port field you can actually clear.
//
// Every port box was written as `parseInt(e.target.value) || <default>`. Delete