Search code examples
javascriptreactjsmodal-dialogheadless-ui

Dialog within Popover in React HeadlessUI


I have a navbar that uses HeadlessUI's Popover on mobile for the hamburger menu. By default, this menu closes when you click out/focus on an element that is not in it.

Now I'm trying to add a modal (HeadlessUI Dialog) that I want to open when clicking on a button that is in the popover menu. The modal is used within a ModalButton component definition (<><button><dialog></>). This is done for separation of concerns (everything relating to the modal is within ModalButton).

The issue is: when I'm in the navbar's popover menu and click on the button to open the dialog. The browser focuses on this new dialog, and so the popover loses focus, making it close. Since it closed, the button (and thus the dialog sibling) are no longer rendered, and so the dialog disappears instantly.

For reference, this is a pseudocode of the react tree:

<navbar>
  <popover>
    <>            {/* "ModalButton" containing both the button and the dialog */}
      <button />  {/* Button that opens the dialog */}
      <dialog />  {/* This uses a portal internally (with HeadlessUI) */}
    </>
  </popover>
</navbar>

I can think of a few ways to solve this but neither are very good:

  1. Pull the modal higher in the tree, outside of the popover/hamburger menu but still inside the navbar. But that breaks separation of concerns since the navbar now has to worry about the open state of the modal.
  2. Put the modal somewhere at the top of the tree, and use some kind of global state (requires a state management library) to handle the opening of the modal.
  3. Maybe there's a way to prevent the Popover from closing when focusing on the dialog? (but still allow it to close when focusing anything that is not the dialog)

I'd love to hear any ideas on fixing this issue.


Solution

  • You should put the dialog higher up in the tree. Usually these can go at the page level, or even the app level, depending on how global these dialogs are.

    You can then use your favourite global state manager or the useContext hook to tell these dialogs to open programmatically from anywhere in your app.

    In which case the popover closing automatically shouldn't be an issue anymore.