primitives: Dialog.Trigger does not work if trigger is Dropdown.Item

Bug report

Current Behavior

A dropdown menu consists of several Dropdown.Item components. If I pass one of these Item components to a Dialog.Trigger, it does not open the dialog on click. If I pass plain text to the Dialog.Trigger, then it works.

Expected behavior

I expect Dropdown.Item to be a valid Dialog.Trigger.

Reproducible example

https://codesandbox.io/s/shy-hill-5ymyp0?file=/src/App.js

Your environment

See sandbox for package versions.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 10
  • Comments: 52 (4 by maintainers)

Most upvoted comments

I prevented the default event in the onSelect event of DropdownMenuItem, so the Dialog can be opened now. The code is as follows:

<DropdownMenu>
  <DropdownMenuTrigger> Dropdown Menu </DropdownMenuTrigger>
  <DropdownMenuContent>
    <Dialog>
      <DialogTrigger asChild>
        <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
          Test
        </DropdownMenuItem>
      </DialogTrigger>
      <DialogContent>
        This is a modal.
      </DialogContent>
    </Dialog>
  </DropdownMenuContent>
</DropdownMenu>

If anyone has come here from shadcn/ui dialog notes and wondering how to show different dialogs for different menu items

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="ghost" className="h-8 w-8 p-0">
      <span className="sr-only">Open menu</span>
      <MoreHorizontal className="h-4 w-4" />
    </Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="end">
    <DropdownMenuLabel>Actions</DropdownMenuLabel>
    <Dialog>
      <DialogTrigger>
        <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
          Update item
        </DropdownMenuItem>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Update form</DialogTitle>
          <DialogDescription>
            Here you can add fields to update your form
          </DialogDescription>
        </DialogHeader>
      </DialogContent>
    </Dialog>
    <DropdownMenuSeparator />
    <Dialog>
      <DialogTrigger>
        <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
          Delete item
        </DropdownMenuItem>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you sure absolutely sure?</DialogTitle>
          <DialogDescription>
            This action cannot be undone. This will permanently delete
            your account and remove your data from our servers.
          </DialogDescription>
        </DialogHeader>
      </DialogContent>
    </Dialog>
  </DropdownMenuContent>
</DropdownMenu>
 

Hi @monokizsolt, here some examples that can help

https://codesandbox.io/embed/r9sq1q

For anyone who are still having issue with this, I fixed it by using modal={false} on my DropdownMenu.Root component.

I tried the solution in the sandbox link but I was unable to use it in my code, not sure why. However this modal setting did the trick for me.

Hope it helps!

Hello,

Is there a way to have multiple Dropdown.Item components each with their own Dialog? With your solution only 1 dialog can be opened.

Thanks

Wrapping Dropdown with Dialog fixes this:

      <Dialog.Root>
        <DropdownMenu.Root>
          <DropdownMenu.Trigger>Dropdown Menu</DropdownMenu.Trigger>
          <DropdownMenu.Portal>
            <DropdownMenu.Content>
              <Dialog.Trigger>
                <DropdownMenu.Item>"Test"</DropdownMenu.Item>
              </Dialog.Trigger>
            </DropdownMenu.Content>
          </DropdownMenu.Portal>
        </DropdownMenu.Root>
        <Dialog.Portal>
          <Dialog.Overlay className="DialogOverlay" />
          <Dialog.Content className="DialogContent">
            This is a modal.
          </Dialog.Content>
        </Dialog.Portal>
      </Dialog.Root>

If you put the “modal” property to false, your DropdownMenuItem with the modal/dialog children will work normally

Fixing the problem with the DropdownMenu overlay not closing after click

<DropdownMenu modal={false}>
  <DropdownMenuTrigger asChild>
    <Button variant="ghost" className="h-8 w-8 p-0">
      <MoreHorizontal className="h-4 w-4" />
    </Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="end">
    <DropdownMenuItem
      onSelect={() => router.push(`tickets/edit`)}
    >
      Edit
    </DropdownMenuItem>
    <Dialog>
      <DialogTrigger asChild>
        <DropdownMenuItem>
          Children Button
        </DropdownMenuItem>
      </DialogTrigger>
      <DialogContent>
        This is a modal.
      </DialogContent>
    </Dialog>
  </DropdownMenuContent>
</DropdownMenu>

Not working for me. Dialog not opening. Using preventDefault works but doesn’t close the dropdown.

If you put the “modal” property to false, your DropdownMenuItem with the modal/dialog children will work normally

Fixing the problem with the DropdownMenu overlay not closing after click

<DropdownMenu modal={false}>
  <DropdownMenuTrigger asChild>
    <Button variant="ghost" className="h-8 w-8 p-0">
      <MoreHorizontal className="h-4 w-4" />
    </Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="end">
    <DropdownMenuItem
      onSelect={() => router.push(`tickets/edit`)}
    >
      Edit
    </DropdownMenuItem>
    <Dialog>
      <DialogTrigger asChild>
        <DropdownMenuItem>
          Children Button
        </DropdownMenuItem>
      </DialogTrigger>
      <DialogContent>
        This is a modal.
      </DialogContent>
    </Dialog>
  </DropdownMenuContent>
</DropdownMenu>
  1. make each dropdown item set some state on click
  2. pass that state into your dialog component.
  3. render different content inside the dialog component depending on the state

But if you mean open multiple dialogs at the same time, then I don’t know.

This is working for me.
Using https://codesandbox.io/embed/r9sq1q as a starting point, I adapted it for typescript and shadcn/ui.

'use client'

import { useState, useRef, ReactNode } from 'react'
import { Button } from '@/components/ui/button'
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogPortal,
  DialogTitle,
  DialogTrigger
} from '@/components/ui/dialog'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { CheckIcon, MoreHorizontal, PencilIcon, Trash2Icon, XIcon } from 'lucide-react'

export const DropdownActions = () => {
  const [dropdownOpen, setDropdownOpen] = useState(false)
  const [hasOpenDialog, setHasOpenDialog] = useState(false)
  const dropdownTriggerRef = useRef<null | HTMLButtonElement>(null)
  const focusRef = useRef<null | HTMLButtonElement>(null)

  const handleDialogItemSelect = () => {
    focusRef.current = dropdownTriggerRef.current
  }

  const handleDialogItemOpenChange = (open: boolean) => {
    setHasOpenDialog(open)
    if (open === false) {
      setDropdownOpen(false)
    }
  }

  return (
    <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen} modal={false}>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" size="xs">
          <MoreHorizontal />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent
        className="w-56"
        align="end"
        hidden={hasOpenDialog}
        onCloseAutoFocus={event => {
          if (focusRef.current) {
            focusRef.current.focus()
            focusRef.current = null
            event.preventDefault()
          }
        }}
      >
        <DialogItem
          triggerChildren={
            <>
              <PencilIcon className="mr-4 h-4 w-4" />
              <span>Edit Project</span>
            </>
          }
          onSelect={handleDialogItemSelect}
          onOpenChange={handleDialogItemOpenChange}
        >
          <DialogTitle className="DialogTitle">Edit</DialogTitle>
          <DialogDescription className="DialogDescription">
            Edit this record below.
          </DialogDescription>
          <p>…</p>
        </DialogItem>

        <DialogItem
          triggerChildren={
            <>
              <Trash2Icon className="mr-4 h-4 w-4" />
              <span>Delete Project</span>
            </>
          }
          onSelect={handleDialogItemSelect}
          onOpenChange={handleDialogItemOpenChange}
        >
          <DialogTitle className="DialogTitle">Delete</DialogTitle>
          <DialogDescription className="DialogDescription">
            Are you sure you want to delete this record?
          </DialogDescription>
        </DialogItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

type Props = {
  triggerChildren: ReactNode
  children: ReactNode
  onSelect: () => void
  onOpenChange: (open: boolean) => void
}

const DialogItem = ({ triggerChildren, children, onSelect, onOpenChange }: Props) => {
  return (
    <Dialog onOpenChange={onOpenChange}>
      <DialogTrigger asChild>
        <DropdownMenuItem
          className="p-3"
          onSelect={event => {
            event.preventDefault()
            onSelect && onSelect()
          }}
        >
          {triggerChildren}
        </DropdownMenuItem>
      </DialogTrigger>
      <DialogPortal>
        <DialogContent>
          {children}
          <DialogClose asChild>
            <button className="IconButton" aria-label="Close">
              <XIcon />
            </button>
          </DialogClose>
        </DialogContent>
      </DialogPortal>
    </Dialog>
  )
}

Not working for me. Dialog not opening. Using preventDefault works but doesn’t close the dropdown.

Same here.

I prevented the default event in the onSelect event of DropdownMenuItem, so the Dialog can be opened now. The code is as follows:

<DropdownMenu>
  <DropdownMenuTrigger> Dropdown Menu </DropdownMenuTrigger>
  <DropdownMenuContent>
    <Dialog>
      <DialogTrigger asChild>
        <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
          Test
        </DropdownMenuItem>
      </DialogTrigger>
      <DialogContent>
        This is a modal.
      </DialogContent>
    </Dialog>
  </DropdownMenuContent>
</DropdownMenu>

This solution is great, but it does not close the dropdown element, as clicking on the Dropdown Item would normally do. Is there any way to keep that functionality while also allowing to open the Dialog?

@juanrgon that’s clean ☺️ but just a heads up that you’re breaking a11y there so if you’re worried about that, i think the most relevant part is to make sure you return focus to the trigger when the dialog closes.

export function useDialog() {
  const [isOpen, setIsOpen] = useState(false);
  const triggerRef = React.useRef();
  
  function trigger() {
    setIsOpen(true);
  }

  function dismiss() {
    setIsOpen(false);
    triggerRef.current?.focus();
  }

  return {
    triggerProps: {
      ref: triggerRef,
      onClick: trigger,
    },
    dialogProps: {
      open: isOpen,
      onOpenChange: open => {
        if (open) trigger();
        else dismiss();
      },
    },
    trigger,
    dismiss,
  };
}
import { Dialog, DialogContent } from "@radix-ui/react-dialog";
import { useDialog } from "@/components/ui/use-dialog.tsx"

function MyComponent() {
  const infoDialog = useDialog();
  return (
    <>
      <button {...infoDialog.triggerProps}>Launch the info dialog</button>
      <Dialog {...infoDialog.dialogProps}>
        <DialogContent> Info </DialogContent>
      </Dialog>
    </>
  );
}

this is why radix usually recommends using the parts they provide instead of doing your own thing, because we can’t know for sure what a11y support we’re losing by breaking away from them.

Sorry for intruding in a closed issue, but I don’t really get WHY I should do this (nesting dialogs inside a dropdown)… why not doing what we normally do in react, that is defining whatever number of dialogs we want in another place and then controlling them using state vars? Maybe I’m missing something, but if it’s only to spare some state I think it isn’ worth it. Just my 2c 😉

If you put the “modal” property to false, your DropdownMenuItem with the modal/dialog children will work normally

Fixing the problem with the DropdownMenu overlay not closing after click

<DropdownMenu modal={false}>
  <DropdownMenuTrigger asChild>
    <Button variant="ghost" className="h-8 w-8 p-0">
      <MoreHorizontal className="h-4 w-4" />
    </Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="end">
    <DropdownMenuItem
      onSelect={() => router.push(`tickets/edit`)}
    >
      Edit
    </DropdownMenuItem>
    <Dialog>
      <DialogTrigger asChild>
        <DropdownMenuItem>
          Children Button
        </DropdownMenuItem>
      </DialogTrigger>
      <DialogContent>
        This is a modal.
      </DialogContent>
    </Dialog>
  </DropdownMenuContent>
</DropdownMenu>

Yep this fixed it for me, this should get more attention!

If anyone has come here from shadcn/ui dialog notes and wondering how to show different dialogs for different menu items

<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="ghost" className="h-8 w-8 p-0">
      <span className="sr-only">Open menu</span>
      <MoreHorizontal className="h-4 w-4" />
    </Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="end">
    <DropdownMenuLabel>Actions</DropdownMenuLabel>
    <Dialog>
      <DialogTrigger>
        <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
          Update item
        </DropdownMenuItem>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Update form</DialogTitle>
          <DialogDescription>
            Here you can add fields to update your form
          </DialogDescription>
        </DialogHeader>
      </DialogContent>
    </Dialog>
    <DropdownMenuSeparator />
    <Dialog>
      <DialogTrigger>
        <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
          Delete item
        </DropdownMenuItem>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you sure absolutely sure?</DialogTitle>
          <DialogDescription>
            This action cannot be undone. This will permanently delete
            your account and remove your data from our servers.
          </DialogDescription>
        </DialogHeader>
      </DialogContent>
    </Dialog>
  </DropdownMenuContent>
</DropdownMenu>
 

Brilliant! thank you

@Malin88 I have been using this so far, works for me. Found this here from shadcn/ui issues Example:


const [open, setOpen] = useState(false);

return <Dialog open={open} onOpenChange={setOpen} />

If anyone has come here from shadcn/ui dialog notes and wondering how to show different dialogs for different menu items …

But the expanded menu after opening Dialog does not hide, is there any solution for this?

@benoitgrelard Yes, I saw that, but I noticed that the dropmenu doesn’t close after the dialog opens.

Thank you ! A clean and nice solution !

In case this helps anyone, I’ve made my own hook for Dialogs in my app:

// components/ui/use-dialog.tsx

import { useState } from "react";

export function useDialog() {
  const [isOpen, setIsOpen] = useState(false);

  const trigger = () => setIsOpen(true);

  return {
    props: {
      open: isOpen,
      onOpenChange: setIsOpen,
    },
    trigger: trigger,
    dismiss: () => setIsOpen(false),
  };
}

I now use this hook to launch multiple dialogs from anywhere:

import { Dialog, DialogContent } from "@radix-ui/react-dialog";
import { useDialog } from "@/components/ui/use-dialog.tsx"

function MyComponent() {
  const infoDialog = useDialog();
  const warningDialog = useDialog();
  const errorDialog = useDialog();

  return (
    <>
      <button onClick={infoDialog.trigger}>Launch the info dialog</button>
      <button onClick={warningDialog.trigger}>Launch the warning dialog</button>
      <button onClick={errorDialog.trigger}>Launch the error dialog</button>

      <Dialog {...infoDialog.props}>
        <DialogContent> Info </DialogContent>
      </Dialog>

      <Dialog {...warningDialog.props}>
        <DialogContent> Warning </DialogContent>
      </Dialog>

      <Dialog {...errorDialog.props}>
        <DialogContent> Error </DialogContent>
      </Dialog>
    </>
  );
}

Since switching to this pattern, this issue is not really relevant to me anymore

I prevented the default event in the onSelect event of DropdownMenuItem, so the Dialog can be opened now. The code is as follows:

<DropdownMenu>
  <DropdownMenuTrigger> Dropdown Menu </DropdownMenuTrigger>
  <DropdownMenuContent>
    <Dialog>
      <DialogTrigger asChild>
        <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
          Test
        </DropdownMenuItem>
      </DialogTrigger>
      <DialogContent>
        This is a modal.
      </DialogContent>
    </Dialog>
  </DropdownMenuContent>
</DropdownMenu>

Você é muito fera, muito obrigado amigo!!

This probably needs to be truly an actually fixed, since this thread has been proposing half-working workarounds for a year now.

@piotrkulpinski i solved this by making both the Dialog and DropdownMenu controlled. then when the dialog closes, i manually close the dropdown menu

here’s an example:

function ItemTableRow({ item, onItemUpdated }: { item: Item; onItemUpdated: () => void }) {
  const [isMenuOpen, setIsMenuOpen] = useState(false);

  return (
    <DropdownMenu open={isMenuOpen} onOpenChange={(isOpen) => setIsMenuOpen(isOpen)}>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" className="h-8 w-8 p-0">
          <span className="sr-only">Open menu</span>
          <MoreHorizontal className="h-4 w-4" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        {/* EditItemDialog is just an abstraction around Dialog - onClose is a custom thing */}
        <EditItemDialog
          item={item}
          onItemSaved={onItemUpdated}
          onClose={() => setIsMenuOpen(false)}
        >
          <DropdownMenuItem
            onSelect={(e) => {
              e.preventDefault();
            }}
          >
            <Edit className="h-4 w-4 mr-2" />
            Edit
          </DropdownMenuItem>
        </EditItemDialog>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

it’s a bit wonky, but it works

copy.mp4 this above behavior

solved this by creating a replica popover component without the portal as both the dialog and popover from shadcn were using the body element

@AlamMena, here is the correct way to handle composition of Dialog and DropdownMenu: https://codesandbox.io/s/dropdownmenu-dialog-items-r9sq1q

If anyone has come here from shadcn/ui dialog notes and wondering how to show different dialogs for different menu items, and close the menu after closing the dialog you can use this simple example demo: ( <DropdownMenu modal={false} open={opendropdown1} onOpenChange={(v) => { closeDropdown1(v); }} > <DropdownMenuTrigger> <Button>Dropdown Menu</Button> </DropdownMenuTrigger> <DropdownMenuContent className={shadow-light100_dark100 bg-tertiary-light-dark border-none p-2 `} > <Dialog onOpenChange={closeDropdown1}> <DialogTrigger asChild> <DropdownMenuItem onSelect={(e) => e.preventDefault()}> Test option one </DropdownMenuItem> </DialogTrigger>

        <DialogContent
          className={`shadow-light100_dark100 bg-tertiary-light-dark  border-none p-2 `}
        >
          This is a modal one.
        </DialogContent>
      </Dialog>
      <Dialog onOpenChange={closeDropdown2}>
        <DialogTrigger asChild>
          <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
            Test option two
          </DropdownMenuItem>
        </DialogTrigger>

        <DialogContent
          className={`shadow-light100_dark100 bg-tertiary-light-dark  border-none p-2 `}
        >
          This is a modal two.
        </DialogContent>
      </Dialog>
      <Dialog onOpenChange={closeDropdown3}>
        <DialogTrigger asChild>
          <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
            Test option three
          </DropdownMenuItem>
        </DialogTrigger>

        <DialogContent
          className={`shadow-light100_dark100 bg-tertiary-light-dark  border-none p-2 `}
        >
          This is a modal three.
        </DialogContent>
      </Dialog>
    </DropdownMenuContent>
  </DropdownMenu>

) `

If the whole Dialogs are placed in the DropdownMenu, the Dialog will disappear after the Dropdown is closed, so a better way is to put the dialog at a higher level instead of using preventDefault and other workarounds.


function SomeDialog({ children }: { children: React.ReactNode }) {
  return <Dialog>
    {children}
    <SomeDialog.Content /> // <-- Place dialog content out of the dropdown
  </Dialog>;
}
SomeDialog.Trigger = DialogTrigger;
SomeDialog.Content = () => {
  return (
    <DialogContent>
      You Content
    </DialogContent>
  );
};
<SomeDialog> // <-- Dialog root
  <DropdownMenu>
    <DropdownMenuTrigger>Dropdown Menu</DropdownMenuTrigger>
    <DropdownMenuContent>
      
        <SomeDialog.Trigger asChild>
          <DropdownMenuItem> // <--  no need e.preventDefault()
            Test
          </DropdownMenuItem>
        </SomeDialog.Trigger>
      </Dialog>
    </DropdownMenuContent>
  </DropdownMenu>
</SomeDialog>

@pfurini for many reasons:

  • colocation and composition are core React principles
  • it makes code easier to delete. logic for controlling dialogs isn’t spread out throughout your codebase
  • state changes are isolated meaning only isolated areas on screen rerender
  • enables tree-shaking

a blog post i wrote on this topic might shed some light but it’s an oldie https://jjenzz.com/avoid-global-state-colocate/

If you are interested, I make a small wrapper to add dialog as a dropdown menu item.

https://codesandbox.io/p/sandbox/dropdown-dialog-item-ts-wfvn7k?file=%2Fsrc%2FDropdownDialog.tsx%3A28%2C46

By default, the dropdown menu is closed when Dialog or AlertDialog is closed. If you want the focus to return to the Dialog/AlertDialog dropdown item you can use the returnFocus prop in the DialogTriggerItem/AlertDialogTriggerItem

PS: I used Radix UI utility packages that are not exposed for public use, so it is at your own risk to install these libraries.

Packages

npm install @radix-ui/react-dropdown-menu @radix-ui/react-dialog @radix-ui/react-alert-dialog @radix-ui/react-use-controllable-state @radix-ui/react-context @radix-ui/react-compose-refs

Anatomy

// Dialog

<DropdownMenu.Dialog>
  <DropdownMenu.DialogTriggerItem />
  <DropdownMenu.DialogPortal>
    <DropdownMenu.DialogOverlay />
    <DropdownMenu.DialogContent>
      <DropdownMenu.DialogTitle />
      <DropdownMenu.DialogDescription />
      <DropdownMenu.DialogClose />
    </DropdownMenu.DialogContent>
  </DropdownMenu.DialogPortal>
</DropdownMenu.Dialog>

// AlertDialog

<DropdownMenu.AlertDialog>
  <DropdownMenu.AlertDialogTriggerItem />
  <DropdownMenu.AlertDialogPortal>
    <DropdownMenu.AlertDialogOverlay />
    <DropdownMenu.AlertDialogContent>
      <DropdownMenu.AlertDialogTitle />
      <DropdownMenu.AlertDialogDescription />
      <DropdownMenu.AlertDialogCancel />
      <DropdownMenu.AlertDialogAction />
    </DropdownMenu.AlertDialogContent>
  </DropdownMenu.AlertDialogPortal>
</DropdownMenu.AlertDialog>

Not working for me. Dialog not opening. Using preventDefault works but doesn’t close the dropdown.

Same here.

Still same issue

I prevented the default event in the onSelect event of DropdownMenuItem, so the Dialog can be opened now. The code is as follows:

<DropdownMenu>
  <DropdownMenuTrigger> Dropdown Menu </DropdownMenuTrigger>
  <DropdownMenuContent>
    <Dialog>
      <DialogTrigger asChild>
        <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
          Test
        </DropdownMenuItem>
      </DialogTrigger>
      <DialogContent>
        This is a modal.
      </DialogContent>
    </Dialog>
  </DropdownMenuContent>
</DropdownMenu>

That’s smart ! it fixed my problem

The escape key still causes buggy behavior here even when I use the above solutions. Has anyone found a fix for that?

Thank you!