Files
MABEA/frontend/src/components/ui/alert-dialog.tsx
T
patrickandClaude Sonnet 5 1147ec0872
CI / backend-tests (push) Successful in 2m38s
CI / frontend-build (push) Successful in 17s
feat(ui): UI2-001/006/007/009/010 Design-Tokens, Toast, AlertDialog, Formular-a11y
chore(agents): Flutter/iOS/Swift-Subagenten aus claude-subagents-Repo übernommen

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
2026-09-10 11:36:42 +02:00

95 lines
3.0 KiB
TypeScript

import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { Button } from "./button";
interface Props {
offen: boolean;
titel: string;
beschreibung?: string;
bestaetigenText?: string;
abbrechenText?: string;
gefaehrlich?: boolean;
onBestaetigen: () => void;
onAbbrechen: () => void;
}
// UI2-007: Ersatz für natives window.confirm() - Fokus-Trap (Tab bleibt im
// Dialog) + Escape-zum-Schließen, große Touch-Buttons. Kein @radix-ui/react-
// dialog-Paket (Kachel-Vorgabe "Radix-artiges Pattern", kein Framework-
// Wechsel) - eigenes Portal reicht für den einen Anwendungsfall.
export function AlertDialog({
offen,
titel,
beschreibung,
bestaetigenText = "Bestätigen",
abbrechenText = "Abbrechen",
gefaehrlich = false,
onBestaetigen,
onAbbrechen,
}: Props) {
const dialogRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!offen) return;
const vorherigerFokus = document.activeElement as HTMLElement | null;
dialogRef.current?.querySelector<HTMLElement>("button")?.focus();
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
onAbbrechen();
return;
}
if (e.key !== "Tab") return;
const fokussierbar = dialogRef.current?.querySelectorAll<HTMLElement>("button");
if (!fokussierbar || fokussierbar.length === 0) return;
const erstes = fokussierbar[0];
const letztes = fokussierbar[fokussierbar.length - 1];
if (e.shiftKey && document.activeElement === erstes) {
e.preventDefault();
letztes.focus();
} else if (!e.shiftKey && document.activeElement === letztes) {
e.preventDefault();
erstes.focus();
}
}
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
vorherigerFokus?.focus();
};
}, [offen, onAbbrechen]);
if (!offen) return null;
return createPortal(
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
onClick={onAbbrechen}
>
<div
ref={dialogRef}
role="alertdialog"
aria-modal="true"
aria-labelledby="alert-dialog-titel"
className="w-full max-w-sm rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-5 shadow-lg"
onClick={(e) => e.stopPropagation()}
>
<h2 id="alert-dialog-titel" className="text-base font-semibold text-[var(--color-card-foreground)]">
{titel}
</h2>
{beschreibung && <p className="mt-2 text-sm text-[var(--color-text-muted)]">{beschreibung}</p>}
<div className="mt-5 flex justify-end gap-2">
<Button variant="secondary" onClick={onAbbrechen}>
{abbrechenText}
</Button>
<Button variant={gefaehrlich ? "danger" : "default"} onClick={onBestaetigen}>
{bestaetigenText}
</Button>
</div>
</div>
</div>,
document.body
);
}