Skip to content

Expose a stack of blocking elements #897

Open

Description

@domenic

Previously:

Based on these discussions, especially the last one, I'd like to propose the following API to explain some of the magic of <dialog>:

document.blockingElements.push(element); // note: if element is already in the blocking elements stack, it moves to the top
document.blockingElements.pop();
document.blockingElements.remove(element); // see https://github.com/whatwg/html/issues/897#issuecomment-198565716
document.blockingElements.top; // or .current or .peek()

We would generalize the "pending dialog stack" (which, note, is only used when showModal() is called, not just show()) to a "blocking element stack".

Dialogs would take part in that stack and be fully explained by it and integrated into it. For example:

  • Calling document.blockingElements.top after dialogEl.showModal() would return dialogEl.
  • Calling document.blockingElements.pop() with an active modal dialog would make that dialog un-modal (but not hide it or remove it from the document), and make whatever was below it on the stack become the new blocking element.

Ccing some people form the browser-accessibility-dev discussion: @marcysutton @alice @minorninth. Implementer interest would be especially helpful in moving this forward, @alice @minorninth.

Activity

  1. annevk commented on Mar 18, 2016

    @annevk
    Member

    It seems @cookiecrook might also have valuable input given he filed the bug.

  2. alice commented on Mar 18, 2016

    @alice
    Contributor

    Very glad to see the conversation continuing on this, thanks for pushing it forward @domenic!

    I like this concept in general. I'd like to dig a little deeper into what an element being on the "blocking element stack" means. Here are my guesses, basically copied from <dialog>'s behaviour:

    1. Being at the top of the stack effectively means everything else on the page is inert.
    2. Everything on the stack is in the top layer
      • (Aside: I'm intrigued by the possibilities of this, isolated from the other behaviours described here, as a solution to the problem of people messing with the DOM order to hack layering without using z-index, for things like floating top menu bars.)
    3. The top blocking element is a control group owner object.

    Have I missed anything?

    Also, would the order of the stack determine the visual layering order? If calling pop() doesn't hide the element, what does it do?

  3. alice commented on Mar 18, 2016

    @alice
    Contributor

    Also, I'd like to ensure that we don't prematurely write off inert as a primitive which may be useful separately from this concept.

  4. domenic commented on Mar 18, 2016

    @domenic
    MemberAuthor

    Thanks for working on the precision there @alice. You're right that there are several separate concepts. Currently (1) is handled by blocked by a modal dialog, which is set in showModal(). (2) is the only part that is directly tied to the definition of the pending dialog stack. In theory we could tease them apart, so that you can block all other elements on the page without being in the top layer, but in practice that's probably not so useful.

    (3) is a little strange. See some previous discussion in #744. Currently the spec has all dialogs being control group owner objects. This is meant to cause tabbing to cycle if you're inside the dialog---even if the dialog is non-modal. However, Chrome does not currently do this (and only Chrome implements dialogs). And it's worth noting that for modal dialogs, this doesn't really matter, since everything else being inert means it's not focusable. So maybe we don't need to worry about (3), assuming we are going to do (1). (Which seems very likely, since that was the original ask.)

    Also, would the order of the stack determine the visual layering order? If calling pop() doesn't hide the element, what does it do?

    As long as you are in the stack, you are in the top layer, and the order of the top layer is indeed determined by the order of the stack. (Maybe there are issues here with fullscreen, though, that make it nontrivial to just map the ordering?) But once you are popped from the stack, you are no longer in the top layer. You will be below everything inside the top layer, but still visible, unless you change your visibility (e.g. with CSS).

  5. alice commented on Mar 18, 2016

    @alice
    Contributor

    So maybe we don't need to worry about (3), assuming we are going to do (1). (Which seems very likely, since that was the original ask.)

    Seems fair to me. I'm still struggling with what a non-modal dialog looks like (and what is a dialog group?)

    In theory we could tease them apart, so that you can block all other elements on the page without being in the top layer, but in practice that's probably not so useful.
    ...
    But once you are popped from the stack, you are no longer in the top layer. You will be below everything inside the top layer, but still visible, unless you change your visibility (e.g. with CSS).

    Yep, this works for me too. I definitely think tying (1) and (2) together is a good idea - the visual cue is an intrinsic part of the behaviour.

    Still thinking about naming, by the way, but I don't want to sidetrack into that discussion until we're all on the same page with the core concepts (not least because I think they will affect what nomenclature makes sense).

  6. domenic commented on Mar 18, 2016

    @domenic
    MemberAuthor

    An example of a non-modal dialog is at https://jsbin.com/tojolefele/edit?html,output (Chrome only). Per spec, once you pop open the dialog and focus the button labeled "dialog 1", your tabbing should cycle between "dialog 1" and "dialog 2" and not break out to the "1" and "2" and "Run with JS" buttons. (Unless, presumably, you used something like Esc or other keyboard shortcuts to pop up outside the dialog's control group.)

    Dialog groups appear to be a rather confusing concept meant to express "all dialogs in a control group", to handle cases like a Document with multiple open dialogs? Focus fixup rule two and three seem most relevant...

  7. alice commented on Mar 18, 2016

    @alice
    Contributor

    @domenic If the non-modal dialog did trap focus, what would the difference be between that and a modal dialog? i.e. what is the purpose/function of a non-modal dialog, if it is indistinguishable from a bug?

  8. domenic commented on Mar 18, 2016

    @domenic
    MemberAuthor

    You can still activate (and focus) things that are outside the non-modal dialog, e.g. with the mouse most obviously, but also with the keyboard assuming your UA allows you to escape a focus group using a key like Esc or ctrl + up arrow or similar. It only affects normal sequential focus navigation (i.e. the tab key).

  9. alice commented on Mar 18, 2016

    @alice
    Contributor

    That seems like pretty weird behaviour, but I'm going to assume it makes sense for reasons I just don't currently understand.

    To get back to the current topic, then:

    • open modal dialogs would automatically be on document.blockingElements, basically as if calling dialog.openModal() called document.blockingElements.push(dialog).
    • similarly, closing a modal dialog would effectively call document.blockingElements.remove(dialog) (assuming such an API exists)
    • what would be the effect of document.blockingElements.pop() if the top blocking element is a modal <dialog>? This seems ill defined currently - an open <dialog> must be part of the top layer, but pop() shouldn't automatically close it, as I understand it.
  10. domenic commented on Mar 18, 2016

    @domenic
    MemberAuthor

    Open modal dialogs would automatically be on document.blockingElements, basically as if calling dialog.openModal() called document.blockingElements.push(dialog).

    Yep

    similarly, closing a modal dialog would effectively call document.blockingElements.remove(dialog) (assuming such an API exists)

    Ah, interesting; it's not always the top, so such an API would be necessary to fully explain dialogs. We could add that... although we need to define what happens if you push the same element twice. I guess it gets moved to the top, instead of creating two entries.

    what would be the effect of document.blockingElements.pop() if the top blocking element is a modal ? This seems ill defined currently - an open must be part of the top layer, but pop() shouldn't automatically close it, as I understand it.

    It would remove it from the top layer, but it would still be visible, since you didn't actually change its visibility (e.g. with CSS).

    If there is something else in the top layer then the dialog would be hidden underneath it.

  11. domenic commented on Mar 18, 2016

    @domenic
    MemberAuthor

    It would remove it from the top layer, but it would still be visible, since you didn't actually change its visibility (e.g. with CSS).

    In other words, it would transition the dialog to exactly the same state it would be in if you just called .show(), instead of .showModal().

  12. alice commented on Mar 18, 2016

    @alice
    Contributor

    In other words, it would transition the dialog to exactly the same state it would be in if you just called .show(), instead of .showModal().

    Ah - I'd missed that non-modal dialogs weren't in the top layer. Makes perfect sense in that case.

    similarly, closing a modal dialog would effectively call document.blockingElements.remove(dialog) (assuming such an API exists)

    Ah, interesting; it's not always the top, so such an API would be necessary to fully explain dialogs. We could add that... although we need to define what happens if you push the same element twice. I guess it gets moved to the top, instead of creating two entries.

    Yep, I think that makes a lot of sense.

    I think remove() also just makes sense if there is any case where a blocking element can become non-blocking while it's not the top blocking element.

  13. domenic commented on Mar 18, 2016

    @domenic
    MemberAuthor

    Edited the OP to include .remove() and to note that when you .push() an element that is already in the stack, it moves to the top.

  14. 13 remaining items

  15. cookiecrook commented on Mar 24, 2016

    @cookiecrook

    @domenic wrote:

    I'm going to back off my involvement in this thread as it seems that at this point we've exhausted discussion on the original proposal and are starting to go off-topic.

    Your proposal's good. Don't abandon it. We're trying to help you make it better, not bigger.

  16. cookiecrook commented on Mar 24, 2016

    @cookiecrook

    @alice wrote:

    I think a minimal baseline would be James' (1) proposal: when a new blocking layer is added, the blocked layer should maintain its focus state, which should be reinstated once it is no longer blocked.

    I can let the other points rest if this one is picked up. Authors wanting to focus something else could capture the onblur or ondomfocusout events on the popped layer and explicitly set focus() elsewhere.

    @domenic wrote:

    As I've said already, @cookiecrook's (1) falls out automatically.

    It's unrealistic to assume this will happen automatically. Focus is an inconsistent mess across all browsers because of the poor specification in HTML. Case in point: if the focused element is ever removed from the view, focus falls back to the body element (the nuclear option). Most mouse users are happily oblivious, but this behavior is only acceptable to people who don't use or understand focus.

  17. annevk commented on Mar 24, 2016

    @annevk
    Member

    @cookiecrook your last paragraph seems to make two points. 1) Focus is not well defined. It's not clear whose job it is to define focus but I guess HTML tries to do most of it so we should probably keep making improvements there. 2) The behavior that is defined isn't ideal for users that depend on focus navigation.

    We can fix 1, especially if you help out with tests and maybe even PRs. Unfortunately we lack the necessary manpower to clean everything up at once, but I think we all agree user interface events could use some help.

    It's unclear to me whether we can fix 2 without introducing new features, since focusing the body as backup has been the behavior for so long, it's likely sites depend on it.

  18. cookiecrook commented on Mar 24, 2016

    @cookiecrook

    I realize now another core purpose of my last paragraph was unclear. It's unrealistic to assume blocking stack focus will happen automatically because what will happen is what has always happened:

    • The focused element will be removed from the view.
    • Whether or not it's removed from the DOM is irrelevant; focus will be lost.
    • Focus will fall back to the body element, breaking the "stack" focus handling expectation that is consistent on every platform except the web platform.

    Hopefully that's more clear. Any API (even a primitive) that can cause document focus to be lost—as this API can with regards to inertness—should take care to avoid common focus mistakes. In this case, an explicit one-liner to restore the previously "focused element" and the "sequential focus navigation starting point" in the stack may be sufficient.

    @annevk wrote:

    We can fix 1, especially if you help out with tests and maybe even PRs. Unfortunately we lack the necessary manpower to clean everything up at once, but I think we all agree user interface events could use some help.

    Understood. I don't peruse the specs as often as I used to, but I will file bugs and/or PRs when I see them. On a related note: I just went looking for the only HTML spec focus bug I remembered offhand, and I'm pleased to see the following was added to the spec re: fragment navigation.

    Move the sequential focus navigation starting point to target.
    https://html.spec.whatwg.org/multipage/browsers.html#scroll-to-fragid

    I remember arguing with Ian over this point on numerous occasions. Congrats to whomever made the edit or convinced another editor.

  19. alice commented on Mar 24, 2016

    @alice
    Contributor

    Regarding this proposal: I think we actually have some consensus at this point between @domenic, @cookiecrook and I.

    I spoke with Domenic offline to clarify his "falls out automatically" comment, and he pointed to https://html.spec.whatwg.org/multipage/interaction.html#focus-fixup-rule-three - "Let new focus target be the currently focused area of a top-level browsing context." etc. I think the source of the misunderstanding may have been in part that Chrome's implementation of <dialog> currently does not implement this behaviour.

    Domenic and I also agreed that we can and should generalise this up the blocking element stack [edit: my reading of the "focus fixup" language is that this should already happen] - but without, at least immediately, exposing that state information to developers.

  20. cookiecrook commented on Mar 24, 2016

    @cookiecrook

    Thanks for the link. If this blocking element API will reference the "focus fix-up" rules, that may be sufficient. Ultimately I'd like to see more, but I agree we're in consensus for baseline focus support.

  21. upsuper commented on Nov 23, 2016

    @upsuper
    Member

    How would it interact with the fullscreen stack? Should fullscreen element also be put into this stack as well? If so, a stack containing fullscreen elements and modal dialogs sounds similiar to the existing top layer concept. Should these two share the same stack somehow?

  22. domenic commented on Nov 23, 2016

    @domenic
    MemberAuthor

    I don't think it's related to the fullscreen stack.

  23. upsuper commented on Nov 23, 2016

    @upsuper
    Member

    Shouldn't fullscreen element block all other elements as well?

  24. domenic commented on Nov 23, 2016

    @domenic
    MemberAuthor

    I see, I had forgotten the context. Yes, this is all tied in to the top layer concept. Probably it is best to manipulate that directly, but it does mean that using the fullscreen APIs could manipulate the stack out from under you.

  25. upsuper commented on Nov 23, 2016

    @upsuper
    Member

    Yeah, I think that could be an issue for dialog as well. Probably adding a flag to items of blocking element stack indicating how an element is added, and forbid script from removing an element from the stack if it is added by the Fullscreen API or dialog,showModal?

  26. valdrinkoshi commented on Apr 12, 2017

    @valdrinkoshi

    Elements in the Top Layer should be subject to the Top Layer's stacking context, in other words rendered on top of everything else - even if they're declared inside an element that creates new stacking context.

    This would solve issues like this one http://jsbin.com/kuboqa/1/edit?html,output

    <style>
      .backdrop {
        position: fixed;
        top: 0; bottom: 0;
        left: 0; right: 0;
        z-index: 1;
        background-color: rgba(0, 0, 0, 0.5);
      }
      .overlay {
        position: fixed;
        z-index: 2;
      }
    </style>
    
    <div class="backdrop"></div>
    
    <!-- this creates a new stacking context -->
    <div style="transform: translateZ(0);">
      <div class="overlay">
        <button>click me!</button>
      </div>
    </div>

    ^ you cannot click the button in .overlay because stacking context

  27. domenic commented on Apr 12, 2017

    @domenic
    MemberAuthor

    Yes, that is literally the definition of top layer.

  28. valdrinkoshi commented on Jun 28, 2017

    @valdrinkoshi

    Regarding multiple Top Layer elements opened, I wanted to provide the following use case (try it on chrome http://jsbin.com/bawecix/5/edit?html,output):

    <style>
      body {
        height: 150vh;
      }
      dialog {
        width: 100px;
        overflow: hidden;
      }
    </style>
    
    <button onclick="myDialog.showModal()">Show modal</button>
    
    <dialog id="myDialog">
      <img src="http://lorempixel.com/30/30/" title="Some random image from lorempixel">
      <select>
        <option>item</option>
        <option>item</option>
        <option>item</option>
      </select>
    </dialog>

    We have 3 overlays: <img> tooltip, <dialog> and <select>. All render their contents on the Top Layer.

    Regarding the interactions, the last opened overlay is the first to be notified of events - e.g. node1, node2, then node3 listen for click event -> node3event listener is invoked first.

    In the above example, only <dialog> and <select> take action on click/keydown events.

    • they trap focus within their content when opened
    • they close on Escape and stop that keydown event propagation (that is to say, only <select> closes when you press Escape)
    • they stop click event propagation (<select> closes on click)
    • <select> prevents scrolling from happening when it's opened, <dialog> doesn't

    If I were to implement this, I'd have to implement my own LIFO event listener system to delegate events to the right overlay (here an example).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    accessibilityAffects accessibilityaddition/proposalNew features or enhancementsneeds implementer interestMoving the issue forward requires implementers to express interest

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions