# HTML Single Line `<input>` Element Shadow Dom ## Context ### The `HTMLInputElement` Web Developer Definition: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input HTML `<input>` element is currently implemented without shadow DOM. This however causes a lot of disadventages: - By residing in the light DOM, styling would affect the looks of `<input>` element. - Implementation of `HTMLInputElement`'s functionality would be hard, tedious, and messy. For example, we are currently handling the display of text value by adding extra `TextRun` fragment in fragment tree construction, which give us unnecessary coupling. - Not to mention the implementation of pseudo element selector like `::placeholder`. ### The `InputText` Struct The `InputText` Struct is an encapsulation utilities related to the text shown inside root editing element. Specifically it will: - Store the information about list of editable text, text selection, IPC channel with clipboard provider, etc. - Handles several queries that would require or change it's content. For example, text input (including shortcuts), moving caret, selection changes, etc. - Underlying implementation of `TextControlSelection`. <!-- ### The `TextControlSelection` Struct Spec: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection ``` pub(crate) struct TextControlSelection<'a, E: TextControlElement> { element: &'a E, textinput: &'a DomRefCell<TextInput<EmbedderClipboardProvider>>, } ``` Due to its attributes's nature, this struct act more like an interface/impl rather than a normal struct. --> ### Caret and Selection We have a quite basic implementation of display which will handle insertion point and selection. These selection is passed from an `HTMLInputElement` through `LayoutDom<'dom, HTMLInputElement>`. ```rust impl<'dom> LayoutDom<'dom, HTMLInputElement> { fn get_raw_textinput_value(self) -> DOMString { unsafe { self.unsafe_get() .textinput .borrow_for_layout() .get_content() } } fn get_filelist(self) -> Option<LayoutDom<'dom, FileList>> { unsafe { self.unsafe_get().filelist.get_inner_as_layout() } } fn placeholder(self) -> &'dom str { unsafe { self.unsafe_get().placeholder.borrow_for_layout() } } fn input_type(self) -> InputType { self.unsafe_get().input_type.get() } fn textinput_sorted_selection_offsets_range(self) -> Range<UTF8Bytes> { unsafe { self.unsafe_get() .textinput .borrow_for_layout() .sorted_selection_offsets_range() } } } ``` The only query necessary for text selection right now is `textinput_sorted_selection_offsets_range`, which will return selection range, that will be processed in display list stage. > [!Note] > Other function of this `LayoutDOM` will become obselete by implementing shadow DOM. ### How do Servo Query Parent of a Fragment We need to know the structure of DOM tree. Note the following `Node` with its pointers. ```rust pub struct Node { /// The JavaScript reflector for this node. eventtarget: EventTarget, /// The parent of this node. parent_node: MutNullableDom<Node>, /// The first child of this node. first_child: MutNullableDom<Node>, /// The last child of this node. last_child: MutNullableDom<Node>, /// The next sibling of this node. next_sibling: MutNullableDom<Node>, /// The previous sibling of this node. prev_sibling: MutNullableDom<Node>, /// And the rest of the attributes... } ``` > [!Tip] Tree Structure > Albeit of the quite unusual tree structure the garbage collection would still work well. Each node will store a pointer to it's parent, and we could access the parent node by calling `node.parent_node()`. #### Query parent node for a `Fragment` > [!Note] > This is just a side note, it is not related to this design document. It will be moved whenever it's document is ready. For a `Fragment` in layout, we could also use `parent_node()`, however this would return `ServoThreadSafeLayuout`, therefore thre could be many `Box` that would relate to this Fragment (because of eager pseudo element). Thus, we need to select `ServoLayoutNode::fragments_for_pseudo()` which will: - Get it's `layout_data` attribute that is dynamically dispatched. - We downcast it to be a `DOMLayoutData`, which contains `LayoutBox` that has a pointer to the main `Fragment` and its pseudo element `Fragment`. ```rust fn fragments_for_pseudo(&self, pseudo_element: Option<PseudoElement>) -> Vec<Fragment> { NodeExt::layout_data(self) .and_then(|layout_data| { layout_data .for_pseudo(pseudo_element) .as_ref() .map(LayoutBox::fragments) }) .unwrap_or_default() } ``` ## Implementation Steps ### Create Shadow DOM for `type=text` Input Add logic of handling input `type=text` shadow tree in `HTMLInputElement::update_shadow_tree_if_needed`. This will create and update shadow tree whenever it is necessary. The following is the structure of the shadow tree. ```rs struct InputTypeTextShadowTree { text_container: Dom<HTMLDivElement>, placeholder_container: Dom<HTMLDivElement>, } ``` Although not optimal, styling is done by const text storing styles inside constant variable. This could be improved once pseudo element "marking" is available. ```rust const TEXT_TREE_STYLE: &str = " #input-editing-root::selection, #input-placeholder::selection { background: rgba(176, 214, 255, 1.0); color: black; } #input-editing-root, #input-placeholder { overflow-wrap: normal; white-space: pre; pointer-events: none; } #input-placeholder { color: grey; overflow: hidden; } "; ``` > [!Important] Selection > Currently selection is getting it's color from `::selection` placeholder, but other UA seems to not use this method. > [!Note] Rendering of `field-sizing: content` > This structure of CSS will prevent `field-sizing: content` to render correctly due to it being absolute positioned, but we are not supporting it anyway. <!-- > [!Important] Pointer Events > Ideally `#text-editing-root` should have `pointer-events`, but setting this currenly prevent `<input>` element from receiving `focus` event. Presumably consumed and not propagated to it's parent. --> ### Handle Selection and Caret The `<input>` element's text editing root need to have caret and selection. To handle this we will mark its container by adding new `NodeFlag::TEXT_EDITING_ROOT` and setting set that state to be active. > [!Tip] Flagging with Pseudo Element Selector > If we have pseudo element selector, we could use it as the flag instead of `ElementState` (which is handling something like `FOCUS`, `PROGRESS_OPTIMUM`, etc.). Maybe we could uses element attribute for this like Chrome. I have yet to check how does firefox handle this. In the traversal and box tree construction, we will query up to its parent in DOM tree and get its selection. This will be inexpensive because the depth of text input shadow tree will be no more than 3 (if we are considering `type=number`). Overall this would not impact the performance because we are also dropping the condition check and handling of these input elements inside box tree. Therefore making its construction lighter for non input element. ### Handle Focus Propagation Mouse button event is being handled in `Document::handle_mouse_button_event`. With a quite simple focus handling as follow. ```rs // Prevent click event if form control element is disabled. if let MouseButtonAction::Click = event.action { // The click event is filtered by the disabled state. if el.is_actually_disabled() { return; } self.begin_focus_transaction(); // Try to focus `el`. If it's not focusable, focus the document // instead. self.request_focus(None, FocusInitiator::Local, can_gc); self.request_focus(&*el, FocusInitiator::Local, can_gc); } ``` In this case, if the hit test result is a child of input shadow DOM, then the click event is neglected. Therefore in such cases, we are redirecting the focus to it's shadow host instead. ## Testing ### WPT Test Initial test: https://github.com/stevennovaryo/servo/actions/runs/15046157146 > [!Tip] Plan: Servo Personalized Test > I think it would be useful to add servo side `<input>` element ref test. We shouldn't depend on firefox afterall. ### Scenario Testing ?