LibWeb/HTML: Implement and use "optional value"

Corresponds to f3444c23ff

Also import a test.
This commit is contained in:
Sam Atkins 2025-07-08 11:28:10 +01:00 committed by Tim Ledbetter
parent 22cc36eeaa
commit af17f38bbf
Notes: github-actions[bot] 2025-07-08 16:10:58 +00:00
9 changed files with 228 additions and 7 deletions

View file

@ -126,6 +126,7 @@ public:
bool suffering_from_a_custom_error() const; bool suffering_from_a_custom_error() const;
virtual String value() const { return String {}; } virtual String value() const { return String {}; }
virtual Optional<String> optional_value() const { VERIFY_NOT_REACHED(); }
virtual HTMLElement& form_associated_element_to_html_element() = 0; virtual HTMLElement& form_associated_element_to_html_element() = 0;
HTMLElement const& form_associated_element_to_html_element() const { return const_cast<FormAssociatedElement&>(*this).form_associated_element_to_html_element(); } HTMLElement const& form_associated_element_to_html_element() const { return const_cast<FormAssociatedElement&>(*this).form_associated_element_to_html_element(); }

View file

@ -119,9 +119,17 @@ bool HTMLButtonElement::is_submit_button() const
// https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element:concept-fe-value // https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element:concept-fe-value
String HTMLButtonElement::value() const String HTMLButtonElement::value() const
{ {
// The element's value is the value of the element's value attribute, if there is one; otherwise the empty string.
return attribute(AttributeNames::value).value_or(String {}); return attribute(AttributeNames::value).value_or(String {});
} }
// https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element:concept-fe-optional-value
Optional<String> HTMLButtonElement::optional_value() const
{
// The element's optional value is the value of the element's value attribute, if there is one; otherwise null.
return attribute(AttributeNames::value);
}
bool HTMLButtonElement::has_activation_behavior() const bool HTMLButtonElement::has_activation_behavior() const
{ {
return true; return true;

View file

@ -76,6 +76,7 @@ public:
virtual Optional<ARIA::Role> default_role() const override { return ARIA::Role::button; } virtual Optional<ARIA::Role> default_role() const override { return ARIA::Role::button; }
virtual String value() const override; virtual String value() const override;
virtual Optional<String> optional_value() const override;
virtual bool has_activation_behavior() const override; virtual bool has_activation_behavior() const override;
virtual void activation_behavior(DOM::Event const&) override; virtual void activation_behavior(DOM::Event const&) override;

View file

@ -492,16 +492,14 @@ void HTMLDialogElement::invoker_command_steps(DOM::Element& invoker, String& com
// 2. If command is in the Close state and element has an open attribute, // 2. If command is in the Close state and element has an open attribute,
// then close the dialog given element with invoker's optional value and invoker. // then close the dialog given element with invoker's optional value and invoker.
if (command == "close" && has_attribute(AttributeNames::open)) { if (command == "close" && has_attribute(AttributeNames::open)) {
// FIXME: This assumes invoker is a button. auto const optional_value = as<FormAssociatedElement>(invoker).optional_value();
auto optional_value = invoker.get_attribute(AttributeNames::value);
close_the_dialog(optional_value, invoker); close_the_dialog(optional_value, invoker);
} }
// 3. If command is in the Request Close state and element has an open attribute, // 3. If command is in the Request Close state and element has an open attribute,
// then request to close the dialog element with invoker's optional value and invoker. // then request to close the dialog element with invoker's optional value and invoker.
if (command == "request-close" && has_attribute(AttributeNames::open)) { if (command == "request-close" && has_attribute(AttributeNames::open)) {
// FIXME: This assumes invoker is a button. auto const optional_value = as<FormAssociatedElement>(invoker).optional_value();
auto optional_value = invoker.get_attribute(AttributeNames::value);
request_close_the_dialog(optional_value, invoker); request_close_the_dialog(optional_value, invoker);
} }

View file

@ -225,9 +225,10 @@ WebIDL::ExceptionOr<void> HTMLFormElement::submit_form(GC::Ref<HTMLElement> subm
} }
} }
// 5. Otherwise, if submitter has a value, then set result to that value. // 5. Otherwise, if submitter is a submit button, then set result to submitter's optional value.
if (!result.has_value()) else if (auto* form_associated_element = as_if<FormAssociatedElement>(*submitter); form_associated_element && form_associated_element->is_submit_button()) {
result = submitter->get_attribute_value(AttributeNames::value); result = form_associated_element->optional_value();
}
// 6. Close the dialog subject with result and null. // 6. Close the dialog subject with result and null.
subject->close_the_dialog(move(result), nullptr); subject->close_the_dialog(move(result), nullptr);

View file

@ -659,6 +659,18 @@ String HTMLInputElement::value() const
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();
} }
Optional<String> HTMLInputElement::optional_value() const
{
switch (m_type) {
// https://html.spec.whatwg.org/multipage/input.html#submit-button-state-(type=submit):concept-fe-optional-value
case TypeAttributeState::SubmitButton:
// The element's optional value is the value of the element's value attribute, if there is one; otherwise null.
return get_attribute(AttributeNames::value);
default:
VERIFY_NOT_REACHED();
}
}
WebIDL::ExceptionOr<void> HTMLInputElement::set_value(String const& value) WebIDL::ExceptionOr<void> HTMLInputElement::set_value(String const& value)
{ {
auto& realm = this->realm(); auto& realm = this->realm();

View file

@ -80,6 +80,7 @@ public:
String default_value() const { return get_attribute_value(HTML::AttributeNames::value); } String default_value() const { return get_attribute_value(HTML::AttributeNames::value); }
virtual String value() const override; virtual String value() const override;
virtual Optional<String> optional_value() const override;
WebIDL::ExceptionOr<void> set_value(String const&); WebIDL::ExceptionOr<void> set_value(String const&);
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-textarea/input-relevant-value

View file

@ -0,0 +1,14 @@
Harness status: Error
Found 8 tests
6 Pass
2 Fail
Pass click the form submission button should close the dialog
Pass form submission should return correct value
Pass returnValue doesn't update when there's no value attribute.
Pass returnValue does update when there's an empty value attribute.
Fail input image button should return the coordinates
Pass formmethod attribute should use dialog form submission
Pass closing the dialog while submitting should stop the submission
Fail calling form.submit() in click handler of submit button should start the submission synchronously

View file

@ -0,0 +1,185 @@
<!DOCTYPE html>
<meta charset=urf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>Test dialog form submission</title>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="../../../../resources/testdriver.js"></script>
<script src="../../../../resources/testdriver-actions.js"></script>
<script src="../../../../resources/testdriver-vendor.js"></script>
<body>
<dialog id="favDialog">
<form id="dialogForm" method="dialog">
<button id="confirmBtn" value="default">Confirm</button>
<input id="confirmImgBtn" src="./resources/submit.jpg" width="41"
height="41" type="image" alt="Hello">
</form>
<form method="post">
<input id="confirmImgBtn2" src="./resources/submit.jpg" width="41"
formmethod="dialog" height="41" type="image" alt="Hello">
</form>
</dialog>
<script>
promise_test(async (t) => {
const dialog = document.querySelector('dialog');
const button = document.querySelector('button');
t.add_cleanup(() => {
dialog.close();
dialog.returnValue = "";
button.removeAttribute("value");
});
dialog.showModal();
button.click();
assert_false(dialog.open, "dialog should be closed now");
assert_equals(dialog.returnValue, "default", "Return the default value");
}, 'click the form submission button should close the dialog');
promise_test(async (t) => {
const dialog = document.querySelector('dialog');
const button = document.querySelector('button');
t.add_cleanup(() => {
dialog.close();
dialog.returnValue = "";
button.removeAttribute("value");
});
dialog.returnValue = "initial";
dialog.showModal();
button.value = "sushi";
button.click();
assert_false(dialog.open, "dialog should be closed now");
assert_equals(dialog.returnValue, "sushi", "Return the updated value");
}, 'form submission should return correct value');
promise_test(async (t) => {
const dialog = document.querySelector('dialog');
const button = document.querySelector('button');
t.add_cleanup(() => {
dialog.close();
dialog.returnValue = "";
button.removeAttribute("value");
});
dialog.returnValue = "initial";
dialog.showModal();
button.removeAttribute("value");
button.click();
assert_false(dialog.open, "dialog should be closed now");
assert_equals(dialog.returnValue, "initial", "returnValue should not be updated");
}, "returnValue doesn't update when there's no value attribute.");
promise_test(async (t) => {
const dialog = document.querySelector('dialog');
const button = document.querySelector('button');
t.add_cleanup(() => {
dialog.close();
dialog.returnValue = "";
button.removeAttribute("value");
});
dialog.returnValue = "initial";
dialog.showModal();
button.setAttribute("value", "");
button.click();
assert_false(dialog.open, "dialog should be closed now");
assert_equals(dialog.returnValue, "", "returnValue should be updated");
}, "returnValue does update when there's an empty value attribute.");
promise_test(async (t) => {
const dialog = document.querySelector('dialog');
const button = document.querySelector('input');
t.add_cleanup(() => {
dialog.close();
dialog.returnValue = "";
button.removeAttribute("value");
});
dialog.showModal();
let expectedReturnValue = "";
button.addEventListener('click', function(event) {
expectedReturnValue = event.offsetX + "," + event.offsetY;
});
await test_driver.click(button);
assert_false(dialog.open, "dialog should be closed now");
assert_not_equals(dialog.returnValue, "", "returnValue shouldn't be empty string");
assert_equals(dialog.returnValue, expectedReturnValue, "returnValue should be the offsets of the click");
}, "input image button should return the coordinates");
promise_test(async (t) => {
const dialog = document.querySelector('dialog');
t.add_cleanup(() => {
dialog.close();
dialog.returnValue = "";
button.removeAttribute("value");
});
dialog.showModal();
const button = document.getElementById('confirmImgBtn2');
await test_driver.click(button);
assert_false(dialog.open, "dialog should be closed now");
}, "formmethod attribute should use dialog form submission");
promise_test(async (t) => {
const dialog = document.querySelector('dialog');
t.add_cleanup(() => {
dialog.close();
dialog.returnValue = "";
button.removeAttribute("value");
});
dialog.returnValue = "";
dialog.showModal();
const button = document.querySelector('button');
button.value = "sushi";
const dialogForm = document.getElementById('dialogForm');
dialogForm.onsubmit = function() {
dialog.close();
}
button.click();
assert_false(dialog.open, "dialog should be closed now");
// If the submission request got processed, the returnValue should change
// to "sushi" because that's the value of the submitter
assert_equals(dialog.returnValue, "", "dialog's returnValue remains the same");
}, "closing the dialog while submitting should stop the submission");
promise_test(async (t) => {
const dialog = document.querySelector('dialog');
t.add_cleanup(() => {
dialog.close();
dialog.returnValue = "";
button.removeAttribute("value");
});
dialog.returnValue = undefined;
dialog.showModal();
let submitEvent = false;
const dialogForm = document.getElementById('dialogForm');
dialogForm.onsubmit = function() {
submitEvent = true;
assert_false(dialog.open, "dialog should be closed");
assert_equals(dialog.returnValue, "", "dialog's returnValue remains the same");
};
const button = document.querySelector('button');
button.value = "sushi";
button.onclick = function() {
dialogForm.submit();
assert_false(dialog.open, "dialog should be closed now");
// The returnValue should be "" because there is no submitter
assert_equals(dialog.returnValue, "", "returnValue shouldn be empty string");
};
button.click();
assert_true(submitEvent, "Should have submit event");
assert_false(dialog.open, "dialog should be closed");
assert_equals(dialog.returnValue, "", "dialog's returnValue remains the same");
}, "calling form.submit() in click handler of submit button should start the submission synchronously");
</script>
</body>