Fixes batch 1: modal focus, campaign system, combat difficulty, origin desc
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -15,17 +15,33 @@ interface ModalProps {
|
||||
export function Modal({ open, onClose, title, children, footer, className }: ModalProps) {
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const previouslyFocused = useRef<HTMLElement | null>(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<HTMLElement>('[data-autofocus], input, button, textarea, select')?.focus();
|
||||
const target =
|
||||
panel?.querySelector<HTMLElement>('[data-autofocus]') ??
|
||||
panel?.querySelector<HTMLElement>('input:not([type="hidden"]), textarea, select') ??
|
||||
panel?.querySelector<HTMLElement>('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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user