Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Shadcn Dialog inside of Dropdown closes automatically

I use shadcn in my next.js 13 project. I want to have a dropdown with the option to edit or delete an entry. When the user clicks on "delete" a dialog should pop up and ask them for a confirmation. However, the dialog only shows for about 0.5 seconds before it closes together with the dropdown. How can I prevent that from happening?

Here is the example on codesandbox: Codesandbox

This is the code:

    <DropdownMenu>
      <DropdownMenuTrigger>
        <p>Trigger</p>
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <Dialog>
          <DropdownMenuLabel>Edit Entry</DropdownMenuLabel>
          <DropdownMenuSeparator />
          <DropdownMenuItem
            onClick={() => conosle.log("Navigate to edit page")}
          >
            Edit
          </DropdownMenuItem>
          <DialogTrigger>
            <DropdownMenuItem>Delete</DropdownMenuItem>
          </DialogTrigger>
          <DialogContent>
            <DialogHeader>
              <DialogTitle>Are you sure?</DialogTitle>
              <DialogDescription>
                Do you want to delete the entry? Deleting this entry cannot be
                undone.
              </DialogDescription>
            </DialogHeader>
            <DialogFooter>
              <DialogClose asChild>
                <Button variant="outline">Cancel</Button>
              </DialogClose>
              <Button>Delete</Button>
            </DialogFooter>
          </DialogContent>
        </Dialog>
      </DropdownMenuContent>
    </DropdownMenu>
like image 214
Jon Jampen Avatar asked Sep 12 '25 15:09

Jon Jampen


2 Answers

What is the problem?

When you click any <DropdownMenuItem />, It will trigger the action (onClick) and close (unmount) the <DropdownMenuContent /> which includes the <DialogContent /> so it'll be unmounted with it.

Solutions

1. Move the <DialogContent /> outside of the <DropdownMenuContent />

// ...
export default function App() {
  return (
    <Dialog> {/* 🔴 The dialog provider outside of the DropdownMenuContent */}
      <DropdownMenu>
        <DropdownMenuTrigger>
          <p>Trigger</p>
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem>
            <DialogTrigger>
              Open Popup
            </DialogTrigger>
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      {/* 🔴 DialogContent ouside of DropdownMenuContent */}
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you sure?</DialogTitle>
          <DialogDescription>
            Do you want to delete the entry? Deleting this entry cannot be
            undone.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="outline">Cancel</Button>
          </DialogClose>
          <Button>Delete</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

This solution works well if you have a single item triggers dialog. But what if you have multiple dialogs?

2. Multiple dialogs

Move your dialog outside the <DropdownMenuContent />, create a state for each one:

const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)

then remove any <DialogTrigger />, add onClick instead

<DropdownMenuItem onClick={() => setIsEditDialogOpen(true)}>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsDeleteDialogOpen(true)}>Delete</DropdownMenuItem>

In your dialog add

<Dialog open={isEditDialogOpen || isDeleteDialogOpen} 
        onOpenChange={isEditDialogOpen ? 
            setIsEditDialogOpen : setIsDeleteDialogOpen}>
...
</Dialog>

If you don't want to make it controlled and can render two trigger buttons, you can render two separate dialogs:

<Dialog>
  <DialogTrigger>Edit Post</DialogTrigger>
  <DialogContent>
     Content 
  </DialogContent>
</Dialog>

<Dialog>
  <DialogTrigger>Edit Post</DialogTrigger>
  <DialogContent>
    Content 
  </DialogContent>
</Dialog>
like image 197
Ahmed Abdelbaset Avatar answered Sep 15 '25 04:09

Ahmed Abdelbaset


You can prevent the default behavior of the onClick event which I believe is renamed to onSelect (https://www.radix-ui.com/primitives/docs/components/dropdown-menu) on DropdownMenuItem.

This way you can extract Dialog logic into a separate component and simply include it inside your Dropdown wrapper:

// {...}
<DropdownMenu>
  <DropdownMenuTrigger>
    <p>Trigger</p>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuLabel>Edit Entry</DropdownMenuLabel>
    <DropdownMenuSeparator />
    <DropdownMenuItem onSelect={() => console.log("Navigate to edit page")}>
      Edit
    </DropdownMenuItem>
    <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
      <YourCustomDialog />
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>;
// {...}

const YourCustomDialog = () => {
  return (
    <Dialog>
      <DialogTrigger>Delete</DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you sure?</DialogTitle>
          <DialogDescription>
            Do you want to delete the entry? Deleting this entry cannot be
            undone.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="outline">Cancel</Button>
          </DialogClose>
          <Button>Delete</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
};

The drawback of this implementation is that the dropdown menu stays open while you interact with Dialog, though you can reuse YourCustomDialog component.

like image 30
Mateusz Lasoń Avatar answered Sep 15 '25 04:09

Mateusz Lasoń