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

@ -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") => {