From d78f1d97102aab9c1471f5570b372a11895eaf5f Mon Sep 17 00:00:00 2001 From: stevennovaryo Date: Wed, 21 May 2025 15:07:00 +0800 Subject: [PATCH] Fix focus propagation and use shadow root for query Signed-off-by: stevennovaryo --- Cargo.toml | 18 ++++----- components/layout/dom_traversal.rs | 2 +- components/layout/flow/inline/construct.rs | 2 +- components/script/dom/document.rs | 5 ++- components/script/dom/element.rs | 17 +++++++++ components/script/dom/htmlinputelement.rs | 43 +++++++++++++--------- components/script/dom/node.rs | 31 +++------------- 7 files changed, 64 insertions(+), 54 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c18bdb3dadd..1373440d67e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,7 +118,7 @@ rustls-pemfile = "2.0" rustls-pki-types = "1.12" script_layout_interface = { path = "components/shared/script_layout" } script_traits = { path = "components/shared/script" } -selectors = { git = "https://github.com/stevennovaryo/stylo", branch = "text-editing-root" } +selectors = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } serde = "1.0.219" serde_bytes = "0.11" serde_json = "1.0" @@ -126,7 +126,7 @@ servo-media = { git = "https://github.com/servo/media" } servo-media-dummy = { git = "https://github.com/servo/media" } servo-media-gstreamer = { git = "https://github.com/servo/media" } servo-tracing = { path = "components/servo_tracing" } -servo_arc = { git = "https://github.com/stevennovaryo/stylo", branch = "text-editing-root" } +servo_arc = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } smallbitvec = "2.6.0" smallvec = "1.15" snapshot = { path = "./components/shared/snapshot" } @@ -135,12 +135,12 @@ string_cache = "0.8" string_cache_codegen = "0.5" strum = "0.26" strum_macros = "0.26" -stylo = { git = "https://github.com/stevennovaryo/stylo", branch = "text-editing-root" } -stylo_atoms = { git = "https://github.com/stevennovaryo/stylo", branch = "text-editing-root" } -stylo_config = { git = "https://github.com/stevennovaryo/stylo", branch = "text-editing-root" } -stylo_dom = { git = "https://github.com/stevennovaryo/stylo", branch = "text-editing-root" } -stylo_malloc_size_of = { git = "https://github.com/stevennovaryo/stylo", branch = "text-editing-root" } -stylo_traits = { git = "https://github.com/stevennovaryo/stylo", branch = "text-editing-root" } +stylo = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } +stylo_atoms = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } +stylo_config = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } +stylo_dom = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } +stylo_malloc_size_of = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } +stylo_traits = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } surfman = { git = "https://github.com/servo/surfman", rev = "f7688b4585f9e0b5d4bf8ee8e4a91e82349610b1", features = ["chains"] } syn = { version = "2", default-features = false, features = ["clone-impls", "derive", "parsing"] } synstructure = "0.13" @@ -223,7 +223,7 @@ codegen-units = 1 # # Or for Stylo: # -# [patch."https://github.com/stylo/stylo"] +# [patch."https://github.com/servo/stylo"] # selectors = { path = "../stylo/selectors" } # servo_arc = { path = "../stylo/servo_arc" } # stylo = { path = "../stylo/style" } diff --git a/components/layout/dom_traversal.rs b/components/layout/dom_traversal.rs index f3b1b756fcc..2638c85a39b 100644 --- a/components/layout/dom_traversal.rs +++ b/components/layout/dom_traversal.rs @@ -62,7 +62,7 @@ impl<'dom> NodeAndStyleInfo<'dom> { // Whether this is a container for the editable text within a single-line text input. pub(crate) fn is_single_line_text_input(&self) -> bool { self.node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLInputElement) || - self.node.is_text_editing_root() + self.node.is_text_editing_root() } pub(crate) fn pseudo( diff --git a/components/layout/flow/inline/construct.rs b/components/layout/flow/inline/construct.rs index 1869b58cb54..07a2e914835 100644 --- a/components/layout/flow/inline/construct.rs +++ b/components/layout/flow/inline/construct.rs @@ -2,7 +2,6 @@ * 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::any::Any; use std::borrow::Cow; use std::char::{ToLowercase, ToUppercase}; @@ -303,6 +302,7 @@ impl InlineFormattingContextBuilder { if new_text.is_empty() { return; } + let selection_range = info.get_selection_range(); if let Some(last_character) = new_text.chars().next_back() { self.on_word_boundary = last_character.is_whitespace(); diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 6056f1f1e5a..2756152f190 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -1556,11 +1556,14 @@ impl Document { return; } + // For a node within a text input UA shadow DOM, redirect the focus target into its shadow host. + let target_el = el.find_focusable_shadow_host_if_necessary(); + self.begin_focus_transaction(); // Try to focus `el`. If it's not focusable, focus the document // instead. self.request_focus(None, FocusInitiator::Local, can_gc); - self.request_focus(Some(&*el), FocusInitiator::Local, can_gc); + self.request_focus(target_el.as_deref(), FocusInitiator::Local, can_gc); } let dom_event = DomRoot::upcast::(MouseEvent::for_platform_mouse_event( diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index dbf0f14ab68..bcc834c8c3c 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -1632,6 +1632,23 @@ impl Element { ) } + pub(crate) fn find_focusable_shadow_host_if_necessary(&self) -> Option> { + if self.is_focusable_area() { + Some(DomRoot::from_ref(self)) + } else if self.upcast::().is_text_editing_root() { + let containing_shadow_host = self.containing_shadow_root().map(|root| root.Host()); + if containing_shadow_host + .as_ref() + .is_some_and(|e| e.is_focusable_area()) + { + return containing_shadow_host; + } + panic!("Containing shadow host is not focusable"); + } else { + None + } + } + pub(crate) fn is_actually_disabled(&self) -> bool { let node = self.upcast::(); match node.type_id() { diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index 2989e6c1a07..15fc004a301 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -103,7 +103,20 @@ const DEFAULT_FILE_INPUT_VALUE: &str = "No file chosen"; #[derive(Clone, JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] -/// MYNOTES: document this and check name later. +/// Contains reference to text editing root and placeholder container element in the UA +/// shadow tree for ``. The following is the structure of the shadow tree. +/// +/// ``` +/// +///
+///
+///
+///
+/// +/// ``` +// TODO(stevennovaryo): We have an additional `
` element that contains both placeholder and editing root +// because we are using `position: absolute` to put the editing root and placeholder +// on top of each other. But we should probably provide a specifing layout algorithm instead. struct InputTypeTextShadowTree { text_container: Dom, placeholder_container: Dom, @@ -120,9 +133,6 @@ struct InputTypeColorShadowTree { } // FIXME: These styles should be inside UA stylesheet, but it is not possible without internal pseudo element support. -// FIXME: We are setting `pointer-events: none;` because focus is not propagated to its ancestor. -// FIXME: We are using `position: absolute` to put place the editing root and placeholder -// on top of each other, but this will create a unnecessary element in between. const TEXT_TREE_STYLE: &str = " #input-editing-root::selection, #input-placeholder::selection { background: rgba(176, 214, 255, 1.0); @@ -131,19 +141,18 @@ const TEXT_TREE_STYLE: &str = " #input-container { position: relative; - pointer-events: none; } #input-editing-root, #input-placeholder { overflow-wrap: normal; white-space: pre; - pointer-events: none; } #input-placeholder { position: absolute; color: grey; overflow: hidden; + pointer-events: none; } "; @@ -1145,11 +1154,7 @@ impl HTMLInputElement { text_container .upcast::() .SetId(DOMString::from("input-editing-root"), can_gc); - // We should probably use pseudo element to check this. - // Chrome is using (private?) element attrs, - text_container - .upcast::() - .set_text_editing_root(); + text_container.upcast::().set_text_editing_root(); inner_container .upcast::() .AppendChild(text_container.upcast::(), can_gc) @@ -1191,7 +1196,6 @@ impl HTMLInputElement { } let shadow_tree = self.shadow_tree.borrow(); - // MYNOTES: will check again for shadow tree getter. Ref::filter_map(shadow_tree, |shadow_tree| { let shadow_tree = shadow_tree.as_ref()?; match shadow_tree { @@ -1258,7 +1262,6 @@ impl HTMLInputElement { } let shadow_tree = self.shadow_tree.borrow(); - // MYNOTES: will check again for shadow tree getter. Ref::filter_map(shadow_tree, |shadow_tree| { let shadow_tree = shadow_tree.as_ref()?; match shadow_tree { @@ -1275,11 +1278,18 @@ impl HTMLInputElement { let text_shadow_tree = self.text_shadow_tree(can_gc); let value = self.Value(); - let placeholder_text = match (value.is_empty(), self.placeholder.borrow().is_empty()) { - (true, false) => self.placeholder.to_owned().take(), - _ => DOMString::new(), + let placeholder_text = match value.is_empty() { + true => self.placeholder.to_owned().take(), + false => DOMString::new(), }; + // The addition of zero-width space here forces the text input to have an inline formatting + // context that might otherwise be trimmed if there's no text. This is important to ensure + // that the input element is at least as tall as the line gap of the caret: + // . + // + // This is also used to ensure that the caret will still be rendered when the input is empty. + // TODO: Is there a less hacky way to do this? let value_text = match value.is_empty() { false => value, true => "\u{200B}".into(), @@ -1416,7 +1426,6 @@ impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElem self.unsafe_get().size.get() } - // MYNOTES is implemented for text fn selection_for_layout(self) -> Option> { if !self.upcast::().focus_state() { return None; diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index aa26aa452a8..06bc477b548 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -1627,9 +1627,6 @@ pub(crate) trait LayoutNodeHelpers<'dom> { /// Whether this element is a `` rendered as text or a `