diff --git a/components/script/dom/document/document_event_handler.rs b/components/script/dom/document/document_event_handler.rs index 4a57e2d5064..1a459f70f02 100644 --- a/components/script/dom/document/document_event_handler.rs +++ b/components/script/dom/document/document_event_handler.rs @@ -400,6 +400,20 @@ impl DocumentEventHandler { })); } + /// When an event should be fired on the element that has focus, this returns the target. If + /// there is no associated element with the focused area (such as when the viewport is focused), + /// then the body is returned. If no body is returned then the `Window` is returned. + fn target_for_events_following_focus(&self) -> DomRoot { + let document = self.window.Document(); + match &*document.focus_handler().focused_area() { + FocusableArea::Node { node, .. } => DomRoot::from_ref(node.upcast()), + FocusableArea::Viewport => document + .GetBody() + .map(DomRoot::upcast) + .unwrap_or_else(|| DomRoot::from_ref(self.window.upcast())), + } + } + pub(crate) fn set_cursor(&self, cursor: Option) { if cursor == self.current_cursor.get() { return; @@ -1356,16 +1370,7 @@ impl DocumentEventHandler { keyboard_event: EmbedderKeyboardEvent, can_gc: CanGc, ) -> InputEventResult { - let document = self.window.Document(); - let focused = document.focus_handler().focused_element(); - let body = document.GetBody(); - - let target = match (&focused, &body) { - (Some(focused), _) => focused.upcast(), - (&None, Some(body)) => body.upcast(), - (&None, &None) => self.window.upcast(), - }; - + let target = &self.target_for_events_following_focus(); let keyevent = KeyboardEvent::new_with_platform_keyboard_event( &self.window, keyboard_event.event.state.event_type().into(), @@ -1434,10 +1439,8 @@ 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.focus_handler().focused_element(); - let target = if let Some(elem) = &focused { - elem.upcast() - } else { + let focused_area = document.focus_handler().focused_area(); + let Some(focused_element) = focused_area.element() else { // Event is only dispatched if there is a focused element. return Default::default(); }; @@ -1455,7 +1458,7 @@ impl DocumentEventHandler { ); let event = event.upcast::(); - event.fire(target, can_gc); + event.fire(focused_element.upcast(), can_gc); event.flags().into() } @@ -1751,16 +1754,9 @@ impl DocumentEventHandler { let trusted = true; // Step 6 if the context is editable: - let document = self.window.Document(); - let target = target.or(document.focus_handler().focused_element()); let target = target - .map(|target| DomRoot::from_ref(target.upcast())) - .or_else(|| { - document - .GetBody() - .map(|body| DomRoot::from_ref(body.upcast())) - }) - .unwrap_or_else(|| DomRoot::from_ref(self.window.upcast())); + .map(DomRoot::upcast) + .unwrap_or_else(|| self.target_for_events_following_focus()); // Step 6.2 else TODO require Selection see https://github.com/w3c/clipboard-apis/issues/70 // Step 7 @@ -2019,8 +2015,9 @@ impl DocumentEventHandler { .window .Document() .focus_handler() - .focused_element() - .map(DomRoot::upcast::); + .focused_area() + .element() + .map(|element| DomRoot::from_ref(element.upcast::())); // > 2. If there is a sequential focus navigation starting point defined and it is inside // > starting point, then let starting point be the sequential focus navigation starting point @@ -2244,8 +2241,9 @@ impl DocumentEventHandler { let document = self.window.Document(); let mut scrolling_box = document .focus_handler() - .focused_element() - .or(self.most_recently_clicked_element.get()) + .focused_area() + .element() + .or(self.most_recently_clicked_element.get().as_deref()) .and_then(|element| element.scrolling_box(ScrollContainerQueryFlags::Inclusive)) .unwrap_or_else(|| { document.viewport_scrolling_box(ScrollContainerQueryFlags::Inclusive) diff --git a/components/script/dom/document/documentorshadowroot.rs b/components/script/dom/document/documentorshadowroot.rs index d269835a6ef..e8aa8a15c4d 100644 --- a/components/script/dom/document/documentorshadowroot.rs +++ b/components/script/dom/document/documentorshadowroot.rs @@ -227,14 +227,11 @@ impl DocumentOrShadowRoot { /// pub(crate) fn active_element(&self, this: &Node) -> Option> { // 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.focus_handler().focused_element() { - Some(candidate) => DomRoot::upcast::(candidate), - None => DomRoot::upcast::(document.clone()), - }; + let candidate = document + .focus_handler() + .focused_area() + .dom_anchor(&document); // Step 2. Set candidate to the result of retargeting candidate against this. // diff --git a/components/script/dom/document/focus.rs b/components/script/dom/document/focus.rs index 392da2910b6..f36ff2132b7 100644 --- a/components/script/dom/document/focus.rs +++ b/components/script/dom/document/focus.rs @@ -2,7 +2,7 @@ * 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 std::cell::Cell; +use std::cell::{Cell, Ref}; use bitflags::bitflags; use embedder_traits::FocusSequenceNumber; @@ -12,11 +12,11 @@ use script_bindings::root::{Dom, DomRoot}; use script_bindings::script_runtime::CanGc; use servo_constellation_traits::ScriptToConstellationMessage; -use crate::dom::bindings::root::MutNullableDom; +use crate::dom::bindings::cell::DomRefCell; use crate::dom::execcommand::contenteditable::ContentEditableRange; use crate::dom::focusevent::FocusEventType; use crate::dom::types::{Element, EventTarget, FocusEvent, HTMLElement, HTMLIFrameElement, Window}; -use crate::dom::{Event, EventBubbles, EventCancelable, Node, NodeTraits}; +use crate::dom::{Document, Event, EventBubbles, EventCancelable, Node, NodeTraits}; pub(crate) enum FocusOperation { Focus(FocusableArea), @@ -25,7 +25,7 @@ pub(crate) enum FocusOperation { /// The kind of focusable area a [`FocusableArea`] is. A [`FocusableArea`] may be click focusable, /// sequentially focusable, or both. -#[derive(Clone, Copy, Debug, Default, MallocSizeOf)] +#[derive(Clone, Copy, Debug, Default, JSTraceable, MallocSizeOf, PartialEq)] pub(crate) struct FocusableAreaKind(u8); bitflags! { @@ -45,11 +45,13 @@ bitflags! { } } +#[derive(Clone, Debug, Default, JSTraceable, MallocSizeOf, PartialEq)] pub(crate) enum FocusableArea { Node { node: DomRoot, kind: FocusableAreaKind, }, + #[default] Viewport, } @@ -60,6 +62,27 @@ impl FocusableArea { FocusableArea::Viewport => FocusableAreaKind::Click | FocusableAreaKind::Sequential, } } + + /// If this focusable area is a node, return it as an [`Element`] if it is possible, otherwise + /// return `None`. This is the [`Element`] to use for applying `:focus` state and for firing + /// `blur` and `focus` events if any. + /// + /// Note: This is currently in a transitional state while the code moves more toward the + /// specification. + pub(crate) fn element(&self) -> Option<&Element> { + match self { + FocusableArea::Node { node, .. } => node.downcast(), + FocusableArea::Viewport => None, + } + } + + /// + pub(crate) fn dom_anchor(&self, document: &Document) -> DomRoot { + match self { + FocusableArea::Node { node, .. } => node.clone(), + FocusableArea::Viewport => DomRoot::from_ref(document.upcast()), + } + } } /// Specifies the initiator of a focus operation. @@ -81,8 +104,10 @@ pub(crate) enum FocusInitiator { pub(crate) struct DocumentFocusHandler { /// The [`Window`] element for this [`DocumentFocusHandler`]. window: Dom, - /// The element that currently has focus in the `Document`. - focused_element: MutNullableDom, + /// The focused area of the [`Document`]. + /// + /// + focused_area: DomRefCell, /// The last sequence number sent to the constellation. #[no_trace] focus_sequence: Cell, @@ -96,7 +121,7 @@ impl DocumentFocusHandler { pub(crate) fn new(window: &Window, has_focus: bool) -> Self { Self { window: Dom::from_ref(window), - focused_element: Default::default(), + focused_area: Default::default(), focus_sequence: Cell::new(FocusSequenceNumber::default()), has_focus: Cell::new(has_focus), } @@ -107,17 +132,17 @@ impl DocumentFocusHandler { } /// Return the element that currently has focus. If `None` is returned the viewport itself has focus. - pub(crate) fn focused_element(&self) -> Option> { - self.focused_element.get() + pub(crate) fn focused_area<'a>(&'a self) -> Ref<'a, FocusableArea> { + let focused_area = self.focused_area.borrow(); + Ref::map(focused_area, |focused_area| focused_area) } /// Set the element that currently has focus and update the focus state for both the previously /// set element (if any) and the new one, as well as the new one. This will not do anything if /// the new element is the same as the previous one. Note that this *will not* fire any focus /// events. If that is necessary the [`DocumentFocusHandler::focus`] should be used. - pub(crate) fn set_focused_element(&self, new_element: Option<&Element>) { - let previously_focused_element = self.focused_element.get(); - if new_element == previously_focused_element.as_deref() { + pub(crate) fn set_focused_element(&self, new_focusable_area: FocusableArea) { + if new_focusable_area == *self.focused_area.borrow() { return; } @@ -141,14 +166,14 @@ impl DocumentFocusHandler { recursively_set_focus_status(&shadow_root.Host(), new_state); } - if let Some(previously_focused_element) = previously_focused_element { - recursively_set_focus_status(&previously_focused_element, false); + if let Some(previously_focused_element) = self.focused_area.borrow().element() { + recursively_set_focus_status(previously_focused_element, false); } - if let Some(newly_focused_element) = new_element { + if let Some(newly_focused_element) = new_focusable_area.element() { recursively_set_focus_status(newly_focused_element, true); } - self.focused_element.set(new_element); + *self.focused_area.borrow_mut() = new_focusable_area; } /// Get the last sequence number sent to the constellation. @@ -179,60 +204,18 @@ impl DocumentFocusHandler { focus_initiator: FocusInitiator, can_gc: CanGc, ) { - let (mut new_focused, new_focus_state) = match focus_operation { - FocusOperation::Focus(focusable_area) => ( - match focusable_area { - FocusableArea::Node { node, .. } => DomRoot::downcast::(node), - FocusableArea::Viewport => None, - }, - true, - ), - FocusOperation::Unfocus => ( - self.focused_element.get().as_deref().map(DomRoot::from_ref), - false, - ), + let (new_focused, new_focus_state) = match focus_operation { + FocusOperation::Focus(focusable_area) => (focusable_area, true), + FocusOperation::Unfocus => (FocusableArea::Viewport, false), }; - if !new_focus_state { - // In many browsers, a document forgets its focused area when the - // document is removed from the top-level BC's focus chain - if new_focused.take().is_some() { - trace!( - "Forgetting the document's focused area because the \ - document's container was removed from the top-level BC's \ - focus chain" - ); - } - } - - let old_focused = self.focused_element.get(); let old_focus_state = self.has_focus.get(); - debug!( - "Committing focus transaction: {:?} → {:?}", - (&old_focused, old_focus_state), - (&new_focused, new_focus_state), - ); - // `*_focused_filtered` indicates the local element (if any) included in // the top-level BC's focus chain. - let old_focused_filtered = old_focused.as_ref().filter(|_| old_focus_state); - let new_focused_filtered = new_focused.as_ref().filter(|_| new_focus_state); - - let trace_focus_chain = |name, element, doc| { - trace!( - "{} local focus chain: {}", - name, - match (element, doc) { - (Some(e), _) => format!("[{:?}, document]", e), - (None, true) => "[document]".to_owned(), - (None, false) => "[]".to_owned(), - } - ); - }; - - trace_focus_chain("Old", old_focused_filtered, old_focus_state); - trace_focus_chain("New", new_focused_filtered, new_focus_state); + let old_focused_filtered = old_focus_state.then(|| self.focused_area().clone()); + let new_focused_filtered = new_focus_state.then(|| new_focused.clone()); + debug!("Committing focus transaction: {old_focused_filtered:?} → {new_focused_filtered:?}",); if old_focused_filtered != new_focused_filtered { // Although the "focusing steps" in the HTML specification say to wait until after firing @@ -240,14 +223,17 @@ impl DocumentFocusHandler { // to set it to the viewport before firing the "blur" event. // // See https://github.com/whatwg/html/issues/1569 - self.set_focused_element(None); + self.set_focused_element(FocusableArea::Viewport); - if let Some(element) = &old_focused_filtered { + if let Some(element) = old_focused_filtered + .as_ref() + .and_then(|focusable_area| focusable_area.element()) + { if element.upcast::().is_connected() { self.fire_focus_event( FocusEventType::Blur, element.upcast(), - new_focused_filtered.map(|element| element.upcast()), + new_focused_filtered.as_ref(), can_gc, ); } @@ -258,7 +244,7 @@ impl DocumentFocusHandler { self.fire_focus_event(FocusEventType::Blur, self.window.upcast(), None, can_gc); } - self.set_focused_element(new_focused.as_deref()); + self.set_focused_element(new_focused.clone()); self.has_focus.set(new_focus_state); if old_focus_state != new_focus_state && new_focus_state { @@ -266,7 +252,10 @@ impl DocumentFocusHandler { } if old_focused_filtered != new_focused_filtered { - if let Some(element) = &new_focused_filtered { + if let Some(element) = new_focused_filtered + .as_ref() + .and_then(|focusable_area| focusable_area.element()) + { if let Some(html_element) = element.downcast::() { html_element.handle_focus_state_for_contenteditable(can_gc); } @@ -274,7 +263,7 @@ impl DocumentFocusHandler { self.fire_focus_event( FocusEventType::Focus, element.upcast(), - old_focused_filtered.map(|element| element.upcast()), + old_focused_filtered.as_ref(), can_gc, ); } @@ -309,8 +298,8 @@ impl DocumentFocusHandler { // > `new focus target` to the nested browsing context's // > active document. let child_browsing_context_id = new_focused - .as_ref() - .and_then(|elem| elem.downcast::()) + .element() + .and_then(|element| element.downcast::()) .and_then(|iframe| iframe.browsing_context_id()); let sequence = self.increment_fetch_focus_sequence(); @@ -350,13 +339,15 @@ impl DocumentFocusHandler { &self, focus_event_type: FocusEventType, event_target: &EventTarget, - related_target: Option<&EventTarget>, + related_target: Option<&FocusableArea>, can_gc: CanGc, ) { let (event_name, does_bubble) = match focus_event_type { FocusEventType::Focus => ("focus".into(), EventBubbles::DoesNotBubble), FocusEventType::Blur => ("blur".into(), EventBubbles::DoesNotBubble), }; + let related_target_element = + related_target.and_then(|focusable_area| focusable_area.element()); let event = FocusEvent::new( &self.window, event_name, @@ -364,7 +355,7 @@ impl DocumentFocusHandler { EventCancelable::NotCancelable, Some(&self.window), 0i32, - related_target, + related_target_element.map(|element| element.upcast()), can_gc, ); let event = event.upcast::(); @@ -380,9 +371,9 @@ impl DocumentFocusHandler { /// TODO: Handle the "focus changed during ongoing navigation" flag. pub(crate) fn perform_focus_fixup_rule(&self, can_gc: CanGc) { if self - .focused_element - .get() - .as_deref() + .focused_area + .borrow() + .element() .is_none_or(|focused| focused.is_focusable_area()) { return; diff --git a/components/script/dom/html/htmlelement.rs b/components/script/dom/html/htmlelement.rs index a584df354c8..5aaa351b1b2 100644 --- a/components/script/dom/html/htmlelement.rs +++ b/components/script/dom/html/htmlelement.rs @@ -1342,10 +1342,13 @@ impl VirtualMethods for HTMLElement { let element = self.as_element(); if document .focus_handler() - .focused_element() - .is_some_and(|focused_element| &*focused_element == element) + .focused_area() + .element() + .is_some_and(|focused_element| focused_element == element) { - document.focus_handler().set_focused_element(None); + document + .focus_handler() + .set_focused_element(FocusableArea::Viewport); } // 3. If removedNode is an element whose namespace is the HTML namespace, and this standard diff --git a/components/script/dom/node/focus.rs b/components/script/dom/node/focus.rs index c32b51c1933..258228b82ea 100644 --- a/components/script/dom/node/focus.rs +++ b/components/script/dom/node/focus.rs @@ -90,27 +90,28 @@ impl Node { // TODO: Implement this. // > ↪ If focus target is a shadow host whose shadow root's delegates focus is true - // > Step 1. Let focusedElement be the currently focused area of a top-level - // > traversable's DOM anchor. if self .downcast::() .and_then(Element::shadow_root) .is_some_and(|shadow_root| shadow_root.DelegatesFocus()) { - if let Some(focused_element) = self.owner_document().focus_handler().focused_element() { - // > Step 2. If focus target is a shadow-including inclusive ancestor of - // > focusedElement, then return focusedElement. - if self - .upcast::() - .is_shadow_including_inclusive_ancestor_of(focused_element.upcast()) - { - let kind = focused_element.focusable_area_kind(); - return Some(FocusableArea::Node { - node: DomRoot::upcast(focused_element), - kind, - }); - } + // > Step 1. Let focusedElement be the currently focused area of a top-level + // > traversable's DOM anchor. + // + // Note: This is a bit of a misnomer, because it might be a Node and not an Element. + let document = self.owner_document(); + let focused_area = document.focus_handler().focused_area(); + let focused_element = focused_area.dom_anchor(&document); + + // > Step 2. If focus target is a shadow-including inclusive ancestor of + // > focusedElement, then return focusedElement. + if self + .upcast::() + .is_shadow_including_inclusive_ancestor_of(&focused_element) + { + return Some(focused_area.clone()); } + // > Step 3. Return the focus delegate for focus target given focus trigger. return self.focus_delegate(); }