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
91 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
}
|