Skip to content

script: Distinguish between sequential (keyboard) and click focusability#43019

Merged
mrobinson merged 1 commit into
servo:mainfrom
mrobinson:tab-navigation-2
Mar 5, 2026
Merged

script: Distinguish between sequential (keyboard) and click focusability#43019
mrobinson merged 1 commit into
servo:mainfrom
mrobinson:tab-navigation-2

Conversation

@mrobinson
Copy link
Copy Markdown
Member

@mrobinson mrobinson commented Mar 4, 2026

This changes makes it so that keyboard and click focusability are two
separate concepts in script. Some elements may be focusable only by
the keyboard and some only by the click events.

In addition, the SEQUENTIALLY_FOCUSABLE Node flag is removed in favor of a
more specification-aligned approach of checking lazily if an element is
a focusable area. This allows moving the focus fixup steps to their
correct place, asynchronously during "update the rendering" and
synchronously during unbind_from_tree.

Testing: This causes some WPT tests to start passing and some to start failing:

  • Two subtests tests in tests/wpt/meta/css/css-conditional/container-queries/ because
    we do not implement support for container queries. These are legitimate failures
    and they are compensated by a good subtest pass in the same file.
  • A few subtests in /html/interaction/focus/tabindex-focus-flag.html because now
    we do not allow focusing non-rendering elements. The failures are HTML elements with
    <svg> which we do not rendering and <summary> elements of <details> which
    need special handling we do not implement yet.
  • html/semantics/forms/the-fieldset-element/disabled-003.html: Some of these tests
    start to fail, but they are not spec compliant and browser properly implementing
    the asynchronous focus fixup rule also fail them.

Fixes: #31870.

@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Mar 4, 2026
Comment on lines +1298 to +1299
// This implements <https://html.spec.whatwg.org/#html-element-removing-steps> for HTML
// elements.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: refer to the full algorithm that implements the removing steps for DOM. The way I constructed this URL is as follows:

  1. Clicked on https://html.spec.whatwg.org/multipage/infrastructure.html#html-element-removing-steps
  2. Clicked on it to see its usages
  3. Clicked on "DOM trees (2)"
  4. Observe that the URL has infrastructure.html#dom-trees:
  5. Then append the relevant algorithm name from the DOM spec (concept-node-remove-ext)
Suggested change
// This implements <https://html.spec.whatwg.org/#html-element-removing-steps> for HTML
// elements.
/// <https://html.spec.whatwg.org/multipage#dom-trees:concept-node-remove-ext>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Okay. I've replaced this spec link and converted it to rustdoc. Thanks for the tip.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note thought that this seems to scroll to step 3 of the steps that this function refers to. I feel the link should navigate to the part of the document that starts with "The removing steps for the HTML Standard, given removedNode, isSubtreeRoot, and oldAncestor, are defined as the following:" The link you provided links to the the text "defines HTML element removing steps" in step 3.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am confused. When I open https://html.spec.whatwg.org/multipage#dom-trees:concept-node-remove-ext in my browser, it opens right at the top of the algorithm. Not at step 3. Can you try in a new tab?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Comment thread components/script/dom/element.rs Outdated
/// This is the parts of
/// <https://html.spec.whatwg.org/multipage/interaction.html#focusable-area> that apply for
/// keyboard focusability. See also [`Self::is_click_focusable`].
pub(crate) fn is_keyboard_focusable(&self) -> bool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't see the spec talking about keyboard focusability, but instead it is named sequentially_focusable. Before it was called is_sequentially_focusable and I think that would be more aligned with what the spec says? Basically, if I search for "keyboard focus", nothing shows in the spec, but "click focus" and "sequentially focus" do

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've renamed this reflect the specification terminology, "sequentially focusable."

@mrobinson mrobinson force-pushed the tab-navigation-2 branch 3 times, most recently from 768fcba to 0db8ed1 Compare March 4, 2026 16:57
@mrobinson mrobinson changed the title script: Distinguish between keyboard and click focusability script: Distinguish between sequential (keyboard) and click focusability Mar 4, 2026
@mrobinson mrobinson added the T-linux-wpt Do a try run of the WPT label Mar 4, 2026
@github-actions github-actions Bot removed the T-linux-wpt Do a try run of the WPT label Mar 4, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 4, 2026

🔨 Triggering try run (#22686011943) for Linux (WPT)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 4, 2026

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

Flaky unexpected result (24)
  • OK [expected TIMEOUT] /IndexedDB/idbfactory_open.any.worker.html
    • PASS [expected FAIL] subtest: Calling open() with version argument 1.5 should not throw.
    • PASS [expected TIMEOUT] subtest: Calling open() with version argument 9007199254740991 should not throw.
    • PASS [expected TIMEOUT] subtest: Calling open() with version argument undefined should not throw.
  • OK /_mozilla/css/offset_properties_inline.html (#40543)
    • FAIL [expected PASS] subtest: offsetTop

      assert_equals: offsetTop of #inline-1 should be 0. expected 0 but got -1
      

    • FAIL [expected PASS] subtest: offsetLeft

      assert_equals: offsetLeft of #inline-2 should be 40. expected 40 but got 25
      

  • ERROR [expected OK] /_mozilla/mozilla/img_load_more_than_cache.html
  • FAIL [expected PASS] /css/css-ui/webkit-appearance-menulist-001.html
  • ERROR [expected OK] /fetch/fetch-later/quota/same-origin-iframe/accumulated-oversized-payload.https.window.html (#41705)
  • OK /fetch/metadata/generated/css-font-face.https.sub.tentative.html (#32732)
    • PASS [expected FAIL] subtest: sec-fetch-storage-access - Cross-site
  • CRASH [expected OK] /html/browsers/browsing-the-web/unloading-documents/beforeunload-on-history-back.html
  • OK /html/browsers/history/the-history-interface/traverse_the_history_5.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • CRASH [expected OK] /html/browsers/windows/auxiliary-browsing-contexts/opener-setter.html
  • TIMEOUT /html/semantics/embedded-content/media-elements/autoplay-allowed-by-feature-policy.https.sub.html (#41404)
    • PASS [expected TIMEOUT] subtest: Feature-Policy header: autoplay * allows same-origin iframes.
  • OK /html/semantics/embedded-content/media-elements/seeking/seek-to-negative-time.htm
    • FAIL [expected PASS] subtest: seek to negative time

      The index is not in the allowed range.
      

  • 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"])
      

  • OK /html/webappapis/user-prompts/print-during-unload.html (#35944)
    • PASS [expected FAIL] subtest: print() during unload
  • OK /touch-events/single-tap-when-touchend-listener-use-sync-xhr.html (#41175)
    • PASS [expected FAIL] subtest: Click event should be fired when touchend opens synchronous XHR
  • OK [expected 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.
    • PASS [expected TIMEOUT] subtest: Navigate a frame via anchor with javascript:-urls w/ default policy in report-only mode.
    • FAIL [expected NOTRUN] subtest: Navigate a window via anchor 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 anchor 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!\""
      

  • OK /webdriver/tests/classic/element_send_keys/file_upload.py
    • FAIL [expected PASS] subtest: test_empty_text

      webdriver.error.NoSuchWindowException: no such window (404): No such window
      

  • OK /webdriver/tests/classic/execute_script/execute.py
    • ERROR [expected PASS] subtest: test_no_top_browsing_context

      setup error: webdriver.error.NoSuchElementException: no such element (404)
      

  • OK /webdriver/tests/classic/perform_actions/key.py
    • FAIL [expected PASS] subtest: test_null_response_value

      webdriver.error.NoSuchWindowException: no such window (404): No such window
      

  • OK /webdriver/tests/classic/perform_actions/perform.py
    • FAIL [expected PASS] subtest: test_input_source_action_sequence_actions_pause_duration_valid[none]

      AssertionError: no such window (404): No such window
      

  • OK [expected TIMEOUT] /webmessaging/with-ports/018.html (#24485)
    • PASS [expected TIMEOUT] subtest: origin of the script that invoked the method, javascript:
  • OK [expected TIMEOUT] /webstorage/localstorage-about-blank-3P-iframe-opens-3P-window.partitioned.html (#29053)
    • PASS [expected TIMEOUT] subtest: StorageKey: test 3P about:blank window opened from a 3P iframe
  • ERROR [expected OK] /workers/baseurl/alpha/sharedworker-in-worker.html (#21315)
Stable unexpected results that are known to be intermittent (24)
  • TIMEOUT /FileAPI/url/url-in-tags-revoke.window.html (#19978)
    • TIMEOUT [expected PASS] subtest: Fetching a blob URL immediately before revoking it works in &lt;script&gt; tags.

      Test timed out
      

  • FAIL [expected PASS] /_mozilla/mozilla/sslfail.html (#10760)
  • TIMEOUT [expected OK] /_mozilla/mozilla/window_resize_event.html (#36741)
    • TIMEOUT [expected PASS] subtest: Popup onresize event fires after resizeTo

      Test timed out
      

  • CRASH [expected PASS] /_mozilla/shadow-dom/move-element-with-ua-shadow-tree-crash.html (#39473)
  • OK /_webgl/conformance/textures/misc/texture-upload-size.html (#21770)
    • PASS [expected FAIL] subtest: WebGL test #45
    • PASS [expected FAIL] subtest: WebGL test #47
    • PASS [expected FAIL] subtest: WebGL test #49
    • PASS [expected FAIL] subtest: WebGL test #51
    • FAIL [expected PASS] subtest: WebGL test #53

      assert_true: Texture was smaller than the expected size 2x2 expected true got false
      

    • FAIL [expected PASS] subtest: WebGL test #55

      assert_true: getError expected: INVALID_VALUE. Was NO_ERROR : when calling texSubImage2D with the same texture upload with offset 1, 1 expected true got false
      

    • FAIL [expected PASS] subtest: WebGL test #57

      assert_true: Texture was smaller than the expected size 2x2 expected true got false
      

    • FAIL [expected PASS] subtest: WebGL test #59

      assert_true: getError expected: INVALID_VALUE. Was NO_ERROR : when calling texSubImage2D with the same texture upload with offset 1, 1 expected true got false
      

    • PASS [expected FAIL] subtest: WebGL test #61
    • PASS [expected FAIL] subtest: WebGL test #63
    • And 10 more unexpected results...
  • OK /beacon/beacon-basic.https.window.html (#41723)
    • PASS [expected FAIL] subtest: Payload size restriction should be accumulated: type = string
    • FAIL [expected PASS] subtest: Payload size restriction should be accumulated: type = arraybuffer

      assert_false: expected false got true
      

  • 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-001.html (#37467)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(fangsong)
    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted generic(kai)

      assert_equals: quoted generic(kai) matches  @font-face rule expected 50 but got 30
      

  • OK /css/css-fonts/generic-family-keywords-003.html (#38994)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted cursive (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted fantasy (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted monospace (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted system-ui (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted math (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(fangsong) (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(khmer-mul) (drawing text in a canvas)
  • 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
  • TIMEOUT /fetch/metadata/generated/css-images.sub.tentative.html (#29047)
    • FAIL [expected PASS] subtest: content sec-fetch-site - HTTPS downgrade-upgrade

      assert_unreached: Reached unreachable code
      

  • ERROR /fetch/metadata/generated/serviceworker.https.sub.html (#36247)
    • PASS [expected FAIL] subtest: sec-fetch-site - Same origin, no options - registration
  • OK [expected ERROR] /fetch/metadata/window-open.https.sub.html (#40339)
  • OK [expected ERROR] /focus/focus-event-after-switching-iframes.sub.html (#40368)
  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment.html (#20768)
    • FAIL [expected PASS] subtest: Tests that a fragment navigation in the unload handler will not block the initial navigation

      assert_equals: expected "" but got "#fragment"
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.html (#29049)
    • FAIL [expected PASS] subtest: Same-origin navigation started from unload handler must be ignored

      assert_equals: expected "?pass" but got "?fail"
      

  • OK [expected TIMEOUT] /html/interaction/focus/the-autofocus-attribute/supported-elements.html (#24145)
    • FAIL [expected NOTRUN] subtest: Host element with delegatesFocus should support autofocus

      assert_equals: expected Element node &lt;div autofocus=""&gt;&lt;/div&gt; but got Element node &lt;body&gt;&lt;/body&gt;
      

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

      assert_equals: expected Element node &lt;input autofocus=""&gt;&lt;/input&gt; but got Element node &lt;body&gt;&lt;div autofocus=""&gt;&lt;/div&gt;&lt;input autofocus=""&gt;&lt;/body&gt;
      

    • 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/document-metadata/the-meta-element/pragma-directives/attr-meta-http-equiv-refresh/allow-scripts-flag-changing-1.html (#39694)
    • PASS [expected FAIL] subtest: Meta refresh is blocked by the allow-scripts sandbox flag at its creation time, not when refresh comes due
  • OK /html/semantics/embedded-content/media-elements/media_fragment_seek.html (#24114)
    • PASS [expected FAIL] subtest: Video should seek to time specified in media fragment syntax
  • OK /navigation-timing/test-navigation-type-reload.html (#33334)
    • PASS [expected FAIL] subtest: Reload domComplete &gt; Original domComplete
    • PASS [expected FAIL] subtest: Reload domContentLoadedEventEnd &gt; Original domContentLoadedEventEnd
    • PASS [expected FAIL] subtest: Reload domContentLoadedEventStart &gt; Original domContentLoadedEventStart
    • PASS [expected FAIL] subtest: Reload fetchStart &gt; Original fetchStart
    • PASS [expected FAIL] subtest: Reload loadEventEnd &gt; Original loadEventEnd
    • PASS [expected FAIL] subtest: Reload loadEventStart &gt; Original loadEventStart
  • OK /resource-timing/test_resource_timing.html (#25720)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (link)
  • OK /resource-timing/test_resource_timing.https.html (#25216)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (img)
  • TIMEOUT /trusted-types/trusted-types-navigation.html?31-35 (#38034)
    • TIMEOUT [expected NOTRUN] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in report-only mode.

      Test timed out
      

  • OK [expected ERROR] /webxr/render_state_update.https.html (#27535)
Stable unexpected results (2)
  • OK /html/interaction/focus/processing-model/focus-fixup-rule-one-no-dialogs.html
    • FAIL [expected PASS] subtest: #button2

      assert_equals: active element isn't fixed-up sync expected Element node &lt;button id="button2" hidden=""&gt;Button 2&lt;/button&gt; but got Element node &lt;body&gt;&lt;div&gt;
        &lt;button id="button1" disabled=""&gt;Button 1&lt;/...
      

    • FAIL [expected PASS] subtest: #button4

      assert_equals: active element isn't fixed-up sync expected Element node &lt;button id="button4"&gt;Button 4&lt;/button&gt; but got Element node &lt;body&gt;&lt;div&gt;
        &lt;button id="button1" disabled=""&gt;Button 1&lt;/...
      

    • FAIL [expected PASS] subtest: #div

      assert_equals: active element isn't fixed-up sync expected Element node &lt;div id="div"&gt;Div&lt;/div&gt; but got Element node &lt;body&gt;&lt;div&gt;
        &lt;button id="button1" disabled=""&gt;Button 1&lt;/...
      

  • OK /shadow-dom/focus-navigation/tentative/focus-scroller-layout-update.html
    • FAIL [expected PASS] subtest: When checking that element is a scroller, layout information should be up to date.

      assert_equals: expected Element node &lt;div id="scroller" style="overflow:scroll; width:50px; he... but got Element node &lt;body&gt;&lt;button id="button"&gt;Button&lt;/button&gt;
      &lt;div id="scroll...
      

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 4, 2026

⚠️ Try run (#22686011943) failed!

Copy link
Copy Markdown
Contributor

@TimvdLippe TimvdLippe left a comment

Choose a reason for hiding this comment

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

Overall LGTM. I am a bit surprised how this code differs from the spec, especially how the two types of focus areas are mutually exclusive as I understand it. Still, this is a step in the right direction, so I am fine with it for now.

Comment thread components/script/dom/element.rs Outdated
))
) {
return true;
/// This is an implementation of <https://html.spec.whatwg.org/multipage/#click-focusable>i
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// This is an implementation of <https://html.spec.whatwg.org/multipage/#click-focusable>i
/// This is an implementation of <https://html.spec.whatwg.org/multipage/#click-focusable>

}
let node = self.upcast::<Node>();
if node.get_flag(NodeFlags::SEQUENTIALLY_FOCUSABLE) {
if self.is_click_focusable() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am surprised by this logic here. The spec doesn't imply that click focusable means sequentially focusable. Shouldn't we check for https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation-order instead here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The specification is quite hand-wavy here and leaves a lot of decisions up to the embedder. I guess you've noticed how some of the definitions appear to be practically circular? In any case, we did not seem to find much specification text which describes which kinds of focusable areas should be focusable sequentially versus which should be focusable via click. In the end, it makes sense that the keyboard can focus a superset of things that a mouse can, for accessibility reasons.

That said, if you can find better specification text, I think we'd be happy to implement it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For instance, consider a <div> with overflow: scroll ("The scrollable regions of elements that are being rendered and are not inert." from the specification text). By observation you can see that these are focusable with a keyboard in browsers, but not with a click. You can see this by putting a :focus selector with an outline on the <div>.

On the other hand, the specification also says that "Any other element or part of an element determined by the user agent to be a focusable area, especially to aid with accessibility or to better match platform conventions" can be a focusable area.

document.request_focus(None, FocusInitiator::Local, can_gc);
document.request_focus(target_el.as_deref(), FocusInitiator::Local, can_gc);
document.request_focus(None, FocusInitiator::Click, can_gc);
document.request_focus(target_el.as_deref(), FocusInitiator::Click, can_gc);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this the code related to https://html.spec.whatwg.org/multipage/interaction.html#data-model:click-focusable-5 ? E.g.

When a user activates a click focusable focusable area, the user agent must run the focusing steps on the focusable area with focus trigger set to "click".

If it is, please add that as a spec reference

Copy link
Copy Markdown
Member Author

@mrobinson mrobinson Mar 4, 2026

Choose a reason for hiding this comment

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

I believe the focusing steps are implemented when the transaction is committed. We have a TODO item to make all of that code match the specification more closely.

EDIT: To clarify we'll try to "focus" on this next as well as trying to make the code around click versus sequential focusability more closely align with the specification language.

@servo-highfive servo-highfive removed the S-awaiting-review There is new code that needs to be reviewed. label Mar 4, 2026
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'm only reviewing this part. Nice.

@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Mar 5, 2026
@mrobinson mrobinson enabled auto-merge March 5, 2026 07:01
@mrobinson mrobinson disabled auto-merge March 5, 2026 07:10
This changes makes it so that keyboard and click focusability are two
separate concepts in `script`. Some elements may be focusable only by
the keyboard and some only by the click events.

In addition, the SEQUENTIALLY_FOCUSABLE flag is removed in favor of a
more specification aligned approach of checking lazily if an element is
a focusable area. This allows moving the focus fixup steps to their
correct place, asynchronously during "update the rendering" and
synchronously during `unbind_from_tree`.

Signed-off-by: Martin Robinson <[email protected]>
Co-authored-by: Oriol Brufau <[email protected]>
@mrobinson mrobinson added the T-linux-wpt Do a try run of the WPT label Mar 5, 2026
@github-actions github-actions Bot removed the T-linux-wpt Do a try run of the WPT label Mar 5, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 5, 2026

🔨 Triggering try run (#22706624527) for Linux (WPT)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 5, 2026

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

Flaky unexpected result (34)
  • OK [expected TIMEOUT] /IndexedDB/idbfactory_open.any.worker.html
    • PASS [expected FAIL] subtest: Calling open() with version argument 1.5 should not throw.
    • PASS [expected TIMEOUT] subtest: Calling open() with version argument 9007199254740991 should not throw.
    • PASS [expected TIMEOUT] subtest: Calling open() with version argument undefined should not throw.
  • PASS [expected FAIL] /_mozilla/css/linear_gradients_reverse_a.html
  • ERROR [expected OK] /_mozilla/mozilla/img_find_non_sibling_map.html
  • CRASH [expected PASS] /_mozilla/shadow-dom/move-element-with-ua-shadow-tree-crash.html (#39473)
  • OK /content-security-policy/frame-ancestors/frame-ancestors-path-ignored.window.html (#36468)
    • PASS [expected FAIL] subtest: A 'frame-ancestors' CSP directive with a URL that includes a path should be ignored.
  • FAIL [expected PASS] /css/css-backgrounds/background-size-042.html
  • OK /css/css-fonts/variations/at-font-face-font-matching.html (#20684)
    • FAIL [expected PASS] subtest: Matching font-style: 'oblique 21deg' should prefer 'oblique 40deg 50deg' over 'oblique 20deg'

      assert_equals: Unexpected font on test element expected 487 but got 532
      

  • 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
      

  • OK [expected ERROR] /fetch/fetch-later/quota/cross-origin-iframe/sandboxed-iframe.https.window.html
  • TIMEOUT /fetch/metadata/generated/css-images.https.sub.tentative.html (#42229)
    • PASS [expected FAIL] subtest: content sec-fetch-site - Same-Origin -&gt; Same Origin
  • CRASH [expected OK] /html/browsers/browsing-the-web/history-traversal/document-state.https.html
  • 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 /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-cross-origin.sub.window.html (#29056)
    • PASS [expected FAIL] subtest: Cross-origin navigation started from unload handler must be ignored
  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment.html (#20768)
    • FAIL [expected PASS] subtest: Tests that a fragment navigation in the unload handler will not block the initial navigation

      assert_equals: expected "" but got "#fragment"
      

  • OK /html/browsers/history/the-history-interface/traverse_the_history_3.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • CRASH [expected OK] /html/browsers/history/the-location-interface/location_href.html
  • OK /html/browsers/windows/browsing-context-names/duplicate-name-order.html (#34623)
    • PASS [expected FAIL] subtest: Duplicate name lookup order
  • CRASH [expected OK] /html/canvas/element/color-type/2d.color.type.u8p3.to.u8srgb.to.u8p3.html
  • OK [expected TIMEOUT] /html/semantics/embedded-content/media-elements/autoplay-disabled-by-feature-policy.https.sub.html (#41221)
    • PASS [expected TIMEOUT] subtest: Feature-Policy header: autoplay "none" has no effect on the top level document.
    • FAIL [expected TIMEOUT] subtest: Feature-Policy header: autoplay "none" disallows same-origin iframes.

      assert_false: autoplay expected false got true
      

  • OK /html/semantics/embedded-content/media-elements/media_fragment_seek.html (#24114)
    • PASS [expected FAIL] subtest: Video should seek to time specified in media fragment syntax
  • 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/webappapis/user-prompts/print-during-unload.html (#35944)
    • PASS [expected FAIL] subtest: print() during unload
  • OK /navigation-timing/test-navigation-type-reload.html (#33334)
    • PASS [expected FAIL] subtest: Reload domComplete &gt; Original domComplete
    • PASS [expected FAIL] subtest: Reload domContentLoadedEventEnd &gt; Original domContentLoadedEventEnd
    • PASS [expected FAIL] subtest: Reload domContentLoadedEventStart &gt; Original domContentLoadedEventStart
    • PASS [expected FAIL] subtest: Reload domInteractive &gt; Original domInteractive
    • PASS [expected FAIL] subtest: Reload fetchStart &gt; Original fetchStart
    • PASS [expected FAIL] subtest: Reload loadEventEnd &gt; Original loadEventEnd
    • PASS [expected FAIL] subtest: Reload loadEventStart &gt; Original loadEventStart
  • FAIL [expected PASS] /png/apng/fcTL-blend-source-transparent.html (#39520)
  • FAIL [expected PASS] /png/apng/fcTL-dispose-previous-first.html
  • OK /visual-viewport/resize-event-order.html (#41981)
    • PASS [expected FAIL] subtest: Popup: DOMWindow resize fired before VisualViewport.
  • CRASH [expected OK] /webaudio/the-audio-api/the-mediastreamaudiodestinationnode-interface/closed-audiocontext-construction.html
  • OK /webdriver/tests/classic/delete_cookie/delete.py
    • FAIL [expected PASS] subtest: test_null_response_value

      AssertionError: no such window (404): No such window
      

  • OK /webdriver/tests/classic/delete_cookie/user_prompts.py
    • FAIL [expected PASS] subtest: test_accept[alert-None]

      webdriver.error.NoSuchWindowException: no such window (404): No such window
      

  • OK /webdriver/tests/classic/element_click/scroll_into_view.py
    • FAIL [expected PASS] subtest: test_scroll_into_view

      webdriver.error.NoSuchElementException: no such element (404)
      

    • FAIL [expected PASS] subtest: test_partially_visible_does_not_scroll[9]

      assert 0 == 1
      

  • OK /webdriver/tests/classic/fullscreen_window/from_minimized_window.py
    • ERROR [expected FAIL] subtest: test_fullscreen_from_minimized_window

      setup error: webdriver.error.NoSuchWindowException: no such window (404): No such window
      

  • OK /webdriver/tests/classic/get_title/get.py
    • FAIL [expected PASS] subtest: test_payload

      AssertionError: no such window (404)
      

  • OK /webdriver/tests/classic/is_element_enabled/enabled.py
    • ERROR [expected PASS] subtest: test_no_top_browsing_context

      setup error: webdriver.error.NoSuchElementException: no such element (404)
      

Stable unexpected results that are known to be intermittent (16)
  • TIMEOUT /FileAPI/url/url-in-tags-revoke.window.html (#19978)
    • TIMEOUT [expected PASS] subtest: Fetching a blob URL immediately before revoking it works in &lt;script&gt; tags.

      Test timed out
      

  • FAIL [expected PASS] /_mozilla/mozilla/sslfail.html (#10760)
  • TIMEOUT [expected OK] /_mozilla/mozilla/window_resize_event.html (#36741)
    • TIMEOUT [expected PASS] subtest: Popup onresize event fires after resizeTo

      Test timed out
      

  • OK /_webgl/conformance/textures/misc/texture-upload-size.html (#21770)
    • PASS [expected FAIL] subtest: WebGL test #45
    • PASS [expected FAIL] subtest: WebGL test #47
    • PASS [expected FAIL] subtest: WebGL test #49
    • PASS [expected FAIL] subtest: WebGL test #51
    • FAIL [expected PASS] subtest: WebGL test #53

      assert_true: Texture was smaller than the expected size 2x2 expected true got false
      

    • FAIL [expected PASS] subtest: WebGL test #55

      assert_true: getError expected: INVALID_VALUE. Was NO_ERROR : when calling texSubImage2D with the same texture upload with offset 1, 1 expected true got false
      

    • FAIL [expected PASS] subtest: WebGL test #57

      assert_true: Texture was smaller than the expected size 2x2 expected true got false
      

    • FAIL [expected PASS] subtest: WebGL test #59

      assert_true: getError expected: INVALID_VALUE. Was NO_ERROR : when calling texSubImage2D with the same texture upload with offset 1, 1 expected true got false
      

    • PASS [expected FAIL] subtest: WebGL test #61
    • PASS [expected FAIL] subtest: WebGL test #63
    • And 10 more unexpected results...
  • OK /beacon/beacon-basic.https.window.html (#41723)
    • PASS [expected FAIL] subtest: Payload size restriction should be accumulated: type = string
  • OK /css/css-fonts/generic-family-keywords-001.html (#37467)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(fangsong)
  • OK /css/css-fonts/generic-family-keywords-003.html (#38994)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted fantasy (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted monospace (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted math (drawing text in a canvas)
  • ERROR [expected OK] /fetch/fetch-later/quota/same-origin-iframe/accumulated-oversized-payload.https.window.html (#41705)
  • TIMEOUT /fetch/metadata/generated/css-images.sub.tentative.html (#29047)
    • TIMEOUT [expected PASS] subtest: background-image sec-fetch-mode - Not sent to non-trustworthy cross-site destination

      Test timed out
      

  • OK [expected ERROR] /fetch/metadata/window-open.https.sub.html (#40339)
    • FAIL [expected PASS] subtest: Cross-site window, forced, reloaded

      The operation is insecure.
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.html (#29049)
    • FAIL [expected PASS] subtest: Same-origin navigation started from unload handler must be ignored

      assert_equals: expected "?pass" but got "?fail"
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/refresh/same-document-refresh.html (#34597)
    • FAIL [expected PASS] subtest: Same-Document Referrer from Refresh

      assert_equals: original page loads expected "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh-with-section.sub.html?url=%23section" but got "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh-with-section.sub.html?url=%23section#section"
      

  • TIMEOUT /html/interaction/focus/the-autofocus-attribute/supported-elements.html (#24145)
    • TIMEOUT [expected FAIL] subtest: Element with tabindex should support autofocus

      Test timed out
      

    • NOTRUN [expected PASS] subtest: Non-HTMLElement should not support autofocus
    • NOTRUN [expected TIMEOUT] subtest: Host element with delegatesFocus should support autofocus
  • OK /resource-timing/test_resource_timing.html (#25720)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (img)
  • TIMEOUT /trusted-types/trusted-types-navigation.html?31-35 (#38034)
    • TIMEOUT [expected NOTRUN] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in report-only mode.

      Test timed out
      

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 5, 2026

✨ Try run (#22706624527) succeeded.

@mrobinson mrobinson added this pull request to the merge queue Mar 5, 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 Mar 5, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Mar 5, 2026
@servo-highfive servo-highfive added S-tests-failed The changes caused existing tests to fail. and removed S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. labels Mar 5, 2026
@mrobinson mrobinson added this pull request to the merge queue Mar 5, 2026
@servo-highfive servo-highfive added S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. and removed S-tests-failed The changes caused existing tests to fail. labels Mar 5, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Mar 5, 2026
@servo-highfive servo-highfive added S-tests-failed The changes caused existing tests to fail. and removed S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. labels Mar 5, 2026
@mrobinson mrobinson added this pull request to the merge queue Mar 5, 2026
@servo-highfive servo-highfive added S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. and removed S-tests-failed The changes caused existing tests to fail. labels Mar 5, 2026
@mrobinson
Copy link
Copy Markdown
Member Author

Failure is #43030 (more intermittency in largest contentful paint tests), sending back to merge queue.

Merged via the queue into servo:main with commit b47b39f Mar 5, 2026
80 checks passed
@mrobinson mrobinson deleted the tab-navigation-2 branch March 5, 2026 09:33
@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 Mar 5, 2026
offline-ant pushed a commit to offline-ant/havi that referenced this pull request Jun 4, 2026
…ity (servo#43019)

This changes makes it so that keyboard and click focusability are two
separate concepts in `script`. Some elements may be focusable only by
the keyboard and some only by the click events.

In addition, the `SEQUENTIALLY_FOCUSABLE` `Node` flag is removed in
favor of a
more specification-aligned approach of checking lazily if an element is
a focusable area. This allows moving the focus fixup steps to their
correct place, asynchronously during "update the rendering" and
synchronously during `unbind_from_tree`.

Testing: This causes some WPT tests to start passing and some to start
failing:
- Two subtests tests in
`tests/wpt/meta/css/css-conditional/container-queries/` because
we do not implement support for container queries. These are legitimate
failures
   and they are compensated by a good subtest pass in the same file.
- A few subtests in `/html/interaction/focus/tabindex-focus-flag.html`
because now
we do not allow focusing non-rendering elements. The failures are HTML
elements with
`<svg>` which we do not rendering and `<summary>` elements of
`<details>` which
   need special handling we do not implement yet.
- `html/semantics/forms/the-fieldset-element/disabled-003.html`: Some of
these tests
start to fail, but they are not spec compliant and browser properly
implementing
   the asynchronous focus fixup rule also fail them. 

Fixes: servo#31870.

Signed-off-by: Martin Robinson <[email protected]>
Co-authored-by: Oriol Brufau <[email protected]>
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.

Update the rendering: run the focusing steps for for unfocusable

4 participants