ui
ui copied to clipboard
Multiple Dialog in Context Menu
Hello, I would like to put 2 alert dialog and a dialog in my context menu. The problem is that the dialog closes instantly when it opens.This is because the context menu is closing. There is a discussion on RadixUI which allows to fix this but for only 1 dialog (https://github.com/radix-ui/primitives/discussions/1436). I would like to add several.
Same issue, need to have multiple dialogs in dropdown menu
@isaacdarcilla You can do that with the dropdown menu https://github.com/radix-ui/primitives/discussions/1436#discussioncomment-2898397 But it doesn't work with the context menu...
Hello, I would like to put 2 alert dialog and a dialog in my context menu. The problem is that the dialog closes instantly when it opens.This is because the context menu is closing. There is a discussion on RadixUI which allows to fix this but for only 1 dialog (radix-ui/primitives#1436). I would like to add several.
have you solve the issue? I meet the same problem ...
I've encountered a partial solution for this issue. Here's an example code snippet:
<Dialog>
<ContextMenu>
<ContextMenuTrigger>
Right-click me
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>
<DialogTrigger>
Edit Details
</DialogTrigger>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<DialogContent>I am here</DialogContent>
</Dialog>
However, as you can see, we currently only have one dialog here. I'm still waiting for more helpful responses. Meanwhile, feel free to use this as an emergency solution.
I've encountered a partial solution for this issue. Here's an example code snippet:
<Dialog> <ContextMenu> <ContextMenuTrigger> Right-click me </ContextMenuTrigger> <ContextMenuContent> <ContextMenuItem> <DialogTrigger> Edit Details </DialogTrigger> </ContextMenuItem> </ContextMenuContent> </ContextMenu> <DialogContent>I am here</DialogContent> </Dialog>
However, as you can see, we currently only have one dialog here. I'm still waiting for more helpful responses. Meanwhile, feel free to use this as an emergency solution.
You can see this issue and write as follow. Hope this helps.
<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>
I've encountered a partial solution for this issue. Here's an example code snippet:
<Dialog> <ContextMenu> <ContextMenuTrigger> Right-click me </ContextMenuTrigger> <ContextMenuContent> <ContextMenuItem> <DialogTrigger> Edit Details </DialogTrigger> </ContextMenuItem> </ContextMenuContent> </ContextMenu> <DialogContent>I am here</DialogContent> </Dialog>
However, as you can see, we currently only have one dialog here. I'm still waiting for more helpful responses. Meanwhile, feel free to use this as an emergency solution.
You can see this issue and write as follow. Hope this helps.
<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>
This method has no effect on the ContextMenu, because when you right-click on the ContextMenuItem, it loses its mount and the dialog disappears quickly
- To activate the Dialog component from within a Context Menu or Dropdown Menu, you must encase the Context Menu or Dropdown Menu component in the Dialog component. For more information, refer to the linked issue here.
<Dialog>
<ContextMenu>
<ContextMenuTrigger>Right click</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Open</ContextMenuItem>
<ContextMenuItem>Download</ContextMenuItem>
<DialogTrigger asChild>
<ContextMenuItem>
<span>Delete</span>
</ContextMenuItem>
</DialogTrigger>
</ContextMenuContent>
</ContextMenu>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. Are you sure you want to permanently
delete this file from our servers?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button type="submit">Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
- For a Context Menu or Dropdown Menu where different menu items opens different dialogs, you actually don't need multiple dialogs. Insted, you can implement a logic that renders different components inside a single Dialog.
I'm using a React state to choose between different dialog contents.
enum Dialogs {
dialog1 = 'dialog1',
dialog2 = 'dialog2',
}
const [dialog, setDialog] = useState()
// return
<Dialog>
<DropdownMenu>
<DropdownMenuTrigger>
Click here
<DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>
Label
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DialogTrigger
asChild
onClick={() => {
setDialog(Dialogs.dialog1)
}}
>
<DropdownMenuItem>
Dialog 1
</DropdownMenuItem>
</DialogTrigger>
<DialogTrigger
asChild
onClick={() => {
setDialog(Dialogs.dialog2)
}}
>
<DropdownMenuItem>
Dialog 2
</DropdownMenuItem>
</DialogTrigger>
</DropdownMenuContent>
</DropdownMenu>
<DialogContent>
{dialog === Dialogs.dialog1
? <Dialog1Component />
: <Dialog2Component />
}
</DialogContent>
</Dialog>
I just made a way around it, thanks to @victorassiso for suggesting using a state.
What I made is took that state and put in context. So now I can use multiple dialogs and triggers inside ContextMenu
or DropdownMenu
:
import { Slot, SlotProps } from "@radix-ui/react-slot";
type Maybe<T> = T | null | undefined;
const MultiDialogContainerContext = createContext<unknown>(null);
MultiDialogContainerContext.displayName = "MultiDialogContainerContext";
export const useMultiDialog = <T = unknown,>() => {
const s = useContext(MultiDialogContainerContext);
if (!s)
throw new Error(
"Cannot use 'useMultiDialog' outside 'MultiDialogProvider'.",
);
return s as [Maybe<T>, React.Dispatch<React.SetStateAction<Maybe<T>>>];
};
export function MultiDialogTrigger<T>({
value,
onClick,
...props
}: SlotProps &
React.RefAttributes<HTMLElement> & {
value: T;
}) {
const [, open] = useMultiDialog();
const oc = useCallback<React.MouseEventHandler<HTMLElement>>(
(e) => {
open(value);
onClick && onClick(e);
},
[value, onClick],
);
return <Slot onClick={oc} {...props} />;
}
export function MultiDialogContainer<T>({
value,
children,
}: {
value: T;
children: React.ReactNode;
}) {
const [opened] = useMultiDialog();
return opened === value ? children ?? null : null;
}
type Builder<T> = {
Trigger: (
...args: Parameters<typeof MultiDialogTrigger<T>>
) => React.ReactNode;
Container: (
...args: Parameters<typeof MultiDialogContainer<T>>
) => React.ReactNode;
};
const builder = {
Trigger: MultiDialogTrigger,
Container: MultiDialogContainer,
};
export const MultiDialogProvider = <T,>({
defaultOpen = null,
children,
}: {
defaultOpen?: T | null;
children?: React.ReactNode | ((builder: Builder<T>) => React.ReactNode);
}) => {
const [state, setState] = useState<T | null>(defaultOpen);
return (
<MultiDialogContainerContext.Provider value={[state, setState]}>
<Dialog
open={state != null}
onOpenChange={(v) => {
if (!v) setState(null);
}}
>
{typeof children === "function" ? children(builder) : children}
</Dialog>
</MultiDialogContainerContext.Provider>
);
};
Usage
enum dialogs {
Edit = 1,
Delete = 2
}
const SomeMenu = () => (
<MultiDialogProvider<dialogs>>
{({ Trigger, Container }) => (
<>
<DropdownMenu>
<DropdownMenuTrigger>Open Menu</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuContent>
<DropdownMenuLabel>Label</DropdownMenuLabel>
<DropdownMenuSeparator />
<Trigger value={dialogs.Edit}>
<DropdownMenuItem>Edit</DropdownMenuItem>
</Trigger>
<Trigger value={dialogs.Delete}>
<DropdownMenuItem>Delete</DropdownMenuItem>
</Trigger>
</DropdownMenuContent>
</DropdownMenuContent>
</DropdownMenu>
<Container value={dialogs.Edit}>
<EditModalContent />
</Container>
<Container value={dialogs.Delete}>
<DeleteModalContent />
</Container>
</>
)}
</MultiDialogProvider>
)
EDIT
After having animation issue, I changed this implementation completely. You can check the gist.
Well I can say it happens as well with Sheet and Drawer they conflict each other. The overlays target mixed etc. I think this need a refactor for better encapsulation.
Are you talking about the gist implementation or the one that appears in my comment?
Cause the code in gist should fix that as the dialog no longer lives in the container, so you should specify your type for dialog, whether it's sheet, normal dialog or other.
E.g, You define two container for two separate dialogs, and they don't overlap or appear at the same time.
Maybe you can show a code sample where you're having an issue.
Sorry. Now I found that using portal with a reference works nice
<Sheet key="main-menu" open={false} onOpenChange={toggleMenu}>
<SheetPortal container={mainMenuContainerRef.current}>
<SheetOverlay className="bg-black/20" />
<SheetContent side="left" className="w-[70%] p-3 pt-12 sm:w-[60%]">
<MainMenu />
</SheetContent>
</SheetPortal>
</Sheet>
<Drawer
key="auth-forms"
open={isAuthSheetOpen}
onOpenChange={toggleAuthSheet}
>
<DrawerPortal container={authFormContainerRef.current}>
<DrawerOverlay className="bg-black/20" />
<DrawerContent>
<AuthForms />
</DrawerContent>
</DrawerPortal>
</Drawer>
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
[!NOTE] For full a11y support, try this alternative from @jjenzz: https://github.com/radix-ui/primitives/issues/1836#issuecomment-2051812652
const [dialog, setDialog] = useState("dailog1");
<Dialog>
{dialog === "dailog1" ? (
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Reason for reject</DialogTitle>
<DialogDescription>
Specify the reason for rejecting the booking.
</DialogDescription>
</DialogHeader>
<div className="flex w-full mt-4">
<div className="w-full">
<Input
id="name"
placeholder="Resion"
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button
onClick={() => {
bookingAction(payment._id, "reject");
}}
>
Reject
</Button>
</DialogFooter>
</DialogContent>
) : (
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Update Bookings</DialogTitle>
<DialogDescription>
Specify the reason for rejecting the booking.
</DialogDescription>
</DialogHeader>
<div className="flex w-full mt-4">
<div className="w-full">
<Input
id="name"
placeholder="Resion"
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button
onClick={() => {
bookingAction(payment._id, "reject");
}}
>
Reject
</Button>
</DialogFooter>
</DialogContent>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<DotsHorizontalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem
className="space-x-2 cursor-pointer"
onClick={() =>
navigator.clipboard.writeText(payment.bookingId)
}
>
<MdContentCopy /> <span>Booking ID</span>
</DropdownMenuItem>
{payment.status === "pending" ? (
<>
<DropdownMenuItem
className="space-x-2 cursor-pointer"
onClick={() => {
bookingAction(payment._id, "accept");
}}
>
<FaCheck /> <span>Accept</span>
</DropdownMenuItem>
<DialogTrigger
asChild
onClick={() => {
console.log("dialog1");
setDialog("dialog1");
}}
>
<DropdownMenuItem className="space-x-2 cursor-pointer">
<FaXmark /> <span>Reject</span>
</DropdownMenuItem>
</DialogTrigger>
</>
) : payment.status === "accept" ? (
<DialogTrigger
asChild
onClick={() => {
console.log("dialog2");
setDialog("dialog2");
}}
>
<DropdownMenuItem className="space-x-2 cursor-pointer">
<Edit size={15} /> <span>Edit</span>
</DropdownMenuItem>
</DialogTrigger>
) : (
<></>
)}
</DropdownMenuContent>
</DropdownMenu>
</Dialog>
when I click on Edit or Reject button for first time it not opening the dailogContant Box but when it click again it open the respective dailogContant Box. please let me where i am wrong
@Ankit6098 you can't wrap all dialog's with one <Dialog>
unfortunately. each DialogTrigger
/DialogContent
must be wrapped in its own Dialog
.
ok thanks a lot.
This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you.
How to create Multiple Dialogs within a Shadcn UI Dropdown Menu
This is an example of how to implement multiple dialogs in a context menu using React/NextJS and Shadcn UI components. You can use the following code as a guide:
"use client";
import { useState } from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export function MultipleDialogInContextMenu() {
const [isDropdownMenuOpen, setIsDropdownMenuOpen] = useState(false);
const [isDialogOpen1, setIsDialogOpen1] = useState(false);
const [isDialogOpen2, setIsDialogOpen2] = useState(false);
return (
<>
<DropdownMenu
open={isDropdownMenuOpen}
onOpenChange={setIsDropdownMenuOpen}
>
<DropdownMenuTrigger asChild>
<Button>
Open Dropdown
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40">
<DropdownMenuItem
onClick={() => {
setIsDialogOpen1(true);
setIsDropdownMenuOpen(false);
}}
>
Dialog 1
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setIsDialogOpen2(true);
setIsDropdownMenuOpen(false);
}}
>
Dialog 2
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog open={isDialogOpen1} onOpenChange={setIsDialogOpen1}>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog 1</DialogTitle>
</DialogHeader>
<Input />
</DialogContent>
</Dialog>
<Dialog open={isDialogOpen2} onOpenChange={setIsDialogOpen2}>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog 2</DialogTitle>
</DialogHeader>
<Input />
<DialogFooter>
<Button onClick={() => setIsDialogOpen2(false)}>Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
This code demonstrates how to create a dropdown menu with two items, each opening a different dialog when clicked. The state is managed using React's useState hook, and the dialogs are rendered conditionally based on their respective state values. This approach allows for easy handling of multiple dialogs within a single context menu.