primefaces / primereact

The Most Complete React UI Component Library
https://primereact.org
MIT License
6.85k stars 1.04k forks source link

Components that use Overlay Listener: unusable inside Shadow DOM #6931

Open peconomou929 opened 3 months ago

peconomou929 commented 3 months ago

Describe the bug

There is a general bug which affects all overlay components (i.e. those which useOverlayListener), when used inside a Shadow DOM.

Stackblitz

I will describe the issue in detail later, but for now, here are two consequences that can be seen in the StackBlitz:

1) if you use a filterable Dropdown inside a Shadow DOM, then the dropdown disappers when you click on the filter search bar. 2) if you use a Calendar inside a Shadow DOM, and you try to select a date, the Calendar disappears and no date is selected.

These are just two examples. I haven't tested all overlay components, but there could many more similar issues.

Event Targets and Shadow DOMs

The issue boils down to the following: When a JavaScript event crosses a Shadow boundary, the event is "retargeted" to the shadow host. So when the PrimeReact overlay listener (which listens on the window level) hears events coming from inside the shadow DOM, it misjudges the target of those events.

Let me give a specific example. Let's say you click on a button inside the shadow DOM. An event is dispatched with target the button. But, when this event bubbles up the DOM, crosses the shadow boundary, and is heard by an event listener outside the shadow DOM, that event listener will see that the target of the event is actually the shadow host, not the button. This is the inherent behavior of Shadow DOMs: they try to hide as much as possible about the details of their contents. The event listener outside the Shadow DOM should not know about the button inside the Shadow DOM.

PrimeReact Overlay Listener

Now, why is this a problem with PrimeReact? Well, PrimeReact uses an "Overlay Listener" for overlay components, like Overlay Panel, Dropdown, Calendar, Menu, etc. This Overlay Listener listens for events at the window level. It then triggers the overlay (e.g. the Dropdown panel) to close, whenever the user resizes, scrolls, or clicks outside of the overlay.

The issue is that when this Overlay Listener hears a mouse event that happens inside the Shadow DOM, it always thinks that the target of the event is the shadow host. So any time the user clicks, even if it truly is inside the overlay, the Overlay Listener thinks the user clicked on the Shadow host, which is deemed to be outside the overlay. Hence it will trigger the overlay to close. This is why the Dropdown closes when you click on the search filter bar inside the Dropdown panel.

More Component-Specific Details

Note, you can still select options inside the Dropdown panel without any issues. This is because when the user clicks on an option, the selection is processed before the event bubbles up to the window level (and in fact, the option selection actually triggers the Overlay Listener to unbind).

BUT, when it comes to the calendar, you cannot event select a date. This is because the Overlay Listener for the Calendar is configured to listen to "mousedown" events. Hence, if you try to click on a date, first the "mousedown" event is dispatched, bubbles up the DOM, and is heard by the Overlay Listener (which thinks the event happened outside the overlay), and it triggers the overlay to close. This is before the click event is ever registered, preventing the date from being selected.

Suggested Solutions

These solutions are not 100% researched - just here to start discussion.

  1. Copy what MUI does with modals and popovers. I think the basic idea is that whenever you want to render an overlay, you render something like
<OverlayWrapper>
   <Mask className="invisible fixed-full-screen"/>
   <Overlay/>
</OverlayWrapper>

The invisible mask covers the whole screen, and sits right behind the overlay. Then any clicks on the screen are processed by event listeners on the overlay wrapper, and this can determine if you clicked inside the overlay or not. The benefit here is that the event listener sits right next to overlay in the DOM. If the overlay is inside the shadow DOM, then so is the listener (as opposed to if you listen on the window). The downside is that the mask captures all click events, so if the user wants to click on a button which is behind the (invisible) mask, then they have to click twice: once to dismiss the mask, and then once more to click on the button.

  1. Add a configurable parameter to the PrimeReactProvider. i.e., in addition to "styleContainer", "appendTo", etc. we could also have "OverlayListenerRoot." This determines where the overlay listener for a particular component is registered (the default will be 'window', which is the current behavior). Then anybody who wants to render an overlay inside the shadow DOM could use the PrimeReact provider to register the shadow root as the "OverlayListenerRoot." Then this listener can correctly pinpoint the target of click events inside the shadow DOM. This approach clearly has the downside that if you click outside the shadow DOM, the overlay will not know that it needs to close.

  2. The current Overlay Listener queries the event "target" when determining where the user has clicked. An alternative to this would be to query event.composedPath()[0]. In general this gives the same value, but it has the added benefit that, when dealing with open shadow DOMs, it gives you the true original target of the event, instead of the shadow host.

Reproducer

https://stackblitz.com/edit/vitejs-vite-g1yosz

PrimeReact version

10.7.0

React version

17.x

Language

TypeScript

Build / Runtime

Create React App (CRA)

Browser(s)

No response

Steps to reproduce the behavior

See Stackblitz.

Expected behavior

Dropdown panels and calendars should not close unexpectedly when used inside Shadow DOMs.

melloware commented 3 months ago

I don't love what MUI does with adding more and more wrapper components. So one of the other suggestions would be better in my opinion.

peconomou929 commented 3 months ago

MUI also uses different mechanisms, for example in the AutoComplete component. Here the appearance of the overlay is coupled with the React-Native "Focus" and "Blur" states of the corresponding input component. Because this relies on React Synthetic events, you don't run into the problem of event listeners not knowing where events came from. This could be added as # 4 in the of suggested solutions.

rynrn commented 2 months ago

I have same problem with prime react calendar component

nnachman commented 2 months ago

Same for me