Conversation
Carry the initiating document through direct focus, dialog, and popover algorithms. Use explicit invoking documents for user-agent paths and block the focused-subtree exception while a focused descendant is fullscreen. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e74da64d-6b71-41bf-8b15-4e34d5f7a6ec
Prevent caller-based authorization from reaching dialog focusing steps that dereference a null node navigable for detached or synthetic documents. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e74da64d-6b71-41bf-8b15-4e34d5f7a6ec
Keep the focus-without-user-activation change scoped to caller attribution and focused descendants. Fullscreen interaction can be specified separately once its exact boundary is agreed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e74da64d-6b71-41bf-8b15-4e34d5f7a6ec
Builds on the threading in this branch. `allow focus steps` took only
`focusSetterDocument`, so the document the focus would move *into* was never
consulted. Two consequences.
A document that holds the focus could move it anywhere. A (top, allowed) embeds
cross-origin B and V, both denied. The user clicked into B once and the
activation has expired, so the focus is in B. B runs
`top.frames['v'].focus()`: B is not allowed and has no activation, but B holds
the focus, so the steps returned true and the focus landed in V -- which was
denied the feature precisely to stop that. V is not related to B at all.
And three things that work on main stopped working, because element.focus()
consulted the setter instead of the element's own document:
* a denied frame calling `aWin.focus()` on its allowed parent;
* a denied frame focusing an element of an allowed same-origin ancestor;
* a same-origin helper frame driving focus through a shared ancestor's DOM,
e.g. `parent.document.getElementById('fB').focus()`.
Both are fixed by consulting the target first and scoping the setter's authority
to its own subtree:
1. target is allowed to use the feature -> true
2. target's relevant global has transient activation -> true
3. target is not fully active -> false
4. has focus steps(target) -> true
5. focusSetterDocument is not fully active -> false
6. focusSetterDocument does not contain target -> false
7. focusSetterDocument is allowed to use the feature -> true
8. focusSetterDocument has transient activation -> true
9. has focus steps(focusSetterDocument) -> true
10. false
Steps 1 and 2 are main's, unchanged and still first, so nothing that focuses
today stops focusing. Steps 5-9 say: the setter could have moved the focus to
itself, and the target is inside the setter's subtree. Scoping by containment is
what stops the cousin case, and it is safe within the subtree because an
embedder determines its descendants' permissions policy.
The two focus APIs still agree. `iframeB.focus()` is checked against A with A as
the setter and passes at step 1; `bWin.focus()` is checked against B with A as
the setter and passes at step 7. A child calling `window.focus()` on itself is
its own setter and passes none of 7-9.
Also collapses the descendant loop. `has focus steps` walks down the focus chain
from the top-level traversable and returns true as soon as it reaches its
argument, so it is already true when the focus is in that document or in any of
its descendant navigables. The loop over inclusive descendant navigables was
exactly one `has focus steps` call.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove target activation and target-focus shortcuts that could authorize an unrelated focus initiator before containment was checked. For a policy-denied target, require the target to be in the setter's subtree and use only the setter's policy, activation, or existing focus. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e74da64d-6b71-41bf-8b15-4e34d5f7a6ec
Allow a genuinely activated setter to move focus outside its subtree while keeping policy- and focus-based fallback authorization contained. Remove the dialog fully-active guard made redundant by the target check in allow focus steps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e74da64d-6b71-41bf-8b15-4e34d5f7a6ec
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This extends
allow focus stepswith an explicit initiating document, allowing parent-to-child focus delegation and continued focus management within an already-focused embedded application.The motivation is to preserve ordinary focus behavior in nested applications, including Teams experiences, without letting an unrelated, unactivated frame use the target's activation or focus state to authorize a move.
This follows up on whatwg#11519 and the discussions in whatwg#11839, whatwg#12032, and whatwg#12470. Parent-to-child delegation was part of the TPAC 2024 resolution; continued internal focus management and parent recapture are the additional behavior proposed here.
Authorization model
The updated algorithm is:
There are three distinct authorization paths:
has focus stepsincludes descendant focus: it returns true for an ancestor of the focused document when the top-level traversable has system focus. This lets an embedded application delegate focus to a child and later recapture it.Changes since the previous revision
The target's transient-activation shortcut has been removed. Activation belonging to C no longer independently authorizes unrelated setter B to focus C, addressing whatwg#12470.
The target's
has focus stepsshortcut has also been removed. Previously, an unrelated setter could focus a restricted document whose descendant held focus, bypassing containment. Once containment holds, checking the setter's focus is sufficient: focus inside the target also places the setter on the focus chain.Setter activation is checked before containment. Otherwise, a genuinely activated child would be prevented from returning focus to a restricted parent. Only the setter-policy and existing-focus fallback paths are subtree-limited.
The separate fully-active guard in the dialog focusing steps is now redundant and has been removed:
allow focus stepsrejects an inactive target itself.Threading the setter
The initiating document is captured from the incumbent global object at six script entry points:
Window.focus(),HTMLOrSVGOrMathMLElement.focus(),dialog.show(),dialog.showModal(),showPopover(), andtogglePopover().It is then passed explicitly through the dialog and popover algorithms. The incumbent identifies the calling context rather than relying on the current realm of a method obtained from another document. Explicit threading keeps that context available throughout nested algorithms.
Declarative button-command, popover-target, and select-picker paths pass the invoking element's node document. Autofocus insertion passes its element's document as both target and setter, without consulting the script stack. It therefore also receives the new existing-focus allowance.
Behavior
Unless noted, targets are policy-denied and the setter has no transient activation. “Allow” means that this policy check succeeds; other focusing requirements still apply.
Compatibility and scope
This is intentionally not behavior-preserving in every case. Requests previously authorized solely by a restricted target's activation can now be rejected. Conversely, setter activation can authorize cross-document requests that the old target-activation check rejected.
The target-policy allowance remains unchanged. This is therefore not a universal requirement that every focus request be authorized by the setter. The existing distinction between an iframe element's node document and its content document is also retained; this PR does not claim that the element and Window focus APIs are equivalent for every caller.
Popups are separate top-level traversables, not descendants of their openers. The contained-delegation path does not cover opener/popup relationships; target policy or setter activation may still authorize those requests.
No fullscreen-specific restriction is added. Fullscreen concerns remain deferred to a possible follow-up, rather than being treated as resolved by this change.
Policy points for review
(See WHATWG Working Mode: Changes for more details.)