v157 · capabilities (fugu) · experimental
Popup Capability Delegation
Clicking a service-worker notification runs in a context with no DOM — so today it cannot lead to an existing tab opening a connected popup (window.open() gets popup-blocked: no transient activation). This proposal lets the notificationclick handler delegate its one transient "popup" capability to a same-origin client window with client.postMessage(msg, {delegate: 'popup'}); the tab's message handler then has one second to call window.open() — restoring parity with the legacy document-bound Notification API, with a strict 1-click-1-popup invariant.
concepts
-
Transient activation playground
The ground truth the proposal builds on, fully real in today's browsers:
window.open()on a click succeeds, after a 6-second delay the expired activation gets it popup-blocked, and a second open from the same click finds the activation consumed. Livenavigator.userActivationreadout throughout — this is the gate the delegated token bypasses. -
Delegation probe
The exact proposed surface, probed behaviourally: this page's service worker calls
client.postMessage(msg, {delegate: 'popup'})outside a notification click. A supporting browser must throwNotAllowedError; a browser without the feature silently ignores the unknown option and the message arrives. Either way you see the verbatim outcome — a probe that can genuinely return false. Plus the window-sidepostMessage({delegate: 'payment'})contrast showing the Capability Delegation framework this extends. -
Notification → popup lab
The full flow, end to end and real: register the worker, grant notification permission, fire a notification, click it — the worker focuses this tab and delegates
"popup"; the page synchronously callswindow.open()on a connected preview window that talks back overwindow.opener. On browsers without the feature the same click honestly hits the popup blocker — the exact gap this proposal closes. Edge toggles: a no-delegation control arm, a deliberate 1.5 s delay (token expiry), and a double-open (single-use invariant).
why it is proposed
Webmail, chat and productivity apps open compose/preview popups from an existing tab so the popup can share in-memory state via window.opener. With service-worker notifications (registration.showNotification()), the click lands in the worker: it may focus() a client or Clients.openWindow() a cold new tab, but the focused tab cannot then window.open() — it never received the user's activation. The explainer chose explicit delegation over ambient worker activation so that an intervening analytics postMessage() cannot accidentally consume the capability, and over a hypothetical clients.openAuxiliaryWindow() because only the tab itself can hold a synchronous WindowProxy, parse window features against real screen geometry, and reuse existing popup-management code. Lifespans are double-bounded (≤5 s in the worker event, 1 s in the receiving tab) and one click yields at most one focus plus one popup.
the proposed surface
// Service Workers spec addition (from the explainer):
dictionary ClientPostMessageOptions : StructuredSerializeOptions {
DOMString? delegate;
};
partial interface Client {
undefined postMessage(any message, optional ClientPostMessageOptions options = {});
};
// sw.js — inside a trusted notificationclick only:
await client.focus(); // visibility is still required
client.postMessage(payload, { delegate: 'popup' }); // consumes the event's token
// page — synchronously in the 'message' handler (1-second token):
const popup = window.open('/preview', 'preview', 'width=600,height=500,popup');