-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Comments
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. |
Just to add on that, the MDN web docs states the following (https://developer.mozilla.org/en-US/docs/Web/API/Popover_API):
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. |
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.
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. |
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. |
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 : 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. |
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. |
@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: An example we have in Qwik UI: 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. |
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. |
Sure, components whose lifetime are tied to the dialog can evolve to techniques other than a Portal at the end of 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.
Assuming you managed to even "display" your notifications on top, which is not easy. You need to either work around the It's almost like we need a "top-most" layer for content that's always on top of everything else, including the top layer 😄 |
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? |
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. |
I think it would be an easier lift to move portals to using popover than to re-implement |
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. |
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.
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 <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 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 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:
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 thingSometimes, 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:
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 modalsA 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:
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 somethingI 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.
(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 (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. |
As I understand it, allowing the popover element to be intractable sends us back to square one where we then need Having said that, one proposed solution can be as follows:
The second situation also ensures that the popover element necessarily opened after the dialog was opened. Which would be in line with the user's expectations that what was shown later is over on top of what was shown earlier. I believe this hits a compromise between the two ends and solves a few of the use-cases discussed above. On a personal note: I strongly feel that having popover and dialog in the same layer is a no-win situation, we seem to be digressing in the earlier state in terms of layering elements on top of one another. |
Isn't this how things work right now? I think that dialogs opened after a popover currently work correctly: the popover will be inert, and will be under the dialog in the toplayer. The thing I am sad about is that if you open a dialog and then open a non-descendant popover, the popover will be placed on top of the dialog, visually obscuring it, but it will be inert.
If you open a dialog, and then open a non-descendant popover, the popover should be put on its own layer, but that layer should be below the dialog At some point in the future, we might also want to consider some notion of "global notification" elements, which will be exert from inertness, and be placed above all other toplayer components. |
Could this problem also be why we can't use nested modal <dialog id="dlg">
<form method="dialog">
<input />
<button id="btn2">Open dialog 2</button>
<input />
<button type="submit">Close</button>
</form>
<dialog id="dlg2">
<form method="dialog">
<input />
<input />
<button type="submit">Close</button>
</form>
</dialog>
</dialog> |
Hitting this with an combobox inside a native dialog. The alternative is to remove the native But because the popover can't be visually interacted with by mouse/touch we'd have to break the ARIA accessibility in favor of supporting mouse/touch operations. Edit: Apologies. I took a second look at the spec and it seems reasonable. Making the listbox a child of the dialog itself and then assigning said listbox a |
There already is a concept of elements added later to the topLayer not becoming inert even if there is already a modal dialog open, opening a second dialog. I'd love for inert to only traverse backwards through the stack only rendering elements lower than the modal inert but I recognise that the web should strive to never change established behaviour even if we want it to. Having someway of telling dialog that it's inert reach is only down the stack might be useful. Default behaviour stays as is but we have an new opt-in option. There was talk a while ago of adding something to dialog to allow declaring it as a modal box in html. So this might be something that can be wrapped in with that. <dialog type="default | modal | visual-modal">[...]</dialog> Then we could do something like this. <div popover>[...]</div> <!-- visually behind dialog, is inert -->
<dialog type="visual-modal">[...]</dialog>
<div popover>[...]</div> <!-- visually in front of dialog, is not inert --> To take full advantage of this power we'd need a way to control stacking but that's a whole other issue. In another issue I suggesting moving popover elements into open modal dialogs, in my example I used react |
From reading your post, I think what you are saying the main contract of a dialog is that everything else is inert when it's opened, am I understanding you correctly? I say "when it's opened" because consider this case: Do you think a more accurate description of the dialog contract should then be is everything below it is inert. Which at the time of opening is everything. Only when the page designer allows other things to be opened in the top layer above it, are they buying into more interactable things. The description means if popup is opened in a dialog, then it should also be interactable, just like if a dialog is opened it becomes interactable too. A nice UI pattern is notifications that stack up until they timeout/are dismissed. These are often required to be above a dialog. I have been on the exact journey as the original posted, and the same frustration. I was so happy to finally see my notifier appear above the notifier... only to be followed by mass disappointment that I could not dismiss the notification. Once again a giant gotcha that makes the API unusable to something beyond a toy example. |
I doubt there is a single example (if somebody knows of one please so share) of a single website that opens a dialog, that then opens a popover on top of the dialog to show it but have it inert. That sounds more like a bug (an element rendered on top the backdrop so it looks interactable, but is actually inert) than a feature to me. If that behavior is required one could just put an What further leads me to believe the current behavior is a bug: |
Unfortunately this solution does not solve the OP's original problem of a stacking notifier, since the notifier would not be a direct descendant of the dialog element that is currently open. |
I ran into the same issue this week while trying to get a long running project updated with a new modal which uses the dialog tag, but needs to support kendo-ui / selec2 / jquery-ui stuff for legacy reasons. A lot of overlays/dropdowns/filters are simply moved to the "body". I created a MutationObserver on the body to polyfill the "popover" feature for those elements, but while they show up above the modal, they are therefore not accessible in the modal. What would really help in my opinion is to allow the elements to have their "inert" property set to FALSE in JS. |
This behaviour seems wrong. When a popover is on top of the layer of the dialog and it is visible, it should be also interactive. For example if you have dialog and a toaster for notifications, you would have to close the dialog to be able to interact with notifications, even if they are on top layer above dialog. That doesn't seem intuitive. It's simple impossible doing stuff like Sooner / Toaster with native api's https://ui.shadcn.com/docs/components/sonner if you have an open dialog. |
- floating action 수정: floating element의 조상 중 top layer에 표시되는 요소가 있다면 거기에 추가하고 없으면 body에 추가합니다. floating element와 상호작용할 수 있게 됨. 관련 이슈: whatwg/html#9936 - Alert 컴포넌트도 Dialog 컴포넌트 사용하도록 함. 세팅 모달 위에 Alert가 이제 잘 뜸
Same problem here, we have a snackbar/toast notification system, and those needs to appear on top of the modal always regardless whether the modal is opened after or before the notification appeared. Duplicate the snackbar system honestly sucks for the same reason others talked about (some notification below the backdrop, some others on top, snackbars being one on top of each other), and any workaround that moves the snackbars in the last opened top-layer is unacceptable since it requires some very shady code to do something today is very easy. This problem makes the modal/popover api a no-go and we are stuck using z-index. |
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.
The text was updated successfully, but these errors were encountered: