Dialog
Un modal en la capa superior con foco atrapado, botón de cierre anidado y acciones al final.
Instalación
$ pnpm dlx shadcn@latest add @ssntl/dialogUso
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
<Dialog>
<DialogTrigger asChild><Button variant="destructive">Eliminar producto</Button></DialogTrigger>
<DialogContent closeLabel="Cerrar">
<DialogTitle>¿Eliminar Taza de cerámica?</DialogTitle>
<DialogDescription>Sale de tu tienda y de tu inventario.</DialogDescription>
<DialogFooter>
<DialogClose asChild><Button variant="secondary">Conservar producto</Button></DialogClose>
<Button variant="destructive" onClick={eliminar}>Eliminar producto</Button>
</DialogFooter>
</DialogContent>
</Dialog>Reglas
Lo que este componente espera de ti, tal como se instala en components/ui/dialog.md junto al código. El archivo completo agrega cuándo usarlo, teclado, los fundamentos de UI que aplica y cada regla compartida (en inglés).
## Reglas de Dialog
- **Profundidad modal.** Elevation-4 sobre un velo al 40%, la capa superior; la página detrás queda inerte.
- El foco entra y queda atrapado; Escape y el botón de cierre lo descartan; el foco vuelve al disparador (Radix).
- **Ubicación de acciones.** Las acciones van al final, la que confirma de última, en cada diálogo.
- Un diálogo destructivo dice qué elimina y nombra ambos botones con verbos (Conservar producto, Eliminar producto), nunca Aceptar y Cancelar.
- **Concéntrico.** El botón de cierre de 48px queda a 8px de la esquina de 32px.
- Nunca más alto que la pantalla: pon el contenido largo en `DialogBody`, que se desplaza solo mientras el título y las acciones quedan a la vista; el desplazamiento nunca llega a la página de atrás.Código
"use client"
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { CloseButton } from "@/registry/ssntl/ui/close-button"
// A modal dialog (Radix): focus moves in and is trapped, Escape and the
// close button dismiss it, focus returns to the trigger, and the page behind
// is inert. Modal depth: elevation 4 over a scrim, the highest layer.
// Concentric: the close button (48px) sits 8px from a 32px corner.
// Action placement: the footer keeps actions at the end, the confirming one
// last, in every dialog.
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogClose = DialogPrimitive.Close
function DialogContent({
className,
children,
closeLabel = "Close dialog",
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
/** The close button's accessible name. */
closeLabel?: string
}) {
return (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className="fixed inset-0 z-50 grid place-items-center overscroll-contain bg-black/40 p-4 data-[state=closed]:pop-out data-[state=open]:pop-in"
>
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
// Never taller than the screen: long content scrolls in DialogBody
// while the title and the actions stay in view.
"relative flex max-h-[calc(100dvh-2rem)] w-full max-w-md flex-col gap-2 rounded-lg bg-card p-6 pe-16 text-left text-card-foreground elevation-4 [--pop-duration:var(--duration-panel)]",
"data-[state=closed]:pop-out data-[state=open]:pop-in",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close asChild>
<CloseButton label={closeLabel} className="absolute end-2 top-2" />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Overlay>
</DialogPrimitive.Portal>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-heading", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-body text-pretty text-muted-foreground", className)}
{...props}
/>
)
}
/** Long content: scrolls on its own so the actions never leave the screen. */
function DialogBody({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-body"
className={cn(
"-me-10 min-h-0 overflow-y-auto overscroll-contain pe-10",
className
)}
{...props}
/>
)
}
/** Actions at the end; the one that confirms goes last. */
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-me-10 mt-4 flex flex-wrap items-center justify-end gap-2",
className
)}
{...props}
/>
)
}
export {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
DialogTrigger,
}