// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file AlertDialog.tsx * @input Uses React, StyleX, Dialog, Layout, Heading, Text, Button * @output Exports AlertDialog component, AlertDialogProps type * @position Core implementation; consumed by index.ts, tested by AlertDialog.test.tsx * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/AlertDialog/AlertDialog.doc.mjs (props table, features, examples) * - /packages/core/src/AlertDialog/AlertDialog.test.tsx (tests for new/changed behavior) * - /packages/core/src/AlertDialog/index.ts (exports if types change) * - /apps/storybook/stories/AlertDialog.stories.tsx (storybook stories) * - /packages/cli/assets/templates/blocks/components/AlertDialog/ (showcase blocks) */ import React, { useId, useCallback } from 'react'; import * as stylex from "../style-runtime.js"; import { Dialog } from "../Dialog/index.js"; import { Layout } from "../Layout/Layout.js"; import { LayoutContent } from "../Layout/LayoutContent.js"; import { LayoutFooter } from "../Layout/LayoutFooter.js"; import { Stack } from "../Stack/index.js"; import { Heading } from "../Heading/Heading.js"; import { Text } from "../Text/Text.js"; import { Button, type ButtonVariant } from "../Button/index.js"; import type { BaseProps } from "../BaseProps.js"; import { mergeProps } from "../utils/index.js"; import { themeProps } from "../utils/themeProps.js"; import { useTranslator } from "../i18n/index.js"; import { useMediaQuery } from "../hooks/index.js"; const SMALL_SCREEN_QUERY = '(max-width: 640px)'; const styles = { action: { ks0D6T: "x193iq5w", k7Eaqz: "xeuugli", $$css: true as const }, stackedAction: { kzqmXN: "xh8yej3", $$css: true as const } }; export interface AlertDialogProps extends BaseProps { ref?: React.Ref; /** * Whether the dialog is open. */ isOpen: boolean; /** * Renders alert dialog content inline without modal behavior. * For documentation previews and showcases only. The inline path is not a * modal, so it renders `role="group"` rather than `role="alertdialog"`. * @default false */ isInline?: boolean; /** * Callback fired when the dialog visibility changes. * Called with `false` when cancel is clicked or Escape is pressed. */ onOpenChange: (isOpen: boolean) => unknown; /** * Dialog title. Linked to the dialog via `aria-labelledby`. */ title: string; /** * Consequence description. Linked to the dialog via `aria-describedby`. */ description: string; /** * Label for the cancel button. Rendered as a ghost Button. * Clicking cancel calls `onOpenChange(false)`. * @default 'Cancel' */ cancelLabel?: string; /** * Label for the action button. */ actionLabel: string; /** * Variant for the action button. * @default 'destructive' */ actionVariant?: ButtonVariant; /** * Whether the action button shows a loading spinner. */ isActionLoading?: boolean; /** * Callback fired when the action button is clicked. * The dialog does NOT auto-close — call `onOpenChange(false)` when done. */ onAction: () => unknown; /** * The width of the dialog. * Numbers are treated as pixels, strings are used as-is. * @default 400 */ width?: number | string; } /** * A confirmation dialog for destructive or irreversible actions. * * Implements the WAI-ARIA APG Alert Dialog pattern: * https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/ * * Uses `role="alertdialog"` and requires explicit user action to dismiss. * Cannot be dismissed by clicking outside. Escape key triggers cancel. * Initial focus goes to the cancel button (least destructive action), pinned * with `data-autofocus` so it survives any change to the footer's order. * * The `isInline` preview path is not modal, so it renders `role="group"` * instead — the alertdialog role would promise modality it does not have. * * @example * ``` * { await deleteItem(); setIsOpen(false); }} * /> * ``` */ export function AlertDialog({ ref, isOpen, isInline, onOpenChange, title, description, cancelLabel: cancelLabelFromProps, actionLabel, actionVariant = 'destructive', isActionLoading, onAction, width = 400, xstyle, className, style, 'data-testid': testId, ...rest }: AlertDialogProps) { const t = useTranslator(); const cancelLabel = cancelLabelFromProps ?? t('@astryx.alertDialog.cancel'); const titleId = useId(); const descriptionId = useId(); // Width stays delegated to Dialog: this query exists only so AlertDialog can // keep narrow visual, DOM, and tab order aligned for its destructive/cancel // semantics without coupling action order to pointer or hover capability. const isSmallScreen = useMediaQuery(SMALL_SCREEN_QUERY); const handleCancel = useCallback(() => { onOpenChange(false); }, [onOpenChange]); const buttonActionStyle = [styles.action, isSmallScreen && styles.stackedAction]; const cancelButton =