mirror of
https://github.com/servo/servo.git
synced 2025-08-07 06:25:32 +01:00
Fix focus propagation and use shadow root for query
Signed-off-by: stevennovaryo <steven.novaryo@gmail.com>
This commit is contained in:
parent
335a1d7ad2
commit
d78f1d9710
7 changed files with 64 additions and 54 deletions
18
Cargo.toml
18
Cargo.toml
|
@ -118,7 +118,7 @@ rustls-pemfile = "2.0"
|
||||||
rustls-pki-types = "1.12"
|
rustls-pki-types = "1.12"
|
||||||
script_layout_interface = { path = "components/shared/script_layout" }
|
script_layout_interface = { path = "components/shared/script_layout" }
|
||||||
script_traits = { path = "components/shared/script" }
|
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 = "1.0.219"
|
||||||
serde_bytes = "0.11"
|
serde_bytes = "0.11"
|
||||||
serde_json = "1.0"
|
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-dummy = { git = "https://github.com/servo/media" }
|
||||||
servo-media-gstreamer = { git = "https://github.com/servo/media" }
|
servo-media-gstreamer = { git = "https://github.com/servo/media" }
|
||||||
servo-tracing = { path = "components/servo_tracing" }
|
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"
|
smallbitvec = "2.6.0"
|
||||||
smallvec = "1.15"
|
smallvec = "1.15"
|
||||||
snapshot = { path = "./components/shared/snapshot" }
|
snapshot = { path = "./components/shared/snapshot" }
|
||||||
|
@ -135,12 +135,12 @@ string_cache = "0.8"
|
||||||
string_cache_codegen = "0.5"
|
string_cache_codegen = "0.5"
|
||||||
strum = "0.26"
|
strum = "0.26"
|
||||||
strum_macros = "0.26"
|
strum_macros = "0.26"
|
||||||
stylo = { 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/stevennovaryo/stylo", branch = "text-editing-root" }
|
stylo_atoms = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
|
||||||
stylo_config = { git = "https://github.com/stevennovaryo/stylo", branch = "text-editing-root" }
|
stylo_config = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
|
||||||
stylo_dom = { git = "https://github.com/stevennovaryo/stylo", branch = "text-editing-root" }
|
stylo_dom = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
|
||||||
stylo_malloc_size_of = { git = "https://github.com/stevennovaryo/stylo", branch = "text-editing-root" }
|
stylo_malloc_size_of = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
|
||||||
stylo_traits = { git = "https://github.com/stevennovaryo/stylo", branch = "text-editing-root" }
|
stylo_traits = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
|
||||||
surfman = { git = "https://github.com/servo/surfman", rev = "f7688b4585f9e0b5d4bf8ee8e4a91e82349610b1", features = ["chains"] }
|
surfman = { git = "https://github.com/servo/surfman", rev = "f7688b4585f9e0b5d4bf8ee8e4a91e82349610b1", features = ["chains"] }
|
||||||
syn = { version = "2", default-features = false, features = ["clone-impls", "derive", "parsing"] }
|
syn = { version = "2", default-features = false, features = ["clone-impls", "derive", "parsing"] }
|
||||||
synstructure = "0.13"
|
synstructure = "0.13"
|
||||||
|
@ -223,7 +223,7 @@ codegen-units = 1
|
||||||
#
|
#
|
||||||
# Or for Stylo:
|
# Or for Stylo:
|
||||||
#
|
#
|
||||||
# [patch."https://github.com/stylo/stylo"]
|
# [patch."https://github.com/servo/stylo"]
|
||||||
# selectors = { path = "../stylo/selectors" }
|
# selectors = { path = "../stylo/selectors" }
|
||||||
# servo_arc = { path = "../stylo/servo_arc" }
|
# servo_arc = { path = "../stylo/servo_arc" }
|
||||||
# stylo = { path = "../stylo/style" }
|
# stylo = { path = "../stylo/style" }
|
||||||
|
|
|
@ -62,7 +62,7 @@ impl<'dom> NodeAndStyleInfo<'dom> {
|
||||||
// Whether this is a container for the editable text within a single-line text input.
|
// 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 {
|
pub(crate) fn is_single_line_text_input(&self) -> bool {
|
||||||
self.node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLInputElement) ||
|
self.node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLInputElement) ||
|
||||||
self.node.is_text_editing_root()
|
self.node.is_text_editing_root()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pseudo(
|
pub(crate) fn pseudo(
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
use std::any::Any;
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::char::{ToLowercase, ToUppercase};
|
use std::char::{ToLowercase, ToUppercase};
|
||||||
|
|
||||||
|
@ -303,6 +302,7 @@ impl InlineFormattingContextBuilder {
|
||||||
if new_text.is_empty() {
|
if new_text.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selection_range = info.get_selection_range();
|
let selection_range = info.get_selection_range();
|
||||||
if let Some(last_character) = new_text.chars().next_back() {
|
if let Some(last_character) = new_text.chars().next_back() {
|
||||||
self.on_word_boundary = last_character.is_whitespace();
|
self.on_word_boundary = last_character.is_whitespace();
|
||||||
|
|
|
@ -1556,11 +1556,14 @@ impl Document {
|
||||||
return;
|
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();
|
self.begin_focus_transaction();
|
||||||
// Try to focus `el`. If it's not focusable, focus the document
|
// Try to focus `el`. If it's not focusable, focus the document
|
||||||
// instead.
|
// instead.
|
||||||
self.request_focus(None, FocusInitiator::Local, can_gc);
|
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::<Event>(MouseEvent::for_platform_mouse_event(
|
let dom_event = DomRoot::upcast::<Event>(MouseEvent::for_platform_mouse_event(
|
||||||
|
|
|
@ -1632,6 +1632,23 @@ impl Element {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn find_focusable_shadow_host_if_necessary(&self) -> Option<DomRoot<Element>> {
|
||||||
|
if self.is_focusable_area() {
|
||||||
|
Some(DomRoot::from_ref(self))
|
||||||
|
} else if self.upcast::<Node>().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 {
|
pub(crate) fn is_actually_disabled(&self) -> bool {
|
||||||
let node = self.upcast::<Node>();
|
let node = self.upcast::<Node>();
|
||||||
match node.type_id() {
|
match node.type_id() {
|
||||||
|
|
|
@ -103,7 +103,20 @@ const DEFAULT_FILE_INPUT_VALUE: &str = "No file chosen";
|
||||||
|
|
||||||
#[derive(Clone, JSTraceable, MallocSizeOf)]
|
#[derive(Clone, JSTraceable, MallocSizeOf)]
|
||||||
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
#[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 `<input type=text>`. The following is the structure of the shadow tree.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// <input type="text">
|
||||||
|
/// <div id="inner-container">
|
||||||
|
/// <div id="input-editing-root"></div>
|
||||||
|
/// <div id="input-placeholder"></div>
|
||||||
|
/// </div>
|
||||||
|
/// </input>
|
||||||
|
/// ```
|
||||||
|
// TODO(stevennovaryo): We have an additional `<div>` 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 {
|
struct InputTypeTextShadowTree {
|
||||||
text_container: Dom<HTMLDivElement>,
|
text_container: Dom<HTMLDivElement>,
|
||||||
placeholder_container: Dom<HTMLDivElement>,
|
placeholder_container: Dom<HTMLDivElement>,
|
||||||
|
@ -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: 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 = "
|
const TEXT_TREE_STYLE: &str = "
|
||||||
#input-editing-root::selection, #input-placeholder::selection {
|
#input-editing-root::selection, #input-placeholder::selection {
|
||||||
background: rgba(176, 214, 255, 1.0);
|
background: rgba(176, 214, 255, 1.0);
|
||||||
|
@ -131,19 +141,18 @@ const TEXT_TREE_STYLE: &str = "
|
||||||
|
|
||||||
#input-container {
|
#input-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#input-editing-root, #input-placeholder {
|
#input-editing-root, #input-placeholder {
|
||||||
overflow-wrap: normal;
|
overflow-wrap: normal;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#input-placeholder {
|
#input-placeholder {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: grey;
|
color: grey;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
";
|
";
|
||||||
|
|
||||||
|
@ -1145,11 +1154,7 @@ impl HTMLInputElement {
|
||||||
text_container
|
text_container
|
||||||
.upcast::<Element>()
|
.upcast::<Element>()
|
||||||
.SetId(DOMString::from("input-editing-root"), can_gc);
|
.SetId(DOMString::from("input-editing-root"), can_gc);
|
||||||
// We should probably use pseudo element to check this.
|
text_container.upcast::<Node>().set_text_editing_root();
|
||||||
// Chrome is using (private?) element attrs,
|
|
||||||
text_container
|
|
||||||
.upcast::<Node>()
|
|
||||||
.set_text_editing_root();
|
|
||||||
inner_container
|
inner_container
|
||||||
.upcast::<Node>()
|
.upcast::<Node>()
|
||||||
.AppendChild(text_container.upcast::<Node>(), can_gc)
|
.AppendChild(text_container.upcast::<Node>(), can_gc)
|
||||||
|
@ -1191,7 +1196,6 @@ impl HTMLInputElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
let shadow_tree = self.shadow_tree.borrow();
|
let shadow_tree = self.shadow_tree.borrow();
|
||||||
// MYNOTES: will check again for shadow tree getter.
|
|
||||||
Ref::filter_map(shadow_tree, |shadow_tree| {
|
Ref::filter_map(shadow_tree, |shadow_tree| {
|
||||||
let shadow_tree = shadow_tree.as_ref()?;
|
let shadow_tree = shadow_tree.as_ref()?;
|
||||||
match shadow_tree {
|
match shadow_tree {
|
||||||
|
@ -1258,7 +1262,6 @@ impl HTMLInputElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
let shadow_tree = self.shadow_tree.borrow();
|
let shadow_tree = self.shadow_tree.borrow();
|
||||||
// MYNOTES: will check again for shadow tree getter.
|
|
||||||
Ref::filter_map(shadow_tree, |shadow_tree| {
|
Ref::filter_map(shadow_tree, |shadow_tree| {
|
||||||
let shadow_tree = shadow_tree.as_ref()?;
|
let shadow_tree = shadow_tree.as_ref()?;
|
||||||
match shadow_tree {
|
match shadow_tree {
|
||||||
|
@ -1275,11 +1278,18 @@ impl HTMLInputElement {
|
||||||
let text_shadow_tree = self.text_shadow_tree(can_gc);
|
let text_shadow_tree = self.text_shadow_tree(can_gc);
|
||||||
let value = self.Value();
|
let value = self.Value();
|
||||||
|
|
||||||
let placeholder_text = match (value.is_empty(), self.placeholder.borrow().is_empty()) {
|
let placeholder_text = match value.is_empty() {
|
||||||
(true, false) => self.placeholder.to_owned().take(),
|
true => self.placeholder.to_owned().take(),
|
||||||
_ => DOMString::new(),
|
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:
|
||||||
|
// <https://drafts.csswg.org/css-ui/#element-with-default-preferred-size>.
|
||||||
|
//
|
||||||
|
// 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() {
|
let value_text = match value.is_empty() {
|
||||||
false => value,
|
false => value,
|
||||||
true => "\u{200B}".into(),
|
true => "\u{200B}".into(),
|
||||||
|
@ -1416,7 +1426,6 @@ impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElem
|
||||||
self.unsafe_get().size.get()
|
self.unsafe_get().size.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MYNOTES is implemented for text
|
|
||||||
fn selection_for_layout(self) -> Option<Range<usize>> {
|
fn selection_for_layout(self) -> Option<Range<usize>> {
|
||||||
if !self.upcast::<Element>().focus_state() {
|
if !self.upcast::<Element>().focus_state() {
|
||||||
return None;
|
return None;
|
||||||
|
|
|
@ -1627,9 +1627,6 @@ pub(crate) trait LayoutNodeHelpers<'dom> {
|
||||||
/// Whether this element is a `<input>` rendered as text or a `<textarea>`.
|
/// Whether this element is a `<input>` rendered as text or a `<textarea>`.
|
||||||
fn is_text_input(&self) -> bool;
|
fn is_text_input(&self) -> bool;
|
||||||
|
|
||||||
/// Whether this element is a text input with Shadow DOM.
|
|
||||||
fn is_text_input_with_shadow_dom(&self) -> bool;
|
|
||||||
|
|
||||||
/// Whether this element serve as a container of editable text for a text input.
|
/// Whether this element serve as a container of editable text for a text input.
|
||||||
fn is_text_editing_root(&self) -> bool;
|
fn is_text_editing_root(&self) -> bool;
|
||||||
fn text_content(self) -> Cow<'dom, str>;
|
fn text_content(self) -> Cow<'dom, str>;
|
||||||
|
@ -1815,21 +1812,6 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_text_input_with_shadow_dom(&self) -> bool {
|
|
||||||
let type_id = self.type_id_for_layout();
|
|
||||||
if type_id ==
|
|
||||||
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
|
||||||
HTMLElementTypeId::HTMLInputElement,
|
|
||||||
))
|
|
||||||
{
|
|
||||||
let input = self.unsafe_get().downcast::<HTMLInputElement>().unwrap();
|
|
||||||
|
|
||||||
matches!(input.input_type(), InputType::Color | InputType::Text)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_text_editing_root(&self) -> bool {
|
fn is_text_editing_root(&self) -> bool {
|
||||||
self.unsafe_get().is_text_editing_root()
|
self.unsafe_get().is_text_editing_root()
|
||||||
}
|
}
|
||||||
|
@ -1854,19 +1836,18 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
|
||||||
// This container is a text editing root of a <input> or <textarea> element.
|
// This container is a text editing root of a <input> or <textarea> element.
|
||||||
// So we should find those corresponding element, and get its selection.
|
// So we should find those corresponding element, and get its selection.
|
||||||
if self.is_text_editing_root() {
|
if self.is_text_editing_root() {
|
||||||
let mut maybe_parent_node = self.composed_parent_node_ref();
|
let shadow_root = self.containing_shadow_root_for_layout();
|
||||||
while let Some(parent_node) = maybe_parent_node {
|
if let Some(containing_shadow_host) = shadow_root.map(|root| root.get_host_for_layout())
|
||||||
if let Some(area) = parent_node.downcast::<HTMLTextAreaElement>() {
|
{
|
||||||
|
if let Some(area) = containing_shadow_host.downcast::<HTMLTextAreaElement>() {
|
||||||
return area.selection_for_layout();
|
return area.selection_for_layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(input) = parent_node.downcast::<HTMLInputElement>() {
|
if let Some(input) = containing_shadow_host.downcast::<HTMLInputElement>() {
|
||||||
return input.selection_for_layout();
|
return input.selection_for_layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
maybe_parent_node = parent_node.composed_parent_node_ref();
|
|
||||||
}
|
}
|
||||||
panic!("Text input element not found!");
|
panic!("Text input element not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(area) = self.downcast::<HTMLTextAreaElement>() {
|
if let Some(area) = self.downcast::<HTMLTextAreaElement>() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue