primitives icon indicating copy to clipboard operation
primitives copied to clipboard

[Bug]. Dialog and Dropdown Menu components conflict

Open xkomiks opened this issue 1 year ago • 3 comments

Bug report

Current Behavior

When using Dialog and Dropdown Menu components, a bug occurs.

Play

  1. open Code Sandbox
  2. Open Drowdown by clicking on button
  3. Open Dialog by clicking the Open button in Dropdown
  4. Close the Modal Window by clicking the Close button or the Background button
  5. Check that the open dropdown button does not open because of pointer-events: none on the body tag.

Note: When closing the window by clicking on the Dropdown button, the modal window closes as expected.

Expected behavior

There should not be pointer-events: none on the body tag when closing the modal window

Reproducible example

https://codesandbox.io/p/sandbox/radix-ui-dialog-forked-5zzj6h?workspaceId=a4b71791-adc5-4752-b50f-7be66d4db979

Suggested solution

Additional context

Your environment

Software Name(s) Version
Radix Package(s)
React n/a
Browser
Assistive tech
Node n/a
npm/yarn
Operating System

xkomiks avatar Sep 25 '24 12:09 xkomiks

This is a known "issue", you can use modal={false} on the Dropdown to fix it as mentioned here.

meszarosdezso avatar Sep 26 '24 08:09 meszarosdezso

@meszarosdezso interesting, thanks. I'll check it out.

By the way, I wrote my solution for this 5 minutes ago. Maybe someone will need it

// This is a workaround for the issue described here:
// https://github.com/radix-ui/primitives/issues/3141

// Dialog animation duration defined in Dialog.module.scss
const DIALOG_ANIMATION_DURATION = 300;

// Adding a slight buffer (100ms) to ensure the animation is complete before changing pointer events
const DELAYED_VALUE_TIMEOUT = DIALOG_ANIMATION_DURATION + 100;

export function useRadixDialogPointerEventsFix(open: boolean) {
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      if (open) {
        return;
      }
      if (document.body.style.pointerEvents === 'none') {
        document.body.style.pointerEvents = '';
      }
    }, DELAYED_VALUE_TIMEOUT);

    return () => clearTimeout(timeoutId);
  }, [open]);
}

xkomiks avatar Sep 26 '24 08:09 xkomiks

This might be a separate issue but it seems similar to this. I found a bug where I had the following:

  • A dialog portaled to the body
  • A dialog portalled to a div container

When the div container dialog closed, and the body level dialog opened, if I refresh the page, the pointer-event: none never gets reset on the currently visible dialog, leaving the dialog visible but unable to be clicked. I was able to fix the issue by removing the dialog portal from the div container dialog. 🤷‍♀️

meganspaulding avatar Oct 23 '24 15:10 meganspaulding

I just hit this as well. I have a dialog that triggers an alert. Closing both of them at the same time correctly removes data-scroll-locked="2" from the body, however pointer-events: none is left on style attribute.

I've made a solution similar to @xkomiks, however I put the hook in a single place and it takes care everywhere.

const useRadixDialogPointerEventsFix = () => {
  useEffect(() => {
    const body = document.body;

    const observer = new MutationObserver((mutationsList) => {
      for (const mutation of mutationsList) {
        if (
          mutation.type !== 'attributes' ||
          mutation.attributeName !== 'data-scroll-locked'
        ) {
          return;
        }

        const total = parseInt(body.dataset.scrollLocked ?? '0', 10);

        if (total > 0) {
          return;
        }

        body.style.pointerEvents = '';
      }
    });

    observer.observe(body, {
      attributes: true,
      attributeFilter: ['data-scroll-locked']
    });

    return () => observer.disconnect();
  }, []);
};

felipedeboni avatar Nov 05 '24 20:11 felipedeboni

I was facing a similar issue with the Select component where my sticky header was disappearing after scrolling and clicking on Select. Maybe this will help someone else find a solution. I've slightly rewritten @felipedeboni's solution:

const useRadixDialogPointerEventsFix = () => {
  useEffect(() => {
    const body = document.body

    // Force reset all scroll locking
    const forceResetScroll = () => {
      if (body.hasAttribute('data-scroll-locked')) {
        body.removeAttribute('data-scroll-locked')
        body.style.pointerEvents = ''
        body.style.removeProperty('padding-right')
        body.style.removeProperty('overflow')
      }
    }

    // Create observer to watch for scroll lock
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.attributeName === 'data-scroll-locked') {
          forceResetScroll()
        }
      })
    })

    // Start observing
    observer.observe(body, {
      attributes: true,
      attributeFilter: ['data-scroll-locked'],
    })

    // Initial reset
    forceResetScroll()

    return () => {
      observer.disconnect()
      forceResetScroll()
    }
  }, [])
}


philtru avatar Dec 19 '24 03:12 philtru

Yep. https://github.com/radix-ui/primitives/issues/3141#issuecomment-2458092407 worked great for my use case. Great workaround 👍

Liam-Twinkl avatar Aug 26 '25 07:08 Liam-Twinkl