Awesome Cursor Rules
uialert-dialogsource: ok · refreshed
Alert Dialog
Alert dialog for Tile UI.
npx shadcn add "https://react.tileui.zmorg.cn/r/alert-dialog.json"free
Interaction
modal
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightui-primitives@radix-ui/react-slot
Registry dependencies
@tile-ui/corebutton@tile-ui/portal@tile-ui/styles
Files3 · source cached 10/2/2026, 12:45:40 AM
packages/react/src/components/alert-dialog/alert-dialog.tsx
import React, { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Slot } from '@radix-ui/react-slot';
import type { ButtonSize, ButtonVariant } from '../lib/core';
import { alertDialogStyleKeys, getAlertDialogState } from '../lib/core';
import type { AlertDialogBaseProps, AlertDialogSize } from '../lib/core';
import { Button } from '../button';
import { usePortalContainer, type PortalContainer } from '../portal';
import styles from './alert-dialog.module.scss';
interface AlertDialogContextValue {
open: boolean;
setOpen: (open: boolean) => void;
close: () => void;
titleId: string;
descriptionId: string;
}
const AlertDialogContext = createContext<AlertDialogContextValue | null>(null);
function useAlertDialogContext(): AlertDialogContextValue {
const context = useContext(AlertDialogContext);
if (!context) {
throw new Error('AlertDialog sub-components must be used within <AlertDialog>.');
}
return context;
}
export interface AlertDialogProps extends AlertDialogBaseProps {
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
children?: React.ReactNode;
}
function AlertDialog({ open, defaultOpen = false, onOpenChange, children }: AlertDialogProps) {
const [internalOpen, setInternalOpen] = useState(defaultOpen);
const onOpenChangeRef = useRef(onOpenChange);
useEffect(() => {
onOpenChangeRef.current = onOpenChange;
});
const setOpen = useCallback(
(next: boolean) => {
if (open === undefined) {
setInternalOpen(next);
}
onOpenChangeRef.current?.(next);
},
[open],
);
const close = useCallback(() => {
setOpen(false);
}, [setOpen]);
const isOpen = open !== undefined ? open : internalOpen;
const titleId = useId();
const descriptionId = useId();
return <AlertDialogContext.Provider value={{ open: isOpen, setOpen, close, titleId, descriptionId }}>{children}</AlertDialogContext.Provider>;
}
AlertDialog.displayName = 'AlertDialog';
export interface AlertDialogTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
}
const AlertDialogTrigger = React.forwardRef<HTMLButtonElement, AlertDialogTriggerProps>(({ asChild = false, className = '', children, onClick, ...props }, ref) => {
const context = useAlertDialogContext();
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
type={asChild ? undefined : 'button'}
className={className}
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event);
context.setOpen(true);
}}
{...props}>
{children}
</Comp>
);
});
AlertDialogTrigger.displayName = 'AlertDialogTrigger';
export interface AlertDialogOverlayProps extends React.HTMLAttributes<HTMLDivElement> {}
const AlertDialogOverlay = React.forwardRef<HTMLDivElement, AlertDialogOverlayProps>(({ className = '', onClick, ...props }, ref) => {
const context = useAlertDialogContext();
if (!context.open) {
return null;
}
return (
<div
ref={ref}
data-state={getAlertDialogState(context.open)}
className={`${styles[alertDialogStyleKeys.overlay]} ${className}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
onClick?.(event);
context.close();
}}
{...props}
/>
);
});
AlertDialogOverlay.displayName = 'AlertDialogOverlay';
export interface AlertDialogContentProps extends React.HTMLAttributes<HTMLDivElement> {
size?: AlertDialogSize;
container?: PortalContainer;
}
const AlertDialogContent = React.forwardRef<HTMLDivElement, AlertDialogContentProps>(({ className = '', size = 'default', container, children, ...props }, ref) => {
const { open, close, titleId, descriptionId } = useAlertDialogContext();
const portalContainer = usePortalContainer(container);
const contentRef = useRef<HTMLDivElement | null>(null);
function setContentRef(element: HTMLDivElement | null) {
contentRef.current = element;
if (typeof ref === 'function') {
ref(element);
} else if (ref) {
ref.current = element;
}
}
useLayoutEffect(() => {
if (!open) {
return;
}
const previouslyFocused = document.activeElement as HTMLElement | null;
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
close();
return;
}
if (event.key === 'Tab') {
const container = contentRef.current;
if (!container) {
return;
}
const focusables = Array.from(
container.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
),
).filter((el) => el.getClientRects().length > 0);
if (focusables.length === 0) {
event.preventDefault();
return;
}
const first = focusables[0];
const last = focusables[focusables.length - 1];
const active = document.activeElement;
if (event.shiftKey) {
if (active === first || active === container || !container.contains(active)) {
event.preventDefault();
last.focus();
}
} else if (active === last || !container.contains(active)) {
event.preventDefault();
first.focus();
}
}
};
document.addEventListener('keydown', handleKeyDown);
const contentEl = contentRef.current;
if (contentEl) {
contentEl.focus();
}
return () => {
document.body.style.overflow = previousOverflow;
document.removeEventListener('keydown', handleKeyDown);
previouslyFocused?.focus();
};
}, [close, open]);
if (!open || !portalContainer) {
return null;
}
return createPortal(
<>
<AlertDialogOverlay />
<div
ref={setContentRef}
role="alertdialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={descriptionId}
tabIndex={-1}
data-state={getAlertDialogState(open)}
data-size={size}
className={`${styles[alertDialogStyleKeys.content]} ${className}`}
{...props}>
{children}
</div>
</>,
portalContainer,
);
});
AlertDialogContent.displayName = 'AlertDialogContent';
export interface AlertDialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
const AlertDialogHeader = React.forwardRef<HTMLDivElement, AlertDialogHeaderProps>(({ className = '', children, ...props }, ref) => {
return (
<div ref={ref} className={`${styles[alertDialogStyleKeys.header]} ${className}`} {...props}>
{children}
</div>
);
});
AlertDialogHeader.displayName = 'AlertDialogHeader';
export interface AlertDialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {}
const AlertDialogFooter = React.forwardRef<HTMLDivElement, AlertDialogFooterProps>(({ className = '', children, ...props }, ref) => {
return (
<div ref={ref} className={`${styles[alertDialogStyleKeys.footer]} ${className}`} {...props}>
{children}
</div>
);
});
AlertDialogFooter.displayName = 'AlertDialogFooter';
export interface AlertDialogTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {}
const AlertDialogTitle = React.forwardRef<HTMLHeadingElement, AlertDialogTitleProps>(({ className = '', children, ...props }, ref) => {
const context = useAlertDialogContext();
return (
<h2 ref={ref} id={context.titleId} className={`${styles[alertDialogStyleKeys.title]} ${className}`} {...props}>
{children}
</h2>
);
});
AlertDialogTitle.displayName = 'AlertDialogTitle';
export interface AlertDialogDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {}
const AlertDialogDescription = React.forwardRef<HTMLParagraphElement, AlertDialogDescriptionProps>(({ className = '', children, ...props }, ref) => {
const context = useAlertDialogContext();
return (
<p ref={ref} id={context.descriptionId} className={`${styles[alertDialogStyleKeys.description]} ${className}`} {...props}>
{children}
</p>
);
});
AlertDialogDescription.displayName = 'AlertDialogDescription';
export interface AlertDialogActionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
}
const AlertDialogAction = React.forwardRef<HTMLButtonElement, AlertDialogActionProps>(
({ className = '', variant = 'default', size = 'default', children, onClick, ...props }, ref) => {
const context = useAlertDialogContext();
return (
<Button
ref={ref}
type="button"
variant={variant}
size={size}
className={className}
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event);
context.close();
}}
{...props}>
{children}
</Button>
);
},
);
AlertDialogAction.displayName = 'AlertDialogAction';
export interface AlertDialogCancelProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
}
const AlertDialogCancel = React.forwardRef<HTMLButtonElement, AlertDialogCancelProps>(
({ className = '', variant = 'outline', size = 'default', children, onClick, ...props }, ref) => {
const context = useAlertDialogContext();
return (
<Button
ref={ref}
type="button"
variant={variant}
size={size}
className={className}
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event);
context.close();
}}
{...props}>
{children}
</Button>
);
},
);
AlertDialogCancel.displayName = 'AlertDialogCancel';
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogOverlay,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};
export default AlertDialog;
packages/styles/scss/components/alert-dialog.module.scss
@use '../../../styles/variables/colors' as *;
// 半透明遮罩
.overlay {
position: fixed;
inset: 0;
z-index: $z-modal;
background-color: $overlay;
&[data-state='open'] {
animation: alert-dialog-overlay-in $transition-normal;
}
&[data-state='closed'] {
opacity: 0;
}
}
@keyframes alert-dialog-overlay-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
// 弹层内容面板
.content {
position: fixed;
top: 50%;
left: 50%;
z-index: $z-modal;
display: grid;
width: 100%;
max-width: calc(100% - 2rem);
transform: translate(-50%, -50%);
gap: $spacing-4;
border: 1px solid $border;
border-radius: $radius-lg;
background-color: $card;
color: $card-foreground;
padding: $spacing-6;
box-shadow: $shadow-lg;
outline: none;
@media (min-width: 640px) {
max-width: 32rem;
}
&[data-size='sm'] {
max-width: 20rem;
}
&[data-state='open'] {
animation: alert-dialog-content-in $transition-normal;
}
&[data-state='closed'] {
opacity: 0;
transform: translate(-50%, -50%) scale(0.95);
}
}
@keyframes alert-dialog-content-in {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.95);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
.header {
display: flex;
flex-direction: column;
align-items: center;
gap: $spacing-1_5;
text-align: center;
@media (min-width: 640px) {
.content[data-size='default'] & {
align-items: flex-start;
text-align: left;
}
}
}
.footer {
display: flex;
flex-direction: column-reverse;
gap: $spacing-2;
@media (min-width: 640px) {
.content[data-size='default'] & {
flex-direction: row;
justify-content: flex-end;
}
}
}
.title {
margin: 0;
font-size: $text-lg;
font-weight: $font-semibold;
line-height: $leading-tight;
color: $foreground;
}
.description {
margin: 0;
font-size: $text-sm;
line-height: $leading-normal;
color: $muted-foreground;
}
packages/react/src/components/alert-dialog/index.ts
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogOverlay,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
type AlertDialogProps,
type AlertDialogTriggerProps,
type AlertDialogOverlayProps,
type AlertDialogContentProps,
type AlertDialogHeaderProps,
type AlertDialogFooterProps,
type AlertDialogTitleProps,
type AlertDialogDescriptionProps,
type AlertDialogActionProps,
type AlertDialogCancelProps,
} from './alert-dialog';