Revert "Implement Input UA Shadow DOM (#37065)" (#37296)

This reverts commit 5580704438.

Let's re-land that fix when a working solution is found. Keeping that
regression makes it hard to evaluate other potential improvements.

Signed-off-by: webbeef <me@webbeef.org>
This commit is contained in:
webbeef 2025-06-06 08:23:08 -07:00 committed by GitHub
parent aff2a85372
commit a1f43ab06d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 36 additions and 635 deletions

View file

@ -1556,14 +1556,11 @@ impl Document {
return;
}
// For a node within a text input UA shadow DOM, delegate the focus target into its shadow host.
// TODO: This focus delegation should be done with shadow DOM delegateFocus attribute.
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.
// Try to focus `el`. If it's not focusable, focus the document
// instead.
self.request_focus(None, FocusInitiator::Local, can_gc);
self.request_focus(target_el.as_deref(), FocusInitiator::Local, can_gc);
self.request_focus(Some(&*el), FocusInitiator::Local, can_gc);
}
let dom_event = DomRoot::upcast::<Event>(MouseEvent::for_platform_mouse_event(

View file

@ -1703,27 +1703,6 @@ impl Element {
)
}
/// Returns the focusable shadow host if this is a text control inner editor.
/// This is a workaround for the focus delegation of shadow DOM and should be
/// used only to delegate focusable inner editor of [HTMLInputElement] and
/// [HTMLTextAreaElement].
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_control_inner_editor() {
let containing_shadow_host = self.containing_shadow_root().map(|root| root.Host());
assert!(
containing_shadow_host
.as_ref()
.is_some_and(|e| e.is_focusable_area()),
"Containing shadow host is not focusable"
);
containing_shadow_host
} else {
None
}
}
pub(crate) fn is_actually_disabled(&self) -> bool {
let node = self.upcast::<Node>();
match node.type_id() {

View file

@ -101,29 +101,6 @@ const DEFAULT_RESET_VALUE: &str = "Reset";
const PASSWORD_REPLACEMENT_CHAR: char = '●';
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 control inner editor 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">
/// #shadow-root
/// <div id="inner-container">
/// <div id="input-editor"></div>
/// <div id="input-placeholder"></div>
/// </div>
/// </input>
/// ```
// TODO(stevennovaryo): We are trying to use CSS to mimic Chrome and Firefox's layout for the <input> element.
// But, this could be slower in performance and does have some discrepancies. For example,
// they would try to vertically align <input> text baseline with the baseline of other
// TextNode within an inline flow. Another example is the horizontal scroll.
struct InputTypeTextShadowTree {
text_container: Dom<HTMLDivElement>,
placeholder_container: Dom<HTMLDivElement>,
}
#[derive(Clone, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
/// Contains references to the elements in the shadow tree for `<input type=range>`.
@ -134,49 +111,10 @@ struct InputTypeColorShadowTree {
color_value: Dom<HTMLDivElement>,
}
// FIXME: These styles should be inside UA stylesheet, but it is not possible without internal pseudo element support.
const TEXT_TREE_STYLE: &str = "
#input-editor::selection {
background: rgba(176, 214, 255, 1.0);
color: black;
}
:host:not(:placeholder-shown) #input-placeholder {
visibility: hidden !important
}
#input-editor {
overflow-wrap: normal;
pointer-events: auto;
}
#input-container {
position: relative;
height: 100%;
pointer-events: none;
display: flex;
}
#input-editor, #input-placeholder {
white-space: pre;
margin-block: auto !important;
inset-block: 0 !important;
block-size: fit-content !important;
}
#input-placeholder {
overflow: hidden !important;
position: absolute !important;
color: grey;
pointer-events: none !important;
}
";
#[derive(Clone, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
#[non_exhaustive]
enum ShadowTree {
Text(InputTypeTextShadowTree),
Color(InputTypeColorShadowTree),
// TODO: Add shadow trees for other input types (range etc) here
}
@ -1133,7 +1071,7 @@ impl HTMLInputElement {
ShadowRootMode::Closed,
false,
false,
true,
false,
SlotAssignmentMode::Manual,
can_gc,
)
@ -1141,92 +1079,6 @@ impl HTMLInputElement {
})
}
fn create_text_shadow_tree(&self, can_gc: CanGc) {
let document = self.owner_document();
let shadow_root = self.shadow_root(can_gc);
Node::replace_all(None, shadow_root.upcast::<Node>(), can_gc);
let inner_container =
HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc);
inner_container
.upcast::<Element>()
.SetId(DOMString::from("input-container"), can_gc);
shadow_root
.upcast::<Node>()
.AppendChild(inner_container.upcast::<Node>(), can_gc)
.unwrap();
let placeholder_container =
HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc);
placeholder_container
.upcast::<Element>()
.SetId(DOMString::from("input-placeholder"), can_gc);
inner_container
.upcast::<Node>()
.AppendChild(placeholder_container.upcast::<Node>(), can_gc)
.unwrap();
let text_container = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc);
text_container
.upcast::<Element>()
.SetId(DOMString::from("input-editor"), can_gc);
text_container
.upcast::<Node>()
.set_text_control_inner_editor();
inner_container
.upcast::<Node>()
.AppendChild(text_container.upcast::<Node>(), can_gc)
.unwrap();
let style = HTMLStyleElement::new(
local_name!("style"),
None,
&document,
None,
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::<Node>()
.SetTextContent(Some(DOMString::from(TEXT_TREE_STYLE)), can_gc);
shadow_root
.upcast::<Node>()
.AppendChild(style.upcast::<Node>(), can_gc)
.unwrap();
let _ = self
.shadow_tree
.borrow_mut()
.insert(ShadowTree::Text(InputTypeTextShadowTree {
text_container: text_container.as_traced(),
placeholder_container: placeholder_container.as_traced(),
}));
}
fn text_shadow_tree(&self, can_gc: CanGc) -> Ref<InputTypeTextShadowTree> {
let has_text_shadow_tree = self
.shadow_tree
.borrow()
.as_ref()
.is_some_and(|shadow_tree| matches!(shadow_tree, ShadowTree::Text(_)));
if !has_text_shadow_tree {
self.create_text_shadow_tree(can_gc);
}
let shadow_tree = self.shadow_tree.borrow();
Ref::filter_map(shadow_tree, |shadow_tree| {
let shadow_tree = shadow_tree.as_ref()?;
match shadow_tree {
ShadowTree::Text(text_tree) => Some(text_tree),
_ => None,
}
})
.ok()
.expect("UA shadow tree was not created")
}
fn create_color_shadow_tree(&self, can_gc: CanGc) {
let document = self.owner_document();
let shadow_root = self.shadow_root(can_gc);
@ -1284,53 +1136,27 @@ impl HTMLInputElement {
let shadow_tree = self.shadow_tree.borrow();
Ref::filter_map(shadow_tree, |shadow_tree| {
let shadow_tree = shadow_tree.as_ref()?;
match shadow_tree {
ShadowTree::Color(color_tree) => Some(color_tree),
_ => None,
}
let ShadowTree::Color(color_tree) = shadow_tree;
Some(color_tree)
})
.ok()
.expect("UA shadow tree was not created")
}
fn update_shadow_tree_if_needed(&self, can_gc: CanGc) {
match self.input_type() {
InputType::Text => {
let text_shadow_tree = self.text_shadow_tree(can_gc);
let value = self.Value();
// 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() {
false => value,
true => "\u{200B}".into(),
};
text_shadow_tree
.text_container
.upcast::<Node>()
.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::<Element>()
.set_string_attribute(&local_name!("style"), style.into(), 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::<Element>()
.set_string_attribute(&local_name!("style"), style.into(), can_gc);
}
}
}
@ -1639,29 +1465,22 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
fn SetValue(&self, mut value: DOMString, can_gc: CanGc) -> ErrorResult {
match self.value_mode() {
ValueMode::Value => {
{
// Step 3.
self.value_dirty.set(true);
// Step 3.
self.value_dirty.set(true);
// Step 4.
self.sanitize_value(&mut value);
// Step 4.
self.sanitize_value(&mut value);
let mut textinput = self.textinput.borrow_mut();
let mut textinput = self.textinput.borrow_mut();
// Step 5.
if *textinput.single_line_content() != value {
// Steps 1-2
textinput.set_content(value);
// Step 5.
if *textinput.single_line_content() != value {
// Steps 1-2
textinput.set_content(value);
// Step 5.
textinput.clear_selection_to_limit(Direction::Forward);
}
textinput.clear_selection_to_limit(Direction::Forward);
}
// Additionaly, update the placeholder shown state. This is
// normally being done in the attributed mutated. And, being
// done in another scope to prevent borrow checker issues.
self.update_placeholder_shown_state();
},
ValueMode::Default | ValueMode::DefaultOn => {
self.upcast::<Element>()
@ -2244,19 +2063,6 @@ impl HTMLInputElement {
el.set_placeholder_shown_state(has_placeholder && !has_value);
}
// Update the placeholder text in the text shadow tree.
// To increase the performance, we would only do this when it is necessary.
fn update_text_shadow_tree_placeholder(&self, can_gc: CanGc) {
if self.input_type() != InputType::Text {
return;
}
self.text_shadow_tree(can_gc)
.placeholder_container
.upcast::<Node>()
.SetTextContent(Some(self.placeholder.borrow().clone()), can_gc);
}
// https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file)
// Select files by invoking UI or by passed in argument
fn select_files(&self, opt_test_paths: Option<Vec<DOMString>>, can_gc: CanGc) {
@ -2882,11 +2688,8 @@ impl VirtualMethods for HTMLInputElement {
},
}
self.update_text_shadow_tree_placeholder(can_gc);
self.update_placeholder_shown_state();
},
// FIXME(stevennovaryo): This is only reachable by Default and DefaultOn value mode. While others
// are being handled in [Self::SetValue]. Should we merge this two together?
local_name!("value") if !self.value_dirty.get() => {
let value = mutation.new_value(attr).map(|value| (**value).to_owned());
let mut value = value.map_or(DOMString::new(), DOMString::from);
@ -2935,7 +2738,6 @@ impl VirtualMethods for HTMLInputElement {
.extend(attr.value().chars().filter(|&c| c != '\n' && c != '\r'));
}
}
self.update_text_shadow_tree_placeholder(can_gc);
self.update_placeholder_shown_state();
},
local_name!("readonly") => {

View file

@ -230,10 +230,6 @@ bitflags! {
/// Whether this node has a weird parser insertion mode. i.e whether setting innerHTML
/// needs extra work or not
const HAS_WEIRD_PARSER_INSERTION_MODE = 1 << 11;
/// Whether this node serves as the text container for editable content of
/// <input> or <textarea> element.
const IS_TEXT_CONTROL_INNER_EDITOR = 1 << 12;
}
}
@ -701,16 +697,6 @@ impl Node {
self.flags.get().contains(NodeFlags::IS_CONNECTED)
}
pub(crate) fn set_text_control_inner_editor(&self) {
self.set_flag(NodeFlags::IS_TEXT_CONTROL_INNER_EDITOR, true)
}
pub(crate) fn is_text_control_inner_editor(&self) -> bool {
self.flags
.get()
.contains(NodeFlags::IS_TEXT_CONTROL_INNER_EDITOR)
}
/// Returns the type ID of this node.
pub(crate) fn type_id(&self) -> NodeTypeId {
match *self.eventtarget.type_id() {
@ -1608,7 +1594,6 @@ pub(crate) trait LayoutNodeHelpers<'dom> {
fn assigned_slot_for_layout(self) -> Option<LayoutDom<'dom, HTMLSlotElement>>;
fn is_element_for_layout(&self) -> bool;
fn is_text_node_for_layout(&self) -> bool;
unsafe fn get_flag(self, flag: NodeFlags) -> bool;
unsafe fn set_flag(self, flag: NodeFlags, value: bool);
@ -1644,9 +1629,6 @@ pub(crate) trait LayoutNodeHelpers<'dom> {
/// Whether this element is a `<input>` rendered as text or a `<textarea>`.
fn is_text_input(&self) -> bool;
/// Whether this element serve as a container of editable text for a text input.
fn is_text_control_inner_editor(&self) -> bool;
fn text_content(self) -> Cow<'dom, str>;
fn selection(self) -> Option<Range<usize>>;
fn image_url(self) -> Option<ServoUrl>;
@ -1679,11 +1661,6 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
(*self).is::<Element>()
}
fn is_text_node_for_layout(&self) -> bool {
self.type_id_for_layout() ==
NodeTypeId::CharacterData(CharacterDataTypeId::Text(TextTypeId::Text))
}
#[inline]
fn composed_parent_node_ref(self) -> Option<LayoutDom<'dom, Node>> {
let parent = self.parent_node_ref();
@ -1825,8 +1802,8 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
{
let input = self.unsafe_get().downcast::<HTMLInputElement>().unwrap();
// FIXME: All the non-color and non-text input types currently render as text
!matches!(input.input_type(), InputType::Color | InputType::Text)
// FIXME: All the non-color input types currently render as text
input.input_type() != InputType::Color
} else {
type_id ==
NodeTypeId::Element(ElementTypeId::HTMLElement(
@ -1835,10 +1812,6 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
}
}
fn is_text_control_inner_editor(&self) -> bool {
self.unsafe_get().is_text_control_inner_editor()
}
fn text_content(self) -> Cow<'dom, str> {
if let Some(text) = self.downcast::<Text>() {
return text.upcast().data_for_layout().into();
@ -1856,25 +1829,6 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
}
fn selection(self) -> Option<Range<usize>> {
// This node is a text node of a text control inner editor in a <input> or <textarea> element.
// So we should find those corresponding element, and get its selection.
if self.is_text_node_for_layout() &&
self.parent_node_ref()
.is_some_and(|parent| parent.is_text_control_inner_editor())
{
let shadow_root = self.containing_shadow_root_for_layout();
if let Some(containing_shadow_host) = shadow_root.map(|root| root.get_host_for_layout())
{
if let Some(area) = containing_shadow_host.downcast::<HTMLTextAreaElement>() {
return area.selection_for_layout();
}
if let Some(input) = containing_shadow_host.downcast::<HTMLInputElement>() {
return input.selection_for_layout();
}
}
}
if let Some(area) = self.downcast::<HTMLTextAreaElement>() {
return area.selection_for_layout();
}

View file

@ -28,7 +28,7 @@ use style::selector_parser::PseudoElement;
use super::{
ServoLayoutDocument, ServoLayoutElement, ServoShadowRoot, ServoThreadSafeLayoutElement,
};
use crate::dom::bindings::inheritance::NodeTypeId;
use crate::dom::bindings::inheritance::{CharacterDataTypeId, NodeTypeId, TextTypeId};
use crate::dom::bindings::root::LayoutDom;
use crate::dom::element::{Element, LayoutElementHelpers};
use crate::dom::node::{LayoutNodeHelpers, Node, NodeFlags, NodeTypeIdWrapper};
@ -100,10 +100,6 @@ impl<'dom> ServoLayoutNode<'dom> {
pub fn is_text_input(&self) -> bool {
self.node.is_text_input()
}
pub fn is_text_control_inner_editor(&self) -> bool {
self.node.is_text_control_inner_editor()
}
}
impl style::dom::NodeInfo for ServoLayoutNode<'_> {
@ -112,7 +108,8 @@ impl style::dom::NodeInfo for ServoLayoutNode<'_> {
}
fn is_text_node(&self) -> bool {
self.node.is_text_node_for_layout()
self.script_type_id() ==
NodeTypeId::CharacterData(CharacterDataTypeId::Text(TextTypeId::Text))
}
}