Skip to content

Commit

Permalink
Align spec with implementation experience
Browse files Browse the repository at this point in the history
* The constructor needs to _consume_ transient user activation, not just check it.
* We need a reentrancy flag for the cancel event.
* Note the discussion about the Esc key being user activation (#7).
  • Loading branch information
domenic committed Sep 21, 2021
1 parent e49767e commit b96e092
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 10 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ _Note: the name of this event, i.e. `cancel` instead of something like `beforecl

For abuse prevention purposes, this event only fires if the page has received user activation. Furthermore, once it fires for one `CloseWatcher` instance, it will not fire again for any `CloseWatcher` instances until the page again gets user activation. This ensures that if the user sends a close signal twice in a row without any intervening user activation, the signal definitely goes through, destroying the `CloseWatcher`.

_Currently the <kbd>Esc</kbd> key itself counts as user activation, so on desktop platforms where that is the close signal, this clause has no effect. That is, there is always a recent user activation and so `cancel` will always be fired. See discussion in [#7](https://github.com/WICG/close-watcher/issues/7) about whether this is a good or bad thing._

Note that the `cancel` event is not fired when the user navigates away from the page: i.e., it has no overlap with `beforeunload`. `beforeunload` remains the best way to confirm a page unload, with `cancel` only used for confirming a close signal.

If called from within transient user activation, `watcher.signalClosed()` also invokes `cancel` event handlers, which would trigger event listeners like the above example code. If called without user activation, then it skips straight to the `close` event.
Expand Down
30 changes: 20 additions & 10 deletions spec.bs
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,20 @@ A <dfn export>close watcher</dfn> is a [=struct=] with the following [=struct/it
</div>

<div algorithm>
We <dfn lt="can create a developer-controlled close watcher|cannot create a developer-controlled close watcher">can create a developer-controlled close watcher</dfn> for a {{Document}} |document| if the following steps return true:
To <dfn lt="check if we can create a developer-controlled close watcher">check if we can create a developer-controlled close watcher</dfn> for a {{Window}} |window|:

1. Let |document| be |window|'s [=associated Document=].
1. If |document| is not [=Document/fully active=], then return false.
1. If |document|'s [=relevant global object=] has [=transient activation=], then return true.
1. Let |needsUserActivation| be false.
1. [=list/For each=] |closeWatcher| in |document|'s [=close watcher stack=]:
1. If |closeWatcher|'s [=close watcher/is still valid=] steps return true, and |closeWatcher|'s [=close watcher/blocks further developer-controlled close watchers=] is true, then return false.
1. Return true.
1. If |closeWatcher|'s [=close watcher/is still valid=] steps return true, and |closeWatcher|'s [=close watcher/blocks further developer-controlled close watchers=] is true, then set |needsUserActivation| to true and [=iteration/break=].
1. Let |canCreate| be false.
1. If |needsUserActivation| is false, then set |canCreate| to true.
1. Otherwise, if |window| has [=transient activation=], then:
1. [=Consume user activation=] given |window|.
1. Set |canCreate| to true.
1. If |canCreate| is true, then set |window|'s [=timestamp of last activation used for close watchers=] to |window|'s <a spec="HTML">last activation timestamp</a>.
1. Return |canCreate|.
</div>

<h3 id="close-watcher-api">Close watcher API</h3>
Expand Down Expand Up @@ -195,19 +202,19 @@ interface CloseWatcher : EventTarget {
</dd>
</dl>

Each {{CloseWatcher}} has an <dfn for="CloseWatcher">is active</dfn>, which is a boolean.
Each {{CloseWatcher}} has an <dfn for="CloseWatcher">is active</dfn>, which is a boolean, and an <dfn for="CloseWatcher">firing cancel event</dfn>, which is a boolean.

<div algorithm>
The <dfn constructor for="CloseWatcher" lt="CloseWatcher()">new CloseWatcher()</dfn> constructor steps are:

1. If [=this=]'s [=relevant global object=]'s [=associated Document=] is not [=Document/fully active=], then throw an "{{InvalidStateError}}" {{DOMException}}.
1. If we [=cannot create a developer-controlled close watcher=] for [=this=]'s [=relevant global object=]'s [=associated document=], then throw a "{{NotAllowedError}}" {{DOMException}}.
1. 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}}.
1. Set [=this=]'s [=CloseWatcher/is active=] to true.
1. Set [=this=]'s [=CloseWatcher/firing cancel event=] to false.
1. [=stack/Push=] a new [=close watcher=] on [=this=]'s [=relevant global object=]'s [=associated document=]'s [=close watcher stack=], with its [=struct/items=] set as follows:
* [=close watcher/close action=] being to [=CloseWatcher/signal close=] on [=this=]
* [=close watcher/is still valid=] steps being to return [=this=]'s [=CloseWatcher/is active=]
* [=close watcher/blocks further developer-controlled close watchers=] being true
1. Set [=this=]'s [=relevant global object=]'s [=timestamp of last activation used for close watchers=] to [=this=]'s [=relevant global object=]'s <a spec="HTML">last activation timestamp</a>.
</div>

<p algorithm>
Expand All @@ -224,10 +231,13 @@ Objects implementing the {{CloseWatcher}} interface must support the <dfn attrib
To <dfn for="CloseWatcher">signal close</dfn> on a {{CloseWatcher}} |closeWatcher|:

1. If |closeWatcher|'s [=CloseWatcher/is active=] is false, then return.
1. If |closeWatcher|'s [=CloseWatcher/firing cancel event=] is true, then return.
1. Let |window| be |closeWatcher|'s [=relevant global object=].
1. If |window|'s [=associated Document=] is [=Document/fully active=], and |window|'s [=timestamp of last activation used for close watchers=] does not equal |window|'s <a spec="HTML">last activation timestamp</a>, then:
1. Let |shouldContinue| be the result of [=firing an event=] named {{CloseWatcher/cancel}} at |closeWatcher|, with the {{Event/cancelable}} attribute initialized to true.
1. Set |window|'s [=timestamp of last activation used for close watchers=] to |window|'s <a spec="HTML">last activation timestamp</a>.
1. Set |closeWatcher|'s [=CloseWatcher/firing cancel event=] to true.
1. Let |shouldContinue| be the result of [=firing an event=] named {{CloseWatcher/cancel}} at |closeWatcher|, with the {{Event/cancelable}} attribute initialized to true.
1. Set |closeWatcher|'s [=CloseWatcher/firing cancel event=] to false.
1. If |shouldContinue| is false, then return.
1. If |window|'s [=associated Document=] is [=Document/fully active=], then [=fire an event=] named {{CloseWatcher/close}} at |closeWatcher|.
1. Set |closeWatcher|'s [=CloseWatcher/is active=] to false.
Expand All @@ -248,12 +258,12 @@ Update <cite>HTML</cite>'s <a href="https://html.spec.whatwg.org/multipage/inter
<div algorithm="showModal patch">
In the {{HTMLDialogElement/showModal()}} steps, after adding |subject| to the [=top layer=], append the following step:

1. If we [=can create a developer-controlled close watcher=] given |subject|'s [=Node/node document=], then [=stack/push=] a new [=close watcher=] on |subject|'s [=Node/node document=]'s [=close watcher stack=], with its [=struct/items=] set as follows:
1. If the result of [=checking if we can create a developer-controlled close watcher=] given |subject|'s [=relevant global object=] is true, then [=stack/push=] a new [=close watcher=] on |subject|'s [=Node/node document=]'s [=close watcher stack=], with its [=struct/items=] set as follows:
* [=close watcher/close action=] being to [=cancel the dialog=] |subject|
* [=close watcher/is still valid=] steps being to return true if |subject|'s [=Node/node document=] is <a spec="HTML" lt="blocked by a modal dialog">blocked by the modal dialog</a> |subject|, and return false otherwise
* [=close watcher/blocks further developer-controlled close watchers=] being true

<p class="note">If we [=cannot create a developer-controlled close watcher=], then this modal dialog will not respond to [=close signals=]. The {{HTMLDialogElement/showModal()}} method proceeds without any exception or other indication of this, although the browser could [=report a warning to the console=].
<p class="note">If we cannot create a developer-controlled close watcher, then this modal dialog will not respond to [=close signals=]. The {{HTMLDialogElement/showModal()}} method proceeds without any exception or other indication of this, although the browser could [=report a warning to the console=].
</div>

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 [[#close-signals]].)
Expand Down

0 comments on commit b96e092

Please sign in to comment.