ui icon indicating copy to clipboard operation
ui copied to clipboard

Multiple Dialog in Context Menu

Open Natchii59 opened this issue 11 months ago • 15 comments

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.

Natchii59 avatar Jul 23 '23 14:07 Natchii59

Same issue, need to have multiple dialogs in dropdown menu

isaacdarcilla avatar Jul 24 '23 01:07 isaacdarcilla

@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...

Natchii59 avatar Jul 24 '23 06:07 Natchii59

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 ...

Tensionteng avatar Nov 23 '23 08:11 Tensionteng

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.

IPreferToEatDinner avatar Jan 23 '24 13:01 IPreferToEatDinner

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>
 

Tensionteng avatar Jan 23 '24 14:01 Tensionteng

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

IPreferToEatDinner avatar Jan 24 '24 07:01 IPreferToEatDinner

  1. 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>

  1. 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>

victorassiso avatar Feb 06 '24 15:02 victorassiso

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.

adnanalbeda avatar Feb 08 '24 19:02 adnanalbeda

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.

luishdez avatar Mar 31 '24 23:03 luishdez

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.

adnanalbeda avatar Apr 01 '24 14:04 adnanalbeda

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>

luishdez avatar Apr 03 '24 00:04 luishdez

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

juanrgon avatar Apr 12 '24 22:04 juanrgon

 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 avatar Apr 19 '24 15:04 Ankit6098

@Ankit6098 you can't wrap all dialog's with one <Dialog> unfortunately. each DialogTrigger/DialogContent must be wrapped in its own Dialog.

jjenzz avatar Apr 23 '24 09:04 jjenzz