Latch a Quest log removal as Remove is activated and keep focus in its confirmation
CI / ci (push) Successful in 3m0s
CI / ci (pull_request) Successful in 2m42s

The pending state reached the confirmation only on a later render, so
Keep it, Escape, the scrim or a second Remove straight after Remove could
still close the confirmation or send a second DELETE. QuestLogSheet now
latches the removal synchronously in a ref, refuses dismissal and a
repeated confirmation while it is set, and renders busy from it until
the removal settles.

A focused button that becomes disabled loses focus to the page, outside
the dialog. Confirm now moves focus to its dialog when the focused
control is disabled by busy or dismissible changing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 04:32:18 -04:00
co-authored by Claude Opus 5.5
parent 5b334c9181
commit c6fb09a785
3 changed files with 75 additions and 21 deletions
+10 -2
View File
@@ -1,4 +1,4 @@
import { useEffect, useId, type ReactNode } from 'react'
import { useEffect, useId, useLayoutEffect, useRef, type ReactNode } from 'react'
import { useBlocker } from 'react-router'
import { Button } from '../ui/Button.tsx'
import { Sheet } from '../ui/Sheet.tsx'
@@ -58,12 +58,20 @@ export function Confirm({
onConfirm,
}: ConfirmProps) {
const titleId = useId()
const sheetRef = useRef<HTMLDivElement>(null)
// A focused button that becomes disabled loses focus to the page, outside
// the dialog; the dialog takes it instead.
useLayoutEffect(() => {
const dialog = sheetRef.current?.closest<HTMLElement>('[role="dialog"]')
const active = document.activeElement
if (dialog && active instanceof HTMLElement && active.matches(':disabled') && dialog.contains(active)) dialog.focus()
}, [busy, dismissible])
function dismiss() {
if (dismissible) onCancel()
}
return (
<Sheet open={open} onClose={dismiss} aria-labelledby={titleId}>
<div className={styles.sheet}>
<div ref={sheetRef} className={styles.sheet}>
<h2 id={titleId} className={styles.title}>
{title}
</h2>