Errori e fix
Shadcn: il Dialog si chiude quando lo apri da un Dropdown
Il menu Radix smonta la voce e restituisce il focus mentre il dialog si apre. Tre correzioni, con il compromesso di ciascuna.
In sintesi
- La causa è il ciclo di vita del menu: la voce che apre il dialog viene smontata mentre il dialog monta.
- La correzione più pulita è tenere il Dialog fuori dal DropdownMenu, controllato da uno stato.
- `onSelect={(e) => e.preventDefault()}` sblocca il caso, ma il menu resta aperto sotto al dialog.
Risposta breve: il menu chiude la voce che hai cliccato e smonta il subtree in cui vive il dialog. Tieni il <Dialog> fuori dal <DropdownMenu> e controllane l'apertura con uno stato: è la correzione che non lascia effetti collaterali.
Cosa succede, passo per passo
- Clicchi una voce del menu: Radix avvia la chiusura del menu.
- La voce viene smontata e il focus torna al trigger.
- Il dialog, che era figlio della voce, viene smontato insieme a lei.
- Lo stato del dialog resta aperto ma non c'è più un nodo che lo renderizzi.
Il risultato visibile è un dialog che appare per un frame e sparisce, oppure che non appare affatto.
Perché la voce sparisce
Il DropdownMenuContent di Radix non resta nel DOM quando il menu è chiuso: usa una presence che monta il contenuto all'apertura e lo smonta a chiusura completata. Il Dialog scritto dentro una DropdownMenuItem è quindi figlio di un nodo che sta per sparire. Radix chiude il menu al click, restituisce il focus al trigger con onCloseAutoFocus e smonta il contenuto: il dialog se ne va con lui, anche se il suo stato è ancora aperto.
Il menu è anche modale per impostazione predefinita. Mentre è aperto, Radix applica pointer-events: none al resto del documento e intrappola il focus nel contenuto. Sono due comportamenti indipendenti dalla smontatura, e la seconda parte spiega perché alcune correzioni sembrano funzionare e poi bloccano i click.
La correzione pulita: stato fuori dal menu
"use client";
import { useState } from "react";
import { DropdownMenu, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuContent } from "@/components/ui/dropdown-menu";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
export function RowActions() {
const [open, setOpen] = useState(false);
return (
Azioni
setOpen(true)}>
Rinomina
Rinomina
);
}
I due componenti sono fratelli: la chiusura del menu non tocca il dialog. È anche il modo più semplice da testare, perché lo stato è uno solo.
Le alternative, con il loro costo
onSelect={(e) => e.preventDefault()}: impedisce la chiusura della voce, ma il menu resta aperto sotto al dialog e va chiuso a mano.<DropdownMenu modal={false}>: cambia la gestione di focus e scroll; utile se il problema è il blocco dello scroll, non la smontatura.- Un solo stato condiviso per menu e dialog: riduce i nodi, ma rende più difficile capire quale dei due controlla cosa.
Il passo diagnostico
{
// when the close comes from focus return, this fires right after the open
console.log("dialog", next, new Error().stack);
setOpen(next);
}}
>
Apri il dialog da tastiera con Invio e leggi la sequenza. Se vedi true seguito da false nello stesso tick, la chiusura arriva dal ritorno del focus e non da uno smontaggio: la traccia dello stack indica quale gestore l'ha innescata. Se onOpenChange viene chiamato una volta sola con true e il dialog non compare, il problema è a monte, nel ramo che lo renderizza.
Quando la correzione non funziona
Se il dialog continua a chiudersi, il problema può essere un onOpenChange che riceve false dal focus esterno: quando il menu restituisce il focus, Radix può interpretarlo come click fuori. In quel caso apri il dialog dopo il completo smontaggio del menu, per esempio con requestAnimationFrame nel gestore, oppure con onCloseAutoFocus sul menu.
Quando la correzione comune peggiora le cose
La correzione più citata è e.preventDefault() su onSelect: impedisce la chiusura della voce e lascia il menu aperto. Con il menu modale, però, il pointer-events: none sul resto della pagina resta attivo, quindi il dialog appare ma non risponde al mouse: i pulsanti non si premono e il campo di testo non prende il focus. Il sintomo cambia da «il dialog si chiude» a «il dialog non si usa», ed è più difficile da collegare alla causa. Chi reagisce aggiungendo modal={false} al menu perde la trappola del focus, e il Tab esce dal menu mentre il dialog è aperto.
Un limite con un nome
Limite dichiarato del rimedio con requestAnimationFrame: non ha un evento su cui agganciarsi. Aspetta un frame, non la fine dell'animazione di uscita del menu, quindi con un'animazione più lunga o un dispositivo lento il dialog si apre mentre il contenuto del menu è ancora montato, e la chiusura ritorna. Funziona, ma in modo probabilistico: il rimedio affidabile resta spostare il dialog fuori dal menu, dove l'ordine dei montaggi non conta.
Come prevenirlo
- Regola: overlay annidati no. Un dialog non vive dentro un menu.
- Testa da tastiera: apri il menu, seleziona con Invio, verifica che il focus entri nel dialog.
- Se il menu serve solo per aprire azioni, valuta un
Popovercon contenuto statico: meno cicli di vita da coordinare. - Tieni un solo stato per l'apertura e passalo al dialog come prop controllata: due fonti di verità producono proprio questo bug.
Dove continuare
Nel catalogo: dialog block con preview, sidebar con menu annidati e tutti i blocchi. Le guide: i migliori dialog block misurati e checklist di accessibilità; il confronto: Radix, Base UI e React Aria.
Domande frequenti
Perché il dialog si apre e si chiude subito?
Il menu chiude la propria voce, restituisce il focus e smonta il subtree in cui vive il dialog. Il dialog viene smontato insieme al resto.
`e.preventDefault()` su onSelect è sufficiente?
Sblocca il caso più comune, ma lascia il menu aperto sotto. Per un dialog modale è meglio spostare il Dialog fuori dal menu.
`modal={false}` sul DropdownMenu aiuta?
Sì nei casi in cui il problema è il blocco dello scroll e la gestione del focus, ma perdi il comportamento modale del menu: valuta il compromesso.
Succede anche con Popover?
Sì, ogni primitiva che smonta il contenuto alla chiusura. La regola è la stessa: controlla lo stato del dialog dall'esterno.
Come verifico che il dialog abbia il focus giusto?
Apri da tastiera, premi Tab e controlla che il focus resti dentro il dialog. Poi chiudi con Esc e verifica che torni sulla voce del menu.