[Bug]. Dialog and Dropdown Menu components conflict
Bug report
Current Behavior
When using Dialog and Dropdown Menu components, a bug occurs.
Play
- open Code Sandbox
- Open Drowdown by clicking on button
- Open Dialog by clicking the
Openbutton in Dropdown - Close the Modal Window by clicking the Close button or the Background button
- Check that the open dropdown button does not open because of
pointer-events: noneon thebodytag.
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 |
This is a known "issue", you can use modal={false} on the Dropdown to fix it as mentioned here.
@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]);
}
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. 🤷♀️
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();
}, []);
};
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()
}
}, [])
}
Yep. https://github.com/radix-ui/primitives/issues/3141#issuecomment-2458092407 worked great for my use case. Great workaround 👍