diff --git a/components/layout/dom_traversal.rs b/components/layout/dom_traversal.rs index 2638c85a39b..2fe87cac856 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_control_inner_editor() } pub(crate) fn pseudo( @@ -221,7 +221,9 @@ fn traverse_children_of<'dom>( } else { handler.handle_text(&info, node_text_content); } - } else if parent_element.is_text_editing_root() { + } else if parent_element.is_text_control_inner_editor() { + // We are using parent's style for caret and selection color. + // FIXME: Once textarea and input element's shadow tree is supported, this would replace the if condition above. let info = NodeAndStyleInfo::new( parent_element, parent_element.style(context.shared_context()), diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index bcc834c8c3c..ddbe59e396d 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -1635,7 +1635,7 @@ 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() { + } else if self.upcast::().is_text_control_inner_editor() { let containing_shadow_host = self.containing_shadow_root().map(|root| root.Host()); if containing_shadow_host .as_ref() diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index 15fc004a301..95748a185ce 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -103,20 +103,21 @@ const DEFAULT_FILE_INPUT_VALUE: &str = "No file chosen"; #[derive(Clone, JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] -/// Contains reference to text editing root and placeholder container element in the UA +/// Contains reference to text control inner editor and placeholder container element in the UA /// shadow tree for ``. The following is the structure of the shadow tree. /// /// ``` /// -///
-///
-///
-///
+/// #shadow-root +///
+///
+///
+///
/// /// ``` -// 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. +// TODO(stevennovaryo): We are trying to use CSS to mimic Chrome and Firefox's layout for the element. +// But this does have some discrepencies. For example, they would try to vertically align +// text baseline with the baseline of other TextNode within an inline flow. struct InputTypeTextShadowTree { text_container: Dom, placeholder_container: Dom, @@ -134,25 +135,38 @@ struct InputTypeColorShadowTree { // FIXME: These styles should be inside UA stylesheet, but it is not possible without internal pseudo element support. const TEXT_TREE_STYLE: &str = " -#input-editing-root::selection, #input-placeholder::selection { +#input-editor::selection, #input-placeholder::selection { background: rgba(176, 214, 255, 1.0); color: black; } -#input-container { - position: relative; +:host:not(:placeholder-shown) #input-placeholder { + visibility: hidden !important } -#input-editing-root, #input-placeholder { +#input-container { + position: relative !important; + height: 100% !important; + pointer-events: none !important; +} + +#input-editor { + pointer-events: auto !important; +} + +#input-editor, #input-placeholder { + position: absolute !important; overflow-wrap: normal; white-space: pre; + margin-block: auto; + inset-block: 0; + block-size: fit-content; } #input-placeholder { - position: absolute; color: grey; - overflow: hidden; - pointer-events: none; + overflow: hidden !important; + pointer-events: none !important; } "; @@ -1153,8 +1167,10 @@ impl HTMLInputElement { let text_container = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc); text_container .upcast::() - .SetId(DOMString::from("input-editing-root"), can_gc); - text_container.upcast::().set_text_editing_root(); + .SetId(DOMString::from("input-editor"), can_gc); + text_container + .upcast::() + .set_text_control_inner_editor(); inner_container .upcast::() .AppendChild(text_container.upcast::(), can_gc) @@ -1168,6 +1184,8 @@ impl HTMLInputElement { ElementCreator::ScriptCreated, can_gc, ); + // TODO(stevennovaryo): Either use UA stylesheet with internal pseudo element or preemptively parse the stylesheet + // to reduce the costly operation and avoid CSP related error. style .upcast::() .SetTextContent(Some(DOMString::from(TEXT_TREE_STYLE)), can_gc); @@ -1274,49 +1292,48 @@ impl HTMLInputElement { } fn update_shadow_tree_if_needed(&self, can_gc: CanGc) { - if self.input_type() == InputType::Text { - let text_shadow_tree = self.text_shadow_tree(can_gc); - let value = self.Value(); + match self.input_type() { + InputType::Text => { + let text_shadow_tree = self.text_shadow_tree(can_gc); + let value = self.Value(); - 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(), + }; - // 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(), - }; - - text_shadow_tree - .placeholder_container - .upcast::() - .SetTextContent(Some(placeholder_text), can_gc); - text_shadow_tree - .text_container - .upcast::() - .SetTextContent(Some(value_text), can_gc); - } - if self.input_type() == InputType::Color { - let color_shadow_tree = self.color_shadow_tree(can_gc); - let mut value = self.Value(); - if value.str().is_valid_simple_color_string() { - value.make_ascii_lowercase(); - } else { - value = DOMString::from("#000000"); - } - let style = format!("background-color: {value}"); - color_shadow_tree - .color_value - .upcast::() - .set_string_attribute(&local_name!("style"), style.into(), can_gc); + // TODO(stevennovaryo): Introduce caching to prevent costly update. + text_shadow_tree + .placeholder_container + .upcast::() + .SetTextContent(Some(self.placeholder.to_owned().take()), can_gc); + text_shadow_tree + .text_container + .upcast::() + .SetTextContent(Some(value_text), can_gc); + }, + InputType::Color => { + let color_shadow_tree = self.color_shadow_tree(can_gc); + let mut value = self.Value(); + if value.str().is_valid_simple_color_string() { + value.make_ascii_lowercase(); + } else { + value = DOMString::from("#000000"); + } + let style = format!("background-color: {value}"); + color_shadow_tree + .color_value + .upcast::() + .set_string_attribute(&local_name!("style"), style.into(), can_gc); + }, + _ => {}, } } } diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 06bc477b548..5f9a955f4ab 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -233,7 +233,7 @@ bitflags! { /// Whether this node has a serve as the text container for editable content of /// or