Skip to content

Commit 6cd8578

Browse files
dom: Textual Input UA Shadow Dom (servo#37527)
Depend on: - servo#37427 - servo#37483 Utilize input `type=text` for the display of all textual input. In which, consist of https://html.spec.whatwg.org/#the-input-element-as-a-text-entry-widget and https://html.spec.whatwg.org/#the-input-element-as-domain-specific-widgets inputs. For `password`, `url`, `tel`, and, `email` input, the appearance of input container is exactly the same as the `text` input. Other types of textual input simply extends `text` input by adding extra components inside the container. Testing: Servo textual input appearance WPT. --------- Signed-off-by: stevennovaryo <[email protected]> Signed-off-by: Jo Steven Novaryo <[email protected]>
1 parent 1d89669 commit 6cd8578

36 files changed

+546
-279
lines changed

components/script/dom/element.rs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,25 +1119,16 @@ impl<'dom> LayoutElementHelpers<'dom> for LayoutDom<'dom, Element> {
11191119
));
11201120
}
11211121

1122+
// Textual input, specifically text entry and domain specific input has
1123+
// a default preferred size.
1124+
//
1125+
// <https://html.spec.whatwg.org/multipage/#the-input-element-as-a-text-entry-widget>
1126+
// <https://html.spec.whatwg.org/multipage/#the-input-element-as-domain-specific-widgets>
11221127
let size = if let Some(this) = self.downcast::<HTMLInputElement>() {
11231128
// FIXME(pcwalton): More use of atoms, please!
11241129
match self.get_attr_val_for_layout(&ns!(), &local_name!("type")) {
1125-
// Not text entry widget
1126-
Some("hidden") |
1127-
Some("date") |
1128-
Some("month") |
1129-
Some("week") |
1130-
Some("time") |
1131-
Some("datetime-local") |
1132-
Some("number") |
1133-
Some("range") |
1134-
Some("color") |
1135-
Some("checkbox") |
1136-
Some("radio") |
1137-
Some("file") |
1138-
Some("submit") |
1139-
Some("image") |
1140-
Some("reset") |
1130+
Some("hidden") | Some("range") | Some("color") | Some("checkbox") |
1131+
Some("radio") | Some("file") | Some("submit") | Some("image") | Some("reset") |
11411132
Some("button") => None,
11421133
// Others
11431134
_ => match this.size_for_layout() {

components/script/dom/htmlinputelement.rs

Lines changed: 72 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ const DEFAULT_FILE_INPUT_VALUE: &str = "No file chosen";
104104
#[derive(Clone, JSTraceable, MallocSizeOf)]
105105
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
106106
/// Contains reference to text control inner editor and placeholder container element in the UA
107-
/// shadow tree for `<input type=text>`. The following is the structure of the shadow tree.
107+
/// shadow tree for `text`, `password`, `url`, `tel`, and `email` input. The following is the
108+
/// structure of the shadow tree.
108109
///
109110
/// ```
110111
/// <input type="text">
@@ -115,6 +116,7 @@ const DEFAULT_FILE_INPUT_VALUE: &str = "No file chosen";
115116
/// </div>
116117
/// </input>
117118
/// ```
119+
///
118120
// TODO(stevennovaryo): We are trying to use CSS to mimic Chrome and Firefox's layout for the <input> element.
119121
// But, this could be slower in performance and does have some discrepancies. For example,
120122
// they would try to vertically align <input> text baseline with the baseline of other
@@ -128,7 +130,7 @@ struct InputTypeTextShadowTree {
128130

129131
#[derive(Clone, JSTraceable, MallocSizeOf)]
130132
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
131-
/// Contains references to the elements in the shadow tree for `<input type=range>`.
133+
/// Contains references to the elements in the shadow tree for `<input type=color>`.
132134
///
133135
/// The shadow tree consists of a single div with the currently selected color as
134136
/// the background.
@@ -219,10 +221,11 @@ pub(crate) enum InputType {
219221
}
220222

221223
impl InputType {
222-
// Note that Password is not included here since it is handled
223-
// slightly differently, with placeholder characters shown rather
224-
// than the underlying value.
225-
fn is_textual(&self) -> bool {
224+
/// Defines which input type that should perform like a text input,
225+
/// specifically when it is interacting with JS. Note that Password
226+
/// is not included here since it is handled slightly differently,
227+
/// with placeholder characters shown rather than the underlying value.
228+
pub(crate) fn is_textual(&self) -> bool {
226229
matches!(
227230
*self,
228231
InputType::Date |
@@ -1238,9 +1241,35 @@ impl HTMLInputElement {
12381241
.expect("UA shadow tree was not created")
12391242
}
12401243

1241-
fn update_text_shadow_tree_if_needed(&self, can_gc: CanGc) {
1242-
// Should only do this for `type=text` input.
1243-
debug_assert_eq!(self.input_type(), InputType::Text);
1244+
/// Should this input type render as a basic text UA widget.
1245+
// TODO(#38251): Ideally, the most basic shadow dom should cover only `text`, `password`, `url`, `tel`,
1246+
// and `email`. But we are leaving the others textual inputs here while tackling them one
1247+
// by one.
1248+
pub(crate) fn is_textual_widget(&self) -> bool {
1249+
matches!(
1250+
self.input_type(),
1251+
InputType::Date |
1252+
InputType::DatetimeLocal |
1253+
InputType::Email |
1254+
InputType::Month |
1255+
InputType::Number |
1256+
InputType::Password |
1257+
InputType::Range |
1258+
InputType::Search |
1259+
InputType::Tel |
1260+
InputType::Text |
1261+
InputType::Time |
1262+
InputType::Url |
1263+
InputType::Week
1264+
)
1265+
}
1266+
1267+
/// Construct the most basic shadow tree structure for textual input.
1268+
/// TODO(stevennovaryo): The rest of textual input shadow dom structure should act like an
1269+
/// exstension to this one.
1270+
fn update_textual_shadow_tree(&self, can_gc: CanGc) {
1271+
// Should only do this for textual input widget.
1272+
debug_assert!(self.is_textual_widget());
12441273

12451274
let text_shadow_tree = self.text_shadow_tree(can_gc);
12461275
let value = self.Value();
@@ -1253,9 +1282,15 @@ impl HTMLInputElement {
12531282
// This is also used to ensure that the caret will still be rendered when the input is empty.
12541283
// TODO: Could append `<br>` element to prevent collapses and avoid this hack, but we would
12551284
// need to fix the rendering of caret beforehand.
1256-
let value_text = match value.is_empty() {
1257-
false => value,
1258-
true => "\u{200B}".into(),
1285+
let value_text = match (value.is_empty(), self.input_type()) {
1286+
// For a password input, we replace all of the character with its replacement char.
1287+
(false, InputType::Password) => value
1288+
.chars()
1289+
.map(|_| PASSWORD_REPLACEMENT_CHAR)
1290+
.collect::<String>()
1291+
.into(),
1292+
(false, _) => value,
1293+
(true, _) => "\u{200B}".into(),
12591294
};
12601295

12611296
// FIXME(stevennovaryo): Refactor this inside a TextControl wrapper
@@ -1269,7 +1304,7 @@ impl HTMLInputElement {
12691304
.SetData(value_text);
12701305
}
12711306

1272-
fn update_color_shadow_tree_if_needed(&self, can_gc: CanGc) {
1307+
fn update_color_shadow_tree(&self, can_gc: CanGc) {
12731308
// Should only do this for `type=color` input.
12741309
debug_assert_eq!(self.input_type(), InputType::Color);
12751310

@@ -1287,10 +1322,10 @@ impl HTMLInputElement {
12871322
.set_string_attribute(&local_name!("style"), style.into(), can_gc);
12881323
}
12891324

1290-
fn update_shadow_tree_if_needed(&self, can_gc: CanGc) {
1325+
fn update_shadow_tree(&self, can_gc: CanGc) {
12911326
match self.input_type() {
1292-
InputType::Text => self.update_text_shadow_tree_if_needed(can_gc),
1293-
InputType::Color => self.update_color_shadow_tree_if_needed(can_gc),
1327+
_ if self.is_textual_widget() => self.update_textual_shadow_tree(can_gc),
1328+
InputType::Color => self.update_color_shadow_tree(can_gc),
12941329
_ => {},
12951330
}
12961331
}
@@ -1317,10 +1352,6 @@ impl<'dom> LayoutDom<'dom, HTMLInputElement> {
13171352
unsafe { self.unsafe_get().filelist.get_inner_as_layout() }
13181353
}
13191354

1320-
fn placeholder(self) -> &'dom str {
1321-
unsafe { self.unsafe_get().placeholder.borrow_for_layout() }
1322-
}
1323-
13241355
fn input_type(self) -> InputType {
13251356
self.unsafe_get().input_type.get()
13261357
}
@@ -1336,6 +1367,9 @@ impl<'dom> LayoutDom<'dom, HTMLInputElement> {
13361367
}
13371368

13381369
impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElement> {
1370+
/// In the past, we are handling the display of <input> element inside the dom tree traversal.
1371+
/// With the introduction of shadow DOM, these implementations will be replaced one by one
1372+
/// and these will be obselete,
13391373
fn value_for_layout(self) -> Cow<'dom, str> {
13401374
fn get_raw_attr_value<'dom>(
13411375
input: LayoutDom<'dom, HTMLInputElement>,
@@ -1349,7 +1383,9 @@ impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElem
13491383
}
13501384

13511385
match self.input_type() {
1352-
InputType::Checkbox | InputType::Radio | InputType::Image => "".into(),
1386+
InputType::Checkbox | InputType::Radio | InputType::Image | InputType::Hidden => {
1387+
"".into()
1388+
},
13531389
InputType::File => {
13541390
let filelist = self.get_filelist();
13551391
match filelist {
@@ -1372,31 +1408,23 @@ impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElem
13721408
InputType::Button => get_raw_attr_value(self, ""),
13731409
InputType::Submit => get_raw_attr_value(self, DEFAULT_SUBMIT_VALUE),
13741410
InputType::Reset => get_raw_attr_value(self, DEFAULT_RESET_VALUE),
1375-
InputType::Password => {
1376-
let text = self.get_raw_textinput_value();
1377-
if !text.is_empty() {
1378-
text.chars()
1379-
.map(|_| PASSWORD_REPLACEMENT_CHAR)
1380-
.collect::<String>()
1381-
.into()
1382-
} else {
1383-
self.placeholder().into()
1384-
}
1385-
},
1386-
InputType::Color => {
1387-
unreachable!("Input type color is explicitly not rendered as text");
1388-
},
1411+
// FIXME(#22728): input `type=range` has yet to be implemented.
1412+
InputType::Range => "".into(),
13891413
_ => {
1390-
let text = self.get_raw_textinput_value();
1391-
if !text.is_empty() {
1392-
text.into()
1393-
} else {
1394-
self.placeholder().into()
1395-
}
1414+
unreachable!("Input with shadow tree should use internal shadow tree for layout");
13961415
},
13971416
}
13981417
}
13991418

1419+
/// Textual input, specifically text entry and domain specific input has
1420+
/// a default preferred size.
1421+
///
1422+
/// <https://html.spec.whatwg.org/multipage/#the-input-element-as-a-text-entry-widget>
1423+
/// <https://html.spec.whatwg.org/multipage/#the-input-element-as-domain-specific-widgets>
1424+
// FIXME(stevennovaryo): Implement the calculation of default preferred size
1425+
// for domain specific input widgets correctly.
1426+
// FIXME(#4378): Implement the calculation of average character width for
1427+
// textual input correctly.
14001428
fn size_for_layout(self) -> u32 {
14011429
self.unsafe_get().size.get()
14021430
}
@@ -2228,7 +2256,7 @@ impl HTMLInputElement {
22282256
// Update the placeholder text in the text shadow tree.
22292257
// To increase the performance, we would only do this when it is necessary.
22302258
fn update_text_shadow_tree_placeholder(&self, can_gc: CanGc) {
2231-
if self.input_type() != InputType::Text {
2259+
if !self.is_textual_widget() {
22322260
return;
22332261
}
22342262

@@ -2690,7 +2718,7 @@ impl HTMLInputElement {
26902718

26912719
fn value_changed(&self, can_gc: CanGc) {
26922720
self.update_related_validity_states(can_gc);
2693-
self.update_shadow_tree_if_needed(can_gc);
2721+
self.update_shadow_tree(can_gc);
26942722
}
26952723

26962724
/// <https://html.spec.whatwg.org/multipage/#show-the-picker,-if-applicable>
@@ -3034,7 +3062,7 @@ impl VirtualMethods for HTMLInputElement {
30343062
// WHATWG-specified activation behaviors are handled elsewhere;
30353063
// this is for all the other things a UI click might do
30363064

3037-
//TODO: set the editing position for text inputs
3065+
//TODO(#10083): set the editing position for text inputs
30383066

30393067
if self.input_type().is_textual_or_password() &&
30403068
// Check if we display a placeholder. Layout doesn't know about this.

components/script/dom/node.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,6 +1852,9 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
18521852
}
18531853
}
18541854

1855+
/// Whether this element should layout as a special case input element.
1856+
// TODO(#38251): With the implementation of Shadow DOM, we could implement the construction properly
1857+
// in the DOM, instead of delegating it to layout.
18551858
fn is_text_input(&self) -> bool {
18561859
let type_id = self.type_id_for_layout();
18571860
if type_id ==
@@ -1861,8 +1864,7 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
18611864
{
18621865
let input = self.unsafe_get().downcast::<HTMLInputElement>().unwrap();
18631866

1864-
// FIXME: All the non-color and non-text input types currently render as text
1865-
!matches!(input.input_type(), InputType::Color | InputType::Text)
1867+
!input.is_textual_widget() && input.input_type() != InputType::Color
18661868
} else {
18671869
type_id ==
18681870
NodeTypeId::Element(ElementTypeId::HTMLElement(

components/script/textinput.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -902,14 +902,14 @@ impl<T: ClipboardProvider> TextInput<T> {
902902
KeyReaction::RedrawSelection
903903
})
904904
.shortcut(CMD_OR_CONTROL, 'X', || {
905-
// FIXME: this is unreachable because ClipboardEvent is fired instead of keydown
906905
if let Some(text) = self.get_selection_text() {
907906
self.clipboard_provider.set_text(text);
908907
self.delete_char(Direction::Backward);
909908
}
910909
KeyReaction::DispatchInput
911910
})
912911
.shortcut(CMD_OR_CONTROL, 'C', || {
912+
// TODO(stevennovaryo): we should not provide text to clipboard for type=password
913913
if let Some(text) = self.get_selection_text() {
914914
self.clipboard_provider.set_text(text);
915915
}

tests/wpt/meta/MANIFEST.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357056,7 +357056,7 @@
357056357056
"input-date-content-size.html": [
357057357057
"d026771f3c89c736b397db6a10c15fde5d73a3a8",
357058357058
[
357059-
null,
357059+
"html/rendering/widgets/input-date-content-size.html",
357060357060
[
357061357061
[
357062357062
"/html/rendering/widgets/input-date-content-size-ref.html",
@@ -357147,7 +357147,7 @@
357147357147
"input-time-content-size.html": [
357148357148
"4a378f6923a8910b96f8afa84125a8fbac4a5d05",
357149357149
[
357150-
null,
357150+
"html/rendering/widgets/input-time-content-size.html",
357151357151
[
357152357152
[
357153357153
"/html/rendering/widgets/input-time-content-size-ref.html",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[number-placeholder-right-aligned.html]
2+
expected: FAIL

tests/wpt/meta/html/rendering/widgets/field-sizing-input-number.html.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@
1313

1414
[number: Update field-sizing property dynamically]
1515
expected: FAIL
16+
17+
[number: Text caret is taller than the placeholder]
18+
expected: FAIL
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[input-date-content-size.html]
2+
expected: FAIL
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[input-number-text-size.tentative.html]
2+
expected: FAIL
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[input-time-content-size.html]
2+
expected: FAIL

0 commit comments

Comments
 (0)