Close Watcher API

Draft Community Group Report,

Editor:
Domenic Denicola (Google)
Participate:
GitHub WICG/close-watcher (new issue, open issues)
Commits:
GitHub spec.bs commits

Abstract

The close watcher API provides a platform-agnostic way of handling close signals.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Close signals

(This section could be introduced as a new subsection of [HTML]'s User interaction section.)

In an implementation-defined (and likely device-specific) manner, a user can send a close signal to the user agent. This indicates that the user wishes to close something which is currently being shown on the screen, such as a popup, menu, dialog, picker, or display mode.

Some example close signals are:

Whenever the user agent receives a potential close signal targeted at a Document document, it must perform the following close signal steps:

  1. If document’s fullscreen element is non-null, then fully exit fullscreen and return.

    This does not fire any relevant event, such as keydown; it only fires fullscreenchange.

  2. Fire any relevant event, per UI Events or other relevant specifications. [UI-EVENTS]

    As an example of a relevant event that is outside of the model given in UI Events, current thinking is that assistive technology would synthesize an Esc keydown and keyup event sequence when the user sends a close signal by using a dimiss gesture.

    If multiple such events are fired, the user agent must pick one for the purposes of the following steps.

    For example, it is typical on desktop platforms for pressing down on the Esc key to be a close signal. So, if assistive technology is synthesizing both keydown and keyup events, then it would likely pick the keydown event for the next steps, to better match behavior of desktop platforms without assistive technology in play.

  3. If such an event was fired, and its canceled flag is set, then return.

  4. If such an event was fired, then perform the following steps within the same task as that event was fired in, immediately after firing the event. Otherwise, queue a global task on the user interaction task source given document’s relevant global object to perform the following steps.

  5. If document is not fully active, then return.

  6. Let closedSomething be the result of signaling close watchers on document’s relevant global object.

  7. If closedSomething was true, then return.

  8. Otherwise, there was nothing watching for a close signal. The user agent may instead interpret this interaction as some other action, instead of as a close signal.

On a desktop platform where Esc is the close signal, the user agent will first fire an appropriately-initialized keydown event. If the web developer intercepts this event and calls preventDefault(), then nothing further happens. But if the event is fired without being canceled, then the user agent proceeds to signal close watchers.

On Android where the back button is a potential close signal, no event is involved, so when the user agent determines that the back button represents a close signal, it queues a task to signal close watchers. If there is a close watcher on the close watcher stack, then that will get triggered; otherwise, the user agent will interpret the back button press as a request to traverse the history by a delta of −1.

1.1. Close watcher infrastructure

Each Window has a close watcher stack, a list of close watchers, initially empty.

It isn’t quite a proper stack, as items can get removed from the middle of it if a close watcher is destroyed or closed via web developer code. User-initiated close signals always act on the top of the close watcher stack, however.

Each Window has a timestamp of last activation used for close watchers. This is either a DOMHighResTimeStamp value, positive infinity, or negative infinity (i.e. the same value space as the last activation timestamp). It is initially positive infinity.

This value is used to ensure that a given user activation only enables a single CloseWatcher cancel or dialog cancel event to be fired, per user activation. This is different than requiring transient activation to fire the event, because we want to allow the event to happen arbitrarily long after the user activation.

A close watcher is a struct with the following items:

The is grouped with previous boolean is set if a close watcher is created without transient activation. (Except the very first close watcher in the close watcher stack that is created without transient activation, which gets a pass on this restriction.) It causes a user-triggered close signal to close all such grouped-together close watchers, thus ensuring that web developers can’t make close signals ineffective by creating an excessive number of close watchers which all intercept the signal.

To create a close watcher given a Window window, a list of steps cancelAction, and a list of steps closeAction:
  1. Assert: window’s associated Document is fully active.

  2. Let stack be window’s close watcher stack.

  3. Let wasCreatedWithUserActivation and groupedWithPrevious be null.

  4. If window has transient activation, then:

    1. Consume user activation given window.

    2. Set wasCreatedWithUserActivation to true.

    3. Set groupedWithPrevious to false.

  5. Otherwise, if there is no close watcher in stack whose was created with user activation is false, then:

    1. Set wasCreatedWithUserActivation to false.

    2. Set groupedWithPrevious to false.

    This will be the one "free" close watcher created without user activation, which is useful for cases like session inactivity timeout dialogs or notifications from the server.

  6. Otherwise:

    1. Set wasCreatedWithUserActivation to false.

    2. Set groupedWithPrevious to true.

  7. Set window’s timestamp of last activation used for close watchers to window’s last activation timestamp.

  8. Let closeWatcher be a new close watcher whose window is window, cancel action is cancelAction, close action is closeAction, was created with user activation is wasCreatedWithUserActivation, and is grouped with previous is groupedWithPrevious.

  9. Push closeWatcher onto stack.

  10. Return closeWatcher.

A close watcher closeWatcher is active if closeWatcher’s window's close watcher stack contains closeWatcher.

To destroy a close watcher closeWatcher, remove closeWatcher from closeWatcher’s window's close watcher stack.

To close a close watcher closeWatcher:
  1. If closeWatcher is not active, then return.

  2. If closeWatcher’s is running cancel action is true, then return.

  3. Let window be closeWatcher’s window.

  4. If window’s associated Document is fully active, and window’s timestamp of last activation used for close watchers does not equal window’s last activation timestamp, then:

    1. Set window’s timestamp of last activation used for close watchers to window’s last activation timestamp.

    2. Set closeWatcher’s is running cancel action to true.

    3. Let shouldContinue be the result of running closeWatcher’s cancel action.

    4. Set closeWatcher’s is running cancel action to false.

    5. If shouldContinue is false, then return.

  5. If closeWatcher is not active, then return.

  6. If window’s associated Document is not fully active, then return.

  7. Remove closeWatcher from window’s close watcher stack.

  8. Run closeWatcher’s close action.

To signal close watchers given a Window window:
  1. Let processedACloseWatcher be false.

  2. While window’s close watcher stack is not empty:

    1. Let closeWatcher be the last item in window’s close watcher stack.

    2. Close closeWatcher.

    3. Set processedACloseWatcher to true.

    4. If closeWatcher’s is grouped with previous is false, then break.

  3. Return processedACloseWatcher.

1.2. Close watcher API

[Exposed=Window]
interface CloseWatcher : EventTarget {
  constructor(optional CloseWatcherOptions options = {});

  undefined destroy();
  undefined close();

  attribute EventHandler oncancel;
  attribute EventHandler onclose;
};

dictionary CloseWatcherOptions {
  AbortSignal signal;
};
watcher = new CloseWatcher()
watcher = new CloseWatcher({ signal })

Attempts to create a new CloseWatcher instance.

If the signal option is provided, watcher can be destroyed (as if by watcher.destroy()) by aborting the given AbortSignal.

If any close watcher (including both CloseWatcher objects and modal dialog elements) is already active, and the Window does not have transient user activation, then the resulting CloseWatcher will be closed together with that already-active close watcher in response to any close signal.

watcher.destroy()

Deactivates this CloseWatcher instance, so that it will no longer receive close events and so that new independent CloseWatcher instances can be constructed.

This is intended to be called if the relevant UI element is closed in some other way than via a close signal, e.g. by pressing an explicit "Close" button.

watcher.close()

Acts as if a close signal was sent targeting this CloseWatcher instance, by firing a cancel and close event, and deactivating the close watcher as if destroy() was called.

This is a helper utility that can be used to consolidate closing logic into the close event handler, by having all non-close signal closing affordances call close().

Each CloseWatcher has an internal close watcher, which is a close watcher.

The new CloseWatcher(options) constructor steps are:
  1. If this's relevant global object's associated Document is not fully active, then throw an "InvalidStateError" DOMException.

  2. Let closeWatcher be the result of creating a close watcher given this's relevant global object, with:

  3. If options["signal"] exists, then:

    1. If options["signal"] is aborted, then destroy closeWatcher.

    2. Add the following abort steps to options["signal"]:

      1. Destroy closeWatcher.

  4. Set this's internal close watcher to closeWatcher.

The destroy() method steps are to destroy this's internal close watcher.

The close() method steps are to close this's internal close watcher.

Objects implementing the CloseWatcher interface must support the oncancel and onclose event handler IDL attribute, whose event handler event types are respectively cancel and close.

2. Updates to other specifications

2.1. Fullscreen

Replace the sentence about "If the end user instructs..." in Fullscreen API § 4 UI with the following:

If the user initiates a close signal, this will trigger the fully exit fullscreen algorithm as part of the close signal steps. This takes precedence over any close watchers.

2.2. The dialog element

Update HTML’s The dialog element section as follows: [HTML]

Each dialog element has a close watcher, which is a close watcher or null, initially null.

In the showModal() steps, after adding subject to the top layer, append the following step:
  1. Set subject’s close watcher to the result of creating a close watcher given subject’s relevant global object, with:

The resultant close watcher might be grouped with others on the stack, so that it responds alongside them to a single close signal. For example, opening multiple modal dialogs from a single user activation can cause this. The showModal() method proceeds without any exception or other indication of this, although the browser could report a warning to the console.

Remove the "Canceling dialogs" section entirely. It is now handled entirely by the infrastructure in § 1 Close signals and the creation of a close watcher.

Modify the close the dialog steps to destroy the dialog's close watcher and set it to null, if it is not already null.

Similarly, modify the "If at any time a dialog element is remo ved from a Document" steps to do the same.

3. Security and privacy considerations

3.1. Security considerations

The main security consideration with this API is preventing abusive pages from hijacking the fallback behavior in the last part of the close signal steps. A concrete example is on Android, where the close signal is the software back button, and this fallback behavior is to traverse the history by a delta of −1. If developers could always intercept Android back button presses via CloseWatcher instances and dialog elements, then they could effectively break the back button by never letting it pass through to the fallback behavior.

Much of the complexity of this specification is designed around preventing such abuse. Without it, the API could consist of a single event. (Although that would make it ergonomically difficult for developers to coordinate on which component of their application should handle the close signal.) But with this constraint, we need an API surface such as the CloseWatcher() constructor which can note whether transient activation was present at construction time, as well as the close watcher stack to ensure that we remove at least one close watcher per close signal.

Concretely, the mechanism of creating a close watcher ensures that web developers can only create CloseWatcher instances, or call preventDefault() on cancel events, by attempting to consume user activation. If a CloseWatcher instance is created without transient activation, then after one "free" close watcher, any such close watcher is grouped together with the currently-top close watcher in the stack, so that both of them are closed by a single close signal. This gives similar protections to what browsers have in place today, where back button UI skips entries that were added without user activation. Similarly, if there hasn’t been any transient activation, the cancel event is not fired.

We do allow one "free" ungrouped CloseWatcher to be created, even without transient activation, to handle cases like session inactivity timeout dialogs, or urgent notifications of server-triggered events. The end result is that this specification expands the number of Android back button presses that a maximally-abusive page could require to escape from number of user activations + 1 to number of user activations + 2. (See the explainer for a full analysis.) We believe this tradeoff is worthwhile.

3.2. Privacy considerations

We believe the privacy impact of this API is minimal. The only information it gives about the user to the web developer is that a close signal has occurred, which is a very infrequent and coarse piece of user input.

In all cases we’re aware of today, such close signals are already detectable by web developers (e.g., by using keydown listeners on desktop or popstate listeners on Android). In theory, by correlating these existing events with the CloseWatcher's close event, a web developer could determine some information about the platform. (I.e., if they correlate with keydown events, the user is likely on desktop, or at least on a keyboard-attached mobile device.) This is similar to existing techniques which detect whether touch events or mouse events are fired, and user agents which want to emulate a different platform in order to mask the user’s choice might want to apply similar mitigation techniques for close watchers as they do for other platform-revealing events.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FULLSCREEN]
Philip Jägenstedt. Fullscreen API Standard. Living Standard. URL: https://fullscreen.spec.whatwg.org/
[HR-TIME-2]
Ilya Grigorik. High Resolution Time Level 2. URL: https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[UI-EVENTS]
Gary Kacmarcik; Travis Leithead. UI Events. URL: https://w3c.github.io/uievents/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Informative References

[CONSOLE]
Dominic Farolino; Robert Kowalski; Terin Stock. Console Standard. Living Standard. URL: https://console.spec.whatwg.org/

IDL Index

[Exposed=Window]
interface CloseWatcher : EventTarget {
  constructor(optional CloseWatcherOptions options = {});

  undefined destroy();
  undefined close();

  attribute EventHandler oncancel;
  attribute EventHandler onclose;
};

dictionary CloseWatcherOptions {
  AbortSignal signal;
};