LibWeb/HTML: Dispatch command events

Command events are now dispatched when buttons are activated. The
special commands for popovers and dialogs are also implemented.
This commit is contained in:
Glenn Skrzypczak 2025-04-04 14:30:56 +02:00
parent 9e31ee3778
commit 8f0b967c05
14 changed files with 1037 additions and 4 deletions

View file

@ -26,6 +26,7 @@ interface mixin GlobalEventHandlers {
attribute EventHandler onchange;
attribute EventHandler onclick;
attribute EventHandler onclose;
attribute EventHandler oncommand;
attribute EventHandler oncontextlost;
attribute EventHandler oncontextmenu;
attribute EventHandler oncontextrestored;

View file

@ -151,6 +151,7 @@ namespace AttributeNames {
__ENUMERATE_HTML_ATTRIBUTE(onchange, "onchange") \
__ENUMERATE_HTML_ATTRIBUTE(onclick, "onclick") \
__ENUMERATE_HTML_ATTRIBUTE(onclose, "onclose") \
__ENUMERATE_HTML_ATTRIBUTE(oncommand, "oncommand") \
__ENUMERATE_HTML_ATTRIBUTE(oncontextlost, "oncontextlost") \
__ENUMERATE_HTML_ATTRIBUTE(oncontextmenu, "oncontextmenu") \
__ENUMERATE_HTML_ATTRIBUTE(oncontextrestored, "oncontextrestored") \

View file

@ -31,6 +31,7 @@ namespace Web::HTML::EventNames {
__ENUMERATE_HTML_EVENT(change) \
__ENUMERATE_HTML_EVENT(click) \
__ENUMERATE_HTML_EVENT(close) \
__ENUMERATE_HTML_EVENT(command) \
__ENUMERATE_HTML_EVENT(complete) \
__ENUMERATE_HTML_EVENT(connect) \
__ENUMERATE_HTML_EVENT(contextlost) \

View file

@ -22,6 +22,7 @@
E(onchange, HTML::EventNames::change) \
E(onclick, UIEvents::EventNames::click) \
E(onclose, HTML::EventNames::close) \
E(oncommand, HTML::EventNames::command) \
E(oncontextlost, HTML::EventNames::contextlost) \
E(oncontextmenu, HTML::EventNames::contextmenu) \
E(oncontextrestored, HTML::EventNames::contextrestored) \

View file

@ -7,8 +7,10 @@
#include <LibWeb/Bindings/HTMLButtonElementPrototype.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/HTML/CommandEvent.h>
#include <LibWeb/HTML/HTMLButtonElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/Namespace.h>
namespace Web::HTML {
@ -153,12 +155,111 @@ void HTMLButtonElement::activation_behavior(DOM::Event const& event)
return;
}
// FIXME: 4. Let target be the result of running element's get the commandfor associated element.
// FIXME: 5. If target is not null:
// ...
// 4. Let target be the result of running element's get the commandfor associated element.
// AD-HOC: Target needs to be an HTML Element in the following steps.
GC::Ptr<HTMLElement> target = as_if<HTMLElement>(m_command_for_element.ptr());
if (!target) {
auto target_id = attribute(AttributeNames::commandfor);
if (target_id.has_value()) {
root().for_each_in_inclusive_subtree_of_type<HTMLElement>([&](auto& candidate) {
if (candidate.attribute(HTML::AttributeNames::id) == target_id.value()) {
target = &candidate;
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
});
}
}
// 5. If target is not null:
if (target) {
// 1. Let command be element's command attribute.
auto command = this->command();
// 2. If command is in the Unknown state, then return.
if (command.is_empty()) {
return;
}
// 3. Let isPopover be true if target's popover attribute is not in the no popover state; otherwise false.
auto is_popover = target->popover().has_value();
// 4. If isPopover is false and command is not in the Custom state:
auto command_is_in_custom_state = command.starts_with_bytes("--"sv);
if (!is_popover && !command.starts_with_bytes("--"sv)) {
// 1. Assert: target's namespace is the HTML namespace.
VERIFY(target->namespace_uri() == Namespace::HTML);
// 2. If this standard does not define is valid invoker command steps for target's local name, then return.
// 3. Otherwise, if the result of running target's corresponding is valid invoker command steps given command is false, then return.
if (!target->is_valid_invoker_command(command))
return;
}
// 5. Let continue be the result of firing an event named command at target, using CommandEvent, with its command attribute initialized to command, its source attribute initialized to element, and its cancelable and composed attributes initialized to true.
// SPEC-NOTE: DOM standard issue #1328 tracks how to better standardize associated event data in a way which makes sense on Events. Currently an event attribute initialized to a value cannot also have a getter, and so an internal slot (or map of additional fields) is required to properly specify this.
CommandEventInit event_init {};
event_init.command = command;
event_init.source = this;
event_init.cancelable = true;
event_init.composed = true;
auto event = CommandEvent::create(realm(), HTML::EventNames::command, move(event_init));
event->set_is_trusted(true);
auto continue_ = target->dispatch_event(event);
// 6. If continue is false, then return.
if (!continue_)
return;
// 7. If target is not connected, then return.
if (!target->is_connected())
return;
// 8. If command is in the Custom state, then return.
if (command_is_in_custom_state)
return;
// AD-HOC: The parameters provided in the spec do not match the function signatures in the following steps.
// The inconsistent parameters were therefore selected ad hoc.
// 9. If command is in the Hide Popover state:
if (command == "hide-popover") {
// 1. If the result of running check popover validity given target, true, false, and null is true, then run the hide popover algorithm given target, true, true, and false.
if (MUST(target->check_popover_validity(ExpectedToBeShowing::Yes, ThrowExceptions::No, nullptr, IgnoreDomState::No))) {
MUST(target->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No));
}
}
// 10. Otherwise, if command is in the Toggle Popover state:
else if (command == "toggle-popover") {
// 1. If the result of running check popover validity given target, false, false, and null is true, then run the show popover algorithm given target, true, true, and false.
if (MUST(target->check_popover_validity(ExpectedToBeShowing::No, ThrowExceptions::No, nullptr, IgnoreDomState::No))) {
MUST(target->show_popover(ThrowExceptions::No, this));
}
// 2. Otheriwse, if the result of running check popover validity given target, true, false, and null is true, then run the hide popover algorithm given target, true, true, and false.
else if (MUST(target->check_popover_validity(ExpectedToBeShowing::Yes, ThrowExceptions::No, nullptr, IgnoreDomState::No))) {
MUST(target->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No));
}
}
// 11. Otherwise, if command is in the Show Popover state:
else if (command == "show-popover") {
// 1. If the result of running check popover validity given target, false, false, and null is true, then run the show popover algorithm given target, true, true, and false.
if (MUST(target->check_popover_validity(ExpectedToBeShowing::No, ThrowExceptions::No, nullptr, IgnoreDomState::No))) {
MUST(target->show_popover(ThrowExceptions::No, this));
}
}
// 12. Otherwise, if this standard defines invoker command steps for target's local name, then run the corresponding invoker command steps given target, element and command.
else {
target->invoker_command_steps(*this, command);
}
}
// 6. Otherwise, run the popover target attribute activation behavior given element and event's target.
if (event.target() && event.target()->is_dom_node())
else if (event.target() && event.target()->is_dom_node())
PopoverInvokerElement::popover_target_activation_behaviour(*this, as<DOM::Node>(*event.target()));
}

View file

@ -443,4 +443,39 @@ void HTMLDialogElement::set_is_modal(bool is_modal)
invalidate_style(DOM::StyleInvalidationReason::NodeRemove);
}
// 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")
return true;
// 2. Return false.
return false;
}
// https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element:invoker-command-steps
void HTMLDialogElement::invoker_command_steps(DOM::Element& invoker, String& command)
{
// 1. If element is in the popover showing state, then return.
if (popover_visibility_state() == PopoverVisibilityState::Showing) {
return;
}
// 2. If command is in the Close state and element has an open attribute:
if (command == "close" && has_attribute(AttributeNames::open)) {
// 1. Let value be invoker's value.
// FIXME: This assumes invoker is a button.
auto value = invoker.get_attribute(AttributeNames::value);
// 2. Close the dialog element with value.
close_the_dialog(value);
}
// 3. If command is the Show Modal state and element does not have an open attribute, then show a modal dialog given element.
if (command == "show-modal" && !has_attribute(AttributeNames::open)) {
MUST(show_a_modal_dialog(*this));
}
}
}

View file

@ -39,6 +39,9 @@ public:
bool is_modal() const { return m_is_modal; }
void set_is_modal(bool);
bool is_valid_invoker_command(String&) override;
void invoker_command_steps(DOM::Element&, String&) override;
private:
HTMLDialogElement(DOM::Document&, DOM::QualifiedName);

View file

@ -151,6 +151,9 @@ public:
bool is_inert() const { return m_inert; }
virtual bool is_valid_invoker_command(String&) { return false; }
virtual void invoker_command_steps(DOM::Element&, String&) { }
protected:
HTMLElement(DOM::Document&, DOM::QualifiedName);

View file

@ -0,0 +1,45 @@
Harness status: OK
Found 39 tests
38 Pass
1 Fail
Pass event dispatches on click with addEventListener
Pass event dispatches on click with oncommand property
Pass setting custom command property to --foo (must include dash) sets event command
Pass setting custom command attribute to --foo (must include dash) sets event command
Pass setting custom command property to --foo- (must include dash) sets event command
Pass setting custom command attribute to --foo- (must include dash) sets event command
Pass setting custom command property to --cAsE-cArRiEs (must include dash) sets event command
Pass setting custom command attribute to --cAsE-cArRiEs (must include dash) sets event command
Pass setting custom command property to -- (must include dash) sets event command
Pass setting custom command attribute to -- (must include dash) sets event command
Pass setting custom command property to --a- (must include dash) sets event command
Pass setting custom command attribute to --a- (must include dash) sets event command
Pass setting custom command property to --a-b (must include dash) sets event command
Pass setting custom command attribute to --a-b (must include dash) sets event command
Pass setting custom command property to --- (must include dash) sets event command
Pass setting custom command attribute to --- (must include dash) sets event command
Pass setting custom command property to --show-picker (must include dash) sets event command
Pass setting custom command attribute to --show-picker (must include dash) sets event command
Pass setting custom command property to -foo (no dash) did not dispatch an event
Pass setting custom command attribute to -foo (no dash) did not dispatch an event
Pass setting custom command property to -foo- (no dash) did not dispatch an event
Pass setting custom command attribute to -foo- (no dash) did not dispatch an event
Pass setting custom command property to foo-bar (no dash) did not dispatch an event
Pass setting custom command attribute to foo-bar (no dash) did not dispatch an event
Pass setting custom command property to -foo bar (no dash) did not dispatch an event
Pass setting custom command attribute to -foo bar (no dash) did not dispatch an event
Pass setting custom command property to —-emdash (no dash) did not dispatch an event
Pass setting custom command attribute to —-emdash (no dash) did not dispatch an event
Pass setting custom command property to hidedocument (no dash) did not dispatch an event
Pass setting custom command attribute to hidedocument (no dash) did not dispatch an event
Pass event does not dispatch if click:preventDefault is called
Pass event does not dispatch on input[type=button]
Pass event does not dispatch if invoker is disabled
Pass event does NOT dispatch if button is form associated, with implicit type
Pass event does NOT dispatch if button is form associated, with explicit type=invalid
Pass event dispatches if button is form associated, with explicit type=button
Pass event does NOT dispatch if button is form associated, with explicit type=submit
Pass event does NOT dispatch if button is form associated, with explicit type=reset
Fail event dispatches if invokee is non-HTML Element

View file

@ -0,0 +1,56 @@
Harness status: OK
Found 50 tests
48 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
Pass invoking (with command property as show-modal) while changing command still opens as modal
Pass invoking (with command attribute as show-modal) closed dialog opens as modal
Pass invoking (with command attribute as show-modal) closed dialog with preventDefault is noop
Pass invoking (with command attribute as show-modal) while changing command still opens as modal
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
Pass invoking (with command property as sHoW-mOdAl) while changing command still opens as modal
Pass invoking (with command attribute as sHoW-mOdAl) closed dialog opens as modal
Pass invoking (with command attribute as sHoW-mOdAl) closed dialog with preventDefault is noop
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 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 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 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 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 (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 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 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

View file

@ -0,0 +1,32 @@
Harness status: OK
Found 27 tests
27 Pass
Pass changing command attribute inside invokeevent doesn't impact the invocation
Pass invoking (as toggle-popover) closed popover opens
Pass invoking (as toggle-popover) closed popover with preventDefault does not open
Pass invoking (as show-popover) closed popover opens
Pass invoking (as show-popover) closed popover with preventDefault does not open
Pass invoking (as tOgGlE-pOpOvEr) closed popover opens
Pass invoking (as tOgGlE-pOpOvEr) closed popover with preventDefault does not open
Pass invoking (as sHoW-pOpOvEr) closed popover opens
Pass invoking (as sHoW-pOpOvEr) closed popover with preventDefault does not open
Pass invoking (as toggle-popover) open popover closes
Pass invoking (as toggle-popover) open popover with preventDefault does not close
Pass invoking (as toggle-popover) from within open popover closes
Pass invoking (as toggle-popover) from within open popover with preventDefault does not close
Pass invoking (as hide-popover) open popover closes
Pass invoking (as hide-popover) open popover with preventDefault does not close
Pass invoking (as hide-popover) from within open popover closes
Pass invoking (as hide-popover) from within open popover with preventDefault does not close
Pass invoking (as tOgGlE-pOpOvEr) open popover closes
Pass invoking (as tOgGlE-pOpOvEr) open popover with preventDefault does not close
Pass invoking (as tOgGlE-pOpOvEr) from within open popover closes
Pass invoking (as tOgGlE-pOpOvEr) from within open popover with preventDefault does not close
Pass invoking (as hIdE-pOpOvEr) open popover closes
Pass invoking (as hIdE-pOpOvEr) open popover with preventDefault does not close
Pass invoking (as hIdE-pOpOvEr) from within open popover closes
Pass invoking (as hIdE-pOpOvEr) from within open popover with preventDefault does not close
Pass invoking (as show-popover) open popover is noop
Pass invoking (as hide-popover) closed popover is noop

View file

@ -0,0 +1,226 @@
<!doctype html>
<meta charset="utf-8" />
<meta name="author" title="Keith Cirkel" href="mailto:wpt@keithcirkel.co.uk" />
<meta name="timeout" content="long" />
<link rel="help" href="https://open-ui.org/components/invokers.explainer/" />
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="resources/invoker-utils.js"></script>
<div id="invokee"></div>
<button id="invokerbutton" commandfor="invokee" command="--custom-command"></button>
<input type="button" id="invalidbutton" commandfor="invokee" command="--custom-command">
<form id="aform"></form>
<script>
aform.addEventListener('submit', (e) => (e.preventDefault()));
function resetState() {
invokerbutton.setAttribute("commandfor", "invokee");
invokerbutton.setAttribute("command", "--custom-command");
invokerbutton.removeAttribute("disabled");
invokerbutton.removeAttribute("form");
invokerbutton.removeAttribute("type");
}
test(function (t) {
let event = null;
invokee.addEventListener("command", (e) => (event = e), { once: true });
invokerbutton.click();
assert_true(event instanceof CommandEvent, "event is CommandEvent");
assert_equals(event.type, "command", "type");
assert_equals(event.bubbles, false, "bubbles");
assert_equals(event.composed, true, "composed");
assert_equals(event.isTrusted, true, "isTrusted");
assert_equals(event.command, "--custom-command", "command");
assert_equals(event.target, invokee, "target");
assert_equals(event.source, invokerbutton, "source");
}, "event dispatches on click with addEventListener");
test(function (t) {
let event = null;
t.add_cleanup(() => {
invokee.oncommand = null;
});
invokee.oncommand = (e) => (event = e);
invokerbutton.click();
assert_true(event instanceof CommandEvent, "event is CommandEvent");
assert_equals(event.type, "command", "type");
assert_equals(event.bubbles, false, "bubbles");
assert_equals(event.composed, true, "composed");
assert_equals(event.isTrusted, true, "isTrusted");
assert_equals(event.command, "--custom-command", "command");
assert_equals(event.target, invokee, "target");
assert_equals(event.source, invokerbutton, "source");
}, "event dispatches on click with oncommand property");
// valid custom invokeactions
["--foo", "--foo-", "--cAsE-cArRiEs", "--", "--a-", "--a-b", "---", "--show-picker"].forEach(
(command) => {
test(function (t) {
t.add_cleanup(resetState);
let event = null;
invokee.addEventListener("command", (e) => (event = e), { once: true });
invokerbutton.command = command;
invokerbutton.click();
assert_true(event instanceof CommandEvent, "event is CommandEvent");
assert_equals(event.type, "command", "type");
assert_equals(event.bubbles, false, "bubbles");
assert_equals(event.composed, true, "composed");
assert_equals(event.isTrusted, true, "isTrusted");
assert_equals(event.command, command, "command");
assert_equals(event.target, invokee, "target");
assert_equals(event.source, invokerbutton, "source");
}, `setting custom command property to ${command} (must include dash) sets event command`);
test(function (t) {
t.add_cleanup(resetState);
let event = null;
invokee.addEventListener("command", (e) => (event = e), { once: true });
invokerbutton.setAttribute("command", command);
invokerbutton.click();
assert_true(event instanceof CommandEvent, "event is CommandEvent");
assert_equals(event.type, "command", "type");
assert_equals(event.bubbles, false, "bubbles");
assert_equals(event.composed, true, "composed");
assert_equals(event.isTrusted, true, "isTrusted");
assert_equals(event.command, command, "command");
assert_equals(event.target, invokee, "target");
assert_equals(event.source, invokerbutton, "source");
}, `setting custom command attribute to ${command} (must include dash) sets event command`);
},
);
// invalid custom invokeactions
["-foo", "-foo-", "foo-bar", "-foo bar", "—-emdash", "hidedocument"].forEach((command) => {
test(function (t) {
t.add_cleanup(resetState);
let event = null;
invokee.addEventListener("command", (e) => (event = e), { once: true });
invokerbutton.command = command;
invokerbutton.click();
assert_equals(event, null, "event should not have fired");
}, `setting custom command property to ${command} (no dash) did not dispatch an event`);
test(function (t) {
t.add_cleanup(resetState);
let event = null;
invokee.addEventListener("command", (e) => (event = e), { once: true });
invokerbutton.setAttribute("command", command);
invokerbutton.click();
assert_equals(event, null, "event should not have fired");
}, `setting custom command attribute to ${command} (no dash) did not dispatch an event`);
});
test(function (t) {
let called = false;
invokerbutton.addEventListener(
"click",
(event) => {
event.preventDefault();
},
{ once: true },
);
invokee.addEventListener(
"command",
(event) => {
called = true;
},
{ once: true },
);
invokerbutton.click();
assert_false(called, "event was not called");
}, "event does not dispatch if click:preventDefault is called");
test(function (t) {
let event = null;
invokee.addEventListener("command", (e) => (event = e), { once: true });
invalidbutton.click();
assert_equals(event, null, "command should not have fired");
}, "event does not dispatch on input[type=button]");
test(function (t) {
t.add_cleanup(resetState);
let called = false;
invokee.addEventListener("command", (e) => (called = true), { once: true });
invokerbutton.setAttribute("disabled", "");
invokerbutton.click();
assert_false(called, "event was not called");
}, "event does not dispatch if invoker is disabled");
test(function (t) {
t.add_cleanup(resetState);
let called = false;
invokee.addEventListener("command", (e) => (called = true), { once: true });
invokerbutton.setAttribute("form", "aform");
invokerbutton.removeAttribute("type");
invokerbutton.click();
assert_false(called, "event was not called");
}, "event does NOT dispatch if button is form associated, with implicit type");
test(function (t) {
t.add_cleanup(resetState);
let called = false;
invokee.addEventListener("command", (e) => (called = true), { once: true });
invokerbutton.setAttribute("form", "aform");
invokerbutton.setAttribute("type", "invalid");
invokerbutton.click();
assert_false(called, "event was not called");
}, "event does NOT dispatch if button is form associated, with explicit type=invalid");
test(function (t) {
t.add_cleanup(resetState);
let event;
invokee.addEventListener("command", (e) => (event = e), { once: true });
invokerbutton.setAttribute("form", "aform");
invokerbutton.setAttribute("type", "button");
invokerbutton.click();
assert_true(event instanceof CommandEvent, "event is CommandEvent");
assert_equals(event.type, "command", "type");
assert_equals(event.bubbles, false, "bubbles");
assert_equals(event.composed, true, "composed");
assert_equals(event.isTrusted, true, "isTrusted");
assert_equals(event.command, "--custom-command", "command");
assert_equals(event.target, invokee, "target");
assert_equals(event.source, invokerbutton, "source");
}, "event dispatches if button is form associated, with explicit type=button");
test(function (t) {
t.add_cleanup(resetState);
let called = false;
invokee.addEventListener("command", (e) => (called = true), { once: true });
invokerbutton.setAttribute("form", "aform");
invokerbutton.setAttribute("type", "submit");
invokerbutton.click();
assert_false(called, "event was not called");
}, "event does NOT dispatch if button is form associated, with explicit type=submit");
test(function (t) {
t.add_cleanup(resetState);
let called = false;
invokee.addEventListener("command", (e) => (called = true), { once: true });
invokerbutton.setAttribute("form", "aform");
invokerbutton.setAttribute("type", "reset");
invokerbutton.click();
assert_false(called, "event was called");
}, "event does NOT dispatch if button is form associated, with explicit type=reset");
test(function (t) {
svgInvokee = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgInvokee.setAttribute("id", "svg-invokee");
t.add_cleanup(resetState);
document.body.append(svgInvokee);
assert_false(svgInvokee instanceof HTMLElement);
assert_true(svgInvokee instanceof Element);
let event = null;
svgInvokee.addEventListener("command", (e) => (event = e), { once: true });
invokerbutton.setAttribute("commandfor", "svg-invokee");
invokerbutton.setAttribute("command", "--custom-command");
assert_equals(invokerbutton.commandForElement, svgInvokee);
invokerbutton.click();
assert_not_equals(event, null, "event was called");
assert_true(event instanceof CommandEvent, "event is CommandEvent");
assert_equals(event.source, invokerbutton, "event.invoker is set to right element");
assert_equals(event.target, svgInvokee, "event.target is set to right element");
}, "event dispatches if invokee is non-HTML Element");
</script>

View file

@ -0,0 +1,376 @@
<!doctype html>
<meta charset="utf-8" />
<meta name="author" title="Keith Cirkel" href="mailto:wpt@keithcirkel.co.uk" />
<meta name="timeout" content="long">
<link rel="help" href="https://open-ui.org/components/invokers.explainer/" />
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="resources/invoker-utils.js"></script>
<dialog id="invokee">
<button id="containedinvoker" commandfor="invokee" command="close"></button>
</dialog>
<button id="invokerbutton" commandfor="invokee" command="show-modal"></button>
<script>
function resetState() {
invokee.close();
try { invokee.hidePopover(); } catch {}
invokee.removeAttribute("popover");
invokee.returnValue = '';
invokerbutton.setAttribute("command", "show-modal");
containedinvoker.setAttribute("command", "close");
containedinvoker.removeAttribute("value");
}
// opening a dialog
["show-modal", /* test case sensitivity */ "sHoW-mOdAl"].forEach(
(command) => {
["property", "attribute"].forEach((setType) => {
test(
function (t) {
t.add_cleanup(resetState);
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
if (setType === "property") {
invokerbutton.command = command;
} else {
invokerbutton.setAttribute("command", command);
}
invokerbutton.click();
assert_true(invokee.open, "invokee.open");
assert_true(invokee.matches(":modal"), "invokee :modal");
},
`invoking (with command ${setType} as ${command}) closed dialog opens as modal`,
);
test(
function (t) {
t.add_cleanup(resetState);
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
invokee.addEventListener("command", (e) => e.preventDefault(), {
once: true,
});
if (setType === "property") {
invokerbutton.command = command;
} else {
invokerbutton.setAttribute("command", command);
}
invokerbutton.click();
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
},
`invoking (with command ${setType} as ${command}) closed dialog with preventDefault is noop`,
);
test(
function (t) {
t.add_cleanup(resetState);
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
invokee.addEventListener(
"command",
(e) => {
invokerbutton.setAttribute("command", "close");
},
{ once: true },
);
if (setType === "property") {
invokerbutton.command = command;
} else {
invokerbutton.setAttribute("command", command);
}
invokerbutton.click();
assert_true(invokee.open, "invokee.open");
assert_true(invokee.matches(":modal"), "invokee :modal");
},
`invoking (with command ${setType} as ${command}) while changing command still opens as modal`,
);
});
},
);
// closing an already open dialog
["close", /* test case sensitivity */ "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 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 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 (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 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 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 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 close (with command ${setType} as ${command}) open modal dialog while changing command still closes`,
);
});
});
// show-modal explicit behaviours
promise_test(async function (t) {
t.add_cleanup(resetState);
containedinvoker.setAttribute("command", "show-Modal");
invokee.show();
assert_true(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
containedinvoker.click();
assert_true(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
}, "invoking (as show-modal) open dialog is noop");
promise_test(async function (t) {
t.add_cleanup(resetState);
containedinvoker.setAttribute("command", "show-modal");
invokee.showModal();
assert_true(invokee.open, "invokee.open");
assert_true(invokee.matches(":modal"), "invokee :modal");
invokee.addEventListener(
"command",
(e) => {
containedinvoker.setAttribute("command", "close");
},
{ once: true },
);
invokerbutton.click();
assert_true(invokee.open, "invokee.open");
assert_true(invokee.matches(":modal"), "invokee :modal");
}, "invoking (as show-modal) open modal, while changing command still a no-op");
promise_test(async function (t) {
t.add_cleanup(resetState);
invokerbutton.setAttribute("command", "show-modal");
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
invokee.setAttribute("popover", "auto");
invokerbutton.click();
assert_true(invokee.open, "invokee.open");
assert_true(invokee.matches(":modal"), "invokee :modal");
}, "invoking (as show-modal) closed popover dialog opens as modal");
// close explicit behaviours
promise_test(async function (t) {
t.add_cleanup(resetState);
invokerbutton.setAttribute("command", "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 close) already closed dialog is noop");
// Open Popovers using Dialog actions
["show-modal", "close"].forEach((command) => {
["manual", "auto"].forEach((popoverState) => {
test(
function (t) {
t.add_cleanup(resetState);
invokee.setAttribute("popover", popoverState);
invokee.showPopover();
containedinvoker.setAttribute("command", command);
assert_true(
invokee.matches(":popover-open"),
"invokee :popover-open",
);
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
invokee.addEventListener("command", (e) => e.preventDefault(), {
once: true,
});
containedinvoker.click();
assert_true(
invokee.matches(":popover-open"),
"invokee :popover-open",
);
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
},
`invoking (as ${command}) dialog as open popover=${popoverState} is noop`,
);
});
});
// Elements being disconnected during invoke steps
["show-modal", "close"].forEach((command) => {
promise_test(
async function (t) {
t.add_cleanup(() => {
document.body.prepend(invokee);
resetState();
});
const invokee = document.querySelector("#invokee");
invokerbutton.setAttribute("command", command);
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
invokee.addEventListener(
"command",
(e) => {
invokee.remove();
},
{
once: true,
},
);
invokerbutton.click();
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
},
`invoking (as ${command}) dialog that is removed is noop`,
);
promise_test(
async function (t) {
const invokerbutton = document.createElement("button");
invokerbutton.commandForElement = invokee;
invokerbutton.setAttribute("command", command);
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
invokerbutton.click();
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
},
`invoking (as ${command}) dialog from a detached invoker`,
);
promise_test(
async function (t) {
const invokerbutton = document.createElement("button");
const invokee = document.createElement("dialog");
invokerbutton.commandForElementt = invokee;
invokerbutton.setAttribute("command", command);
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
invokerbutton.click();
assert_false(invokee.open, "invokee.open");
assert_false(invokee.matches(":modal"), "invokee :modal");
},
`invoking (as ${command}) detached dialog from a detached invoker`,
);
});
</script>

View file

@ -0,0 +1,152 @@
<!doctype html>
<meta charset="utf-8" />
<meta name="author" title="Keith Cirkel" href="mailto:wpt@keithcirkel.co.uk" />
<meta name="timeout" content="long" />
<link rel="help" href="https://open-ui.org/components/invokers.explainer/" />
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script src="resources/invoker-utils.js"></script>
<div id="invokee" popover>
<button id="containedinvoker" commandfor="invokee" command="hide-popover"></button>
</div>
<button id="invokerbutton" commandfor="invokee" command="toggle-popover"></button>
<script>
function resetState() {
invokerbutton.setAttribute("commandfor", "invokee");
invokerbutton.setAttribute("command", "toggle-popover");
containedinvoker.setAttribute("commandfor", "invokee");
containedinvoker.setAttribute("command", "hide-popover");
try {
invokee.hidePopover();
} catch {}
invokee.setAttribute("popover", "");
}
promise_test(async function (t) {
assert_false(invokee.matches(":popover-open"));
invokee.addEventListener("command", (e) => { invokerbutton.setAttribute('command', 'hide-popover'); }, {
once: true,
});
invokerbutton.click();
t.add_cleanup(resetState);
assert_true(invokee.matches(":popover-open"));
}, "changing command attribute inside invokeevent doesn't impact the invocation");
// Open actions
[
"toggle-popover",
"show-popover",
/* test case sensitivity */
"tOgGlE-pOpOvEr",
"sHoW-pOpOvEr",
].forEach((command) => {
test(
function (t) {
t.add_cleanup(resetState);
invokerbutton.command = command;
assert_false(invokee.matches(":popover-open"));
invokerbutton.click();
assert_true(invokee.matches(":popover-open"));
},
`invoking (as ${command}) closed popover opens`,
);
test(
function (t) {
t.add_cleanup(resetState);
invokerbutton.command = command;
assert_false(invokee.matches(":popover-open"));
invokee.addEventListener("command", (e) => e.preventDefault(), {
once: true,
});
invokerbutton.click();
assert_false(invokee.matches(":popover-open"));
},
`invoking (as ${command}) closed popover with preventDefault does not open`,
);
});
// Close actions
[
"toggle-popover",
"hide-popover",
/* test case sensitivity */
"tOgGlE-pOpOvEr",
"hIdE-pOpOvEr",
].forEach((command) => {
test(
function (t) {
t.add_cleanup(resetState);
invokerbutton.command = command;
invokee.showPopover();
assert_true(invokee.matches(":popover-open"));
invokerbutton.click();
assert_false(invokee.matches(":popover-open"));
},
`invoking (as ${command}) open popover closes`,
);
test(
function (t) {
t.add_cleanup(resetState);
invokerbutton.command = command;
invokee.showPopover();
assert_true(invokee.matches(":popover-open"));
invokee.addEventListener("command", (e) => e.preventDefault(), {
once: true,
});
invokerbutton.click();
assert_true(invokee.matches(":popover-open"));
},
`invoking (as ${command}) open popover with preventDefault does not close`,
);
test(
function (t) {
t.add_cleanup(resetState);
containedinvoker.command = command;
invokee.showPopover();
assert_true(invokee.matches(":popover-open"));
containedinvoker.click();
assert_false(invokee.matches(":popover-open"));
},
`invoking (as ${command}) from within open popover closes`,
);
test(
function (t) {
t.add_cleanup(resetState);
containedinvoker.command = command;
invokee.showPopover();
invokee.addEventListener("command", (e) => e.preventDefault(), {
once: true,
});
assert_true(invokee.matches(":popover-open"));
containedinvoker.click();
assert_true(invokee.matches(":popover-open"));
},
`invoking (as ${command}) from within open popover with preventDefault does not close`,
);
});
// show-popover specific
test(function (t) {
t.add_cleanup(resetState);
invokerbutton.setAttribute("command", "show-popover");
invokee.showPopover();
assert_true(invokee.matches(":popover-open"));
invokerbutton.click();
assert_true(invokee.matches(":popover-open"));
}, "invoking (as show-popover) open popover is noop");
// hide-popover specific
test(function (t) {
t.add_cleanup(resetState);
invokerbutton.setAttribute("command", "hide-popover");
assert_false(invokee.matches(":popover-open"));
invokerbutton.click();
assert_false(invokee.matches(":popover-open"));
}, "invoking (as hide-popover) closed popover is noop");
</script>