Skip to content

script: Add support for ShadowRoot.delegatesFocus and start implementing the focusing steps#43811

Merged
mrobinson merged 1 commit into
servo:mainfrom
mrobinson:add-support-for-delegates-focus
Mar 31, 2026
Merged

script: Add support for ShadowRoot.delegatesFocus and start implementing the focusing steps#43811
mrobinson merged 1 commit into
servo:mainfrom
mrobinson:add-support-for-delegates-focus

Conversation

@mrobinson

Copy link
Copy Markdown
Member

This change has two main interdependent parts:

  1. It starts to align Servo's focus code with what is written in the
    HTML specification. This is going to be a gradual change, so there
    are still many parts that do not match the specification yet. Still,
    this adds the major pieces.
  2. It adds initial support for the ShadowRoot.delegatesFocus property
    which controls how focusing a shadow DOM root can delegate focus to
    one of its children.

Testing: This causes a few WPT tests to start passing.

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

Copy link
Copy Markdown

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


'SVGElement': {
'canGc': ['SetAutofocus', 'SetTabIndex']
'canGc': ['Focus', 'SetAutofocus', 'SetTabIndex']

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: make it cx so that we don't need to migrate afterwards

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.

Sure.

///
/// In addition to returning the focus delegate for this [`Element`], this method also returns
/// the [`FocusableAreaKind`] for efficiency reasons.
fn focus_delegate(&self) -> Option<(DomRoot<Element>, FocusableAreaKind)> {

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.

Since these are all newly implemented methods, can you move them into script/dom/element/focus.rs as per #43720 ?

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'm happy to do this, but it may be the case that a subsequent refactor moves this to a FocusTarget struct.

@github-actions

Copy link
Copy Markdown

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

Flaky unexpected result (29)
  • CRASH [expected OK] /_mozilla/mozilla/img_find_non_sibling_map.html
  • CRASH [expected TIMEOUT] /_webgl/conformance/textures/misc/tex-video-using-tex-unit-non-zero.html (#39735)
  • TIMEOUT [expected OK] /credential-management/credentialscontainer-frame-basics.https.html (#39430)
    • TIMEOUT [expected FAIL] subtest: navigator.credentials should be undefined in documents generated from data: URLs.

      Test timed out
      

  • OK /css/css-cascade/layer-cssom-order-reverse.html (#36094)
    • FAIL [expected PASS] subtest: Delete layer invalidates @font-face

      assert_equals: expected "220px" but got "133px"
      

  • OK /css/css-grid/grid-definition/grid-support-grid-template-columns-rows-001.html (#41194)
    • FAIL [expected PASS] subtest: 'grid' with: grid-template-columns: none; and grid-template-rows: none;

      assert_in_array: gridTemplateColumns value "59px" not in array ["90px"]
      

    • FAIL [expected PASS] subtest: 'grid' with: grid-template-columns: auto; and grid-template-rows: auto;

      assert_in_array: gridTemplateColumns value "59px" not in array ["90px"]
      

    • FAIL [expected PASS] subtest: 'grid' with: grid-template-columns: max-content; and grid-template-rows: max-content;

      assert_in_array: gridTemplateColumns value "59px" not in array ["90px"]
      

    • FAIL [expected PASS] subtest: 'grid' with: grid-template-columns: min-content; and grid-template-rows: min-content;

      assert_in_array: gridTemplateColumns value "28px" not in array ["40px"]
      

    • FAIL [expected PASS] subtest: 'grid' with: grid-template-columns: auto 1fr; and grid-template-rows: auto 1fr;

      assert_in_array: gridTemplateColumns value "59px 741px" not in array ["90px 710px"]
      

    • FAIL [expected PASS] subtest: 'grid' with: grid-template-columns: min-content 1fr calc(20px + 10%) minmax(30em, 50em); and grid-template-rows: min-content 1fr calc(10% + 40px) minmax(3em, 5em);

      assert_in_array: gridTemplateColumns value "28px 172px 100px 500px" not in array ["40px 160px 100px 500px"]
      

    • FAIL [expected PASS] subtest: 'grid' with: grid-template-columns: foo; and grid-template-rows: bar;

      assert_in_array: gridTemplateColumns value "59px" not in array ["90px"]
      

    • FAIL [expected PASS] subtest: 'grid' with: grid-template-columns: auto none; and grid-template-rows: none auto;

      assert_in_array: gridTemplateColumns value "59px" not in array ["90px"]
      

    • FAIL [expected PASS] subtest: 'grid' with: grid-template-columns: 100px, 200px; and grid-template-rows: 300px, 400px;

      assert_in_array: gridTemplateColumns value "59px" not in array ["90px"]
      

    • FAIL [expected PASS] subtest: 'grid' with: grid-template-columns: minmax(100px, 200px, 300px); and grid-template-rows: minmax(100px, 200px, 300px);

      assert_in_array: gridTemplateColumns value "59px" not in array ["90px"]
      

  • PASS [expected FAIL] /css/css-ui/appearance-menulist-button-002.tentative.html
  • OK /fetch/content-length/api-and-duplicate-headers.any.html (#35873)
    • 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 /fetch/metadata/generated/css-font-face.https.sub.tentative.html (#32732)
    • PASS [expected FAIL] subtest: sec-fetch-mode
  • OK /html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html (#28697)
    • FAIL [expected PASS] subtest: aElement.click() before the load event must NOT replace

      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)&amp;code=%0A%20%20%20%20const%20a%20%3D%20document.createElement(%22a%22)%3B%0A%20%20%20%20a.href%20%3D%20%22%2Fcommon%2Fblank.html%3Fthereplacement%22%3B%0A%20%20%20%20document.currentScript.before(a)%3B%0A%20%20%20%20a.click()%3B%0A%20%20"
      

  • CRASH [expected OK] /html/browsers/browsing-the-web/remote-context-helper-tests/addWorker.window.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/the-window-object/window-open-noopener.html?_top
  • ERROR [expected OK] /html/canvas/offscreen/text/2d.text.measure.getActualBoundingBox.tentative.html (#43710)
  • OK [expected TIMEOUT] /html/semantics/embedded-content/media-elements/src_object_blob.html (#40340)
    • PASS [expected TIMEOUT] subtest: HTMLMediaElement.srcObject blob
  • 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
      

  • CRASH [expected ERROR] /html/semantics/forms/the-select-element/customizable-select/select-appearance-button-after-span.html
  • OK /html/semantics/scripting-1/the-script-element/module/dynamic-import/blob-url.any.html (#33948)
    • 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"
      

  • CRASH [expected OK] /html/webappapis/scripting/processing-model-2/runtime-error-same-origin-with-hash.html
  • 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"])
      

  • OK /mixed-content/tentative/autoupgrades/audio-upgrade.https.sub.html (#41697)
    • FAIL [expected PASS] subtest: Audio of other host autoupgraded

      assert_equals: Length of other host audio is correct expected 1 but got Infinity
      

  • FAIL [expected PASS] /png/apng/fcTL-blend-source-solid.html (#41560)
  • CRASH [expected TIMEOUT] /preload/link-header-modulepreload.html
  • CRASH [expected OK] /shadow-dom/untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-010.html
  • OK [expected TIMEOUT] /trusted-types/trusted-types-navigation.html?01-05 (#38975)
    • PASS [expected TIMEOUT] subtest: Navigate a window via anchor with javascript:-urls in report-only mode.
    • PASS [expected NOTRUN] subtest: Navigate a window via anchor with javascript:-urls w/ default policy in report-only mode.
    • PASS [expected NOTRUN] subtest: Navigate a frame via anchor with javascript:-urls in enforcing mode.
  • 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 [expected TIMEOUT] /trusted-types/trusted-types-navigation.html?26-30 (#38807)
    • PASS [expected TIMEOUT] subtest: Navigate a window via form-submission with javascript:-urls in report-only mode.
    • PASS [expected NOTRUN] subtest: Navigate a window via form-submission with javascript:-urls w/ default policy in report-only mode.
    • PASS [expected NOTRUN] subtest: Navigate a frame via form-submission with javascript:-urls in enforcing mode.
    • PASS [expected NOTRUN] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in enforcing mode.
  • OK /visual-viewport/resize-event-order.html (#41981)
    • PASS [expected FAIL] subtest: Popup: DOMWindow resize fired before VisualViewport.
  • OK /webxr/xrSession_features_deviceSupport.https.html (#24357)
    • FAIL [expected PASS] subtest: Immersive XRSession requests with no supported device should reject

      assert_unreached: Should have rejected: undefined Reached unreachable code
      

  • CRASH [expected ERROR] /workers/Worker-constructor-proto.any.serviceworker.html
Stable unexpected results that are known to be intermittent (19)
  • 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)
    • FAIL [expected PASS] subtest: WebGL test #45

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

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

      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 #49

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

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

      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 #53
    • PASS [expected FAIL] subtest: WebGL test #55
    • PASS [expected FAIL] subtest: WebGL test #57
    • PASS [expected FAIL] subtest: WebGL test #59
    • FAIL [expected PASS] subtest: WebGL test #61

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

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

      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
      

    • And 6 more unexpected results...
  • 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
      

  • OK /css/css-fonts/generic-family-keywords-001.html (#37467)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(fangsong)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(kai)
  • OK /css/css-fonts/generic-family-keywords-003.html (#38994)
    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted cursive (drawing text in a canvas)

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

    • 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)
    • FAIL [expected PASS] subtest: @font-face matching for quoted and unquoted generic(kai) (drawing text in a canvas)

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

    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted ui-serif (drawing text in a canvas)
  • OK /dom/nodes/moveBefore/iframe-document-preserve.window.html (#43152)
    • PASS [expected FAIL] subtest: moveBefore(): cross-origin iframe is preserved: remove self
  • OK /html/browsers/browsing-the-web/navigating-across-documents/005.html (#27062)
    • FAIL [expected PASS] subtest: Link with onclick navigation and href navigation

      assert_equals: expected "href" but got "click"
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin-fragment.html (#20768)
    • PASS [expected FAIL] subtest: Tests that a fragment navigation in the unload handler will not block the initial navigation
  • 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 TIMEOUT] subtest: Non-HTMLElement should not support autofocus
  • OK [expected TIMEOUT] /html/interaction/focus/the-autofocus-attribute/update-the-rendering.html (#24145)
    • FAIL [expected TIMEOUT] subtest: "Flush autofocus candidates" should be happen before a scroll event and animation frame callbacks

      assert_array_equals: animationFrame lengths differ, expected array ["autofocus", "scroll", "animationFrame"] length 3, got ["animationFrame"] length 1
      

  • OK /mixed-content/tentative/autoupgrades/mixed-content-cors.https.sub.html (#41123)
    • PASS [expected FAIL] subtest: Cross-Origin video should get upgraded even if CORS is set
  • OK /mixed-content/tentative/autoupgrades/video-upgrade.https.sub.html (#41135)
    • FAIL [expected PASS] subtest: Video autoupgraded

      assert_equals: Length. expected 1 but got Infinity
      

    • FAIL [expected PASS] subtest: Video of other host autoupgraded

      assert_equals: Length. Other host expected 1 but got Infinity
      

  • TIMEOUT [expected OK] /preload/modulepreload-sri-importmap.html (#43354)
    • TIMEOUT [expected PASS] subtest: Script should not be loaded if modulepreload's integrity is invalid

      Test timed out
      

  • OK [expected CRASH] /resource-timing/render-blocking-status-link.html (#41664)
  • OK /resource-timing/test_resource_timing.https.html (#25216)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (link)
  • OK /touch-events/single-tap-when-touchend-listener-use-sync-xhr.html (#41175)
    • FAIL [expected PASS] subtest: Click event should be fired when touchend opens synchronous XHR

      assert_equals: expected "touchend@div, mousedown@div, mouseup@div, click@div" but got "touchend@div"
      

  • 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
Stable unexpected results (1)
  • OK /shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html
    • PASS [expected FAIL] subtest: Order when all tabindex=-1 is and delegatesFocus = true

@github-actions

Copy link
Copy Markdown

⚠️ Try run (#23817199076) failed!

…nting the focusing steps

This change has two main interdependent parts:

1. It starts to align Servo's focus code with what is written in the
   HTML specification. This is going to be a gradual change, so there
   are still many parts that do not match the specification yet. Still,
   this adds the major pieces.
2. It adds initial support for the `ShadowRoot.delegatesFocus` property
   which controls how focusing a shadow DOM root can delegate focus to
   one of its children.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
@mrobinson mrobinson force-pushed the add-support-for-delegates-focus branch from e63b54f to d7d2de5 Compare March 31, 2026 21:14
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Mar 31, 2026
@mrobinson mrobinson enabled auto-merge March 31, 2026 21:18
@mrobinson mrobinson added this pull request to the merge queue Mar 31, 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 31, 2026
Merged via the queue into servo:main with commit d3c39bb Mar 31, 2026
33 checks passed
@mrobinson mrobinson deleted the add-support-for-delegates-focus branch March 31, 2026 22:11
@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 31, 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.

4 participants