mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-25 00:50:22 +00:00
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:
parent
2d7547921b
commit
1b74104c17
Notes:
github-actions[bot]
2024-09-10 15:13:57 +00:00
Author: https://github.com/tcl3 Commit: https://github.com/LadybirdBrowser/ladybird/commit/1b74104c177 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/1360 Reviewed-by: https://github.com/trflynn89 ✅
6 changed files with 138 additions and 13 deletions
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue