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

A popover on top of a modal dialog should be interactable #9936

Open
benfrain opened this issue Nov 16, 2023 · 15 comments
Open

A popover on top of a modal dialog should be interactable #9936

benfrain opened this issue Nov 16, 2023 · 15 comments
Labels
topic: dialog The <dialog> element. topic: popover The popover attribute and friends

Comments

@benfrain
Copy link

benfrain commented Nov 16, 2023

Here is a write up of the issue: https://benfrain.com/failing-with-multiple-dialog-elements-understanding-the-top-layer-and-popovers/

And from that a simple reduction of what I feel is an issue: https://benfrain.com/playground/modals/popover/

I'd opened a bug originally on the Chromium bug tracker: https://bugs.chromium.org/p/chromium/issues/detail?id=1502869

Following that and the related bug https://bugs.chromium.org/p/chromium/issues/detail?id=1502133 I am here.

I feel it is counterintuitive for authors to have popovers in the top layer, appearing above a modal dialog but for that popover to be impossible to interact with.

If a popover is appearing on top of a dialog, in this case by design, it stands to reason (to me anway) one might want to interact with it.

@nt1m
Copy link
Member

nt1m commented Nov 20, 2023

This is indeed the current spec behavior: https://html.spec.whatwg.org/multipage/interaction.html#blocked-by-a-modal-dialog

Perhaps the definition could be tweaked to not block elements on above the modal dialog in the top layer.

@npenin
Copy link

npenin commented Nov 28, 2023

Just to add on that, the MDN web docs states the following (https://developer.mozilla.org/en-US/docs/Web/API/Popover_API):

Typical use cases for the popover API include user-interactive elements like action menus, custom "toast" notifications, form element suggestions, content pickers, or teaching UI.

That means that toast notifications, menus, ... are not interactable when a modal is open. And potentially even hidden because of the https://html.spec.whatwg.org/multipage/popover.html#show-popover algorithm, responsible for hiding all popovers.

@askvortsov1
Copy link

I also ran into this. Another argument for changing the current behavior+spec is that sibling dialog + popover (code) vs nested dialog > popover (code) stack the same way, but in the former, the popover is inert even though it covers the modal. I would expect these two to behave consistently.

Perhaps the definition could be tweaked to not block elements on above the modal dialog in the top layer.

This seems reasonable. If A is displayed on top of B, and B is interactive, I would expect A to also be interactive, and for interactions on the intersection of A and B to go to A.

@scottaohara
Copy link
Collaborator

i'm torn on this. because it'd be a really unexpected change in established behavior that a modal dialog can have content accessible outside of it. That essentially breaks the whole concept of a modal being modal (the content of the primary page being inert) - particularly in the case where content being revealed from a control in the modal dialog, but focus doesn't automatically move to it.... so why would anyone using a screen reader, for instance, even think that now there's content outside of the dialog that they can interact with?

For many related use cases that I've been made aware of, I would often end up suggesting that what "should" have been done is the additional popups/overs be descendants of the modal dialog, so a user (specifically user of AT) continues to remain in the context of the modal dialog - which is again, what has been the established expectation/promise of what a modal dialog represents - so the content that popups 'on top' of the dialog is actually accessible.

With that said, I acknowledge problems that exist where one might have global notifications that live outside of the modal dialog, and may be triggered due to an action within the dialog. These sorts of things do need to be exposed to users. The solution I've had to work with developers to implement is to essentially have modal dialogs have their own instance of notifications, which i understand is not ideal. Ideally the notification API proposal could help solve for some of these cases, but if a notification has a nested interactive element (e.g., a link) - well, that's just a whole separate issue that i'll not dive into here (yet?)

I just think it's really important to consider the ramifications of the ask - where there are definitely some use cases which would need this revised behavior. But I worry the simplification of "if a popover is displayed on top of a modal dialog, it should be interactable" glosses over the "well why is this UI built this way / does it need to be build this way?" and "why would someone invoke a modal dialog but also expect for people to interact with content that isn't part of that modal dialog, since the point of a modal is to make everything outside of it inert?" It seems to be coming from a place of "visually this makes sense" but it goes against what the developer has built with their markup. Again, i acknowledge there are some use cases that should be considered here, but the reduced test cases in this thread are too generic to draw any strong conclusions on as to whether the use situations that these reduced cases derived from are actually valid for consideration or not. The position of content in the DOM (and thus how it is exposed in the accessibility tree by browsers) is important to not make unpredictable.

@npenin
Copy link

npenin commented Mar 14, 2024

Really interesting insight ! I would actually not put such a strong condition on the title of this issue. In the example below, the popover would not be "on top" of the popup, but just aside from it :
The typical use case I can imagine would be an error snackbar that is "popping up" after a modal dialog action (like api interaction). If action fails for whatever reason, we would want to display the snackbar and let the user interact with it for a retry operation, close it or just report the problem to the technical support.

I actually really like the way chrome did it: having a top layer. What about introducing a similar layer concept and let web developers handle the layer ordering ? or just defining a predefined set of layers that each "layerable" content can decide to use (showModal could take an optional argument for the layer name/index, similar rule would apply to other "layerable" content like the showPopover, ...). In that case those layers would be more deterministic and might help providing guidance on which layer to use when.

@thejackshelton
Copy link

thejackshelton commented Apr 17, 2024

A good use case I could really see for this is portals. Imagine you have a form with a headless select component inside a modal dialog. That select component will "portal" its contents outside of your top layer, and since you don't control the rendering, it will remain behind the modal dialog. (also remain inert)

By setting a popover attribute, it should be promoted to the top layer and be interactable. That way it's backwards compatible to some degree, and is also opt-in.

@keithamus
Copy link
Contributor

@thejackshelton in that instance the popover is inside the dialog and therefore inter-actable.

Perhaps, another way to look at this, is to question why the popover is being opened in the first place, and what that does to the document? Perhaps one solution is that showing a popover outside of the modal should close the modal itself? I know this goes against the problem space presented in the OP but it makes the authoring error much more visible.

@thejackshelton
Copy link

thejackshelton commented Apr 17, 2024

@thejackshelton in that instance the popover is inside the dialog and therefore inter-actable.

Perhaps, another way to look at this, is to question why the popover is being opened in the first place, and what that does to the document? Perhaps one solution is that showing a popover outside of the modal should close the modal itself? I know this goes against the problem space presented in the OP but it makes the authoring error much more visible.

While the component or trigger itself might be in the dialog, its contents are "moved" out of the top layer of the modal dialog (or were never there, and were conditionally rendered to the end of the body).

As a result, you can't currently use libraries that portal their contents in a dialog element.

Example I've found in the wild:
https://www.linkedin.com/posts/joshwcomeau_ive-been-experimenting-with-the-native-activity-7153416928533835776-gRwW/

An example we have in Qwik UI:
qwikifiers/qwik-ui#694

My current thoughts, are though you cannot control the rendering of the portalled content, perhaps you can programmatically make it a popover, and when that happens the content that was originally meant to be inside the dialog is interactable.

Another solution I have tried, is a mutation observer that moves it back into the dialog, which unfortunately seems to break most of these libraries.

@keithamus
Copy link
Contributor

As a result, you can't currently use libraries that portal their contents in a dialog element.

Those libraries should probably update to either not use portals, inject their portal root into the nearest dialog, or to make the portals popovers themselves. Personally I don't see much utility in portals given popover effectively supersedes them.

@jods4
Copy link

jods4 commented Apr 17, 2024

Sure, components whose lifetime are tied to the dialog can evolve to techniques other than a Portal at the end of <body>.
I have implemented dropdowns and tooltips inside dialogs, it's possible. And with new popover APIs there are additional options that may even be easier.

The problem still remains for components that should outlive the dialog. Toasters/notifications have been mentioned before and are the use-case that led me to this issue.

  • it's a common UX pattern to display notifications to user, even when a dialog is open.
  • when dialog closes, you don't want all notifications to suddenly disappear, so parenting inside the dialog seems wrong.
  • if you have interactions in your notifications (even just "click to dismiss") they don't work when a dialog is open.

Assuming you managed to even "display" your notifications on top, which is not easy. You need to either work around the ::backdrop, or popover the notifications container every time a dialog opens.

It's almost like we need a "top-most" layer for content that's always on top of everything else, including the top layer 😄

@thejackshelton
Copy link

Those libraries should probably update to either not use portals, inject their portal root into the nearest dialog, or to make the portals popovers themselves. Personally I don't see much utility in portals given popover effectively supersedes them.

While I agree, it is such a common pattern that I think it will be quite a while before that becomes a reality. As a result, you can't effectively use these libraries in dialogs at the moment.

For the time being, would it be accessible to have a custom aria widget with a custom backdrop that is a "inert" piece, and then promote that into the top layer as a popover instead?

@thejackshelton
Copy link

thejackshelton commented Apr 17, 2024

Sure, components whose lifetime are tied to the dialog can evolve to techniques other than a Portal at the end of <body>. I have implemented dropdowns and tooltips inside dialogs, it's possible. And with new popover APIs there are additional options that may even be easier.

The problem still remains for components that should outlive the dialog. Toasters/notifications have been mentioned before and are the use-case that led me to this issue.

  • it's a common UX pattern to display notifications to user, even when a dialog is open.
  • when dialog closes, you don't want all notifications to suddenly disappear, so parenting inside the dialog seems wrong.
  • if you have interactions in your notifications (even just "click to dismiss") they don't work when a dialog is open.

Assuming you managed to even "display" your notifications on top, which is not easy. You need to either work around the ::backdrop, or popover the notifications container every time a dialog opens.

It's almost like we need a "top-most" layer for content that's always on top of everything else, including the top layer 😄

It is definitely possible, we are doing it in Qwik UI. What I am saying is that for example if someone uses the Qwik UI modal which uses the dialog element under the hood, and then they decide to use a headless select component, that is not going to work. (as most of these portal it outside of the top layer)

Now it would work if they use a Qwik UI date picker for example, but then they cannot leverage the previous ecosystem (across everything). It is a breaking change for the web.

@keithamus
Copy link
Contributor

For the time being, would it be accessible to have a custom aria widget with a custom backdrop that is a "inert" piece, and then promote that into the top layer as a popover instead?

I think it would be an easier lift to move portals to using popover than to re-implement <dialog>.

@thejackshelton
Copy link

thejackshelton commented Apr 17, 2024

For the time being, would it be accessible to have a custom aria widget with a custom backdrop that is a "inert" piece, and then promote that into the top layer as a popover instead?

I think it would be an easier lift to move portals to using popover than to re-implement <dialog>.

If I had control over the rendering of the library completely agree.

As a library author making use of the dialog we don't, the portalled content moves outside of the dialog, and isn't interactive, even if it is visually when programmatically made a popover, as per this issue. I would classify this as a bug, but perhaps that is a matter of opinion seen from the discussion above.

While I'd love to get rid of portals, there are thousands of libraries that use them. In this case, we have a Modal component in Qwik UI that builds on top of the native dialog element. Should I be informing those who consume the component that the respective library should always move to a popover or else it can't be used?

Now my second thought was using a portal implementation like the other libraries and not using the popover, but then you get the same problem in reverse. Libraries that now use the dialog or popover will take precedence because of the top layer.

@askvortsov1
Copy link

askvortsov1 commented Apr 18, 2024

In the example below, the popover would not be "on top" of the popup, but just aside from it

In terms of DOM layout, yes. But the current toplayer spec doesn't allow 2 elements in the toplayer to be "besides" each other; things are stacked in the order in which they were opened.

could take an optional argument for the layer name/index

I think it would be sad if the toplayer became z-index v2. I'm not opposed to more nuanced semantics than "elements are stacked in the order they were opened", but I feel like allowing developers to explicitly dictate the order of toplayer elements would significantly weaken the assumptions you can make when writing / using components, just as a blanket "exempt me from inertness" would weaken inertness.


Portalling toplayer elements has an advantage outside of making sure they don't get clipped by overflow/contain: it allows you to add a tooltip / popover anchored to_any_ element without disrupting CSS selectors for your app. For instance:

<html lang="en">
  <body>
    <ul>
      <li>Hi</li>
      <li>I'm</li>
      <li>A</li>
      <dialog>This is a modal</dialog>
      <li>List</li>
    </ul>
  </body>
  <style>
    li:nth-child(2n) {
      color: red;
    }
    li:nth-child(2n + 1) {
      color: green;
    }
  </style>
</html>

https://fish-auspicious-composer.glitch.me/

Of course, this particular <dialog /> could be placed somewhere where it doesn't interfere with these particular selectors.
But it will always interfere with some selectors. Suddenly, you need to think about where to place every toplayer element, or accept the uncertainty of "some one could wrap my component in a tooltip, and mess up my css". display: contents; gets us fairly far w.r.t. the stylings themselves, but selectors are still going to be disrupted.

We don't want to blanket-exclude it from selectors while not visible either; that would likely break animating closing / opening it, and feels like a pretty radical deviation from the rules of the DOM we are used to.

If you portal toplayer elements outside the root of your app's DOM, you don't need to worry about this. Global styles can still be set via :root, and global event listeners can still be attached to the document element / window. And if you can use portalling, you're probably using enough JS that you can dynamically specify classes / other elements for each thing you're portalling, so you don't lose power there.

Additionally, if your DOM structure consists of (1) your app root, and (2) a bunch of portalled toplayer elements, it becomes really easy to implement the modal behavior proposed in above via popovers by:

  • keeping track of the time at which each popover element opened, and a set of timestamps at which currently open modals were opened
  • Whenever a modal opens or closes:
    • if any modals are open, set "inert" on the app root. Otherwise, remove inert
    • Set "inert" on any popovers opened before the most recently opened modal; remove it from all others

This all being said, I still think we should change the spec, although I'm less certain exactly how. Here are some thoughts.

Global notifications are a thing

Sometimes, it's easy to put a locally-triggered popover inside of a modal. If the popover is anchored to something in the modal, this seems like the obviously correct decision. And if a popover is only triggerable by actions from inside the modal, it's probably reasonable to put it inside the modal too. But there are other cases:

  • Some popovers might be opened in response to actions from one of several modals, or from actions anywhere on the page.
  • Some might be triggered asynchronously after some user action, which may / may not have occurred inside a modal. And when the popover appears, that modal may / may not be open.
  • Some might be triggered by background polling network events, or something else completely disconnected from user actions

And so unless you move all your popovers every time any modal opens or closes, it's difficult to keep them inside the modal. It also doesn't feel "correct": globally-relevant elements should probably live somewhere, uh, global.

There are multiple kinds of modals

A question I've been thinking about a lot in relation to this issue is "Why does this have to be a modal?”

A big benefit of modals over separate pages is that they feel less disruptive to a user's interaction. You're navigating away, with the potential to return. You're stepping aside to do something heavily related to the page you're currently on, and will definitely return to your main task when done.

A benefit of modals over regular popovers is that forcibly contain you within a subview. You still have context behind the modal, but you can't interact with it until you're finished doing whatever you set out to do. Some benefits of this are:

  • You can expect the user not to change state of the main view until they are finished dealing with the subview
  • You could require that modal state is valid before allowing it to be closed
  • The user's attention is more focused, because they can't do anything outside the modal

But I claim that there are different degrees to which you might want to constrain the user.

The "share" dialog on Google Drive is pleasant UI: the user needs to do a task in the context of their document, so a modal is used to let them complete the task and then finish. But if you disconnect from the server, or get a notification, you might want/need to do something about it, interrupting your current task. And maybe that includes clicking an "info" link in a popup. Or closing a notification, then closing the dialog and dealing with something.

In contrast, if you need to enforce a paywall, or an age verification check (e.g. on alcohol-related purchases in the U.S.), you might want a non-interruptible modal, that forces the user to complete the task before dealing with anything extraneous. I see this category kinda like more powerful, customizable "alert"/"confirm"s.

I think we should change something

I claim that interruptible and non-interruptible modals are both useful components in a UI toolkit.

I would love to see native support for interruptible modals (i.e. popovers placed above these escape inertness). This is the implementation I described above with portalling + manual inertness, but it relies on a bunch of custom JS / tracking open / closed state + order, and on all popovers being portalled. If any end up inside the app root, they will be inert. And if one of these pseudo-modals is placed inside the app root, opening it will make everything inert with no escape.

I am less confident on how "non-interruptible" modals should be handled. <dialog /> today is pretty close to this visually / behaviorally, and almost exactly this from an accessibility standpoint. I have a few ideas on how to improve it further, none of which I love:

  1. Non-descendant popovers opened while a <dialog /> is open should be placed under that <dialog /> in the toplayer.
  2. showPopover should raise an exception if called while a <dialog /> is open
  3. <dialog />s should somehow pause execution of the main JS thread while they are open

(3) seems impossible to implement, especially with frameworks that have runtimes, but it feels like the most "correct" option in that it would be a stylable alert/confirm.

(2) feels unpleasant: it adds a lot of places where exceptions need to be accounted for, and would be a very breaking change.

(1) is my favorite of these, but it does weaken the definition of the toplayer somewhat. I lean positively towards this being worth it, but I think more discussion is warranted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: dialog The <dialog> element. topic: popover The popover attribute and friends
Development

No branches or pull requests

9 participants