script: Fully implement DocumentOrShadowRoot#activeElement (#43861)

`DocumentOrShadowRoot#activeElement` should return retargeted results.
What that means is that if the DOM anchor of the `Document`'s focused
focusable area is within a shadow root, `Document#activeElement` should
return the shadow host. This change implements that behavior, properly
returning the `activeElement` from both `Document` and `ShadowRoot`.

Testing: This causes a decent number of WPT tests and subtests to start
passing. One subtest starts to fail, because it uses the `autofocus`
attribute
which we do not yet support.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson
2026-04-02 13:12:30 +02:00
committed by GitHub
parent 9a43d43c32
commit 33f74feffd
27 changed files with 68 additions and 165 deletions

View File

@@ -371,7 +371,7 @@ pub(crate) struct Document {
/// Whether the DOMContentLoaded event has already been dispatched.
domcontentloaded_dispatched: Cell<bool>,
/// The element that currently has the document focus context.
focused: MutNullableDom<Element>,
focused_element: MutNullableDom<Element>,
/// The last sequence number sent to the constellation.
#[no_trace]
focus_sequence: Cell<FocusSequenceNumber>,
@@ -1336,8 +1336,8 @@ impl Document {
/// Return the element that currently has focus.
// https://w3c.github.io/uievents/#events-focusevent-doc-focus
pub(crate) fn get_focused_element(&self) -> Option<DomRoot<Element>> {
self.focused.get()
pub(crate) fn focused_element(&self) -> Option<DomRoot<Element>> {
self.focused_element.get()
}
/// Get the last sequence number sent to the constellation.
@@ -1368,7 +1368,7 @@ impl Document {
/// TODO: Handle the "focus changed during ongoing navigation" flag.
pub(crate) fn perform_focus_fixup_rule(&self, can_gc: CanGc) {
if self
.focused
.focused_element
.get()
.as_deref()
.is_none_or(|focused| focused.is_focusable_area())
@@ -1409,9 +1409,10 @@ impl Document {
},
true,
),
FocusOperation::Unfocus => {
(self.focused.get().as_deref().map(DomRoot::from_ref), false)
},
FocusOperation::Unfocus => (
self.focused_element.get().as_deref().map(DomRoot::from_ref),
false,
),
};
if !new_focus_state {
@@ -1426,7 +1427,7 @@ impl Document {
}
}
let old_focused = self.focused.get();
let old_focused = self.focused_element.get();
let old_focus_state = self.has_focus.get();
debug!(
@@ -1470,7 +1471,7 @@ impl Document {
self.fire_focus_event(FocusEventType::Blur, self.global().upcast(), None, can_gc);
}
self.focused.set(new_focused.as_deref());
self.focused_element.set(new_focused.as_deref());
self.has_focus.set(new_focus_state);
if old_focus_state != new_focus_state && new_focus_state {
@@ -3855,7 +3856,7 @@ impl Document {
ready_state: Cell::new(ready_state),
domcontentloaded_dispatched: Cell::new(domcontentloaded_dispatched),
focused: Default::default(),
focused_element: Default::default(),
focus_sequence: Cell::new(FocusSequenceNumber::default()),
has_focus: Cell::new(has_focus),
current_script: Default::default(),
@@ -5051,11 +5052,7 @@ impl DocumentMethods<crate::DomTypeHolder> for Document {
/// <https://html.spec.whatwg.org/multipage/#dom-document-activeelement>
fn GetActiveElement(&self) -> Option<DomRoot<Element>> {
self.document_or_shadow_root.get_active_element(
self.get_focused_element(),
self.GetBody(),
self.GetDocumentElement(),
)
self.document_or_shadow_root.active_element(self.upcast())
}
/// <https://html.spec.whatwg.org/multipage/#dom-document-hasfocus>

View File

@@ -1357,7 +1357,7 @@ impl DocumentEventHandler {
can_gc: CanGc,
) -> InputEventResult {
let document = self.window.Document();
let focused = document.get_focused_element();
let focused = document.focused_element();
let body = document.GetBody();
let target = match (&focused, &body) {
@@ -1434,7 +1434,7 @@ impl DocumentEventHandler {
// spec: https://w3c.github.io/uievents/#compositionupdate
// spec: https://w3c.github.io/uievents/#compositionend
// > Event.target : focused element processing the composition
let focused = document.get_focused_element();
let focused = document.focused_element();
let target = if let Some(elem) = &focused {
elem.upcast()
} else {
@@ -1752,7 +1752,7 @@ impl DocumentEventHandler {
// Step 6 if the context is editable:
let document = self.window.Document();
let target = target.or(document.get_focused_element());
let target = target.or(document.focused_element());
let target = target
.map(|target| DomRoot::from_ref(target.upcast()))
.or_else(|| {
@@ -2018,7 +2018,7 @@ impl DocumentEventHandler {
let mut starting_point = self
.window
.Document()
.get_focused_element()
.focused_element()
.map(DomRoot::upcast::<Node>);
// > 2. If there is a sequential focus navigation starting point defined and it is inside
@@ -2242,7 +2242,7 @@ impl DocumentEventHandler {
let document = self.window.Document();
let mut scrolling_box = document
.get_focused_element()
.focused_element()
.or(self.most_recently_clicked_element.get())
.and_then(|element| element.scrolling_box(ScrollContainerQueryFlags::Inclusive))
.unwrap_or_else(|| {

View File

@@ -10,6 +10,8 @@ use embedder_traits::UntrustedNodeAddress;
use js::rust::HandleValue;
use layout_api::ElementsFromPointFlags;
use rustc_hash::FxBuildHasher;
use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
use script_bindings::error::{Error, ErrorResult};
use script_bindings::script_runtime::{CanGc, JSContext};
use servo_arc::Arc;
@@ -21,7 +23,9 @@ use style::stylesheets::{Stylesheet, StylesheetContents};
use stylo_atoms::Atom;
use webrender_api::units::LayoutPoint;
use crate::dom::Document;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::NodeBinding::GetRootNodeOptions;
use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRootMethods;
use crate::dom::bindings::conversions::{ConversionResult, SafeFromJSValConvertible};
@@ -31,10 +35,9 @@ use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::trace::HashMapTracedValues;
use crate::dom::css::stylesheetlist::StyleSheetListOwner;
use crate::dom::element::Element;
use crate::dom::html::htmlelement::HTMLElement;
use crate::dom::node::{self, Node, VecPreOrderInsertionHelper};
use crate::dom::shadowroot::ShadowRoot;
use crate::dom::types::CSSStyleSheet;
use crate::dom::types::{CSSStyleSheet, EventTarget};
use crate::dom::window::Window;
use crate::stylesheet_set::StylesheetSetRef;
@@ -221,23 +224,48 @@ impl DocumentOrShadowRoot {
elements
}
// https://html.spec.whatwg.org/multipage/#dom-document-activeelement
pub(crate) fn get_active_element(
&self,
focused_element: Option<DomRoot<Element>>,
body: Option<DomRoot<HTMLElement>>,
document_element: Option<DomRoot<Element>>,
) -> Option<DomRoot<Element>> {
// TODO: Step 2.
/// <https://html.spec.whatwg.org/multipage/#dom-documentorshadowroot-activeelement-dev>
pub(crate) fn active_element(&self, this: &Node) -> Option<DomRoot<Element>> {
// Step 1. Let candidate be this's node document's focused area's DOM anchor.
//
// Note: When `Document::focused_element` returns `None`, that means that the
// `Document` / viewport itself is focused.
let document = self.window.Document();
let candidate = match document.focused_element() {
Some(candidate) => DomRoot::upcast::<Node>(candidate),
None => DomRoot::upcast::<Node>(document.clone()),
};
match focused_element {
Some(element) => Some(element), // Step 3. and 4.
None => match body {
// Step 5.
Some(body) => Some(DomRoot::upcast(body)),
None => document_element,
},
// Step 2. Set candidate to the result of retargeting candidate against this.
//
// Note: `retarget()` operates on `EventTarget`, but we can be assured that we are
// only dealing with various kinds of `Node`s here.
let candidate =
DomRoot::downcast::<Node>(candidate.upcast::<EventTarget>().retarget(this.upcast()))?;
// Step 3. If candidate's root is not this, then return null.
if this != &*candidate.GetRootNode(&GetRootNodeOptions::empty()) {
return None;
}
// Step 4. If candidate is not a Document object, then return candidate.
if let Some(candidate) = DomRoot::downcast::<Element>(candidate.clone()) {
return Some(candidate);
}
assert!(candidate.is::<Document>());
// Step 5. If candidate has a body element, then return that body element.
if let Some(body) = document.GetBody() {
return Some(DomRoot::upcast(body));
}
// Step 6. If candidate's document element is non-null, then return that document element.
if let Some(document_element) = document.GetDocumentElement() {
return Some(document_element);
}
// Step 7. Return null.
None
}
/// Remove a stylesheet owned by `owner` from the list of document sheets.

View File

@@ -1339,7 +1339,7 @@ impl VirtualMethods for HTMLElement {
// TODO: Should this also happen for non-HTML elements such as SVG elements?
let element = self.as_element();
if document
.get_focused_element()
.focused_element()
.is_some_and(|focused_element| &*focused_element == element)
{
document.focus(

View File

@@ -95,7 +95,7 @@ impl Node {
.and_then(Element::shadow_root)
.is_some_and(|shadow_root| shadow_root.DelegatesFocus())
{
if let Some(focused_element) = self.owner_document().get_focused_element() {
if let Some(focused_element) = self.owner_document().focused_element() {
// > Step 2. If focus target is a shadow-including inclusive ancestor of
// > focusedElement, then return focusedElement.
if self

View File

@@ -179,11 +179,6 @@ impl ShadowRoot {
&self.document
}
pub(crate) fn get_focused_element(&self) -> Option<DomRoot<Element>> {
// XXX get retargeted focused element
None
}
pub(crate) fn stylesheet_count(&self) -> usize {
self.author_styles.borrow().stylesheets.len()
}
@@ -387,8 +382,7 @@ impl ShadowRoot {
impl ShadowRootMethods<crate::DomTypeHolder> for ShadowRoot {
/// <https://html.spec.whatwg.org/multipage/#dom-document-activeelement>
fn GetActiveElement(&self) -> Option<DomRoot<Element>> {
self.document_or_shadow_root
.get_active_element(self.get_focused_element(), None, None)
self.document_or_shadow_root.active_element(self.upcast())
}
/// <https://drafts.csswg.org/cssom-view/#dom-document-elementfrompoint>

View File

@@ -5,8 +5,5 @@
[Shift+Tab escape from video element inside focusgroup goes to adjacent item]
expected: FAIL
[Tab through video controls outside focusgroup follows normal tab order]
expected: FAIL
[Tab escape from inside video shadow DOM controls reaches adjacent focusgroup item]
expected: FAIL

View File

@@ -0,0 +1,3 @@
[autofocus-in-not-fully-active-document.html]
[Autofocus element in not-fully-active document should not be queued.]
expected: FAIL

View File

@@ -1,6 +0,0 @@
[slot-element-focusable.tentative.html]
[slot element with display: block should be focusable]
expected: FAIL
[slot element with default style should be focusable]
expected: FAIL

View File

@@ -1,6 +0,0 @@
[slot-element-tabbable.tentative.html]
[slot element with display: block should be focusable]
expected: FAIL
[slot element with default style should be focusable]
expected: FAIL

View File

@@ -1,7 +1,4 @@
[dialog-focus-previous-outside.html]
[Focus restore should not occur when the focused element is in a shadowroot outside of the dialog.]
expected: FAIL
[Focus restore should occur when the focused element is in a shadowroot inside the dialog.]
expected: FAIL

View File

@@ -7,6 +7,3 @@
[Focus should be moved to the previously focused element even if it has moved to shadow DOM root in between show/close]
expected: FAIL
[Focus should be moved to the shadow DOM host if the previouly focused element is a shadow DOM node]
expected: FAIL

View File

@@ -1,6 +0,0 @@
[ShadowRoot-interface.html]
[ShadowRoot.activeElement must return the focused element of the context object when shadow root is open.]
expected: FAIL
[ShadowRoot.activeElement must return the focused element of the context object when shadow root is closed.]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[focus-navigation-web-component-radio.html]
[Focus for web component input type elements should be bound by <form> inside shadow DOM]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[focus-within-shadow.html]
[Don't clear focus within shadow root if light DOM children are cleared]
expected: FAIL

View File

@@ -1,12 +0,0 @@
[DocumentOrShadowRoot-activeElement.html]
[activeElement on document & shadow root when focused element is in the shadow tree]
expected: FAIL
[activeElement on a neighboring host when focused element is in another shadow tree]
expected: FAIL
[activeElement when focused element is in a nested shadow tree]
expected: FAIL
[activeElement when focused element is in a parent shadow tree]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[delegatesFocus-tabindex-change.html]
[Setting tabindex on the shadow host of a focused element with delegatesFocus should not change focus.]
expected: FAIL

View File

@@ -5,8 +5,5 @@
[Focus should be delegated to the autofocus element when the inner host has delegates focus]
expected: FAIL
[Focus should not be delegated to the slotted elements]
expected: FAIL
[Focus should be delegated to the nested div which has autofocus based on the tree order]
expected: FAIL

View File

@@ -1,39 +0,0 @@
[focus-method-delegatesFocus.html]
[focus() on host with delegatesFocus, all tabindex=0]
expected: FAIL
[focus() on host with delegatesFocus & tabindex =-1, all other tabindex=0]
expected: FAIL
[focus() on host with delegatesFocus & no tabindex, all other tabindex=0]
expected: FAIL
[focus() on host with delegatesFocus & tabindex = 0, all other tabindex=-1]
expected: FAIL
[focus() on host with delegatesFocus, all tabindex=-1]
expected: FAIL
[focus() on host with delegatesFocus & tabindex=0, #belowSlots with tabindex=0]
expected: FAIL
[focus() on host with delegatesFocus & tabindex=0, #aboveSlots and #belowSlots with tabindex=0]
expected: FAIL
[focus() on host with delegatesFocus & tabindex=0, #aboveSlots with tabindex=0 and #belowSlots with tabindex=1]
expected: FAIL
[focus() on host with delegatesFocus & tabindex=0, #slottedToFirstSlot, #slottedToSecondSlot, #belowSlots with tabindex=0]
expected: FAIL
[focus() on host with delegatesFocus and already-focused non-first shadow descendant]
expected: FAIL
[focus() on host with delegatesFocus with another host with no delegatesFocus and a focusable child]
expected: FAIL
[focus() on host with delegatesFocus with another host with delegatesFocus and a focusable child]
expected: FAIL
[focus() on host with delegatesFocus and slotted focusable children]
expected: FAIL

View File

@@ -1,9 +0,0 @@
[focus-method-with-delegatesFocus.html]
[on focus(), focusable xshadow2 with delegatesFocus=true delegates focus into its inner element.]
expected: FAIL
[if an element within shadow is focused, focusing on shadow host should not slide focus to its inner element.]
expected: FAIL
[xshadow2.focus() shouldn't move focus to #one when its inner element is already focused.]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[focus-slot-box-generated-tabindex-0.html]
[slot with tabindex=0 that generates a box should be focusable]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[focus-tabindex-order-shadow-varying-tabindex-2.html]
[Order with different tabindex on host]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[focus-tabindex-order-shadow-varying-tabindex-3.html]
[Order with different tabindex on host]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[test-007.html]
[A_10_01_01_03_01_T01]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[test-001.html]
[A_07_03_01_T01]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[test-002.html]
[A_07_03_02_T01]
expected: FAIL

View File

@@ -1,2 +0,0 @@
[viewport-apply-initial-scale-after-navigation.html]
expected: TIMEOUT