script: Add support for focus navigation scopes by mrobinson · Pull Request #44684 · servo/servo · GitHub
Skip to content

script: Add support for focus navigation scopes#44684

Merged
mrobinson merged 1 commit into
servo:mainfrom
mrobinson:sequential-focus-navigation-scopes
May 7, 2026
Merged

script: Add support for focus navigation scopes#44684
mrobinson merged 1 commit into
servo:mainfrom
mrobinson:sequential-focus-navigation-scopes

Conversation

@mrobinson

Copy link
Copy Markdown
Member

Add support for the specification's concept of focus navigation scopes.
Effectively tab indices are only considered relative to each other
within the same shadow host, slot, or document. Focus can move from a
containing scope to a nested one and vice-versa, but with the following
caveats:

  • When searching a nested scope, the traversal works like a sequential
    traversal without a starting point.
  • When searching a containing scope, the tab index of the scope owner's
    is used to evaluate candidates.

Testing: This change causes some WPT tests to start to pass.

@mrobinson mrobinson requested a review from gterzian as a code owner May 3, 2026 10:22
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label May 3, 2026
@mrobinson mrobinson added the T-linux-wpt Do a try run of the WPT label May 6, 2026
@github-actions github-actions Bot removed the T-linux-wpt Do a try run of the WPT label May 6, 2026
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

Test results for linux-wpt from try job (#25447134705):

Flaky unexpected result (35)
  • OK /IndexedDB/idbdatabase_deleteObjectStore.any.html (#43823)
    • PASS [expected FAIL] subtest: Deleted object store's name should be removed from database's list. Attempting to use a deleted IDBObjectStore should throw an InvalidStateError
  • TIMEOUT [expected OK] /_mozilla/mozilla/hit-test-pos-fixed.html
  • CRASH [expected OK] /_webgl/conformance2/wasm/readpixels-2gb-in-4gb-wasm-memory.html
  • OK /beacon/beacon-basic.https.window.html (#41723)
    • FAIL [expected PASS] subtest: Payload size restriction should be accumulated: type = arraybuffer

      assert_false: expected false got true
      

  • CRASH [expected OK] /content-security-policy/meta/sandbox-iframe.html (#43478)
  • FAIL [expected PASS] /css/css-backgrounds/background-size-041.html
  • FAIL [expected PASS] /css/css-backgrounds/border-image-repeat-space-9.html
  • OK /css/css-fonts/variations/font-weight-matching.html (#38577)
    • FAIL [expected PASS] subtest: Test @font-face matching for weight 99

      assert_approx_equals: @font-face should be mapped to CSSTest Weights 900. expected 90 +/- 2 but got 180
      

    • FAIL [expected PASS] subtest: Test @font-face matching for weight 100

      assert_approx_equals: @font-face should be mapped to CSSTest Weights 900. expected 90 +/- 2 but got 180
      

    • FAIL [expected PASS] subtest: Test @font-face matching for weight 249

      assert_approx_equals: @font-face should be mapped to CSSTest Weights 900. expected 90 +/- 2 but got 180
      

  • CRASH [expected PASS] /css/css-images/object-fit-contain-svg-006i.html
  • FAIL [expected PASS] /css/css-ui/webkit-appearance-textarea-001.html
  • FAIL [expected PASS] /css/filter-effects/kernel-unit-length-002.html
  • TIMEOUT [expected OK] /fetch/api/redirect/redirect-keepalive.https.any.html (#32153)
    • TIMEOUT [expected PASS] subtest: [keepalive][iframe][load] mixed content redirect; setting up

      Test timed out
      

  • CRASH [expected ERROR] /fetch/api/redirect/redirect-method.any.sharedworker.html
  • CRASH [expected OK] /fetch/api/request/request-cache-no-cache.any.worker.html
  • OK /fetch/content-length/api-and-duplicate-headers.any.worker.html (#35197)
    • FAIL [expected PASS] subtest: fetch() and duplicate Content-Length/Content-Type headers

      promise_test: Unhandled rejection with value: object "TypeError: Network error: HTTP failure: client error (SendRequest)"
      

  • OK [expected ERROR] /fetch/fetch-later/quota/same-origin-iframe/accumulated-oversized-payload.https.window.html (#41705)
  • OK /fetch/metadata/generated/css-font-face.sub.tentative.html (#34624)
    • PASS [expected FAIL] subtest: sec-fetch-storage-access - Not sent to non-trustworthy same-origin destination
    • PASS [expected FAIL] subtest: sec-fetch-storage-access - Not sent to non-trustworthy cross-site destination
  • TIMEOUT /fetch/metadata/generated/css-images.https.sub.tentative.html (#42229)
    • PASS [expected FAIL] subtest: content sec-fetch-site - Same-Origin -> Same Origin
  • OK /html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html (#28691)
    • FAIL [expected PASS] subtest: load event does not fire on window.open('about:blank')

      assert_unreached: load should not be fired Reached unreachable code
      

  • OK [expected TIMEOUT] /html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-requestsubmit.html (#44098)
    • FAIL [expected TIMEOUT] subtest: Replace before load, triggered by formElement.requestSubmit()

      assert_equals: expected "http://web-platform.test:8000/common/blank.html?thereplacement=" but got "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/code-injector.html?pipe=sub(none)&code=%0A%20%20%20%20const%20form%20%3D%20document.createElement(%22form%22)%3B%0A%20%20%20%20form.action%20%3D%20%22%2Fcommon%2Fblank.html%22%3B%0A%0A%20%20%20%20const%20input%20%3D%20document.createElement(%22input%22)%3B%0A%20%20%20%20input.type%20%3D%20%22hidden%22%3B%0A%20%20%20%20input.name%20%3D%20%22thereplacement%22%3B%0A%20%20%20%20form.append(input)%3B%0A%0A%20%20%20%20document.currentScript.before(form)%3B%0A%20%20%20%20form.requestSubmit()%3B%0A%20%20"
      

  • OK /html/browsers/history/the-history-interface/traverse_the_history_2.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • OK /html/browsers/history/the-history-interface/traverse_the_history_5.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/document-with-fragment-top.html (#28259)
    • TIMEOUT [expected FAIL] subtest: Autofocus elements in top-level browsing context's documents with "top" fragments should work.

      Test timed out
      

  • TIMEOUT [expected OK] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_navigate_other_frame_popup.sub.html (#39702)
    • TIMEOUT [expected FAIL] subtest: Sandboxed iframe can not navigate other frame's popup

      Test timed out
      

  • OK /html/semantics/forms/form-submission-0/jsurl-form-submit.tentative.html (#36489)
    • PASS [expected FAIL] subtest: Verifies that form submissions scheduled inside javascript: urls take precedence over the javascript: url's return value.
  • OK /html/semantics/scripting-1/the-script-element/execution-timing/077.html (#22139)
    • FAIL [expected PASS] subtest: adding several types of scripts through the DOM and removing some of them confuses scheduler

      assert_array_equals: expected property 1 to be "Script #1 ran" but got "Script #3 ran" (expected array ["Script #2 ran", "Script #1 ran", "Script #3 ran", "Script #4 ran"] got ["Script #2 ran", "Script #3 ran", "Script #4 ran", "Script #1 ran"])
      

  • TIMEOUT [expected OK] /html/user-activation/navigation-state-reset-sameorigin.html
    • TIMEOUT [expected PASS] subtest: Post-navigation state reset.

      Test timed out
      

  • OK /html/webappapis/user-prompts/print-during-unload.html (#35944)
    • FAIL [expected PASS] subtest: print() during unload

      assert_array_equals: expected property 1 to be "destination" but got "error: window.print is not a function" (expected array ["start", "destination"] got ["start", "error: window.print is not a function"])
      

  • TIMEOUT [expected OK] /infrastructure/testdriver/click_nested.html (#43887)
    • NOTRUN [expected FAIL] subtest: TestDriver click method with multiple windows and nested iframe
  • FAIL [expected PASS] /png/apng/fcTL-dispose-previous.html (#41561)
  • OK /resource-timing/buffer-full-add-then-clear.html (#40819)
    • PASS [expected FAIL] subtest: Test that if the buffer is cleared after entries were added to the secondary buffer, those entries make it into the primary one
  • TIMEOUT /trusted-types/trusted-types-navigation.html?06-10 (#37920)
    • PASS [expected FAIL] subtest: Navigate a frame via anchor with javascript:-urls in report-only mode.
  • OK /visual-viewport/resize-event-order.html (#41981)
    • PASS [expected FAIL] subtest: Popup: DOMWindow resize fired before VisualViewport.
  • OK /webdriver/tests/classic/dismiss_alert/dismiss.py (#39098)
    • FAIL [expected PASS] subtest: test_dismiss_in_popup_window

      AssertionError: no such alert (404): No user prompt is currently active.
      

  • CRASH [expected OK] /webxr/hit-test/ar_hittest_source_cancel.https.html
Stable unexpected results that are known to be intermittent (22)
  • TIMEOUT [expected OK] /_mozilla/mozilla/window-resize-event.html (#36741)
    • TIMEOUT [expected PASS] subtest: Popup onresize event fires after resizeTo

      Test timed out
      

  • OK /css/css-cascade/layer-cssom-order-reverse.html (#36094)
    • PASS [expected FAIL] subtest: Delete layer invalidates @font-face
  • OK /css/css-fonts/generic-family-keywords-003.html (#38994)
    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted serif (drawing text in a canvas)

      assert_equals: quoted serif matches  @font-face rule expected 40 but got 125
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted fantasy (drawing text in a canvas)

      assert_equals: quoted fantasy matches  @font-face rule expected 40 but got 125
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted monospace (drawing text in a canvas)

      assert_equals: quoted monospace matches  @font-face rule expected 40 but got 125
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted system-ui (drawing text in a canvas)

      assert_equals: quoted system-ui matches  @font-face rule expected 40 but got 125
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted math (drawing text in a canvas)

      assert_equals: quoted math matches  @font-face rule expected 40 but got 125
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted generic(fangsong) (drawing text in a canvas)

      assert_equals: quoted generic(fangsong) matches  @font-face rule expected 40 but got 125
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted generic(khmer-mul) (drawing text in a canvas)

      assert_equals: quoted generic(khmer-mul) matches  @font-face rule expected 40 but got 125
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted generic(nastaliq) (drawing text in a canvas)

      assert_equals: quoted generic(nastaliq) matches  @font-face rule expected 40 but got 125
      

    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted ui-serif (drawing text in a canvas)

      assert_equals: quoted ui-serif matches  @font-face rule expected 40 but got 125
      

  • OK [expected ERROR] /fetch/fetch-later/quota/same-origin-iframe/empty-payload.https.window.html (#35176)
    • PASS [expected FAIL] subtest: fetchLater() accepts an empty POST request body of String in same-origin iframe.
    • PASS [expected FAIL] subtest: fetchLater() accepts an empty POST request body of ArrayBuffer in same-origin iframe.
    • PASS [expected FAIL] subtest: fetchLater() accepts an empty POST request body of URLSearchParams in same-origin iframe.
    • PASS [expected FAIL] subtest: fetchLater() accepts an empty POST request body of Blob in same-origin iframe.
    • PASS [expected FAIL] subtest: fetchLater() accepts an empty POST request body of File in same-origin iframe.
  • OK [expected ERROR] /fetch/fetch-later/quota/same-origin-iframe/sandboxed-iframe.https.window.html (#41704)
  • ERROR [expected TIMEOUT] /html/browsers/browsing-the-web/history-traversal/pageswap/pageswap-initial-navigation.html (#40387)
  • OK /html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-204.html (#44521)
    • PASS [expected FAIL] subtest: Navigating to a different document with link click
  • OK [expected TIMEOUT] /html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/form-submit.html (#44028)
  • OK /html/browsers/history/the-history-interface/traverse_the_history_4.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • OK [expected TIMEOUT] /html/interaction/focus/the-autofocus-attribute/supported-elements.html (#24145)
    • FAIL [expected TIMEOUT] subtest: Element with tabindex should support autofocus

      assert_equals: expected "SPAN" but got "BODY"
      

    • PASS [expected NOTRUN] subtest: Non-HTMLElement should not support autofocus
    • FAIL [expected NOTRUN] subtest: Host element with delegatesFocus should support autofocus

      assert_equals: expected Element node <div autofocus=""></div> but got Element node <body><div autofocus=""></div></body>
      

    • FAIL [expected NOTRUN] subtest: Host element with delegatesFocus including no focusable descendants should be skipped

      promise_test: Unhandled rejection with value: object "TypeError: can't access property "appendChild", w.document.body is null"
      

    • FAIL [expected NOTRUN] subtest: Area element should support autofocus

      promise_test: Unhandled rejection with value: object "TypeError: can't access property "appendChild", w.document.querySelector(...) is null"
      

  • OK /html/semantics/embedded-content/media-elements/audio_loop_seek_to_eos.html (#41226)
    • FAIL [expected PASS] subtest: seeking to the end of looping audio

      promise_test: Unhandled rejection with value: object "TypeError: this argument is not a finite floating-point value"
      

  • OK /html/semantics/embedded-content/media-elements/seeking/seek-to-max-value.htm (#40626)
    • PASS [expected FAIL] subtest: seek to Number.MAX_VALUE
  • OK /html/semantics/scripting-1/the-script-element/module/dynamic-import/blob-url.any.worker-module.html (#43510)
    • FAIL [expected PASS] subtest: Revoking a blob URL immediately after calling import will not fail

      promise_test: Unhandled rejection with value: object "TypeError: Module fetching failed"
      

  • OK /html/semantics/scripting-1/the-script-element/module/dynamic-import/blob-url.any.worker.html (#33909)
    • PASS [expected FAIL] subtest: Revoking a blob URL immediately after calling import will not fail
  • OK /mixed-content/tentative/autoupgrades/video-upgrade.https.sub.html (#41135)
    • FAIL [expected PASS] subtest: Video of other host autoupgraded

      assert_equals: Length. Other host expected 1 but got Infinity
      

  • OK /navigation-timing/test-navigation-type-reload.html (#33334)
    • FAIL [expected PASS] subtest: Reload domComplete > Original domComplete

      assert_true: Reload domComplete > Original domComplete expected true got false
      

    • FAIL [expected PASS] subtest: Reload domContentLoadedEventEnd > Original domContentLoadedEventEnd

      assert_true: Reload domContentLoadedEventEnd > Original domContentLoadedEventEnd expected true got false
      

    • FAIL [expected PASS] subtest: Reload domContentLoadedEventStart > Original domContentLoadedEventStart

      assert_true: Reload domContentLoadedEventStart > Original domContentLoadedEventStart expected true got false
      

    • FAIL [expected PASS] subtest: Reload loadEventEnd > Original loadEventEnd

      assert_true: Reload loadEventEnd > Original loadEventEnd expected true got false
      

    • FAIL [expected PASS] subtest: Reload loadEventStart > Original loadEventStart

      assert_true: Reload loadEventStart > Original loadEventStart expected true got false
      

  • OK /pointerevents/compat/pointerevent_touch_target_after_pointerdown_target_removed.tentative.html (#42813)
    • PASS [expected FAIL] subtest: After a pointerdown listener moves the target to different position, touch events should be fired on the pointerdown target, but pointer events should be fired on the pointerdown target parent
    • PASS [expected FAIL] subtest: After a pointerdown listener moves the target to different position, touchmove event should be fired on the pointerdown target parent
  • OK /preload/prefetch-document.html (#37210)
    • FAIL [expected PASS] subtest: different-site document prefetch with 'as=document' should not be consumed

      assert_equals: expected 2 but got 1
      

  • OK /resource-timing/buffer-full-then-increased.html (#44408)
    • FAIL [expected PASS] subtest: Test that overflowing the buffer and immediately increasing its limit does not trigger the resourcetimingbufferfull event

      assert_equals: Number of entries does not match the expected value. expected 3 but got 1
      

  • OK [expected TIMEOUT] /trusted-types/trusted-types-navigation.html?31-35 (#38034)
    • PASS [expected TIMEOUT] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in report-only mode.
    • FAIL [expected NOTRUN] subtest: Navigate a window via form-submission with javascript:-urls w/ a default policy throwing an exception in enforcing mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

    • FAIL [expected NOTRUN] subtest: Navigate a window via form-submission with javascript:-urls w/ a default policy throwing an exception in report-only mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

    • FAIL [expected NOTRUN] subtest: Navigate a window via form-submission with javascript:-urls w/ a default policy making the URL invalid in enforcing mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

  • OK /trusted-types/trusted-types-reporting.html (#43737)
    • FAIL [expected PASS] subtest: Trusted Type violation report: creating a forbidden-but-not-reported policy.

      assert_equals: a single violation reported expected 1 but got 0
      

  • OK [expected ERROR] /webxr/render_state_update.https.html (#27535)

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

✨ Try run (#25447134705) succeeded.

@jdm

jdm commented May 6, 2026

Copy link
Copy Markdown
Member

I'll review this tonight.

@jdm jdm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate all the documentation and naming choices in this PR!

Comment thread components/script/dom/document/focus.rs Outdated
#[derive(PartialEq)]
pub(crate) enum SequentialFocusNavigationSearchContext {
/// The focus scope that initiated this search. Containing contexts and nested contexts can
/// be both be searched.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// be both be searched.
/// both be searched.

Comment thread components/script/dom/document/focus.rs Outdated
should_continue
}

/// Process the node or focus scope owner with the provided tab index for according to this

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Process the node or focus scope owner with the provided tab index for according to this
/// Process the node or focus scope owner with the provided tab index according to this

Comment thread components/script/dom/document/focus.rs Outdated
}
}

/// Process the node or focus scope owner with the provided tab index for searches with the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the "provided tab index" bit might be a copy-paste? It doesn't seem to be relevant here.

Comment thread components/script/dom/document/focus.rs Outdated
fn process_element_for_first_or_last_traversal(
&self,
candidate_element_tab_index: i32,
) -> (bool, Continue) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we always return Continue::Yes. Is it useful to return a Continue value in that case?

@servo-highfive servo-highfive removed the S-awaiting-review There is new code that needs to be reviewed. label May 7, 2026
Add support for the specification's concept of focus navigation scopes.
Effectively tab indices are only considered relative to each other
within the same shadow host, slot, or document. Focus can move from a
containing scope to a nested one and vice-versa, but with the following
caveats:

- When searching a nested scope, the traversal works like a sequential
  traversal without a starting point.
- When searching a containing scope, the tab index of the scope owner
  is used to evaluate candidates.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
@mrobinson mrobinson force-pushed the sequential-focus-navigation-scopes branch from b5e8b93 to b7c2d40 Compare May 7, 2026 08:18
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label May 7, 2026
@mrobinson

Copy link
Copy Markdown
Member Author

@mrobinson mrobinson enabled auto-merge May 7, 2026 08:19
@mrobinson mrobinson added this pull request to the merge queue May 7, 2026
@servo-highfive servo-highfive added the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label May 7, 2026
Merged via the queue into servo:main with commit a70c1de May 7, 2026
33 checks passed
@mrobinson mrobinson deleted the sequential-focus-navigation-scopes branch May 7, 2026 09:02
@servo-highfive servo-highfive removed the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-awaiting-review There is new code that needs to be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants