diff --git a/research/src/pages/popup/popup.research.explainer.mdx b/research/src/pages/popup/popup.research.explainer.mdx index c7aafd8e2..a95872395 100644 --- a/research/src/pages/popup/popup.research.explainer.mdx +++ b/research/src/pages/popup/popup.research.explainer.mdx @@ -6,7 +6,7 @@ pathToResearch: /components/popup.research --- - [@mfreed7](https://github.com/mfreed7), [@scottaohara](https://github.com/scottaohara), [@BoCupp-Microsoft](https://github.com/BoCupp-Microsoft), [@domenic](https://github.com/domenic), [@gregwhitworth](https://github.com/gregwhitworth), [@chrishtr](https://github.com/chrishtr), [@dandclark](https://github.com/dandclark), [@una](https://github.com/una), [@smhigley](https://github.com/smhigley), [@aleventhal](https://github.com/aleventhal) -- May 9, 2022 +- June 1, 2022 @@ -83,18 +83,28 @@ This section lays out the full details of this proposal. If you'd prefer, you ca A new content attribute, **`popup`**, controls both the top layer status and the dismiss behavior. There are several allowed values for this attribute: -* **`popup=popup`** - A top layer element following “Popup” dismiss behaviors (see below). +* **`popup=auto`** - A top layer element following "Auto" dismiss behaviors (see below). * **`popup=hint`** - A top layer element following “Hint” dismiss behaviors (see below). * **`popup=async`** - A top layer element following “Async” dismiss behaviors (see below). So this markup represents popup content: ```html -
I am a popup
+
I am a popup
``` As written above, the `
` will be rendered `display:none` by the UA stylesheet, meaning it will not be shown when the page is loaded. To show the popup, one of several methods can be used: [declarative triggering](#declarative-triggers), [Javascript triggering](#javascript-trigger), or [page load triggering](#page-load-trigger). +Additionally, the `popup` attribute can be used without a value (or with an empty string `""` value), and in that case it will behave identically to `popup=auto`: + +```html +
I am a popup
+
I am also an "auto" popup
+
So am I
+``` + +For convenience and brevity, the remainder of this explainer will use this boolean-like syntax in most cases, e.g. `
`. + ## Showing and Hiding a Popup There are several ways to "show" a popup, and they are discussed in this section. When any of these methods are used to show a popup, it will be made visible and moved (by the UA) to the [top layer](https://fullscreen.spec.whatwg.org/#top-layer). The top layer is a layer that paints on top of all other page content, with the exception of other elements currently in the top layer. This allows, for example, a "stack" of popups to exist. @@ -105,7 +115,7 @@ A common design pattern is to have an activating element, such as a ` -
Popup content
+
Popup content
``` When the button in this example is activated, the UA will call `.showPopup()` on the `
` element if it is currently hidden, or `hidePopup()` if it is showing. In this way, no Javascript will be necessary for this use case. @@ -116,7 +126,7 @@ If the desire is to have a button that only shows or only hides a popup, the fol -
Popup content
+
Popup content
``` Note that all three attributes can be used together like this, pointing to the same element. However, using more than one triggering attribute on **a single button** is not recommended. @@ -145,10 +155,10 @@ There are several conditions that will cause `showPopup()` and/or `hidePopup()` ### Page Load Trigger -As mentioned above, a `
` will be hidden by default. If it is desired that the popup should be shown automatically upon page load, the `defaultopen` attribute can be applied: +As mentioned above, a `
` will be hidden by default. If it is desired that the popup should be shown automatically upon page load, the `defaultopen` attribute can be applied: ```html -
+
``` In this case, the UA will immediately call `showPopup()` on the element, as it is parsed. If multiple such elements exist on the page, only the first such element (in DOM order) on the page will be shown. @@ -166,17 +176,29 @@ Note also that more than one `async` popup can use `defaultopen` and all such po
Also shown on page load
``` +### CSS Pseudo Class + +When a popup (or any element) is in the top layer, it will match the `:top-layer` pseudo class: + +```javascript +const popup = document.createElement('div'); +popup.popup = 'auto'; +popup.matches(':top-layer') === false; +popup.showPopup(); +popup.matches(':top-layer') === true; +``` + ### Shown vs Hidden Popups -The styling for a popup is provided by roughly the following UA stylesheet rules: +The styling for a popup is provided by **roughly** the following UA stylesheet rules: ```css -[popup i]:not(:popup-open) { +[popup]:not(:top-layer) { display: none; } -[popup i] { +[popup] { position: fixed; } ``` @@ -184,15 +206,14 @@ The styling for a popup is provided by roughly the following UA stylesheet rules The above rules mean that a popup, when not "shown", has `display:none` applied, and that style is removed when one of the methods above is used to show the popup. Note that the `display:none` UA stylesheet rule is **not** `!important`. In other words, developer style rules can be used to override this UA style to make a not-showing popup visible in the page. In this case, the popup will **not** be displayed in the top layer, but rather at it's ordinary `z-index` position within the document. This can be used, for example, to animate the show/hide behavior of the popup, or make popup content "return to the page" instead of becoming hidden. - ## IDL Attribute and Feature Detection -The `popup` content attribute will be [reflected](https://html.spec.whatwg.org/#reflect) as an IDL attribute: +The `popup` content attribute will be [reflected](https://html.spec.whatwg.org/#reflect) as a nullable IDL attribute: ```webidl [Exposed=Window] partial interface Element { - attribute DOMString popup; + attribute DOMString? popup; ``` This not only allows developer ease-of-use from Javascript, but also allows for a feature detection mechanism: @@ -203,14 +224,27 @@ function supportsPopup() { } ``` -Further, only [valid values](#html-content-attribute) of the content attribute will be reflected to the IDL property, with invalid values being reflected as the empty string `""`. For example: +Further, only [valid values](#html-content-attribute) of the content attribute will be reflected to the IDL property, with invalid values being reflected as the value `null`. For example: ```javascript const div = document.createElement('div'); div.setAttribute('popup','hint'); div.popup === 'hint'; // true div.setAttribute('popup','invalid!'); -div.popup === ''; // true +div.popup === null; // true +``` + +This allows feature detection of the values, for forward compatibility: + +```javascript +function supportsPopupType(type) { + if !Element.prototype.hasOwnProperty("popup") + return false; // Popup API not supported + // If the assignment fails, it will return null: + return !!(document.createElement('div').popup = type); +} +supportsPopupType('async') === true; +supportsPopupType('invalid!') === false; ``` @@ -231,14 +265,31 @@ Neither of these events are cancellable, and both are fired asynchronously. ## Focus Management -Elements that move into the top layer may require focus to be moved to that element, or a descendant element. However, not all elements in the top layer will require focus. For example, a modal `` will have focus set to its first interactive element, if not the dialog element itself, because a modal dialog is something that requires immediate attention. On the other hand, a `
` (which will more often than not represent a "tooltip") does not receive focus at all (nor is it expected to contain focusable elements). Similarly, a `
` should not immediately receive focus (even if it contains focusable elements) because it is meant for out-of-band communication of state, and is not meant to interrupt a user's current action. Additionally, if the top layer element **should** receive immediate focus, there is a question about **which** part of the element gets that initial focus. For example, the element itself could receive focus, or one of its focusable descendants could receive focus. To provide control over these behaviors, two attributes can be used on popups: +Elements that move into the top layer may require focus to be moved to that element, or a descendant element. However, not all elements in the top layer will require focus. For example, a modal `` will have focus set to its first interactive element, if not the dialog element itself, because a modal dialog is something that requires immediate attention. On the other hand, a `
` (which will more often than not represent a "tooltip") does not receive focus at all (nor is it expected to contain focusable elements). Similarly, a `
`, which may represent a dynamic notification message (commonly referred to as a toast), or potentially a persistent chat widget, should not immediately receive focus (even if it contains focusable elements). This is because such popups are meant for out-of-band communication of state, and are not meant to interrupt a user's current action. Additionally, if the top layer element **should** receive immediate focus, there is a question about **which** part of the element gets that initial focus. For example, the element itself could receive focus, or one of its focusable descendants could receive focus. + +To provide control over these behaviors, the `autofocus` attribute can be used on or within popups. When present on a popup or one of its descendants, it will result in focus being moved to the popup or the specified element when the popup is rendered. Note that `autofocus` is [already a global attribute](https://html.spec.whatwg.org/multipage/interaction.html#the-autofocus-attribute), but the existing behavior applies to element focus on **page load**. This proposal extends that definition to be used within popups, and the focus behavior happens **when they are shown**. Note that adding `autofocus` to a popup descendant does **not** cause the popup to be shown on page load, and therefore it does not cause focus to be moved into the popup **on page load**, unless the `defaultopen` attribute is also used. + +The `autofocus` attribute allows control over the focus behavior when the popup is shown. When the popup is hidden, often the most user friendly thing to do is to return focus to the previously-focused element. The `` element currently behaves this way. However, for popups, there are some nuances. For example, if the popup is being hidden via light dismiss, because the user clicked on another element outside the popup, the focus should **not** be returned to another element, it should go to the clicked element (if focusable, or `` if not). There are a number of other such considerations. The behavior on hiding the popup is: + +- A popup element has a **previously focused element**, initially `null`, which is set equal to `document.activeElement` when the popup is shown, if a) the popup is a `auto` or `hint` popup, and b) if the [popup stack](#the-popup-stack) is currently empty. The **previously focused element** is set back to `null` when a popup is hidden. -- `autofocus`. When present on a popup or one of its descendants, it will result in focus being moved to the specified element when the popup is rendered. Note that `autofocus` is [already a global attribute](https://html.spec.whatwg.org/multipage/interaction.html#the-autofocus-attribute), but the existing behavior applies to element focus on **page load**. This proposal extends that definition to be used within popups, and the focus behavior happens **when they are shown**. Note that adding `autofocus` to a popup descendant does **not** cause the popup to be shown on page load, and therefore it does not cause focus to be moved into the popup **on page load**, unless the `defaultopen` attribute is also used. +- When a popup is hidden, focus is set back to the **previously focused element**, if it is non-`null`, in the following cases: + 1. Light dismiss via [close signal](https://wicg.github.io/close-watcher/#close-signal) (e.g. Escape key pressed). + 2. Hide popup from Javascript via `hidePopup()`. + 3. Hide popup via a **popup-contained**\* triggering element with `hidepopup=popup_id` or `togglepopup=popup=id`. The triggering element must be popup-contained, otherwise the keyboard or mouse activation of the triggering element should have already moved focus to that element. + 4. Hide popup because its `popup` type changes (e.g. via `mypopup.popup='something else';`). -- `delegatesfocus`. When present on the popup element itself, this causes the **first focusable descendent** of the popup to be focused when the popup is shown. The purpose of this attribute is to handle the cases in which the popup content or ordering is not known prior to rendering, so that `autofocus` cannot be used effectively. (Eventually, the `delegatesfocus` attribute might be made applicable to any element, not just popups, to control focus behavior more generally.) + - Any other actions which hide the popup will **not** cause the focus to be changed when the popup hides. In these cases, the "normal" behavior happens for each action. Some examples include: + 1. Click outside the popup (focus the clicked thing). + 2. Click on a **non-popup-contained**\* triggering element to close popup (focus the triggering element). This is a special case of the point just above. + 3. Tab-navigate (focus the next tabbable element in the document's focus order). + 4. Hide popup because it was removed from the document (event handlers not allowed while removing elements). + 5. Hide popup when a modal dialog or fullscreen element is shown (follow dialog/fullscreen focusing steps). + 6. Hide popup via `showPopup()` on another popup that hides this one (follow new popup focusing steps). -If both `autofocus` and `delegatesfocus` are both present on the popup element, the popup element is focused when shown. +The intention of the above set of behaviors is to return focus to the previously focused element when that makes sense, and not do so when the user's intention is to move focus elsewhere or when it would be confusing. +\* In the above, "a popup contained triggering element" means the triggering element is contained within **the popup being hidden**, not any arbitrary popup. ## Anchoring @@ -253,7 +304,7 @@ A new attribute, `anchor`, can be used on a popup element to refer to the popup' Akin to modal `` and fullscreen elements, popups allow access to a `::backdrop` pseudo element, which is a full-screen element placed directly behind the popup in the top layer. This allows the developer to do things like blur out the background when a popup is showing: ```html -
I'm a popup
+
I'm a popup