# Module: `src/components/ConfirmDialog.tsx` > **Source**: `src/components/ConfirmDialog.tsx` (47 lines) > **Topo batch**: B3 (uses `react-i18next` only; no intra-repo deps) ## Purpose A controlled, single-message confirm dialog used wherever the SPA needs an "Are you sure?" gate. Replaces the legacy WPF `MessageBox.Show(..., MessageBoxButton.YesNo, ...)` pattern (`_docs/legacy/wpf-era.md` §4 / §"What survived"). ## Public interface ```ts interface Props { open: boolean title: string message?: string onConfirm: () => void onCancel: () => void } export default function ConfirmDialog(props: Props): JSX.Element | null ``` When `open === false`, returns `null`. When `open === true`, renders a centered modal over a dimmed backdrop. The component is fully controlled — it owns no `open` state itself. ## Internal logic - **Auto-focus**: when `open` flips to `true`, the Cancel button is focused via `cancelRef.current?.focus()` (see `useEffect` keyed on `open`). This makes Cancel the default keyboard target — a common pattern for destructive confirmations. - **Escape-to-cancel**: a `keydown` handler is attached to `window` while `open === true`; pressing `Escape` calls `onCancel()`. The listener is removed on unmount or when `open` flips to `false`. This is the **only** dismiss path other than the explicit Cancel/Confirm buttons (no backdrop click handler). - **Translation**: button labels use `t('common.cancel')` and `t('common.confirm')`. Title and message are passed in by the caller — the caller is responsible for translation. ## Dependencies - **Internal**: none. - **External**: `react` (`useEffect`, `useRef`), `react-i18next` (`useTranslation`). ## Consumers (intra-repo) From the §7a dependency graph in `_docs/02_document/00_discovery.md`: - `src/features/admin/AdminPage.tsx` — confirm before deleting users. - `src/features/annotations/MediaList.tsx` — confirm before deleting media. - `src/features/flights/FlightsPage.tsx` — confirm before deleting a flight. - `src/features/dataset/DatasetPage.tsx` — confirm before deleting dataset items. (`HelpModal.tsx` is the *non-confirm* sibling; it does NOT use `ConfirmDialog` and notably does **not** have Escape-to-close — see `src__components__HelpModal.md` §Notes.) ## Data models None. ## Configuration Tailwind tokens used: `bg-az-panel`, `border-az-border`, `text-az-text`, `bg-az-red`, `bg-az-bg`, `text-white`. All defined in `src/index.css` (per `_docs/02_document/00_discovery.md` §2a). `z-[100]` is the chosen stacking layer. No other modal currently competes with it; if a third-party library introduces a portal at `z-[200]+`, layering will need a docs entry. ## External integrations None. ## Security - **No backdrop dismissal**: clicking outside the dialog does NOT cancel. Combined with Escape-to-cancel and a default-focused Cancel button, this gives a deliberate, keyboard-safe destructive-action flow. - **No `aria-modal` / role="dialog"**: not annotated for screen readers; flag for Step 4 verification against the `_docs/ui_design/README.md` §"Confirmation dialogs" spec (which specifies modal semantics). ## Tests None. ## Notes / open questions - The "destructive" colour (`bg-az-red`) is hardcoded into the Confirm button. Some callers use `ConfirmDialog` for non-destructive confirms (e.g. flight selection in some flows — verify in B7); a future variant prop (`destructive: boolean`) would be cleaner. Defer to Step 8. - No `width` prop — the dialog is fixed at `w-80` (320px). On the mobile breakpoint defined in `_docs/ui_design/README.md` §"Responsive Breakpoints" (640px), this fits, but very long titles or multi-line messages will overflow. Flag for Step 4 cosmetic verification. - Escape handler attaches to `window`, not the dialog DOM. If two `ConfirmDialog`s are mounted simultaneously (the SPA never does this today, but it's not enforced), both would call their `onCancel()` on a single Escape press. Acceptable under current usage.