From 426824fd78084ca23cd13fad1b35bab69439478f Mon Sep 17 00:00:00 2001 From: Nils Briggen Date: Mon, 8 Jun 2026 21:48:31 +0200 Subject: [PATCH] Fixes batch 1: modal focus, campaign system, combat difficulty, origin desc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modal: focus into the dialog ONCE on open (not on every parent re-render), and prefer the first field over the X button — fixes focus jumping to the close button on each keystroke in the handout composer (and every other composer). - Campaign edit: hide the System selector entirely (it's fixed after creation). - Combat difficulty: rate against the PCs actually in the encounter (falls back to the roster) so adding a player updates the reading. - Character builder: ancestry/background description is now a scrollable panel instead of clamped to 3 lines (no longer cut off on small screens). 223 unit + key e2e green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/ui/Modal.tsx | 27 ++++++++++++++----- src/features/campaigns/CampaignsPage.tsx | 25 +++++++++-------- .../characters/builder/CreationWizard.tsx | 2 +- src/features/combat/EncounterTracker.tsx | 5 +++- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index ee5281d..221b512 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -15,17 +15,33 @@ interface ModalProps { export function Modal({ open, onClose, title, children, footer, className }: ModalProps) { const panelRef = useRef(null); const previouslyFocused = useRef(null); + const onCloseRef = useRef(onClose); + onCloseRef.current = onClose; + // Focus into the dialog ONCE when it opens (not on every parent re-render — + // that was stealing focus to the close button on each keystroke). Prefer the + // first real field over the X button so composers land in their input. useEffect(() => { if (!open) return; previouslyFocused.current = document.activeElement as HTMLElement | null; const panel = panelRef.current; - panel?.querySelector('[data-autofocus], input, button, textarea, select')?.focus(); + const target = + panel?.querySelector('[data-autofocus]') ?? + panel?.querySelector('input:not([type="hidden"]), textarea, select') ?? + panel?.querySelector('a[href], button:not([disabled])'); + target?.focus(); + return () => { previouslyFocused.current?.focus(); }; + }, [open]); + // Escape to close + focus trap. Reads onClose via a ref so a changing + // onClose identity never re-binds (and never re-triggers the focus effect). + useEffect(() => { + if (!open) return; + const panel = panelRef.current; function onKey(e: KeyboardEvent) { if (e.key === 'Escape') { e.preventDefault(); - onClose(); + onCloseRef.current(); return; } if (e.key === 'Tab' && panel) { @@ -45,11 +61,8 @@ export function Modal({ open, onClose, title, children, footer, className }: Mod } } document.addEventListener('keydown', onKey); - return () => { - document.removeEventListener('keydown', onKey); - previouslyFocused.current?.focus(); - }; - }, [open, onClose]); + return () => document.removeEventListener('keydown', onKey); + }, [open]); if (!open) return null; diff --git a/src/features/campaigns/CampaignsPage.tsx b/src/features/campaigns/CampaignsPage.tsx index 7611321..299227d 100644 --- a/src/features/campaigns/CampaignsPage.tsx +++ b/src/features/campaigns/CampaignsPage.tsx @@ -258,19 +258,18 @@ function CampaignFormModal({ campaign, onClose }: { campaign?: Campaign; onClose placeholder="Curse of Strahd" /> - - - + {/* System is fixed once a campaign exists (content + characters depend on it). */} + {!campaign && ( + + + + )}