Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dialog: Unable to disable "outside click" behavior #621

Closed
paulwongx opened this issue Jun 15, 2021 · 17 comments
Closed

Dialog: Unable to disable "outside click" behavior #621

paulwongx opened this issue Jun 15, 2021 · 17 comments

Comments

@paulwongx
Copy link

Is there an option to disable "outside click" behaviour? For example, I'd like to keep the dialog opened when click outside. It would be great if we can have the condition inside the useWindowEvent function.

Originally posted by @wengtytt in #212 (comment)

There also seems to be multiple feature requests regarding this. Is there a way to override this default behavior currently?

@paulwongx paulwongx changed the title Dialog: Unable to disable "outside click" behavior. Is this intended? Dialog: Unable to disable "outside click" behavior Jun 15, 2021
@molteber
Copy link

molteber commented Jun 15, 2021

You can make it static and decide when to show the component yourself by using the close method

a react example could be something like this

const foo = () => {
  const [isOpen, setIsOpen] = useState(false);
  
  return (
    <>
      {!isOpen && (
        <button onClick={() => setIsOpen(true)}>Open dialog</button>
      )}
      {isOpen && (
        <Dialog static open={isOpen} onClose={() => setIsOpen(false)}>
          // ...
        </Dialog>
      )}
    </>
  );
}

The open prop is still used for manage scroll locking and focus trapping, but as long as static is present, the actual element will always be rendered regardless of the open value, which allows you to control it yourself externally.

You can read more about it under Transitions
(See the last code example, right before Accessibility notes)

@wengtytt
Copy link

wengtytt commented Jun 15, 2021

Hi there, I forgot to post my solution for this problem. The trick is not the static property. Even you provide the static property, the onClose is still triggered when user clicks outside. (The ideal solution will be a property to disable the listener for clicking outside)

To solve the problem, we can provide an empty function as the onClose function and use static property with custom function to handle open and close for the modal.

<Dialog
    // ...
    static
    onClose={() => null}>
</Dialog>

@dickerstu
Copy link

I only needed to set the onClose to null using the solution from @wengtytt for this to work. Adding the static property kept the custom close events from working.

<Dialog
    // ...
    onClose={() => null}>
</Dialog>

@paulwongx
Copy link
Author

paulwongx commented Jun 15, 2021

Hi there, I forgot to post my solution for this problem. The trick is not the static property. Even you provide the static property, the onClose is still triggered when user clicks outside. (The ideal solution will be a property to disable the listener for clicking outside)

To solve the problem, we can provide an empty function as the onClose function and use static property with custom function to handle open and close for the modal.

<Dialog
    // ...
    static
    onClose={() => null}>
</Dialog>

Amazing. This works. Thank you!
Edit: @iambryanhaney's answer is even better as it allows esc key to work too.

@molteber
Copy link

Hi there, I forgot to post my solution for this problem. The trick is not the static property. Even you provide the static property, the onClose is still triggered when user clicks outside. (The ideal solution will be a property to disable the listener for clicking outside)

To solve the problem, we can provide an empty function as the onClose function and use static property with custom function to handle open and close for the modal.

<Dialog
    // ...
    static
    onClose={() => null}>
</Dialog>

Oh 🤦‍♂️ I might've forgotten what i did to solve this.

Maybe i solved this myself by exchanging the overlay component with my own div, setting aria-hidden="true" and using the same classes.

That way you don't loose out on the close on esc etc.
However, not sure if this has other drawbacks (by not using the provided overlay component)

@iambryanhaney
Copy link

Bypassing onClose will require you to set up your own keyboard listener to capture the esc key.

If we look in dialog.tsx, we can see that there is a mouseDown event listener on the window itself, which closes the Dialog if the event emitter is not a descendant of it.

useWindowEvent('mousedown', event => {
    let target = event.target as HTMLElement

    if (dialogState !== DialogStates.Open) return
    if (hasNestedDialogs) return
    if (internalDialogRef.current?.contains(target)) return

    close()
})

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

Inline: style={{ pointerEvents: 'none' }}
Tailwind: className="pointer-events-none"

...which will effectively disable click events outside of the Dialog's body.

If you have other interactive elements higher in the z-index, such as a toast notification, you can capture the mouseDown event in that element (while still listening for onClick) and prevent it from bubbling to the Dialog's window listener:

// In your toast notification component...
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div onMouseDown={(e) => e.stopPropagation()}>

Notes on the eslint disable: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events

@paulwongx
Copy link
Author

Bypassing onClose will require you to set up your own keyboard listener to capture the esc key.

If we look in dialog.tsx, we can see that there is a mouseDown event listener on the window itself, which closes the Dialog if the event emitter is not a descendant of it.

useWindowEvent('mousedown', event => {
    let target = event.target as HTMLElement

    if (dialogState !== DialogStates.Open) return
    if (hasNestedDialogs) return
    if (internalDialogRef.current?.contains(target)) return

    close()
})

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

Inline: style={{ pointerEvents: 'none' }}
Tailwind: className="pointer-events-none"

...which will effectively disable click events outside of the Dialog's body.

If you have other interactive elements higher in the z-index, such as a toast notification, you can capture the mouseDown event in that element (while still listening for onClick) and prevent it from bubbling to the Dialog's window listener:

// In your toast notification component...
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div onMouseDown={(e) => e.stopPropagation()}>

Notes on the eslint disable: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events

Wow you're absolutely right - esc key not working is an issue with the onclick={()=>null} solution. This should be the accepted answer! Many thanks!

@sujin-sreekumaran
Copy link

static
onClose={() => null}>

this solution doesn't work for me ,
my problem is if i click outside the dialog it never goes away but i can click the outside buttons and it is working when the modal is not even not closed.

anyone help?

@sujin-sreekumaran
Copy link

you can also return the empty obj, it prevents the outside click !
onClose={() => {} }>

@Rasul1996
Copy link

Bypassing onClose will require you to set up your own keyboard listener to capture the esc key.

If we look in dialog.tsx, we can see that there is a mouseDown event listener on the window itself, which closes the Dialog if the event emitter is not a descendant of it.

useWindowEvent('mousedown', event => {
    let target = event.target as HTMLElement

    if (dialogState !== DialogStates.Open) return
    if (hasNestedDialogs) return
    if (internalDialogRef.current?.contains(target)) return

    close()
})

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

Inline: style={{ pointerEvents: 'none' }} Tailwind: className="pointer-events-none"

...which will effectively disable click events outside of the Dialog's body.

If you have other interactive elements higher in the z-index, such as a toast notification, you can capture the mouseDown event in that element (while still listening for onClick) and prevent it from bubbling to the Dialog's window listener:

// In your toast notification component...
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div onMouseDown={(e) => e.stopPropagation()}>

Notes on the eslint disable: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events

This is exactly what i was looking for, many thanks !!!

@ponnamkarthik
Copy link

ponnamkarthik commented May 4, 2023

I tried to set static and create a dummy @close function but its not working i was still able to close the dialog as i click outside

@paulwongx paulwongx reopened this May 4, 2023
@tolerance-go
Copy link

tolerance-go commented Jun 4, 2023

               <Dialog
                  ref={containerRef}
                  as='div'
                  className='relative z-10'
                  onClose={() => {}}
                  onClick={(event) => {
                     /**
                      * disable Close modal when click outside of modal
                      */
                     if (
                        isElementChild(
                           containerRef.current!,
                           event.target as HTMLElement,
                        )
                     ) {
                        closeModal()
                     }
                  }}
               >

I want to close the modal on click outside, but
I use this method to solve the nested modal situation

@sujin-sreekumaran
Copy link

               <Dialog
                  ref={containerRef}
                  as='div'
                  className='relative z-10'
                  onClose={() => {}}
                  onClick={(event) => {
                     /**
                      * disable Close modal when click outside of modal
                      */
                     if (
                        isElementChild(
                           containerRef.current!,
                           event.target as HTMLElement,
                        )
                     ) {
                        closeModal()
                     }
                  }}
               >

I want to close the modal on click outside, but I use this method to solve the nested modal situation

That's the solution that worked for me i mentioned above

@brandalx
Copy link

i had same error so i tried to solve my way and this actually work
i did this by removing the style of pointer events like so:

` onClose: () => {
set({ type: null, isOpen: false });

// Delay restoring pointer events for 2 seconds
setTimeout(() => {
  document.body.style.pointerEvents = "auto";
}, 1000);

},
}));`

Note, i did it with small timeout to make sure that first all modal scripts are completed and then im manually removing that. Hope it helps!

@ramialkaro
Copy link

Bypassing onClose will require you to set up your own keyboard listener to capture the esc key.

If we look in dialog.tsx, we can see that there is a mouseDown event listener on the window itself, which closes the Dialog if the event emitter is not a descendant of it.

useWindowEvent('mousedown', event => {
    let target = event.target as HTMLElement

    if (dialogState !== DialogStates.Open) return
    if (hasNestedDialogs) return
    if (internalDialogRef.current?.contains(target)) return

    close()
})

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

Inline: style={{ pointerEvents: 'none' }} Tailwind: className="pointer-events-none"

...which will effectively disable click events outside of the Dialog's body.

If you have other interactive elements higher in the z-index, such as a toast notification, you can capture the mouseDown event in that element (while still listening for onClick) and prevent it from bubbling to the Dialog's window listener:

// In your toast notification component...
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div onMouseDown={(e) => e.stopPropagation()}>

Notes on the eslint disable: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events

The version UI 1.6 have been depreacted the Dialog.Overlay. https://headlessui.com/react/dialog#dialog-overlay

At least with version 1.7.17 the issue it not happen.

@afterxleep
Copy link

The pointer-events-none did not work for me on 1.7, so I'm just moving the backdrop to be part of the DialogPanel. This way, there are no clicks outside :).

<template>
  <TransitionRoot as="template" :show="open">
    <Dialog class="relative z-10" @close="onClose">      
      <DialogPanel>
        <TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100" leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
          <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
        </TransitionChild>
        <-- Fixed backdrop here -->
        <div id="backdrop" class="fixed inset-0 z-10 w-screen overflow-y-auto">          
          ...rest of your dialog content
        </div>
    </DialogPanel>
    </Dialog>
  </TransitionRoot>
</template>

If you want to also prevent the ESC key handler, you can add a key listener with capture mode:

With Vue, it'd be something like this.

function keyPress(e) {
  if (e.key === "Escape") {
    console.log("Escape key pressed");
    e.stopImmediatePropagation();
  }
}

onMounted(() => {
  console.log("mounted");
  window.addEventListener('keydown', keyPress, true); // 'true' enables capture mode
});

onUnmounted(() => {
  console.log("unmounted");
  window.removeEventListener('keydown', keyPress, true);
});

@sko-kr
Copy link

sko-kr commented Sep 5, 2024

Just make DialogPanel fullscreen, then add dialog content div as a direct child of DialogPanel and style that as though it is DialogPanel. This way we don't lose built in functionality such as close on ESC key.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests