diff --git a/Popup/explainer.md b/Popup/explainer.md index 56c37cfe..2c02aade 100644 --- a/Popup/explainer.md +++ b/Popup/explainer.md @@ -49,40 +49,70 @@ We propose a new HTML element called `popup`. This new element can be used for a `popup` will include: -* A `show()` JavaScript method, for invoking the `popup`. When visible, the `popup` will have a default set of behaviors as well as a default positioning scheme. +* A few options to invoke/show the `popup`: + * A `popup` attribute, applied to whichever elements should invoke a given popup (if applicable). + * An optional `open` attribute, applied to the `popup` to express that it should be shown. + * A `show()` JavaScript method, for invoking the `popup`. +* When visible, a default set of behaviors as well as a default positioning scheme. * Logic for an optional `autofocus` attribute which enables moving focus to the `popup` or to a descendent. * An optional `delegatesfocus` attribute, for passing focus to descendants. * An optional `anchor` attribute, which both relates the `popup` to an activating element and can be used in a separately-proposed, CSS-based anchor positioning scheme. -* A `hide()` method for hiding the `popup` -* [Light dismiss](#light-dismiss) behaviors. +* A couple means to dismiss the `popup`: + * Behaviors tied to the aforementioned `popup` attribute. + * Removing the `open` attribute from `popup`. + * A `hide()` method for hiding the `popup`. + * [Light dismiss](#light-dismiss) behaviors. ### Showing Popups +#### Option A: the `popup` attribute + One use case for a `popup` is a popup menu triggered by a menu button: ![A popup menu displayed beneath a button that says "menu". The popup obscures other text on the page.](menu.png) An author could produce this popup menu using the following markup: + ```html - + ``` -Popup menus are not visible until `show()` is called by the author: +The `popup` attribute on the `button` element (referred to later in this document as the “invoking element”) takes an IDREF pointing to the relevant popup. Invoking this button will likewise invoke the popup with the matching ID. + +This `popup` attribute will apply accessibility semantics equivalent to `aria-haspopup="true"` and `aria-controls="menuPopup"` on the button (as well as a “controlled by” reverse relationship mapping on the `popup` itself). This attribute is valid only on a subset of interactive elements: + +* `button` or `input` in the button state (`input type="button"`). Invoking one of these elements will show the relevant popup. +* `input` in the `text`, `email`, `phone`, or `url` states. Setting focus in one of these elements will show the relevant popup. Note: we may need to explore means of suppressing this invocation on focus, for instances where the author instead wishes to show the `popup` based on text-entry logic. + +#### Option B: the `open` attribute -![A button that says "menu", with no popup beneath it](menu-button-only.png) +![An explanatory popup with links pointing to a button](teaching-bubble.png) -The script to show the `popup` could look like the following: +Some popups, such as teaching UI, might be shown to the user upon initial “page load”. For popups which aren’t shown as the result of a user interaction or JavaScript-controlled logic, apply the `open` attribute to show the `popup`: + +```html + +

New! I’m some sort of educational UI…

+ … +
+``` + +#### Option C: the `show()` method + +Suppose that an author instead wants to show their teaching UI upon some app-internal logic. Such an author could instead call `show()` on the `popup` from script: ```javascript -document.getElementById('menuButton').addEventListener('click', () => { - document.getElementById('menuPopup').show() -}) +if (upsellNewFeature) { + document.getElementById('newFeatureUI').show(); +} ``` -Until `show` is called, the `popup` does not display (has a computed value of `none` for its `display` property). Calling `show` places the `popup` into a browser-managed stack of top-layer elements that allow the `popup` to produce a box in accordance with author-supplied styles. Initial styles from the user agent stylesheet consist of: +#### What happens when a popup is shown + +Until the author has used the `popup` attribute, `open` attribute, or `show()` method, the `popup` does not display (has a computed value of `none` for its `display` property). Showing a popup places the `popup` into a browser-managed stack of top-layer elements that allow the `popup` to produce a box in accordance with author-supplied styles. Initial styles from the user agent stylesheet consist of: ```css popup { @@ -93,12 +123,20 @@ popup { } ``` -Only one “top-level” `popup` may be displayed at a time. When a `popup` is shown and placed on the stack, it will remove all `popup`s from the stack until it encounters a `popup` that is an ancestor of the new `popup`'s anchoring element, or the list is empty. In this way, the user agent will ensure that only one `popup` and its child `popup`s are ever displayed at a time—even across uncoordinated code. +Only one “top-level” `popup` may be displayed at a time. When a `popup` is shown and placed on the stack, it will remove all `popup`s from the stack until it encounters an “ancestral” `popup`, or the list is empty. In this way, the user agent will ensure that only one `popup` and its child `popup`s are ever displayed at a time—even across uncoordinated code. + +The following would be considered an “ancestral” `popup`: + +* A `popup` ancestor of the new `popup`’s invoking element (based on the `popup` attribute) +* A `popup` ancestor of the new `popup`’s anchoring element (based on the `anchor` attribute) +* A `popup` ancestor of the new `popup` itself Other events also remove a `popup` from the stack, including loss of focus, or hitting `ESC` (often referred to as [light dimiss](#light-dismiss) behaviors). Interactions with other elements like `dialog`, or other future types of popup-like elements, for example, showing a menu, must also remove the `popup`s from the top-layer stack. [Dismissing a `popup`](#dismissing-the-popup) will also remove any child `popup`s from the stack. `popup`s in the stack are laid out and rendered from the bottom of the stack to the top. Each `popup` will paint atomically as its own stacking context. +Showing a `popup` via options A (`popup` attribute) or C (calling the `show()`) method will also cause the `open` attribute to be set on the `popup`. + ### `autofocus` logic By default, focus remains on the current active element when the `popup` is invoked. If this element is somehow destroyed, focus moves to a focusable ancestral element. @@ -123,7 +161,7 @@ These `autofocus` rules will be processed each time `show` is called, as opposed ### `delegatesfocus` -Some authors may need to automatically focus the popup's first focusable descendent, and may not wish to write script to determine at runtime which element that is. In such cases the `delegatesfocus` attribute can be applied to the popup: +Some authors may need to automatically focus the popup’s first focusable descendent, and may not wish to write script to determine at runtime which element that is. In such cases the `delegatesfocus` attribute can be applied to the popup: ```html @@ -137,73 +175,58 @@ In the markup above, the `button` element will receive focus any time the `popup ### Anchoring -`popup` supports an `anchor` attribute which takes an ID reference to another element in the `popup`'s owner document. The `anchor` attribute is significant in that: +`popup` supports an `anchor` attribute which takes an ID reference to another element in the `popup`’s owner document. The `anchor` attribute is significant in that: -1. It enables a hierarchical relationship to be established between the `popup` and its anchor element separate from the DOM hierarchy. - 1. The hierarchy affects the event propagation path. - 2. The hierarchy determines if a `popup` is a descendant of another `popup`. A descendant `popup` does not dismiss its ancestor `popup`s when shown. -2. It is also used with anchored positioning ([more below](#anchored-positioning)) +1. It enables a hierarchical relationship to be established between the `popup` and its anchor element separate from the DOM hierarchy. The hierarchy determines if a `popup` is a descendant of another `popup`. A descendant `popup` does not dismiss its ancestor `popup`s when shown. +2. It is also used with anchored positioning. -In the markup below, a table with many rows is rendered, each of which displays a custom popup filled with commands when right-clicked. The popup is defined once and its anchor attribute is adjusted so that it is shown aligned with the table row when the contextmenu event is received. +#### Anchored positioning + +By default, `popup` has a fixed position in the top-left corner of the viewport. With many popup use cases, however, authors need to be able to pin the position of one element to another element in the document; this is the case with our popup menu. Absolute positioning schemes sometimes suffice for this purpose, but require specific DOM structures and provide no functionality for repositioning. -After a command is selected from the menu, the menu dispatches a custom command event which is then handled by the table row. The table row receives the event even though it isn't a DOM ancester of the popup. This is because the event target parent of a popup is its anchor element. +We will soon make an additional proposal for a CSS anchored positioning scheme, which can be applied to `popup` and other top-layer, interactive elements. For now, it is worth noting that a `popup`’s anchor element (the element it will be “pinned” to) can be expressed using a new `anchor` attribute on the popup: ```html - - ... - ... - ... - ... -
- - - - ... + + +

New! I’m some sort of educational UI…

+ …
- +```html + +

New! I’m some sort of educational UI…

+ … +
``` -#### Anchored positioning +Removing the attribute will dismiss the `popup`. -By default, `popup` has a fixed position in the top-left corner of the viewport. With many popup use cases, however, authors need to be able to pin the position of one element to another element in the document; this is the case with our popup menu. Absolute positioning schemes sometimes suffice for this purpose, but require specific DOM structures and provide no functionality for repositioning. +All other following methods of dismissing the `popup` will automatically remove the `open` attribute from the `popup`. + +#### The `popup` attribute -We will soon make an additional proposal for a CSS anchored positioning scheme, which can be applied to `popup` and other top-layer, interactive elements. For now, it is worth noting that a `popup`'s anchor element (the element it will be “pinned” to) can be expressed using a new `anchor` attribute on the popup: +When the `popup` was shown as a result of user interaction on an element with the `popup` attribute… ```html - -Popup + + + + ``` -### Dismissing the `popup` +…repeating/reversing that action will dismiss the popup. In this example, invoking the `button` again when the `popup` is visible will hide the `popup`. Moving focus from `input type="text"` (so long as focus does not then move to the `popup`) will hide an associated `popup`. + +#### The `hide()` method A `popup` can be hidden by calling the `hide()` method: @@ -212,18 +235,18 @@ A `popup` can be hidden by calling the `hide()` method: document.getElementById('menuPopup').hide(); ``` +#### Light dismiss + The `popup` may also be implicitly dismissed due to user interactions which trigger [light dismissal](#light-dismiss). When dismissal occurs: * The `popup` is removed from the browser-managed, top-layer stack so that it is no longer rendered. * A non-cancellable `hide` event is fired on the `popup` when the `popup` is hidden in response to [light dismissal](#light-dismiss). -#### Light dismiss - An opened `popup` will have “light dismiss” behavior, meaning that it will be automatically hidden when: * The user hits the escape key, * The layout of the `popup` or its anchor element is changed. -* Focus moves outside of the `popup` (_and_ its anchor element, if applicable). +* Focus moves outside of the `popup` (_and_ its invoking and anchor elements, if applicable). A generalized definition of “light dismiss” is being developed at [Open UI](https://open-ui.org/components/select#light-dismiss). @@ -234,7 +257,7 @@ While some `popup`s may be entire components in and of themselves, other `popup` ![A select control displaying an open listbox with 3 options](select-popup.png) -Per [“Enabling Custom Control UI”](https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/ControlUICustomization/explainer.md), authors should be able to customize parts of a native control, including the `select` `popup`. While we anticipate discussing the anatomy of a `select` in depth in the [Open UI venue](https://open-ui.org/components/select), any solution for providing arbitrary `popup`s will also be applied to the `select`'s Shadow DOM. +Per [“Enabling Custom Control UI”](https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/ControlUICustomization/explainer.md), authors should be able to customize parts of a native control, including the `select` `popup`. While we anticipate discussing the anatomy of a `select` in depth in the [Open UI venue](https://open-ui.org/components/select), any solution for providing arbitrary `popup`s will also be applied to the `select`’s Shadow DOM. For example, `popup` may be used in the `select` Shadow DOM like so: @@ -280,7 +303,7 @@ If it fits their use case, an author could then entirely replace this listbox wi ## Privacy and Security Considerations -Freedom over the size and position of a `popup` could enable an author to spoof UI belonging to the browser or documents other than the `popup`'s document. For this reason the `popup` will be constrained as all other elements of the relevant document are, by clipping the element to the document's layout viewport. +Freedom over the size and position of a `popup` could enable an author to spoof UI belonging to the browser or documents other than the `popup`’s document. For this reason the `popup` will be constrained as all other elements of the relevant document are, by clipping the element to the document's layout viewport. ## Alternate Solutions Considered @@ -290,21 +313,85 @@ Freedom over the size and position of a `popup` could enable an author to spoof * Only one `popup` can be shown at a time. * More than one `dialog` can be presented at a time. * `dialog` can be modal, such that user interaction with other UX elements is prevented. - * A `dialog` will dismiss a `popup` when shown but the converse isn't true. + * A `dialog` will dismiss a `popup` when shown but the converse isn’t true. * **Introducing a `type` attribute for `popup`**, which would provide a set of default styles, user input behaviors, and accessibility semantics for various classes of popups. However, with the availability of the proposed HTML attributes and CSS property values, this approach did not provide much added authoring value past platform-managed accessibility semantics to the parent popup. Because this approach did not provide accessibility semantics or input behaviors for popup descendents, the authoring story was unclear in cases where the type of popup (e.g. `type="menu"`) must contain particular *descendents* whose semantics could only be managed through ARIA (`role="menuitem"`) unless a new mechanism was proposed. ## Open questions -1. **Collision with CSS contain.** It's worth noting that using the [`contain` CSS property](https://developer.mozilla.org/en-US/docs/Web/CSS/contain) on an ancestor of `popup` will prevent `popup` from being positioned and painted correctly. How common is this use case? How might the platform resolve this unintentional effect? +1. **Collision with CSS contain.** It’s worth noting that using the [`contain` CSS property](https://developer.mozilla.org/en-US/docs/Web/CSS/contain) on an ancestor of `popup` will prevent `popup` from being positioned and painted correctly. How common is this use case? How might the platform resolve this unintentional effect? 2. **Could we require popups to use the DOM hierarchy for event propagation and establishing hierarchical popup relationships?** Elements used for popup UI today are frequently appended to the end of the DOM to ensure they appear on top of other UI. With the new capabilities of the `popup` element, that isn't necessary, yet we still assume in this proposal that DOM positioning of the `popup` needs to be separate from the anchor. One reason why that might still be needed is for anchor elements that can't accept a `popup` descendant, for example, image or input elements or custom-elements that expect a particular content model. Eliminating this requirement would also eliminate the complexity to modify the event propagation path based on the anchor attribute, and would make hierarchical relationships between popups clear just by observing the DOM hierarchy. -3. **Show/hide or show/close?** This proposal introduces a symmetrical `show`/`hide` method pair on the proposed `popup` element. `dialog`, however, sets a precedent for a `show` and `close` method pairing. That seemed less intuitive, but perhaps the existing pattern should be followed. Alternatively, the platform could introduce `hide` on `dialog` and consider `close` deprecated. +3. **Should one of the attributes hoist up a `popup` in trees (e.g. accessibility trees)?** Today, it is common practice to include popup UI as a direct child of the root node. This practice is a workaround for top-layer positioning issues, and it is our hope that this proposal renders this practice obsolete. However, there might still be cases where an author includes a `popup` in a separate point in the DOM to its anchor/invoking element(s). We may want to explore reordering trees such that `popup` is moved into the proper context. Should the `popup` attribute and/or `anchor` attribute cause this reordering? What happens if both these attributes are present but refer to different elements? What happens if multiple `popup` attributes refer to the same `popup` element? -## Areas for exploration +4. **Show/hide or show/close?** This proposal introduces a symmetrical `show`/`hide` method pair on the proposed `popup` element. `dialog`, however, sets a precedent for a `show` and `close` method pairing. That seemed less intuitive, but perhaps the existing pattern should be followed. Alternatively, the platform could introduce `hide` on `dialog` and consider `close` deprecated. -* **Showing `popup`s declaratively:** `popup` requires authors to call `show()` in script, which is not ideal for authors who wish to work declaratively. We may consider an HTML attribute that would show a relevant `popup` when the element the attribute is applied to is invoked. +## Areas for exploration * **Focus trapping:** the [`inert` attribute](https://whatpr.org/html/4288/interaction.html#the-inert-attribute) enables authors to mark portions of a document as inert, such that user input events are ignored. Inverting this model, new primitives could enable focus trapping with parts of a document, e.g. a `popup`. New focus trapping primitives could be useful in cases where the tab cycle should be constrained to the `popup`, but the rest of the document would receive other types of user input. * **Animating state transitions:** applying animations and transitions to interactive elements’ show/hide states can be difficult. For example, to apply a CSS transition the element must first produce an initial box before its properties can be transitioned to new values. That requires a two step process to first show the element, and in a subsequent frame, initiate a transition by applying a class. Likewise, since the browser manages the visibility of the popup for light dismiss behaviors, it is impossible to apply a close animation. To address this issue perhaps the answer is to invent a new CSS animation primitive that is triggered when an element stops producing a box. + +## Appendix + +### The `hidden` attribute + +This proposal specifies that, similarly to the `dialog` element, the `open` attribute can be used to show a `popup`. Currently, authors are advised to add a `hidden` attribute to `dialog` when hiding it, as there are [some quirks with removing the `open` attribute](https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element). Rather than porting over this behavior to `popup`, it would be ideal to [adjust the behavior on `dialog`](https://github.com/whatwg/html/issues/5802). As a result and to provide simpler authoring, we are proposing that authors solely remove/add the `open` attribute in order to toggle visibility of a `popup`, as opposed to introducing the `hidden` attribute to this new element. + +### Anchoring and event bubbling + +In a previous version of this document, we proposed that the hierarchy created by the `anchor` attribute relationship affects the event propagation path. With the introduction of a separate `popup` attribute which creates an invocation relationship, it is less clear whether event bubbling should be changed as a result of the `popup` attribute and/or the `anchor` attribute. + +This behavior as previously proposed adds complexity to the platform, and it is not clear whether there is enough value to authors for the platform to take on that complexity. We welcome feedback on this point and preserve the previous proposal here. + +#### Example of event bubbling + +In the markup below, a table with many rows is rendered, each of which displays a custom popup filled with commands when right-clicked. The popup is defined once and its anchor attribute is adjusted so that it is shown aligned with the table row when the contextmenu event is received. + +After a command is selected from the menu, the menu dispatches a custom command event which is then handled by the table row. The table row receives the event even though it isn’t a DOM ancester of the popup. This is because the event target parent of a popup is its anchor element. + +```html + + ... + ... + ... + ... +
+ + + + ... + + +``` + +Note: if event bubbling remains unchanged by the `anchor` attribute, authors in this case would need to query for the `popup`’s anchor element and dispatch the event from that element. So, `e.currentTarget.dispatchEvent(new CommandEvent(e.currentTarget.id))` becomes `bugCommands.anchor.dispatchEvent(new CommandEvent(e.currentTarget.id))`. \ No newline at end of file diff --git a/Popup/teaching-bubble.png b/Popup/teaching-bubble.png new file mode 100644 index 00000000..0a28c6e3 Binary files /dev/null and b/Popup/teaching-bubble.png differ