Skip to content

Stacking dialogs doesn't work when dialogs come from a different package that both use headlessui #3643

@wszeborowskimateusz

Description

@wszeborowskimateusz

Using

  • @headlessui/react": "^2.2.0" - for both package and example app

In my example I have a package that I am working on that displays headlessui dialogs. When I try to use this package (e.g. in example app) in the app that uses headlessui on it's own, then closing the inner dialog (from the package) by clicking out closes the outer dialog (from the example app). When I have exactly the same code but just in one package everything works as expected.

The example code:

  1. Package code (inner modal)
import {
  Dialog,
  DialogBackdrop,
  DialogPanel,
  DialogTitle,
} from '@headlessui/react';
import { useState } from 'preact/hooks';

function InnerModal({
  open,
  setOpen,
}: {
  open: boolean;
  setOpen: (open: boolean) => void;
}) {
  return (
    <Dialog open={open} onClose={setOpen} className="relative z-30">
      <DialogBackdrop
        transition
        className="fixed inset-0 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in"
      />

      <div className="fixed inset-0 z-30 w-screen overflow-y-auto">
        <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
          <DialogPanel
            transition
            className="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in sm:my-8 sm:w-full sm:max-w-lg sm:p-6 data-closed:sm:translate-y-0 data-closed:sm:scale-95"
          >
            <div className="sm:flex sm:items-start">
              <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
                <DialogTitle
                  as="h3"
                  className="text-base font-semibold text-gray-900"
                >
                  Inner modal
                </DialogTitle>
              </div>
            </div>
            <div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
              <div className="sm:ml-3 sm:w-auto">
                <button onClick={() => setOpen(false)}>Confirm</button>
              </div>
              <div className="mt-3 sm:mt-0 sm:w-auto">
                <button data-autofocus onClick={() => setOpen(false)}>
                  Cancel
                </button>
              </div>
            </div>
          </DialogPanel>
        </div>
      </div>
    </Dialog>
  );
}

export default function InnerComponent() {
  const [open, setOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setOpen(true)}>Open</button>
      <InnerModal open={open} setOpen={setOpen} />
    </div>
  );
}

  1. Example app (outer modal)
import { useState } from 'preact/hooks';

import InnerComponent from 'react-importer';
import {
  Dialog,
  DialogBackdrop,
  DialogPanel,
  DialogTitle,
} from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { ReactNode } from 'preact/compat';

function OuterModal({
  open,
  setOpen,
  children,
}: {
  open: boolean;
  setOpen: (open: boolean) => void;
  children?: ReactNode;
}) {
  return (
    <Dialog open={open} onClose={setOpen} className="relative z-10">
      <DialogBackdrop
        transition
        className="fixed inset-0 z-20 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in"
      />
      <div className="fixed inset-0 z-20 w-screen overflow-y-auto">
        <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
          <DialogPanel
            transition
            className="relative w-full max-w-4xl transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in sm:my-8 sm:p-6 data-closed:sm:translate-y-0 data-closed:sm:scale-95"
          >
            <div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
              <button
                type="button"
                onClick={() => setOpen(false)}
                className="cursor-pointer rounded-md bg-white text-gray-400 hover:text-gray-500 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-hidden"
              >
                <span className="sr-only">Close</span>
                <XMarkIcon aria-hidden="true" className="size-6" />
              </button>
            </div>
            <div className="sm:flex sm:items-start">
              <div className="mt-3 text-center sm:mt-0 sm:text-left">
                <DialogTitle
                  as="h3"
                  className="text-base font-semibold text-gray-900"
                >
                  Outer modal
                </DialogTitle>
              </div>
            </div>
            {children && <div className="mt-5">{children}</div>}
          </DialogPanel>
        </div>
      </div>
    </Dialog>
  );
}

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

  return (
    <div className="min-h-screen w-full">
      <button onClick={() => setOpen(true)}>Open</button>

      <OuterModal open={open} setOpen={setOpen}>
        <div>
          <InnerComponent />
        </div>
      </OuterModal>
    </div>
  );
};

export default App;

When I try to click out to close the inner modal, it closes the outer modal.

Interestingly when I replace outers modal setOpen with an empty function, then even the inner modal doesn't close when I click out. As if the outer modal swallows the click event and doesn't bubble it to the inner modal.

Is there something I am doing wrong in here? How could I make it work?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions