Files
servo/components/script/dom/svg/svgelement.rs
Martin Robinson 1528f31269 script: Add an initial implementation of the "focus update steps" (#44360)
This moves Servo closer to the focus parts of the HTML specification.
The behavior should be the same as before, but now the code in `script`
matches the structure of the specification.

The main goal is to set us up for:
 - Firing focus events in the right order on nested documents
 - A proper implementation of the unfocusing steps.

Testing: This should not change behavior so is covered by existing
tests.

Signed-off-by: Martin Robinson <mrobinson@fastmail.fm>
Co-authored-by: Martin Robinson <mrobinson@fastmail.fm>
2026-04-22 15:55:36 +00:00

207 lines
7.2 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use dom_struct::dom_struct;
use html5ever::{LocalName, Prefix, local_name, ns};
use js::rust::HandleObject;
use script_bindings::codegen::GenericBindings::ElementBinding::ScrollLogicalPosition;
use script_bindings::codegen::GenericBindings::WindowBinding::ScrollBehavior;
use script_bindings::str::DOMString;
use stylo_dom::ElementState;
use crate::dom::attr::Attr;
use crate::dom::bindings::codegen::Bindings::HTMLOrSVGElementBinding::FocusOptions;
use crate::dom::bindings::codegen::Bindings::SVGElementBinding::SVGElementMethods;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
use crate::dom::css::cssstyledeclaration::{
CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner,
};
use crate::dom::document::Document;
use crate::dom::document::focus::FocusableArea;
use crate::dom::element::{AttributeMutation, Element};
use crate::dom::node::{Node, NodeTraits};
use crate::dom::scrolling_box::{ScrollAxisState, ScrollRequirement};
use crate::dom::virtualmethods::VirtualMethods;
use crate::script_runtime::CanGc;
#[dom_struct]
pub(crate) struct SVGElement {
element: Element,
style_decl: MutNullableDom<CSSStyleDeclaration>,
}
impl SVGElement {
fn new_inherited(
tag_name: LocalName,
prefix: Option<Prefix>,
document: &Document,
) -> SVGElement {
SVGElement::new_inherited_with_state(ElementState::empty(), tag_name, prefix, document)
}
pub(crate) fn new_inherited_with_state(
state: ElementState,
tag_name: LocalName,
prefix: Option<Prefix>,
document: &Document,
) -> SVGElement {
SVGElement {
element: Element::new_inherited_with_state(state, tag_name, ns!(svg), prefix, document),
style_decl: Default::default(),
}
}
pub(crate) fn new(
cx: &mut js::context::JSContext,
tag_name: LocalName,
prefix: Option<Prefix>,
document: &Document,
proto: Option<HandleObject>,
) -> DomRoot<SVGElement> {
Node::reflect_node_with_proto(
cx,
Box::new(SVGElement::new_inherited(tag_name, prefix, document)),
document,
proto,
)
}
fn as_element(&self) -> &Element {
self.upcast::<Element>()
}
}
impl VirtualMethods for SVGElement {
fn super_type(&self) -> Option<&dyn VirtualMethods> {
Some(self.as_element() as &dyn VirtualMethods)
}
fn attribute_mutated(
&self,
cx: &mut js::context::JSContext,
attr: &Attr,
mutation: AttributeMutation,
) {
self.super_type()
.unwrap()
.attribute_mutated(cx, attr, mutation);
let element = self.as_element();
if let (&local_name!("nonce"), mutation) = (attr.local_name(), mutation) {
match mutation {
AttributeMutation::Set(..) => {
let nonce = &**attr.value();
element.update_nonce_internal_slot(nonce.to_owned());
},
AttributeMutation::Removed => {
element.update_nonce_internal_slot(String::new());
},
}
}
}
}
impl SVGElementMethods<crate::DomTypeHolder> for SVGElement {
/// <https://html.spec.whatwg.org/multipage/#the-style-attribute>
fn Style(&self) -> DomRoot<CSSStyleDeclaration> {
self.style_decl.or_init(|| {
let global = self.owner_window();
CSSStyleDeclaration::new(
&global,
CSSStyleOwner::Element(Dom::from_ref(self.upcast())),
None,
CSSModificationAccess::ReadWrite,
CanGc::deprecated_note(),
)
})
}
// https://html.spec.whatwg.org/multipage/#globaleventhandlers
global_event_handlers!();
/// <https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce>
fn Nonce(&self) -> DOMString {
self.as_element().nonce_value().into()
}
/// <https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce>
fn SetNonce(&self, value: DOMString) {
self.as_element()
.update_nonce_internal_slot(value.to_string())
}
/// <https://html.spec.whatwg.org/multipage/#dom-fe-autofocus>
fn Autofocus(&self) -> bool {
self.element.has_attribute(&local_name!("autofocus"))
}
/// <https://html.spec.whatwg.org/multipage/#dom-fe-autofocus>
fn SetAutofocus(&self, autofocus: bool, can_gc: CanGc) {
self.element
.set_bool_attribute(&local_name!("autofocus"), autofocus, can_gc);
}
/// <https://html.spec.whatwg.org/multipage/#dom-focus>
fn Focus(&self, cx: &mut js::context::JSContext, options: &FocusOptions) {
// 1. If the allow focus steps given this's node document return false, then return.
// TODO: Implement this.
// 2. Run the focusing steps for this.
if !self
.upcast::<Node>()
.run_the_focusing_steps(None, CanGc::from_cx(cx))
{
// The specification seems to imply we should scroll into view even if this element
// is not a focusable area. No browser does this, so we return early in that case.
// See https://github.com/whatwg/html/issues/12231.
return;
}
// > 3. If options["focusVisible"] is true, or does not exist but in an
// > implementation-defined way the user agent determines it would be best to do so,
// > then indicate focus. TODO: Implement this.
// TODO: Implement this.
// > 4. If options["preventScroll"] is false, then scroll a target into view given this,
// > "auto", "center", and "center".
if !options.preventScroll {
let scroll_axis = ScrollAxisState {
position: ScrollLogicalPosition::Center,
requirement: ScrollRequirement::IfNotVisible,
};
self.upcast::<Element>().scroll_into_view_with_options(
ScrollBehavior::Smooth,
scroll_axis,
scroll_axis,
None,
None,
);
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-blur>
fn Blur(&self, cx: &mut js::context::JSContext) {
// TODO: Run the unfocusing steps. Focus the top-level document, not
// the current document.
if !self.as_element().focus_state() {
return;
}
// <https://html.spec.whatwg.org/multipage/#unfocusing-steps>
self.owner_document()
.focus_handler()
.focus(FocusableArea::Viewport, CanGc::from_cx(cx));
}
/// <https://html.spec.whatwg.org/multipage/#dom-tabindex>
fn TabIndex(&self) -> i32 {
self.element.tab_index()
}
/// <https://html.spec.whatwg.org/multipage/#dom-tabindex>
fn SetTabIndex(&self, tab_index: i32, can_gc: CanGc) {
self.element
.set_int_attribute(&local_name!("tabindex"), tab_index, can_gc);
}
}