mirror of
https://github.com/servo/servo.git
synced 2025-08-07 14:35:33 +01:00
Implement Input type=text
UA Shadow DOM (#37065)
Implement Shadow Tree construction for input `type=text`, adding a text control inner editor container and placeholder container. Subsequently, due to the changes of the DOM tree structure, the changes will add a new NodeFlag `IS_TEXT_CONTROL_INNER_EDITOR` to handle the following cases. - If a mouse click button event hits a text control inner editor, it will redirect the focus target to its shadow host. - In text run's construction, the text control inner editor container queries the selection from its shadow host. This is later used to resolve caret and selection painting in the display list. This will be the first step of fixing input `type=text` and other single-line text input element widgets. Such as, implementing `::placeholder` selector. Testing: Existing WPT test and new Servo specific appearance WPT. Fixes: #36307 --------- Signed-off-by: stevennovaryo <steven.novaryo@gmail.com>
This commit is contained in:
parent
578c52fe2b
commit
5580704438
24 changed files with 635 additions and 36 deletions
|
@ -101,6 +101,29 @@ 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>`.
|
||||
|
@ -111,10 +134,49 @@ 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
|
||||
}
|
||||
|
@ -1071,7 +1133,7 @@ impl HTMLInputElement {
|
|||
ShadowRootMode::Closed,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
SlotAssignmentMode::Manual,
|
||||
can_gc,
|
||||
)
|
||||
|
@ -1079,6 +1141,92 @@ 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);
|
||||
|
@ -1136,27 +1284,53 @@ impl HTMLInputElement {
|
|||
let shadow_tree = self.shadow_tree.borrow();
|
||||
Ref::filter_map(shadow_tree, |shadow_tree| {
|
||||
let shadow_tree = shadow_tree.as_ref()?;
|
||||
let ShadowTree::Color(color_tree) = shadow_tree;
|
||||
Some(color_tree)
|
||||
match shadow_tree {
|
||||
ShadowTree::Color(color_tree) => Some(color_tree),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.expect("UA shadow tree was not created")
|
||||
}
|
||||
|
||||
fn update_shadow_tree_if_needed(&self, can_gc: CanGc) {
|
||||
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);
|
||||
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);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1465,22 +1639,29 @@ 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();
|
||||
|
||||
// Step 5.
|
||||
if *textinput.single_line_content() != value {
|
||||
// Steps 1-2
|
||||
textinput.set_content(value);
|
||||
let mut textinput = self.textinput.borrow_mut();
|
||||
|
||||
// Step 5.
|
||||
textinput.clear_selection_to_limit(Direction::Forward);
|
||||
if *textinput.single_line_content() != value {
|
||||
// Steps 1-2
|
||||
textinput.set_content(value);
|
||||
|
||||
// Step 5.
|
||||
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>()
|
||||
|
@ -2063,6 +2244,19 @@ 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) {
|
||||
|
@ -2688,8 +2882,11 @@ 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);
|
||||
|
@ -2738,6 +2935,7 @@ 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") => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue