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 on document.

  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.

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. If there is a still-valid close watcher, 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 Document has a close watcher stack, a stack of close watchers, initially empty.

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 still valid steps are a spec convenience that allows us to push close watchers onto the stack without having to add hooks to appropriately clean them up every time they become invalidated. Doing so can be tricky as in addition to explicit teardown steps, there are often implicit ones, e.g. by removing a relevant element from the document.

To signal close given a Document document:
  1. While document’s close watcher stack is not empty:

    1. Let closeWatcher be the result of popping from document’s close watcher stack.

    2. If closeWatcher’s is still valid steps return true, then:

      1. Perform closeWatcher’s close action.

      2. Return true.

  2. Return false.

To check if we can create a developer-controlled close watcher for a Window window:
  1. Let document be window’s associated Document.

  2. If document is not fully active, then return false.

  3. Let needsUserActivation be false.

  4. For each closeWatcher in document’s close watcher stack:

    1. If closeWatcher’s is still valid steps return true, and closeWatcher’s blocks further developer-controlled close watchers is true, then set needsUserActivation to true and break.

  5. Let canCreate be false.

  6. If needsUserActivation is false, then set canCreate to true.

  7. Otherwise, if window has transient activation, then:

    1. Consume user activation given window.

    2. Set canCreate to true.

  8. If canCreate is true, then set window’s timestamp of last activation used for close watchers to window’s last activation timestamp.

  9. Return canCreate.

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 a CloseWatcher is already active, and the Window does not have transient user activation, then this will instead throw a "NotAllowedError" DOMException.

watcher.destroy()

Deactivates this CloseWatcher instance, so that it will no longer receive close events and so that new 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 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 is active, which is a boolean, and an firing cancel event, which is a boolean.

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. If the result of checking if we can create a developer-controlled close watcher for this's relevant global object is false, then throw a "NotAllowedError" DOMException.

  3. Set this's is active to true.

  4. Set this's firing cancel event to false.

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

    1. If options["signal"] is aborted, then set this's is active to false and return.

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

      1. Set this's is active to false.

  6. Push a new close watcher on this's relevant global object's associated document's close watcher stack, with its items set as follows:

The destroy() method steps are to set this's is active to false.

The close() method steps are to signal close on this.

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.

To signal close on a CloseWatcher closeWatcher:
  1. If closeWatcher’s is active is false, then return.

  2. If closeWatcher’s firing cancel event is true, then return.

  3. Let window be closeWatcher’s relevant global object.

  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 firing cancel event to true.

    3. Let shouldContinue be the result of firing an event named cancel at closeWatcher, with the cancelable attribute initialized to true.

    4. Set closeWatcher’s firing cancel event to false.

    5. If shouldContinue is false, then return.

  5. If closeWatcher’s is active is true, and window’s associated Document is fully active, then fire an event named close at closeWatcher.

  6. Set closeWatcher’s is active to false.

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]

In the showModal() steps, after adding subject to the top layer, append the following step:
  1. If the result of checking if we can create a developer-controlled close watcher given subject’s relevant global object is true, then push a new close watcher on subject’s node document's close watcher stack, with its items set as follows:

    If we cannot create a developer-controlled close watcher, then this modal dialog will not respond to close signals. The showModal() method proceeds without any exception or other indication of this, although the browser could report a warning to the console.

Replace the "Canceling dialogs" section entirely with the following definition. (The previous prose about providing a user interface to cancel such dialogs, and the task-queuing, is now handled by the infrastructure in § 1 Close signals.)

To cancel the dialog dialog:
  1. Let window be dialog’s relevant global object.

  2. If window’s timestamp of last activation used for close watchers does not equal window’s last activation timestamp, then:

    1. Let shouldContinue to the result of firing an event named cancel at dialog, with the cancelable attribute initialized to true.

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

    3. If shouldContinue is false, then return.

  3. Close the dialog dialog with no return value.

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. But with this constraint, we need an API surface such as the CloseWatcher() constructor which can be gated by additional checks, as well as the close watcher stack to ensure that each close watcher can only consume a single close signal.

Concretely, the mechanism of checking if we can create a developer-controlled close watcher ensures that web developers can only create CloseWatcher instances, or call preventDefault() on cancel events, by consuming user activation. This gives similar protections to what browsers have in place today, where back button UI skips entries that were added without user activation.

We do allow one "free" CloseWatcher to be created, without consuming user 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;
};