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.
This commit is contained in:
Tim Ledbetter 2024-09-10 09:24:00 +01:00 committed by Tim Ledbetter
parent 2d7547921b
commit 1b74104c17
Notes: github-actions[bot] 2024-09-10 15:13:57 +00:00
6 changed files with 138 additions and 13 deletions

View file

@ -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"

View file

@ -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

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
test(() => {
const testData = [
{ initialType: "text", newType: "email", initialValue: " test " },
{ initialType: "text", newType: "URL", initialValue: " test " },
{ initialType: "hidden", newType: "text", initialValue: " t\re\nst " },
{ initialType: "hidden", newType: "search", initialValue: " t\re\nst " },
{ initialType: "hidden", newType: "tel", initialValue: " t\re\nst " },
{ initialType: "hidden", newType: "password", initialValue: " t\re\nst " },
{ initialType: "hidden", newType: "email", initialValue: " t\re\nst " },
{ initialType: "hidden", newType: "URL", initialValue: " t\re\nst " },
{ initialType: "text", newType: "number", initialValue: "123.45" },
{ initialType: "text", newType: "number", initialValue: "not-a-number" },
{ initialType: "text", newType: "date", initialValue: "not-a-date" },
{ initialType: "text", newType: "date", initialValue: "2024-09-10" },
{ initialType: "text", newType: "datetime-local", initialValue: "not-a-local-datetime" },
{ initialType: "text", newType: "datetime-local", initialValue: "2024-09-10 23:59" },
{ initialType: "text", newType: "datetime-local", initialValue: "2024-09-10T23:59" },
{ initialType: "text", newType: "month", initialValue: "not-a-month" },
{ initialType: "text", newType: "month", initialValue: "2024-09" },
{ initialType: "text", newType: "week", initialValue: "not-a-week" },
{ initialType: "text", newType: "week", initialValue: "2024-W26" },
{ initialType: "text", newType: "time", initialValue: "not-a-time" },
{ initialType: "text", newType: "time", initialValue: "23:59:59" },
];
for (const { initialType, newType, initialValue } of testData) {
const inputElement = document.createElement("input");
inputElement.type = initialType;
inputElement.value = initialValue;
inputElement.type = newType;
const initialValueForDisplay = initialValue.replace("\r", "\\r").replace("\n", "\\n");
const mutatedValue = inputElement.value.replace("\r", "\\r").replace("\n", "\\n");
println(`Changing type from "${initialType}" to "${newType}" changes value from "${initialValueForDisplay}" to "${mutatedValue}"`);
}
});
</script>

View file

@ -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",

View file

@ -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<void> 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)

View file

@ -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();