sábado, 28 de febrero de 2026

Dialog de shadcn/ui conteniendo un button

 Tengo un button de HTML como JSX dentro del contenido de un Dialog de shadcn/ui. Quiero que cada vez que hago click en el button se actualice un estado del contenedor y que el Dialog no se cierre. Pero en cambio, cada vez que hago click en el button el Dialog se cierra.


Entiendo perfectamente el problema. El comportamiento que experimentas se debe generalmente a que, dentro de los componentes de diálogo (como Radix UI, que usa Shadcn), ciertos elementos como botones dentro de un DialogFooter o el uso accidental de DialogClose provocan el cierre automático. Además, si el botón está dentro de un formulario, un evento de submit no controlado también podría interferir.

Para solucionar esto, debemos asegurar que el botón tenga el atributo type="button" explícito y que usemos e.preventDefault() y e.stopPropagation() para evitar que el evento "burbujee" hacia los controladores de cierre del Dialog.

Voy a generarte un ejemplo funcional que demuestra cómo mantener el estado y el diálogo abiertos simultáneamente.

Dialog de Shadcn con Estado Persistente
Feb 28, 8:09 PM

import React, { useState, useEffect } from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X, Plus, RotateCcw } from "lucide-react";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

// --- Utilidades de Estilo ---
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

// --- Componentes de Dialog (Versión Simplificada de Shadcn) ---
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;

const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));

const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-slate-800 bg-slate-900 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground text-slate-400">
<X className="h-4 w-4" />
<span className="sr-only">Cerrar</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));

// --- Aplicación Principal ---
export default function App() {
const [contador, setContador] = useState(0);

const incrementarContador = (e) => {
// IMPORTANTE: Prevenir que el evento se propague a componentes padres
// que podrían estar escuchando clics para cerrar el diálogo.
e.preventDefault();
e.stopPropagation();
setContador((prev) => prev + 1);
};

const reiniciarContador = (e) => {
e.preventDefault();
e.stopPropagation();
setContador(0);
};

return (
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-950 text-white font-sans p-4">
<div className="text-center space-y-4 max-w-md">
<h1 className="text-3xl font-bold tracking-tight">Solución de Persistencia</h1>
<p className="text-slate-400">
Haz clic en el botón de abajo para abrir el diálogo y probar el incremento del contador sin que se cierre la ventana.
</p>
<Dialog>
<DialogTrigger asChild>
<button className="px-6 py-3 bg-indigo-600 hover:bg-indigo-500 rounded-lg font-medium shadow-lg transition-all active:scale-95">
Abrir Panel de Control
</button>
</DialogTrigger>

<DialogContent>
<div className="space-y-4">
<div className="flex flex-col space-y-1.5 text-center sm:text-left">
<h2 className="text-lg font-semibold leading-none tracking-tight text-white">
Contador Interno
</h2>
<p className="text-sm text-slate-400">
Este botón actualiza el estado local del contenedor.
</p>
</div>

<div className="flex flex-col items-center justify-center p-8 bg-slate-950 rounded-xl border border-slate-800">
<span className="text-5xl font-mono font-bold text-indigo-400">
{contador}
</span>
<span className="text-xs uppercase tracking-widest text-slate-500 mt-2">
Clicks registrados
</span>
</div>

<div className="flex flex-col sm:flex-row gap-3 pt-4 border-t border-slate-800">
{/* REGLA DE ORO:
1. type="button" para evitar comportamiento de submit.
2. onClick con stopPropagation.
*/}
<button
type="button"
onClick={incrementarContador}
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-md text-sm font-medium transition-colors"
>
<Plus className="h-4 w-4" />
Incrementar
</button>

<button
type="button"
onClick={reiniciarContador}
className="inline-flex items-center justify-center gap-2 px-4 py-2.5 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-md text-sm font-medium transition-colors"
>
<RotateCcw className="h-4 w-4" />
Reset
</button>
</div>

<p className="text-[10px] text-slate-500 italic text-center">
El diálogo solo se cerrará si haces clic en la 'X' o fuera del recuadro.
</p>
</div>
</DialogContent>
</Dialog>
</div>
</div>
);
}


Puntos clave de la solución:

  1. Atributo type="button": Por defecto, los botones dentro de ciertos contextos (como formularios o bibliotecas de UI que inyectan formularios invisibles) pueden actuar como submit. Al especificar type="button", eliminamos cualquier comportamiento predeterminado de envío.

  2. e.stopPropagation(): Este es el paso más importante. Radix UI (la base de Shadcn) a veces escucha eventos de clic en niveles superiores para manejar cierres. Detener la propagación evita que el Dialog "se entere" de que hiciste clic en un botón que no es de cierre.

  3. Evitar el DialogClose: Asegúrate de que tu botón no esté envuelto por un componente <DialogClose />, ya que este componente inyecta automáticamente la lógica de cierre al hacer clic en su hijo.

He aplicado estas correcciones en el archivo generado para que puedas probar la interacción directamente. Sugerencias adicionales: revisa si el botón no está dentro de un elemento <a> o un DialogFooter que tenga comportamientos de cierre inyectados por CSS o JS.

No hay comentarios: