From 1b74104c17726018788034f2ad66201889ce6225 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Tue, 10 Sep 2024 09:24:00 +0100 Subject: [PATCH] LibWeb: Make input type state change handling specification compliant This change ensures that the value sanitization algorithm is run and the text cursor is set to the correct position when the type attribute of an input is changed. --- .../HTMLInputElement-type-state-change.txt | 21 ++++++ .../HTML/HTMLInputElement-valueAsNumber.txt | 8 +- .../HTMLInputElement-type-state-change.html | 38 ++++++++++ .../HTML/HTMLInputElement-valueAsNumber.html | 4 +- .../LibWeb/HTML/HTMLInputElement.cpp | 75 +++++++++++++++++-- .../Libraries/LibWeb/HTML/HTMLInputElement.h | 5 ++ 6 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/HTML/HTMLInputElement-type-state-change.txt create mode 100644 Tests/LibWeb/Text/input/HTML/HTMLInputElement-type-state-change.html diff --git a/Tests/LibWeb/Text/expected/HTML/HTMLInputElement-type-state-change.txt b/Tests/LibWeb/Text/expected/HTML/HTMLInputElement-type-state-change.txt new file mode 100644 index 00000000000..47dc78ca661 --- /dev/null +++ b/Tests/LibWeb/Text/expected/HTML/HTMLInputElement-type-state-change.txt @@ -0,0 +1,21 @@ +Changing type from "text" to "email" changes value from " test " to "test" +Changing type from "text" to "URL" changes value from " test " to "test" +Changing type from "hidden" to "text" changes value from " t\re\nst " to " test " +Changing type from "hidden" to "search" changes value from " t\re\nst " to " test " +Changing type from "hidden" to "tel" changes value from " t\re\nst " to " test " +Changing type from "hidden" to "password" changes value from " t\re\nst " to " test " +Changing type from "hidden" to "email" changes value from " t\re\nst " to "test" +Changing type from "hidden" to "URL" changes value from " t\re\nst " to "test" +Changing type from "text" to "number" changes value from "123.45" to "123.45" +Changing type from "text" to "number" changes value from "not-a-number" to "" +Changing type from "text" to "date" changes value from "not-a-date" to "" +Changing type from "text" to "date" changes value from "2024-09-10" to "2024-09-10" +Changing type from "text" to "datetime-local" changes value from "not-a-local-datetime" to "" +Changing type from "text" to "datetime-local" changes value from "2024-09-10 23:59" to "2024-09-10T23:59" +Changing type from "text" to "datetime-local" changes value from "2024-09-10T23:59" to "2024-09-10T23:59" +Changing type from "text" to "month" changes value from "not-a-month" to "" +Changing type from "text" to "month" changes value from "2024-09" to "2024-09" +Changing type from "text" to "week" changes value from "not-a-week" to "" +Changing type from "text" to "week" changes value from "2024-W26" to "2024-W26" +Changing type from "text" to "time" changes value from "not-a-time" to "" +Changing type from "text" to "time" changes value from "23:59:59" to "23:59:59" diff --git a/Tests/LibWeb/Text/expected/HTML/HTMLInputElement-valueAsNumber.txt b/Tests/LibWeb/Text/expected/HTML/HTMLInputElement-valueAsNumber.txt index 0bd2de383a4..5a1467623e7 100644 --- a/Tests/LibWeb/Text/expected/HTML/HTMLInputElement-valueAsNumber.txt +++ b/Tests/LibWeb/Text/expected/HTML/HTMLInputElement-valueAsNumber.txt @@ -1,4 +1,6 @@ valueAsNumber getter: +number: 100 +range: 100 hidden: NaN text: NaN search: NaN @@ -11,8 +13,6 @@ month: NaN week: NaN time: NaN datetime-local: NaN -number: 100 -range: 100 color: NaN checkbox: NaN radio: NaN @@ -22,6 +22,8 @@ image: NaN reset: NaN button: NaN valueAsNumber setter: +number did not throw: 100 +range did not throw: 100 hidden threw exception: InvalidStateError: valueAsNumber: Invalid input type used text threw exception: InvalidStateError: valueAsNumber: Invalid input type used search threw exception: InvalidStateError: valueAsNumber: Invalid input type used @@ -34,8 +36,6 @@ month did not throw: NaN week did not throw: NaN time did not throw: NaN datetime-local did not throw: NaN -number did not throw: 100 -range did not throw: 100 color threw exception: InvalidStateError: valueAsNumber: Invalid input type used checkbox threw exception: InvalidStateError: valueAsNumber: Invalid input type used radio threw exception: InvalidStateError: valueAsNumber: Invalid input type used diff --git a/Tests/LibWeb/Text/input/HTML/HTMLInputElement-type-state-change.html b/Tests/LibWeb/Text/input/HTML/HTMLInputElement-type-state-change.html new file mode 100644 index 00000000000..cffb198ff7b --- /dev/null +++ b/Tests/LibWeb/Text/input/HTML/HTMLInputElement-type-state-change.html @@ -0,0 +1,38 @@ + + + diff --git a/Tests/LibWeb/Text/input/HTML/HTMLInputElement-valueAsNumber.html b/Tests/LibWeb/Text/input/HTML/HTMLInputElement-valueAsNumber.html index 60042211e58..9ffe7679b47 100644 --- a/Tests/LibWeb/Text/input/HTML/HTMLInputElement-valueAsNumber.html +++ b/Tests/LibWeb/Text/input/HTML/HTMLInputElement-valueAsNumber.html @@ -5,6 +5,8 @@ const inputElement = document.getElementById("input-element"); const allInputTypes = [ + "number", + "range", "hidden", "text", "search", @@ -17,8 +19,6 @@ "week", "time", "datetime-local", - "number", - "range", "color", "checkbox", "radio", diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp index 1e2bf376f64..a69101a3b3f 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -1177,10 +1177,8 @@ void HTMLInputElement::form_associated_element_attribute_changed(FlyString const set_checked(true, ChangeSource::Programmatic); } } else if (name == HTML::AttributeNames::type) { - m_type = parse_type_attribute(value.value_or(String {})); - - set_shadow_root(nullptr); - create_shadow_tree_if_needed(); + auto new_type_attribute_state = parse_type_attribute(value.value_or(String {})); + type_attribute_changed(m_type, new_type_attribute_state); // https://html.spec.whatwg.org/multipage/input.html#image-button-state-(type=image):the-input-element-4 // the input element's type attribute is changed back to the Image Button state, and the src attribute is present, @@ -1223,6 +1221,59 @@ void HTMLInputElement::form_associated_element_attribute_changed(FlyString const } } +// https://html.spec.whatwg.org/multipage/input.html#input-type-change +void HTMLInputElement::type_attribute_changed(TypeAttributeState old_state, TypeAttributeState new_state) +{ + auto new_value_attribute_mode = value_attribute_mode_for_type_state(new_state); + auto old_value_attribute_mode = value_attribute_mode_for_type_state(old_state); + + // 1. If the previous state of the element's type attribute put the value IDL attribute in the value mode, and the element's + // value is not the empty string, and the new state of the element's type attribute puts the value IDL attribute in either + // the default mode or the default/on mode, then set the element's value content attribute to the element's value. + if (old_value_attribute_mode == ValueAttributeMode::Value && !m_value.is_empty() && (first_is_one_of(new_value_attribute_mode, ValueAttributeMode::Default, ValueAttributeMode::DefaultOn))) { + MUST(set_attribute(HTML::AttributeNames::value, m_value)); + } + + // 2. Otherwise, if the previous state of the element's type attribute put the value IDL attribute in any mode other + // than the value mode, and the new state of the element's type attribute puts the value IDL attribute in the value mode, + // then set the value of the element to the value of the value content attribute, if there is one, or the empty string + // otherwise, and then set the control's dirty value flag to false. + else if (old_value_attribute_mode != ValueAttributeMode::Value && new_value_attribute_mode == ValueAttributeMode::Value) { + m_value = attribute(HTML::AttributeNames::value).value_or({}); + m_dirty_value = false; + } + + // 3. Otherwise, if the previous state of the element's type attribute put the value IDL attribute in any mode other + // than the filename mode, and the new state of the element's type attribute puts the value IDL attribute in the filename mode, + // then set the value of the element to the empty string. + else if (old_value_attribute_mode != ValueAttributeMode::Filename && new_value_attribute_mode == ValueAttributeMode::Filename) { + m_value = String {}; + } + + // 4. Update the element's rendering and behavior to the new state's. + m_type = new_state; + set_shadow_root(nullptr); + create_shadow_tree_if_needed(); + + // FIXME: 5. Signal a type change for the element. (The Radio Button state uses this, in particular.) + + // 6. Invoke the value sanitization algorithm, if one is defined for the type attribute's new state. + m_value = value_sanitization_algorithm(m_value); + + // 7. Let previouslySelectable be true if setRangeText() previously applied to the element, and false otherwise. + auto previously_selectable = selection_or_range_applies_for_type_state(old_state); + + // 8. Let nowSelectable be true if setRangeText() now applies to the element, and false otherwise. + auto now_selectable = selection_or_range_applies_for_type_state(new_state); + + // 9. If previouslySelectable is false and nowSelectable is true, set the element's text entry cursor position to the + // beginning of the text control, and set its selection direction to "none". + if (!previously_selectable && now_selectable) { + document().set_cursor_position(DOM::Position::create(realm(), *m_text_node, 0)); + set_selection_direction(OptionalNone {}); + } +} + // https://html.spec.whatwg.org/multipage/input.html#attr-input-src WebIDL::ExceptionOr HTMLInputElement::handle_src_attribute(String const& value) { @@ -2245,7 +2296,12 @@ bool HTMLInputElement::select_applies() const // https://html.spec.whatwg.org/multipage/input.html#do-not-apply bool HTMLInputElement::selection_or_range_applies() const { - switch (type_state()) { + return selection_or_range_applies_for_type_state(type_state()); +} + +bool HTMLInputElement::selection_or_range_applies_for_type_state(TypeAttributeState type_state) +{ + switch (type_state) { case TypeAttributeState::Text: case TypeAttributeState::Search: case TypeAttributeState::Telephone: @@ -2328,9 +2384,9 @@ bool HTMLInputElement::step_up_or_down_applies() const } // https://html.spec.whatwg.org/multipage/input.html#the-input-element:dom-input-value-2 -HTMLInputElement::ValueAttributeMode HTMLInputElement::value_attribute_mode() const +HTMLInputElement::ValueAttributeMode HTMLInputElement::value_attribute_mode_for_type_state(TypeAttributeState type_state) { - switch (type_state()) { + switch (type_state) { case TypeAttributeState::Text: case TypeAttributeState::Search: case TypeAttributeState::Telephone: @@ -2365,6 +2421,11 @@ HTMLInputElement::ValueAttributeMode HTMLInputElement::value_attribute_mode() co VERIFY_NOT_REACHED(); } +HTMLInputElement::ValueAttributeMode HTMLInputElement::value_attribute_mode() const +{ + return value_attribute_mode_for_type_state(type_state()); +} + void HTMLInputElement::selection_was_changed(size_t selection_start, size_t selection_end) { if (!m_text_node) diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h index 3ce4214d170..9260dceeded 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -205,12 +205,16 @@ public: bool select_applies() const; bool selection_or_range_applies() const; + static bool selection_or_range_applies_for_type_state(TypeAttributeState); + protected: void selection_was_changed(size_t selection_start, size_t selection_end) override; private: HTMLInputElement(DOM::Document&, DOM::QualifiedName); + void type_attribute_changed(TypeAttributeState old_state, TypeAttributeState new_state); + // ^DOM::Node virtual bool is_html_input_element() const final { return true; } @@ -280,6 +284,7 @@ private: DefaultOn, Filename, }; + static ValueAttributeMode value_attribute_mode_for_type_state(TypeAttributeState); ValueAttributeMode value_attribute_mode() const; void update_placeholder_visibility();