diff --git a/Libraries/LibWeb/HTML/HTMLButtonElement.cpp b/Libraries/LibWeb/HTML/HTMLButtonElement.cpp index 140314b3f39..0c6fc9bf577 100644 --- a/Libraries/LibWeb/HTML/HTMLButtonElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLButtonElement.cpp @@ -289,9 +289,10 @@ String HTMLButtonElement::command() const // show-popover Show Popover Shows the targeted popover element. // hide-popover Hide Popover Hides the targeted popover element. // close Close Closes the targeted dialog element. + // request-close Request Close Requests to close the targeted dialog element. // show-modal Show Modal Opens the targeted dialog element as modal. // A custom command keyword Custom Only dispatches the command event on the targeted element. - Array valid_values { "toggle-popover"_string, "show-popover"_string, "hide-popover"_string, "close"_string, "show-modal"_string }; + Array valid_values { "toggle-popover"_string, "show-popover"_string, "hide-popover"_string, "close"_string, "request-close"_string, "show-modal"_string }; // 2. If command is in the Custom state, then return command's value. // A custom command keyword is a string that starts with "--". diff --git a/Libraries/LibWeb/HTML/HTMLDialogElement.cpp b/Libraries/LibWeb/HTML/HTMLDialogElement.cpp index 54adc35aaa8..cfa0052f80d 100644 --- a/Libraries/LibWeb/HTML/HTMLDialogElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLDialogElement.cpp @@ -285,11 +285,14 @@ void HTMLDialogElement::close(Optional return_value) // https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-requestclose void HTMLDialogElement::request_close(Optional return_value) { - // AD-HOC: This method is an amalgamation of the requestClose() and "request to close" algorithms from the spec. - // FIXME: Once the "request-close" command is implemented, this will need to be split into two methods. - // For now this implementation is only used for the requestClose() method, which sets source to null. - auto source = nullptr; + // 1. If returnValue is not given, then set it to null. + // 2. Request to close the dialog this with returnValue and null. + request_close_the_dialog(move(return_value), nullptr); +} +// https://html.spec.whatwg.org/multipage/interactive-elements.html#dialog-request-close +void HTMLDialogElement::request_close_the_dialog(Optional return_value, GC::Ptr source) +{ // 1. If this does not have an open attribute, then return. if (!has_attribute(AttributeNames::open)) return; @@ -470,8 +473,8 @@ void HTMLDialogElement::set_is_modal(bool is_modal) // https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element:is-valid-invoker-command-steps bool HTMLDialogElement::is_valid_invoker_command(String& command) { - // 1. If command is in the Close state or in the Show Modal state, then return true. - if (command == "close" || command == "show-modal") + // 1. If command is in the Close state, the Request Close state, or the Show Modal state, then return true. + if (command == "close" || command == "request-close" || command == "show-modal") return true; // 2. Return false. @@ -494,7 +497,15 @@ void HTMLDialogElement::invoker_command_steps(DOM::Element& invoker, String& com close_the_dialog(optional_value, invoker); } - // 3. If command is the Show Modal state and element does not have an open attribute, then show a modal dialog given element and invoker. + // 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. + if (command == "request-close" && has_attribute(AttributeNames::open)) { + // FIXME: This assumes invoker is a button. + auto optional_value = invoker.get_attribute(AttributeNames::value); + request_close_the_dialog(optional_value, invoker); + } + + // 4. If command is the Show Modal state and element does not have an open attribute, then show a modal dialog given element and invoker. if (command == "show-modal" && !has_attribute(AttributeNames::open)) { MUST(show_a_modal_dialog(*this, invoker)); } diff --git a/Libraries/LibWeb/HTML/HTMLDialogElement.h b/Libraries/LibWeb/HTML/HTMLDialogElement.h index c3fd25efdae..fda795215a0 100644 --- a/Libraries/LibWeb/HTML/HTMLDialogElement.h +++ b/Libraries/LibWeb/HTML/HTMLDialogElement.h @@ -29,6 +29,7 @@ public: static WebIDL::ExceptionOr show_a_modal_dialog(HTMLDialogElement&, GC::Ptr source); void close_the_dialog(Optional result, GC::Ptr source); + void request_close_the_dialog(Optional return_value, GC::Ptr source); WebIDL::ExceptionOr show(); WebIDL::ExceptionOr show_modal(); diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/the-button-element/command-and-commandfor/on-dialog-behavior.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/the-button-element/command-and-commandfor/on-dialog-behavior.txt index 1c9afd09d07..7caaed9cea8 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/the-button-element/command-and-commandfor/on-dialog-behavior.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/the-button-element/command-and-commandfor/on-dialog-behavior.txt @@ -1,8 +1,8 @@ Harness status: OK -Found 50 tests +Found 104 tests -48 Pass +102 Pass 2 Fail Pass invoking (with command property as show-modal) closed dialog opens as modal Pass invoking (with command property as show-modal) closed dialog with preventDefault is noop @@ -18,39 +18,93 @@ Pass invoking (with command attribute as sHoW-mOdAl) closed dialog with preventD Pass invoking (with command attribute as sHoW-mOdAl) while changing command still opens as modal Pass invoking to close (with command property as close) open dialog closes Pass invoking to close (with command property as close) open dialog closes and sets returnValue +Pass invoking to close (with command property as close) open dialog closes and overrides returnValue +Pass invoking to close (with command property as close) open dialog closes and overrides returnValue when empty string +Pass invoking to close (with command property as close) open dialog closes and doesn't override returnValue when missing value attribute Pass invoking to close (with command property as close) open dialog with preventDefault is no-op Pass invoking to close (with command property as close) open modal dialog with preventDefault is no-op Pass invoking to close (with command property as close) open dialog while changing command still closes Pass invoking to close (with command property as close) open modal dialog while changing command still closes Pass invoking to close (with command attribute as close) open dialog closes Pass invoking to close (with command attribute as close) open dialog closes and sets returnValue +Pass invoking to close (with command attribute as close) open dialog closes and overrides returnValue +Pass invoking to close (with command attribute as close) open dialog closes and overrides returnValue when empty string +Pass invoking to close (with command attribute as close) open dialog closes and doesn't override returnValue when missing value attribute Pass invoking to close (with command attribute as close) open dialog with preventDefault is no-op Pass invoking to close (with command attribute as close) open modal dialog with preventDefault is no-op Pass invoking to close (with command attribute as close) open dialog while changing command still closes Pass invoking to close (with command attribute as close) open modal dialog while changing command still closes Pass invoking to close (with command property as cLoSe) open dialog closes Pass invoking to close (with command property as cLoSe) open dialog closes and sets returnValue +Pass invoking to close (with command property as cLoSe) open dialog closes and overrides returnValue +Pass invoking to close (with command property as cLoSe) open dialog closes and overrides returnValue when empty string +Pass invoking to close (with command property as cLoSe) open dialog closes and doesn't override returnValue when missing value attribute Pass invoking to close (with command property as cLoSe) open dialog with preventDefault is no-op Pass invoking to close (with command property as cLoSe) open modal dialog with preventDefault is no-op Pass invoking to close (with command property as cLoSe) open dialog while changing command still closes Pass invoking to close (with command property as cLoSe) open modal dialog while changing command still closes Pass invoking to close (with command attribute as cLoSe) open dialog closes Pass invoking to close (with command attribute as cLoSe) open dialog closes and sets returnValue +Pass invoking to close (with command attribute as cLoSe) open dialog closes and overrides returnValue +Pass invoking to close (with command attribute as cLoSe) open dialog closes and overrides returnValue when empty string +Pass invoking to close (with command attribute as cLoSe) open dialog closes and doesn't override returnValue when missing value attribute Pass invoking to close (with command attribute as cLoSe) open dialog with preventDefault is no-op Pass invoking to close (with command attribute as cLoSe) open modal dialog with preventDefault is no-op Pass invoking to close (with command attribute as cLoSe) open dialog while changing command still closes Pass invoking to close (with command attribute as cLoSe) open modal dialog while changing command still closes +Pass invoking to request-close (with command property as request-close) open dialog closes +Pass invoking to request-close with value (with command property as request-close) open dialog closes and sets returnValue +Pass invoking to request-close with value (with command property as request-close) open dialog closes and overrides returnValue +Pass invoking to request-close with value (with command property as request-close) open dialog closes and overrides returnValue when empty string +Pass invoking to request-close with value (with command property as request-close) open dialog closes and doesn't override returnValue when missing value attribute +Pass invoking to request-close (with command property as request-close) open dialog with preventDefault is no-op +Pass invoking to request-close (with command property as request-close) open modal dialog with preventDefault is no-op +Pass invoking to request-close (with command property as request-close) open dialog while changing command still closes +Pass invoking to request-close (with command property as request-close) open modal dialog while changing command still closes +Pass invoking to request-close (with command attribute as request-close) open dialog closes +Pass invoking to request-close with value (with command attribute as request-close) open dialog closes and sets returnValue +Pass invoking to request-close with value (with command attribute as request-close) open dialog closes and overrides returnValue +Pass invoking to request-close with value (with command attribute as request-close) open dialog closes and overrides returnValue when empty string +Pass invoking to request-close with value (with command attribute as request-close) open dialog closes and doesn't override returnValue when missing value attribute +Pass invoking to request-close (with command attribute as request-close) open dialog with preventDefault is no-op +Pass invoking to request-close (with command attribute as request-close) open modal dialog with preventDefault is no-op +Pass invoking to request-close (with command attribute as request-close) open dialog while changing command still closes +Pass invoking to request-close (with command attribute as request-close) open modal dialog while changing command still closes +Pass invoking to request-close (with command property as reQuEst-Close) open dialog closes +Pass invoking to request-close with value (with command property as reQuEst-Close) open dialog closes and sets returnValue +Pass invoking to request-close with value (with command property as reQuEst-Close) open dialog closes and overrides returnValue +Pass invoking to request-close with value (with command property as reQuEst-Close) open dialog closes and overrides returnValue when empty string +Pass invoking to request-close with value (with command property as reQuEst-Close) open dialog closes and doesn't override returnValue when missing value attribute +Pass invoking to request-close (with command property as reQuEst-Close) open dialog with preventDefault is no-op +Pass invoking to request-close (with command property as reQuEst-Close) open modal dialog with preventDefault is no-op +Pass invoking to request-close (with command property as reQuEst-Close) open dialog while changing command still closes +Pass invoking to request-close (with command property as reQuEst-Close) open modal dialog while changing command still closes +Pass invoking to request-close (with command attribute as reQuEst-Close) open dialog closes +Pass invoking to request-close with value (with command attribute as reQuEst-Close) open dialog closes and sets returnValue +Pass invoking to request-close with value (with command attribute as reQuEst-Close) open dialog closes and overrides returnValue +Pass invoking to request-close with value (with command attribute as reQuEst-Close) open dialog closes and overrides returnValue when empty string +Pass invoking to request-close with value (with command attribute as reQuEst-Close) open dialog closes and doesn't override returnValue when missing value attribute +Pass invoking to request-close (with command attribute as reQuEst-Close) open dialog with preventDefault is no-op +Pass invoking to request-close (with command attribute as reQuEst-Close) open modal dialog with preventDefault is no-op +Pass invoking to request-close (with command attribute as reQuEst-Close) open dialog while changing command still closes +Pass invoking to request-close (with command attribute as reQuEst-Close) open modal dialog while changing command still closes Pass invoking (as show-modal) open dialog is noop Pass invoking (as show-modal) open modal, while changing command still a no-op Pass invoking (as show-modal) closed popover dialog opens as modal Pass invoking (as close) already closed dialog is noop +Pass invoking (as request-close) already closed dialog is noop Pass invoking (as show-modal) dialog as open popover=manual is noop Pass invoking (as show-modal) dialog as open popover=auto is noop Pass invoking (as close) dialog as open popover=manual is noop Pass invoking (as close) dialog as open popover=auto is noop +Pass invoking (as request-close) dialog as open popover=manual is noop +Pass invoking (as request-close) dialog as open popover=auto is noop Pass invoking (as show-modal) dialog that is removed is noop Fail invoking (as show-modal) dialog from a detached invoker Pass invoking (as show-modal) detached dialog from a detached invoker Fail invoking (as close) dialog that is removed is noop Pass invoking (as close) dialog from a detached invoker -Pass invoking (as close) detached dialog from a detached invoker \ No newline at end of file +Pass invoking (as close) detached dialog from a detached invoker +Pass invoking (as request-close) dialog that is removed is noop +Pass invoking (as request-close) dialog from a detached invoker +Pass invoking (as request-close) detached dialog from a detached invoker \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/the-button-element/command-and-commandfor/on-dialog-behavior.html b/Tests/LibWeb/Text/input/wpt-import/html/semantics/the-button-element/command-and-commandfor/on-dialog-behavior.html index d6cc93a9f9e..e615d9c76d2 100644 --- a/Tests/LibWeb/Text/input/wpt-import/html/semantics/the-button-element/command-and-commandfor/on-dialog-behavior.html +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/the-button-element/command-and-commandfor/on-dialog-behavior.html @@ -135,6 +135,69 @@ `invoking to close (with command ${setType} as ${command}) open dialog closes and sets returnValue`, ); + test( + function (t) { + t.add_cleanup(resetState); + invokee.show(); + assert_true(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + if (setType === "property") { + containedinvoker.command = command; + } else { + containedinvoker.setAttribute("command", command); + } + invokee.returnValue = "test"; + containedinvoker.setAttribute("value", "foo"); + containedinvoker.click(); + assert_equals(invokee.returnValue, "foo"); + assert_false(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + }, + `invoking to close (with command ${setType} as ${command}) open dialog closes and overrides returnValue`, + ); + + test( + function (t) { + t.add_cleanup(resetState); + invokee.show(); + assert_true(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + if (setType === "property") { + containedinvoker.command = command; + } else { + containedinvoker.setAttribute("command", command); + } + invokee.returnValue = "test"; + containedinvoker.setAttribute("value", ""); + containedinvoker.click(); + assert_equals(invokee.returnValue, ""); + assert_false(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + }, + `invoking to close (with command ${setType} as ${command}) open dialog closes and overrides returnValue when empty string`, + ); + + test( + function (t) { + t.add_cleanup(resetState); + invokee.show(); + assert_true(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + if (setType === "property") { + containedinvoker.command = command; + } else { + containedinvoker.setAttribute("command", command); + } + invokee.returnValue = "test"; + containedinvoker.removeAttribute("value"); + containedinvoker.click(); + assert_equals(invokee.returnValue, "test"); + assert_false(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + }, + `invoking to close (with command ${setType} as ${command}) open dialog closes and doesn't override returnValue when missing value attribute`, + ); + test( function (t) { t.add_cleanup(resetState); @@ -231,6 +294,208 @@ }); }); + // request to close an already open dialog + + ["request-close", /* test case sensitivity */ "reQuEst-Close"].forEach((command) => { + ["property", "attribute"].forEach((setType) => { + test( + function (t) { + t.add_cleanup(resetState); + invokee.show(); + assert_true(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + if (setType === "property") { + containedinvoker.command = command; + } else { + containedinvoker.setAttribute("command", command); + } + containedinvoker.click(); + assert_equals(invokee.returnValue, ""); + assert_false(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + }, + `invoking to request-close (with command ${setType} as ${command}) open dialog closes`, + ); + + test( + function (t) { + t.add_cleanup(resetState); + invokee.show(); + assert_true(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + if (setType === "property") { + containedinvoker.command = command; + } else { + containedinvoker.setAttribute("command", command); + } + containedinvoker.setAttribute("value", "foo"); + containedinvoker.click(); + assert_equals(invokee.returnValue, "foo"); + assert_false(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + }, + `invoking to request-close with value (with command ${setType} as ${command}) open dialog closes and sets returnValue`, + ); + + test( + function (t) { + t.add_cleanup(resetState); + invokee.show(); + assert_true(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + if (setType === "property") { + containedinvoker.command = command; + } else { + containedinvoker.setAttribute("command", command); + } + invokee.returnValue = "test"; + containedinvoker.setAttribute("value", "foo"); + containedinvoker.click(); + assert_equals(invokee.returnValue, "foo"); + assert_false(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + }, + `invoking to request-close with value (with command ${setType} as ${command}) open dialog closes and overrides returnValue`, + ); + + test( + function (t) { + t.add_cleanup(resetState); + invokee.show(); + assert_true(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + if (setType === "property") { + containedinvoker.command = command; + } else { + containedinvoker.setAttribute("command", command); + } + invokee.returnValue = "test"; + containedinvoker.setAttribute("value", ""); + containedinvoker.click(); + assert_equals(invokee.returnValue, ""); + assert_false(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + }, + `invoking to request-close with value (with command ${setType} as ${command}) open dialog closes and overrides returnValue when empty string`, + ); + + test( + function (t) { + t.add_cleanup(resetState); + invokee.show(); + assert_true(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + if (setType === "property") { + containedinvoker.command = command; + } else { + containedinvoker.setAttribute("command", command); + } + invokee.returnValue = "test"; + containedinvoker.removeAttribute("value"); + containedinvoker.click(); + assert_equals(invokee.returnValue, "test"); + assert_false(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + }, + `invoking to request-close with value (with command ${setType} as ${command}) open dialog closes and doesn't override returnValue when missing value attribute`, + ); + + test( + function (t) { + t.add_cleanup(resetState); + invokee.show(); + assert_true(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + if (typeof command === "string") { + if (setType === "property") { + containedinvoker.command = command; + } else { + containedinvoker.setAttribute("command", command); + } + } + invokee.addEventListener("command", (e) => e.preventDefault(), { + once: true, + }); + containedinvoker.click(); + assert_true(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + }, + `invoking to request-close (with command ${setType} as ${command}) open dialog with preventDefault is no-op`, + ); + + test( + function (t) { + t.add_cleanup(resetState); + invokee.showModal(); + assert_true(invokee.open, "invokee.open"); + assert_true(invokee.matches(":modal"), "invokee :modal"); + if (setType === "property") { + containedinvoker.command = command; + } else { + containedinvoker.setAttribute("command", command); + } + invokee.addEventListener("command", (e) => e.preventDefault(), { + once: true, + }); + containedinvoker.click(); + assert_true(invokee.open, "invokee.open"); + assert_true(invokee.matches(":modal"), "invokee :modal"); + }, + `invoking to request-close (with command ${setType} as ${command}) open modal dialog with preventDefault is no-op`, + ); + + test( + function (t) { + t.add_cleanup(resetState); + invokee.show(); + assert_true(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + if (setType === "property") { + containedinvoker.command = command; + } else { + containedinvoker.setAttribute("command", command); + } + invokee.addEventListener( + "command", + (e) => { + containedinvoker.setAttribute("command", "show"); + }, + { once: true }, + ); + containedinvoker.click(); + assert_false(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + }, + `invoking to request-close (with command ${setType} as ${command}) open dialog while changing command still closes`, + ); + + test( + function (t) { + t.add_cleanup(resetState); + invokee.showModal(); + assert_true(invokee.open, "invokee.open"); + assert_true(invokee.matches(":modal"), "invokee :modal"); + if (setType === "property") { + containedinvoker.command = command; + } else { + containedinvoker.setAttribute("command", command); + } + invokee.addEventListener( + "command", + (e) => { + containedinvoker.setAttribute("command", "show"); + }, + { once: true }, + ); + containedinvoker.click(); + assert_false(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + }, + `invoking to request-close (with command ${setType} as ${command}) open modal dialog while changing command still closes`, + ); + }); + }); + // show-modal explicit behaviours promise_test(async function (t) { @@ -285,8 +550,21 @@ assert_false(invokee.matches(":modal"), "invokee :modal"); }, "invoking (as close) already closed dialog is noop"); + + // request-close explicit behaviours + + promise_test(async function (t) { + t.add_cleanup(resetState); + invokerbutton.setAttribute("command", "request-close"); + assert_false(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + containedinvoker.click(); + assert_false(invokee.open, "invokee.open"); + assert_false(invokee.matches(":modal"), "invokee :modal"); + }, "invoking (as request-close) already closed dialog is noop"); + // Open Popovers using Dialog actions - ["show-modal", "close"].forEach((command) => { + ["show-modal", "close", "request-close"].forEach((command) => { ["manual", "auto"].forEach((popoverState) => { test( function (t) { @@ -317,7 +595,7 @@ }); // Elements being disconnected during invoke steps - ["show-modal", "close"].forEach((command) => { + ["show-modal", "close", "request-close"].forEach((command) => { promise_test( async function (t) { t.add_cleanup(() => {