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.
-
The Esc key on desktop platforms
-
The back button on Android
-
The two-finger scrub "z" gesture on iOS when using VoiceOver (or, more generally, any assistive technology dismiss gesture)
-
The square button on a DualShock (PlayStation) controller
Whenever the user agent receives a potential close signal targeted at a Document
document, it must perform the following close signal steps:
-
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 firesfullscreenchange
. -
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
If multiple such events are fired, the user agent must pick one for the purposes of the following steps.keydown
andkeyup
event sequence when the user sends a close signal by using a dimiss gesture.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
andkeyup
events, then it would likely pick thekeydown
event for the next steps, to better match behavior of desktop platforms without assistive technology in play. -
If such an event was fired, and its canceled flag is set, then return.
-
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.
-
If document is not fully active, then return.
-
Let closedSomething be the result of signaling close on document.
-
If closedSomething was true, then return.
-
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:
-
A close action, a list of steps. These steps can never throw an exception.
-
An is still valid list of steps. These steps can never throw an exception, and return either true or false.
-
A blocks further developer-controlled close watchers boolean.
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.
Document
document:
-
While document’s close watcher stack is not empty:
-
Let closeWatcher be the result of popping from document’s close watcher stack.
-
If closeWatcher’s is still valid steps return true, then:
-
Perform closeWatcher’s close action.
-
Return true.
-
-
-
Return false.
Window
window:
-
Let document be window’s associated Document.
-
If document is not fully active, then return false.
-
Let needsUserActivation be false.
-
For each closeWatcher in document’s close watcher stack:
-
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.
-
-
Let canCreate be false.
-
If needsUserActivation is false, then set canCreate to true.
-
Otherwise, if window has transient activation, then:
-
Consume user activation given window.
-
Set canCreate to true.
-
-
If canCreate is true, then set window’s timestamp of last activation used for close watchers to window’s last activation timestamp.
-
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 bywatcher.
) by aborting the givendestroy()
AbortSignal
.If a
CloseWatcher
is already active, and theWindow
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 receiveclose
events and so that newCloseWatcher
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 aclose
event and deactivating the close watcher as ifdestroy()
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 callclose()
.
Each CloseWatcher
has an is active, which is a boolean, and an firing cancel event, which is a boolean.
new CloseWatcher(options)
constructor steps are:
-
If this's relevant global object's associated Document is not fully active, then throw an "
InvalidStateError
"DOMException
. -
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
. -
Set this's firing cancel event to false.
-
Push a new close watcher on this's relevant global object's associated document's close watcher stack, with its items set as follows:
-
close action being to signal close on this
-
is still valid steps being to return this's is active
-
blocks further developer-controlled close watchers being true
-
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
.
CloseWatcher
closeWatcher:
-
If closeWatcher’s is active is false, then return.
-
If closeWatcher’s firing cancel event is true, then return.
-
Let window be closeWatcher’s relevant global object.
-
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:
-
Set window’s timestamp of last activation used for close watchers to window’s last activation timestamp.
-
Set closeWatcher’s firing cancel event to true.
-
Let shouldContinue be the result of firing an event named
cancel
at closeWatcher, with thecancelable
attribute initialized to true. -
Set closeWatcher’s firing cancel event to false.
-
If shouldContinue is false, then return.
-
-
If closeWatcher’s is active is true, and window’s associated Document is fully active, then fire an event named
close
at closeWatcher. -
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]
showModal()
steps, after adding subject to the top layer, append the following step:
-
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:
-
close action being to cancel the dialog subject
-
is still valid steps being to return true if subject’s node document is blocked by the modal dialog subject, and return false otherwise
-
blocks further developer-controlled close watchers being true
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.)
-
Let window be dialog’s relevant global object.
-
If window’s timestamp of last activation used for close watchers does not equal window’s last activation timestamp, then:
-
Let shouldContinue to the result of firing an event named
cancel
at dialog, with thecancelable
attribute initialized to true. -
Set window’s timestamp of last activation used for close watchers to window’s last activation timestamp.
-
If shouldContinue is false, then return.
-
-
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.