dorfteich/apps/web/src/pages/DeletePageDialog.tsx
Claude Fable 5 1eca7c334c #162: Fokus-Management für Dialoge und Such-Palette
Gemeinsamer useModalFocus-Hook: Initialfokus in den Dialog, Tab/Shift-Tab
zyklisch gefangen, Fokus-Rückgabe an den Auslöser (bzw. returnFocusRef,
wenn der öffnende Menüpunkt mit dem Menü unmountet). Dialoge tragen jetzt
aria-labelledby auf ihre Überschrift und tabindex=-1 als Fokus-Fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
2026-07-21 13:52:59 +02:00

91 lines
2.7 KiB
TypeScript

import { useId, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FormError } from '../components/forms';
import { apiDelete } from '../lib/api';
import { useDismissable } from '../lib/use-dismissable';
import { useModalFocus } from '../lib/use-modal-focus';
/**
* The per-case delete decision for a page with subpages (issue #107/#109):
* promote the children to the page's parent, or trash the whole subtree.
* Childless pages keep the plain confirm in the overflow menu — this dialog
* only mounts when there is actually something to decide.
*/
export function DeletePageDialog({
pageId,
title,
childCount,
onDeleted,
onClose,
returnFocusRef,
}: {
pageId: string;
title: string;
childCount: number;
onDeleted: () => void;
onClose: () => void;
/** Where focus goes after closing when the opening control unmounted. */
returnFocusRef?: React.RefObject<HTMLElement | null>;
}): React.JSX.Element {
const { t } = useTranslation('editor');
const [error, setError] = useState<unknown>(null);
const [busy, setBusy] = useState(false);
const dialogRef = useRef<HTMLDivElement>(null);
const titleId = useId();
useDismissable(dialogRef, true, onClose);
useModalFocus(dialogRef, returnFocusRef);
async function remove(mode: 'promote' | 'subtree'): Promise<void> {
setError(null);
setBusy(true);
try {
await apiDelete(`/pages/${pageId}?mode=${mode}`);
onDeleted();
} catch (err) {
setError(err);
setBusy(false);
}
}
return (
<div className="modal-overlay">
<div
className="modal delete-page-dialog"
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
tabIndex={-1}
ref={dialogRef}
>
<h2 className="modal__title" id={titleId}>
{t('page.deleteChildrenTitle')}
</h2>
<FormError error={error} />
<p>{t('page.deleteChildrenHint', { title, count: childCount })}</p>
<div className="modal__actions modal__actions--stacked">
<button
type="button"
className="button delete-page-dialog__promote"
disabled={busy}
onClick={() => void remove('promote')}
>
{t('page.deletePromote')}
</button>
<button
type="button"
className="button button--danger delete-page-dialog__subtree"
disabled={busy}
onClick={() => void remove('subtree')}
>
{t('page.deleteSubtree', { count: childCount })}
</button>
<button type="button" className="linklike" onClick={onClose}>
{t('page.deleteCancel')}
</button>
</div>
</div>
</div>
);
}