script: Add FocusOptions argument to Element.focus and implement FocusOptions.preventScroll (#38495)

This is an implementation of the `prevent_scroll` feature in the focus
transaction system. It allows to control whether focusing an element
should prevent scrolling or not.

Spec:
https://html.spec.whatwg.org/multipage/interaction.html#dom-focusoptions-preventscroll
Testing: Existing WPT tests

Signed-off-by: abdelrahman1234567 <abdelrahman.hossameldin.awadalla@huawei.com>
This commit is contained in:
Abdelrahman Hossam 2025-08-22 22:05:32 +08:00 committed by GitHub
parent 2ac8665e03
commit 176e42d36d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 95 additions and 60 deletions

View file

@ -87,6 +87,7 @@ use crate::dom::bindings::codegen::Bindings::ElementBinding::{
use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
use crate::dom::bindings::codegen::Bindings::HTMLIFrameElementBinding::HTMLIFrameElement_Binding::HTMLIFrameElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLOrSVGElementBinding::FocusOptions;
use crate::dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods;
use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
@ -264,6 +265,8 @@ struct FocusTransaction {
element: Option<Dom<Element>>,
/// See [`Document::has_focus`].
has_focus: bool,
/// Focus options for the transaction
focus_options: FocusOptions,
}
/// Information about a declarative refresh
@ -1140,6 +1143,7 @@ impl Document {
*self.focus_transaction.borrow_mut() = Some(FocusTransaction {
element: self.focused.get().as_deref().map(Dom::from_ref),
has_focus: self.has_focus.get(),
focus_options: FocusOptions::default(),
});
}
@ -1169,15 +1173,27 @@ impl Document {
}
}
/// Request that the given element receive focus with default options.
/// See [`Self::request_focus_with_options`] for the details.
pub(crate) fn request_focus(
&self,
elem: Option<&Element>,
focus_initiator: FocusInitiator,
can_gc: CanGc,
) {
self.request_focus_with_options(elem, focus_initiator, FocusOptions::default(), can_gc);
}
/// Request that the given element receive focus once the current
/// transaction is complete. `None` specifies to focus the document.
///
/// If there's no ongoing transaction, this method automatically starts and
/// commits an implicit transaction.
pub(crate) fn request_focus(
pub(crate) fn request_focus_with_options(
&self,
elem: Option<&Element>,
focus_initiator: FocusInitiator,
focus_options: FocusOptions,
can_gc: CanGc,
) {
// If an element is specified, and it's non-focusable, ignore the
@ -1197,6 +1213,7 @@ impl Document {
let focus_transaction = focus_transaction.as_mut().unwrap();
focus_transaction.element = elem.map(Dom::from_ref);
focus_transaction.has_focus = true;
focus_transaction.focus_options = focus_options;
}
if implicit_transaction {
@ -1236,7 +1253,7 @@ impl Document {
/// Reassign the focus context to the element that last requested focus during this
/// transaction, or the document if no elements requested it.
pub(crate) fn commit_focus_transaction(&self, focus_initiator: FocusInitiator, can_gc: CanGc) {
let (mut new_focused, new_focus_state) = {
let (mut new_focused, new_focus_state, prevent_scroll) = {
let focus_transaction = self.focus_transaction.borrow();
let focus_transaction = focus_transaction
.as_ref()
@ -1247,6 +1264,7 @@ impl Document {
.as_ref()
.map(|e| DomRoot::from_ref(&**e)),
focus_transaction.has_focus,
focus_transaction.focus_options.preventScroll,
)
};
*self.focus_transaction.borrow_mut() = None;
@ -1363,16 +1381,19 @@ impl Document {
}
// Scroll operation to happen after element gets focus.
// This is needed to ensure that the focused element is visible.
elem.ScrollIntoView(BooleanOrScrollIntoViewOptions::ScrollIntoViewOptions(
ScrollIntoViewOptions {
parent: ScrollOptions {
behavior: ScrollBehavior::Smooth,
// Only scroll if preventScroll was not specified
if !prevent_scroll {
elem.ScrollIntoView(BooleanOrScrollIntoViewOptions::ScrollIntoViewOptions(
ScrollIntoViewOptions {
parent: ScrollOptions {
behavior: ScrollBehavior::Smooth,
},
block: ScrollLogicalPosition::Center,
inline: ScrollLogicalPosition::Center,
container: ScrollIntoViewContainer::All,
},
block: ScrollLogicalPosition::Center,
inline: ScrollLogicalPosition::Center,
container: ScrollIntoViewContainer::All,
},
));
));
}
}
}

View file

@ -22,6 +22,7 @@ use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::{
};
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLLabelElementBinding::HTMLLabelElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLOrSVGElementBinding::FocusOptions;
use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRoot_Binding::ShadowRootMethods;
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
@ -417,12 +418,19 @@ impl HTMLElementMethods<crate::DomTypeHolder> for HTMLElement {
element.set_click_in_progress(false);
}
// https://html.spec.whatwg.org/multipage/#dom-focus
fn Focus(&self, can_gc: CanGc) {
/// <https://html.spec.whatwg.org/multipage/#dom-focus>
fn Focus(&self, options: &FocusOptions, can_gc: CanGc) {
// TODO: Mark the element as locked for focus and run the focusing steps.
// https://html.spec.whatwg.org/multipage/#focusing-steps
// <https://html.spec.whatwg.org/multipage/#focusing-steps>
let document = self.owner_document();
document.request_focus(Some(self.upcast()), FocusInitiator::Local, can_gc);
document.request_focus_with_options(
Some(self.upcast()),
FocusInitiator::Local,
FocusOptions {
preventScroll: options.preventScroll,
},
can_gc,
);
}
// https://html.spec.whatwg.org/multipage/#dom-blur

View file

@ -34,6 +34,7 @@ use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMeth
use crate::dom::bindings::codegen::Bindings::HTMLFormControlsCollectionBinding::HTMLFormControlsCollectionMethods;
use crate::dom::bindings::codegen::Bindings::HTMLFormElementBinding::HTMLFormElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLOrSVGElementBinding::FocusOptions;
use crate::dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::{NodeConstants, NodeMethods};
use crate::dom::bindings::codegen::Bindings::NodeListBinding::NodeListMethods;
@ -1101,7 +1102,9 @@ impl HTMLFormElement {
}
if first {
if let Some(html_elem) = elem.downcast::<HTMLElement>() {
html_elem.Focus(can_gc);
// TODO: "Focusing steps" has a different meaning from the focus() method.
// The actual focusing steps should be implemented
html_elem.Focus(&FocusOptions::default(), can_gc);
first = false;
}
}

View file

@ -9,11 +9,12 @@ 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::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner};
use crate::dom::document::Document;
use crate::dom::document::{Document, FocusInitiator};
use crate::dom::element::{AttributeMutation, Element};
use crate::dom::node::{Node, NodeTraits};
use crate::dom::virtualmethods::VirtualMethods;
@ -91,7 +92,7 @@ impl VirtualMethods for SVGElement {
}
impl SVGElementMethods<crate::DomTypeHolder> for SVGElement {
// https://html.spec.whatwg.org/multipage/#the-style-attribute
/// <https://html.spec.whatwg.org/multipage/#the-style-attribute>
fn Style(&self) -> DomRoot<CSSStyleDeclaration> {
self.style_decl.or_init(|| {
let global = self.owner_window();
@ -105,28 +106,41 @@ impl SVGElementMethods<crate::DomTypeHolder> for SVGElement {
})
}
// <https://html.spec.whatwg.org/multipage/#globaleventhandlers>
// https://html.spec.whatwg.org/multipage/#globaleventhandlers
global_event_handlers!();
// https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce
/// <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
/// <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
/// <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
/// <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, options: &FocusOptions) {
let document = self.element.owner_document();
document.request_focus_with_options(
Some(&self.element),
FocusInitiator::Local,
FocusOptions {
preventScroll: options.preventScroll,
},
CanGc::note(),
);
}
}

View file

@ -3,6 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLOrSVGElementBinding::FocusOptions;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
@ -75,7 +76,9 @@ pub(crate) trait Validatable {
validation_message_for_flags(&self.validity_state(), flags)
);
if let Some(html_elem) = self.as_element().downcast::<HTMLElement>() {
html_elem.Focus(can_gc);
// TODO: "Focusing steps" has a different meaning from the focus() method.
// The actual focusing steps should be implemented
html_elem.Focus(&FocusOptions::default(), can_gc);
}
}

View file

@ -42,6 +42,7 @@ use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLOrSVGElementBinding::FocusOptions;
use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
@ -1234,7 +1235,9 @@ pub(crate) fn handle_will_send_keys(
// run the focusing steps for the element.
if let Some(html_element) = element.downcast::<HTMLElement>() {
if !element.is_active_element() {
html_element.Focus(can_gc);
// TODO: "Focusing steps" has a different meaning from the focus() method.
// The actual focusing steps should be implemented
html_element.Focus(&FocusOptions::default(), can_gc);
} else {
element_has_focus = element.focus_state();
}
@ -1772,7 +1775,9 @@ fn clear_a_resettable_element(element: &Element, can_gc: CanGc) -> Result<(), Er
}
// Step 3. Invoke the focusing steps for the element.
html_element.Focus(can_gc);
// TODO: "Focusing steps" has a different meaning from the focus() method.
// The actual focusing steps should be implemented
html_element.Focus(&FocusOptions::default(), can_gc);
// Step 4. Run clear algorithm for element.
if let Some(input_element) = element.downcast::<HTMLInputElement>() {
@ -1920,7 +1925,11 @@ pub(crate) fn handle_element_click(
// Step 8.5
match container.downcast::<HTMLElement>() {
Some(html_element) => html_element.Focus(can_gc),
Some(html_element) => {
// TODO: "Focusing steps" has a different meaning from the focus() method.
// The actual focusing steps should be implemented
html_element.Focus(&FocusOptions::default(), can_gc);
},
None => return Err(ErrorStatus::UnknownError),
}

View file

@ -812,6 +812,10 @@ Dictionaries = {
'derives': ['Clone', 'MallocSizeOf'],
},
'FocusOptions': {
'derives': ['Clone', 'MallocSizeOf']
},
'FontFaceDescriptors': {
'derives': ['Clone', 'MallocSizeOf']
},

View file

@ -35,7 +35,6 @@ interface HTMLElement : Element {
undefined click();
// [CEReactions]
// attribute long tabIndex;
undefined focus();
undefined blur();
// [CEReactions]
// attribute DOMString accessKey;

View file

@ -9,12 +9,17 @@
* liability, trademark and document use rules apply.
*/
dictionary FocusOptions {
boolean preventScroll = false;
// boolean focusVisible;
};
interface mixin HTMLOrSVGElement {
// [SameObject] readonly attribute DOMStringMap dataset;
attribute DOMString nonce; // intentionally no [CEReactions]
[CEReactions] attribute boolean autofocus;
// [CEReactions] attribute long tabIndex;
// undefined focus(optional FocusOptions options = {});
undefined focus(optional FocusOptions options = {});
// undefined blur();
};

View file

@ -5075,9 +5075,6 @@
[SVGElement interface: attribute tabIndex]
expected: FAIL
[SVGElement interface: operation focus(optional FocusOptions)]
expected: FAIL
[SVGElement interface: operation blur()]
expected: FAIL

View file

@ -1,3 +0,0 @@
[preventScroll-nested-scroll-elements.html]
[focus(options) - preventScroll on nested scroll elements]
expected: FAIL

View file

@ -1,3 +0,0 @@
[preventScroll-textarea.html]
[preventScroll: true on a textarea element]
expected: FAIL

View file

@ -1,3 +0,0 @@
[preventScroll.html]
[elm.focus({preventScroll: true})]
expected: FAIL

View file

@ -5,15 +5,9 @@
[A with tabindex=invalid should not be focusable.]
expected: FAIL
[#svg-a should not be focusable by default.]
expected: FAIL
[input[type="hidden"\] should not be focusable by default.]
expected: FAIL
[text with tabindex=0 should be focusable.]
expected: FAIL
[IMG with tabindex=invalid should not be focusable.]
expected: FAIL
@ -23,15 +17,6 @@
[#summary-first should be focusable by default.]
expected: FAIL
[a with tabindex=0 should be focusable.]
expected: FAIL
[a with tabindex=-1 should be focusable.]
expected: FAIL
[#svg-text should not be focusable by default.]
expected: FAIL
[a should not be focusable by default.]
expected: FAIL
@ -46,7 +31,3 @@
[a#with-href with tabindex=invalid should not be focusable.]
expected: FAIL
[text with tabindex=-1 should be focusable.]
expected: FAIL