@@ -101,6 +101,29 @@ const DEFAULT_RESET_VALUE: &str = "Reset";
101101const PASSWORD_REPLACEMENT_CHAR : char = '●' ;
102102const DEFAULT_FILE_INPUT_VALUE : & str = "No file chosen" ;
103103
104+ #[ derive( Clone , JSTraceable , MallocSizeOf ) ]
105+ #[ cfg_attr( crown, crown:: unrooted_must_root_lint:: must_root) ]
106+ /// 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.
108+ ///
109+ /// ```
110+ /// <input type="text">
111+ /// #shadow-root
112+ /// <div id="inner-container">
113+ /// <div id="input-editor"></div>
114+ /// <div id="input-placeholder"></div>
115+ /// </div>
116+ /// </input>
117+ /// ```
118+ // TODO(stevennovaryo): We are trying to use CSS to mimic Chrome and Firefox's layout for the <input> element.
119+ // But, this could be slower in performance and does have some discrepancies. For example,
120+ // they would try to vertically align <input> text baseline with the baseline of other
121+ // TextNode within an inline flow. Another example is the horizontal scroll.
122+ struct InputTypeTextShadowTree {
123+ text_container : Dom < HTMLDivElement > ,
124+ placeholder_container : Dom < HTMLDivElement > ,
125+ }
126+
104127#[ derive( Clone , JSTraceable , MallocSizeOf ) ]
105128#[ cfg_attr( crown, crown:: unrooted_must_root_lint:: must_root) ]
106129/// Contains references to the elements in the shadow tree for `<input type=range>`.
@@ -111,10 +134,49 @@ struct InputTypeColorShadowTree {
111134 color_value : Dom < HTMLDivElement > ,
112135}
113136
137+ // FIXME: These styles should be inside UA stylesheet, but it is not possible without internal pseudo element support.
138+ const TEXT_TREE_STYLE : & str = "
139+ #input-editor::selection {
140+ background: rgba(176, 214, 255, 1.0);
141+ color: black;
142+ }
143+
144+ :host:not(:placeholder-shown) #input-placeholder {
145+ visibility: hidden !important
146+ }
147+
148+ #input-editor {
149+ overflow-wrap: normal;
150+ pointer-events: auto;
151+ }
152+
153+ #input-container {
154+ position: relative;
155+ height: 100%;
156+ pointer-events: none;
157+ display: flex;
158+ }
159+
160+ #input-editor, #input-placeholder {
161+ white-space: pre;
162+ margin-block: auto !important;
163+ inset-block: 0 !important;
164+ block-size: fit-content !important;
165+ }
166+
167+ #input-placeholder {
168+ overflow: hidden !important;
169+ position: absolute !important;
170+ color: grey;
171+ pointer-events: none !important;
172+ }
173+ " ;
174+
114175#[ derive( Clone , JSTraceable , MallocSizeOf ) ]
115176#[ cfg_attr( crown, crown:: unrooted_must_root_lint:: must_root) ]
116177#[ non_exhaustive]
117178enum ShadowTree {
179+ Text ( InputTypeTextShadowTree ) ,
118180 Color ( InputTypeColorShadowTree ) ,
119181 // TODO: Add shadow trees for other input types (range etc) here
120182}
@@ -1071,14 +1133,100 @@ impl HTMLInputElement {
10711133 ShadowRootMode :: Closed ,
10721134 false ,
10731135 false ,
1074- false ,
1136+ true ,
10751137 SlotAssignmentMode :: Manual ,
10761138 can_gc,
10771139 )
10781140 . expect ( "Attaching UA shadow root failed" )
10791141 } )
10801142 }
10811143
1144+ fn create_text_shadow_tree ( & self , can_gc : CanGc ) {
1145+ let document = self . owner_document ( ) ;
1146+ let shadow_root = self . shadow_root ( can_gc) ;
1147+ Node :: replace_all ( None , shadow_root. upcast :: < Node > ( ) , can_gc) ;
1148+
1149+ let inner_container =
1150+ HTMLDivElement :: new ( local_name ! ( "div" ) , None , & document, None , can_gc) ;
1151+ inner_container
1152+ . upcast :: < Element > ( )
1153+ . SetId ( DOMString :: from ( "input-container" ) , can_gc) ;
1154+ shadow_root
1155+ . upcast :: < Node > ( )
1156+ . AppendChild ( inner_container. upcast :: < Node > ( ) , can_gc)
1157+ . unwrap ( ) ;
1158+
1159+ let placeholder_container =
1160+ HTMLDivElement :: new ( local_name ! ( "div" ) , None , & document, None , can_gc) ;
1161+ placeholder_container
1162+ . upcast :: < Element > ( )
1163+ . SetId ( DOMString :: from ( "input-placeholder" ) , can_gc) ;
1164+ inner_container
1165+ . upcast :: < Node > ( )
1166+ . AppendChild ( placeholder_container. upcast :: < Node > ( ) , can_gc)
1167+ . unwrap ( ) ;
1168+
1169+ let text_container = HTMLDivElement :: new ( local_name ! ( "div" ) , None , & document, None , can_gc) ;
1170+ text_container
1171+ . upcast :: < Element > ( )
1172+ . SetId ( DOMString :: from ( "input-editor" ) , can_gc) ;
1173+ text_container
1174+ . upcast :: < Node > ( )
1175+ . set_text_control_inner_editor ( ) ;
1176+ inner_container
1177+ . upcast :: < Node > ( )
1178+ . AppendChild ( text_container. upcast :: < Node > ( ) , can_gc)
1179+ . unwrap ( ) ;
1180+
1181+ let style = HTMLStyleElement :: new (
1182+ local_name ! ( "style" ) ,
1183+ None ,
1184+ & document,
1185+ None ,
1186+ ElementCreator :: ScriptCreated ,
1187+ can_gc,
1188+ ) ;
1189+ // TODO(stevennovaryo): Either use UA stylesheet with internal pseudo element or preemptively parse
1190+ // the stylesheet to reduce the costly operation and avoid CSP related error.
1191+ style
1192+ . upcast :: < Node > ( )
1193+ . SetTextContent ( Some ( DOMString :: from ( TEXT_TREE_STYLE ) ) , can_gc) ;
1194+ shadow_root
1195+ . upcast :: < Node > ( )
1196+ . AppendChild ( style. upcast :: < Node > ( ) , can_gc)
1197+ . unwrap ( ) ;
1198+
1199+ let _ = self
1200+ . shadow_tree
1201+ . borrow_mut ( )
1202+ . insert ( ShadowTree :: Text ( InputTypeTextShadowTree {
1203+ text_container : text_container. as_traced ( ) ,
1204+ placeholder_container : placeholder_container. as_traced ( ) ,
1205+ } ) ) ;
1206+ }
1207+
1208+ fn text_shadow_tree ( & self , can_gc : CanGc ) -> Ref < InputTypeTextShadowTree > {
1209+ let has_text_shadow_tree = self
1210+ . shadow_tree
1211+ . borrow ( )
1212+ . as_ref ( )
1213+ . is_some_and ( |shadow_tree| matches ! ( shadow_tree, ShadowTree :: Text ( _) ) ) ;
1214+ if !has_text_shadow_tree {
1215+ self . create_text_shadow_tree ( can_gc) ;
1216+ }
1217+
1218+ let shadow_tree = self . shadow_tree . borrow ( ) ;
1219+ Ref :: filter_map ( shadow_tree, |shadow_tree| {
1220+ let shadow_tree = shadow_tree. as_ref ( ) ?;
1221+ match shadow_tree {
1222+ ShadowTree :: Text ( text_tree) => Some ( text_tree) ,
1223+ _ => None ,
1224+ }
1225+ } )
1226+ . ok ( )
1227+ . expect ( "UA shadow tree was not created" )
1228+ }
1229+
10821230 fn create_color_shadow_tree ( & self , can_gc : CanGc ) {
10831231 let document = self . owner_document ( ) ;
10841232 let shadow_root = self . shadow_root ( can_gc) ;
@@ -1136,27 +1284,53 @@ impl HTMLInputElement {
11361284 let shadow_tree = self . shadow_tree . borrow ( ) ;
11371285 Ref :: filter_map ( shadow_tree, |shadow_tree| {
11381286 let shadow_tree = shadow_tree. as_ref ( ) ?;
1139- let ShadowTree :: Color ( color_tree) = shadow_tree;
1140- Some ( color_tree)
1287+ match shadow_tree {
1288+ ShadowTree :: Color ( color_tree) => Some ( color_tree) ,
1289+ _ => None ,
1290+ }
11411291 } )
11421292 . ok ( )
11431293 . expect ( "UA shadow tree was not created" )
11441294 }
11451295
11461296 fn update_shadow_tree_if_needed ( & self , can_gc : CanGc ) {
1147- if self . input_type ( ) == InputType :: Color {
1148- let color_shadow_tree = self . color_shadow_tree ( can_gc) ;
1149- let mut value = self . Value ( ) ;
1150- if value. str ( ) . is_valid_simple_color_string ( ) {
1151- value. make_ascii_lowercase ( ) ;
1152- } else {
1153- value = DOMString :: from ( "#000000" ) ;
1154- }
1155- let style = format ! ( "background-color: {value}" ) ;
1156- color_shadow_tree
1157- . color_value
1158- . upcast :: < Element > ( )
1159- . set_string_attribute ( & local_name ! ( "style" ) , style. into ( ) , can_gc) ;
1297+ match self . input_type ( ) {
1298+ InputType :: Text => {
1299+ let text_shadow_tree = self . text_shadow_tree ( can_gc) ;
1300+ let value = self . Value ( ) ;
1301+
1302+ // The addition of zero-width space here forces the text input to have an inline formatting
1303+ // context that might otherwise be trimmed if there's no text. This is important to ensure
1304+ // that the input element is at least as tall as the line gap of the caret:
1305+ // <https://drafts.csswg.org/css-ui/#element-with-default-preferred-size>.
1306+ //
1307+ // This is also used to ensure that the caret will still be rendered when the input is empty.
1308+ // TODO: Is there a less hacky way to do this?
1309+ let value_text = match value. is_empty ( ) {
1310+ false => value,
1311+ true => "\u{200B} " . into ( ) ,
1312+ } ;
1313+
1314+ text_shadow_tree
1315+ . text_container
1316+ . upcast :: < Node > ( )
1317+ . SetTextContent ( Some ( value_text) , can_gc) ;
1318+ } ,
1319+ InputType :: Color => {
1320+ let color_shadow_tree = self . color_shadow_tree ( can_gc) ;
1321+ let mut value = self . Value ( ) ;
1322+ if value. str ( ) . is_valid_simple_color_string ( ) {
1323+ value. make_ascii_lowercase ( ) ;
1324+ } else {
1325+ value = DOMString :: from ( "#000000" ) ;
1326+ }
1327+ let style = format ! ( "background-color: {value}" ) ;
1328+ color_shadow_tree
1329+ . color_value
1330+ . upcast :: < Element > ( )
1331+ . set_string_attribute ( & local_name ! ( "style" ) , style. into ( ) , can_gc) ;
1332+ } ,
1333+ _ => { } ,
11601334 }
11611335 }
11621336}
@@ -1465,22 +1639,29 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
14651639 fn SetValue ( & self , mut value : DOMString , can_gc : CanGc ) -> ErrorResult {
14661640 match self . value_mode ( ) {
14671641 ValueMode :: Value => {
1468- // Step 3.
1469- self . value_dirty . set ( true ) ;
1470-
1471- // Step 4.
1472- self . sanitize_value ( & mut value) ;
1642+ {
1643+ // Step 3.
1644+ self . value_dirty . set ( true ) ;
14731645
1474- let mut textinput = self . textinput . borrow_mut ( ) ;
1646+ // Step 4.
1647+ self . sanitize_value ( & mut value) ;
14751648
1476- // Step 5.
1477- if * textinput. single_line_content ( ) != value {
1478- // Steps 1-2
1479- textinput. set_content ( value) ;
1649+ let mut textinput = self . textinput . borrow_mut ( ) ;
14801650
14811651 // Step 5.
1482- textinput. clear_selection_to_limit ( Direction :: Forward ) ;
1652+ if * textinput. single_line_content ( ) != value {
1653+ // Steps 1-2
1654+ textinput. set_content ( value) ;
1655+
1656+ // Step 5.
1657+ textinput. clear_selection_to_limit ( Direction :: Forward ) ;
1658+ }
14831659 }
1660+
1661+ // Additionaly, update the placeholder shown state. This is
1662+ // normally being done in the attributed mutated. And, being
1663+ // done in another scope to prevent borrow checker issues.
1664+ self . update_placeholder_shown_state ( ) ;
14841665 } ,
14851666 ValueMode :: Default | ValueMode :: DefaultOn => {
14861667 self . upcast :: < Element > ( )
@@ -2063,6 +2244,19 @@ impl HTMLInputElement {
20632244 el. set_placeholder_shown_state ( has_placeholder && !has_value) ;
20642245 }
20652246
2247+ // Update the placeholder text in the text shadow tree.
2248+ // To increase the performance, we would only do this when it is necessary.
2249+ fn update_text_shadow_tree_placeholder ( & self , can_gc : CanGc ) {
2250+ if self . input_type ( ) != InputType :: Text {
2251+ return ;
2252+ }
2253+
2254+ self . text_shadow_tree ( can_gc)
2255+ . placeholder_container
2256+ . upcast :: < Node > ( )
2257+ . SetTextContent ( Some ( self . placeholder . borrow ( ) . clone ( ) ) , can_gc) ;
2258+ }
2259+
20662260 // https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file)
20672261 // Select files by invoking UI or by passed in argument
20682262 fn select_files ( & self , opt_test_paths : Option < Vec < DOMString > > , can_gc : CanGc ) {
@@ -2688,8 +2882,11 @@ impl VirtualMethods for HTMLInputElement {
26882882 } ,
26892883 }
26902884
2885+ self . update_text_shadow_tree_placeholder ( can_gc) ;
26912886 self . update_placeholder_shown_state ( ) ;
26922887 } ,
2888+ // FIXME(stevennovaryo): This is only reachable by Default and DefaultOn value mode. While others
2889+ // are being handled in [Self::SetValue]. Should we merge this two together?
26932890 local_name ! ( "value" ) if !self . value_dirty . get ( ) => {
26942891 let value = mutation. new_value ( attr) . map ( |value| ( * * value) . to_owned ( ) ) ;
26952892 let mut value = value. map_or ( DOMString :: new ( ) , DOMString :: from) ;
@@ -2738,6 +2935,7 @@ impl VirtualMethods for HTMLInputElement {
27382935 . extend ( attr. value ( ) . chars ( ) . filter ( |& c| c != '\n' && c != '\r' ) ) ;
27392936 }
27402937 }
2938+ self . update_text_shadow_tree_placeholder ( can_gc) ;
27412939 self . update_placeholder_shown_state ( ) ;
27422940 } ,
27432941 local_name ! ( "readonly" ) => {
0 commit comments