11

my problem is simple ... I'm using HeadlessUI's Dialog component for React in my app and when I click out of modal I wish it wouldn't close. In the documentation, there is the Dialog. Overlay parameter that deals with this interaction but there are no settings to disable it.

Any solutions? This is the link to the HeadlessUI docs of the component I am using: https://headlessui.dev/react/dialog

Maybe did u know the kind of "Alert blocking modal" for React??

double-beep
  • 5,031
  • 17
  • 33
  • 41
Salvatore Marotta
  • 111
  • 1
  • 1
  • 6

12 Answers12

17

You can make the onClose event do nothing and instead only close the dialog when a button within it is clicked:

// eslint-disable-next-line @typescript-eslint/no-empty-function
<Dialog open={isOpen} onClose={() => {}}>

Instead, create a close button inside the dialog:

<button onClick={handleClose}>Close</button>

with handleClose defined as:

const handleClose = () => {
  setIsOpen(false);
};
Steve Chambers
  • 37,270
  • 24
  • 156
  • 208
Vignesh Raj
  • 171
  • 3
8

Add return empty object in onClose function of Dialog UI. here is my code:

 <Dialog
    initialFocus={completeButtonRef}
    as='div'
    className='fixed inset-0 z-10 overflow-y-auto'
    onClose={() => {}}
    open={false}
  >
Ko Nyan
  • 96
  • 1
  • 3
4

add pointer-events: none to the Dialog.Overlay.

This can be done by adding the pointer-events-none class

https://github.com/tailwindlabs/headlessui/issues/621#issuecomment-867161749

cweitat
  • 1,199
  • 1
  • 11
  • 28
  • I think this is a much better solution than the alternatives proposed (`onClose={()=>{}}`). Assigning an empty arrow function for the onClose will have a collateral of disabling the modal close by user action on "esc" input. – Madalosso Sep 15 '22 at 18:24
1

I had the same problem with https://headlessui.dev/react/dialog ReactJS and Typescript

I fixed passing a new prop called onClose(value: boolean): void; where i can handle the state outside the component onClose={() => {setIsOpen(false) }} and in the Dialog calling the prop onClose={onClose}

Part 1

   <Transition appear show={showModal} as={Fragment}>
        <Dialog
          as="div"
          className={clsx("fixed inset-0 z-50 overflow-y-auto", 
          className)}
          onClose={onClose}
        > 

....

Part 2enter code here

   <Modal
        {...args}
        showModal={isOpen}
        onClose={() => {setIsOpen(false) }}
      />
chicacode
  • 41
  • 5
1
onClose={() => null }>

you can also return null, it prevents the outside click!

Sujin S R
  • 11
  • 1
  • 4
1

I only needed to set the onClose to null. Adding the static property kept the custom close events from working.

<Dialog
  static
  onClose={() => null}>
</Dialog>
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
1

You will have to pass static.

Look for static in this page

Then you will need to handle opening and closing.

  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Adem kriouane Dec 21 '22 at 14:07
1

If you also want to disable closing the Modal on the Escape key press:

Pass a "noop" function to the onClose property, as suggested by others in this thread.

<Dialog
  ...
  onClose={() => {}}
  // or onClose={noop} if you have lodash installed or a noop function helper

If you want to keep the escape keypress dismiss enabled:

Just don't use Dialog.Panel inside of your Modal/Dialog component.

<Dialog
  ...
  onClose={onClose}>
  <div> // <-- use a regular div as your PANEL
  • This is the way! I can't see that Dialog.Panel does anything other than enable the click-outside-to-close behaviour so just reverting to a standard div doesn't have any downsides afaik. – Graham Sep 01 '23 at 09:32
0

change handleClose function to this:

   const handleClose = (event, reason) => {
    if (reason !== "backdropClick") {
      setOpen(false)
    }
  }
0

I wanted to keep the accessibility logic that HeadlessUI adds (i.e. Esc to close) while also adding buttons in the same transition but outside of the dialog area. To do so, I added the handleClose logic both to the transparent background and a new wrapper around my actual dialog content, then used an event.stopPropagation() on any clicks inside the dialog window. In Vue, with additional TransitionRoot logic, my code roughly looks like:

<TransitionRoot :show="isOpen">
  <Dialog @close="handleClose">
    <TransitionChild>
      <div aria-hidden="true" class="fixed inset-0 bg-black/70"
           @click="handleClose" />
      <!-- ... a few <button @click="..." class="fixed z-20 ..."> items ... -->
    </TransitionChild>
    <div class="fixed inset-0 h-screen ..."
         @click="handleClose">
      <div class="flex items-center justify-center ...">
        <TransitionChild as="div" class="rounded-lg bg-white ..."
                         @click.stop>
          <!-- @click.stop is equivalent to onClick(e) => e.stopPropagation() -->
          <!-- ... add dialog body in here ... -->
        </TransitionChild>
      </div>
    </div>
  </Dialog>
</TransitionRoot>
Epic Eric
  • 741
  • 6
  • 8
0

The following worked for me, I'll use the example code from the link that you provided

The example code:

import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'

export default function MyModal() {
  let [isOpen, setIsOpen] = useState(true)

  function closeModal() {
    setIsOpen(false)
  }

  function openModal() {
    setIsOpen(true)
  }

  return (
    <>
      <div className="fixed inset-0 flex items-center justify-center">
        <button
          type="button"
          onClick={openModal}
          className="rounded-md bg-black bg-opacity-20 px-4 py-2 text-sm font-medium text-white hover:bg-opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
        >
          Open dialog
        </button>
      </div>

      <Transition appear show={isOpen} as={Fragment}>
        <Dialog as="div" className="relative z-10" onClose={closeModal}>
          <Transition.Child
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0"
            enterTo="opacity-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <div className="fixed inset-0 bg-black bg-opacity-25" />
          </Transition.Child>

          <div className="fixed inset-0 overflow-y-auto">
            <div className="flex min-h-full items-center justify-center p-4 text-center">
              <Transition.Child
                as={Fragment}
                enter="ease-out duration-300"
                enterFrom="opacity-0 scale-95"
                enterTo="opacity-100 scale-100"
                leave="ease-in duration-200"
                leaveFrom="opacity-100 scale-100"
                leaveTo="opacity-0 scale-95"
              >
                <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
                  <Dialog.Title
                    as="h3"
                    className="text-lg font-medium leading-6 text-gray-900"
                  >
                    Payment successful
                  </Dialog.Title>
                  <div className="mt-2">
                    <p className="text-sm text-gray-500">
                      Your payment has been successfully submitted. We’ve sent
                      you an email with all of the details of your order.
                    </p>
                  </div>

                  <div className="mt-4">
                    <button
                      type="button"
                      className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
                      onClick={closeModal}
                    >
                      Got it, thanks!
                    </button>
                  </div>
                </Dialog.Panel>
              </Transition.Child>
            </div>
          </div>
        </Dialog>
      </Transition>
    </>
  )
}

My Changes:

import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState } from 'react'

export default function MyModal({ closeOnClickingOutside }) {
  let [isOpen, setIsOpen] = useState(true)
    
    const Overlay = closeOnClickingOutside ? 'div' : Dialog.Panel;
    const Content = closeOnClickingOutside ? Dialog.Panel : 'div';

  function closeModal() {
    setIsOpen(false)
  }

  function openModal() {
    setIsOpen(true)
  }

  return (
    <>
      <div className="fixed inset-0 flex items-center justify-center">
        <button
          type="button"
          onClick={openModal}
          className="rounded-md bg-black bg-opacity-20 px-4 py-2 text-sm font-medium text-white hover:bg-opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
        >
          Open dialog
        </button>
      </div>

      <Transition appear show={isOpen} as={Fragment}>
        <Dialog as="div" className="relative z-10" onClose={closeModal}>
          <Transition.Child
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0"
            enterTo="opacity-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <div className="fixed inset-0 bg-black bg-opacity-25" />
          </Transition.Child>

          <Overlay className="fixed inset-0 overflow-y-auto">
            <div className="flex min-h-full items-center justify-center p-4 text-center">
              <Transition.Child
                as={Fragment}
                enter="ease-out duration-300"
                enterFrom="opacity-0 scale-95"
                enterTo="opacity-100 scale-100"
                leave="ease-in duration-200"
                leaveFrom="opacity-100 scale-100"
                leaveTo="opacity-0 scale-95"
              >
                <Content className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
                  <Dialog.Title
                    as="h3"
                    className="text-lg font-medium leading-6 text-gray-900"
                  >
                    Payment successful
                  </Dialog.Title>
                  <div className="mt-2">
                    <p className="text-sm text-gray-500">
                      Your payment has been successfully submitted. We’ve sent
                      you an email with all of the details of your order.
                    </p>
                  </div>

                  <div className="mt-4">
                    <button
                      type="button"
                      className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
                      onClick={closeModal}
                    >
                      Got it, thanks!
                    </button>
                  </div>
                </Content>
              </Transition.Child>
            </div>
          </div>
        </Dialog>
      </Transition>
    </>
  )
}
``
Dream Echo
  • 165
  • 2
  • 8
-1

With the heandlessui update 1.16, the Overlay component was only presented in Dialog.Panel, thus being performed directly in Dialog to interpret the onClose being called from a click out of Dialog.Panel

How it turned out:

<Dialog
                    as="div"
                    className="relative z-10"
                    onClose={() => {
                        if (event instanceof PointerEvent === false) {
                            closeModal()
                        }
                    }}
                >