LibWeb: Light dismiss dialogs on click
Some checks are pending
CI / Lagom (arm64, Sanitizer_CI, false, macos-15, macOS, Clang) (push) Waiting to run
CI / Lagom (x86_64, Fuzzers_CI, false, ubuntu-24.04, Linux, Clang) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, false, ubuntu-24.04, Linux, GNU) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, true, ubuntu-24.04, Linux, Clang) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (arm64, macos-15, macOS, macOS-universal2) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (x86_64, ubuntu-24.04, Linux, Linux-x86_64) (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

This commit is contained in:
Gingeh 2025-04-29 09:44:49 +10:00 committed by Andrew Kaster
parent 1f1884da54
commit aa9f556500
Notes: github-actions[bot] 2025-04-29 01:43:02 +00:00
9 changed files with 217 additions and 7 deletions

View file

@ -97,6 +97,7 @@
#include <LibWeb/HTML/HTMLAreaElement.h>
#include <LibWeb/HTML/HTMLBaseElement.h>
#include <LibWeb/HTML/HTMLBodyElement.h>
#include <LibWeb/HTML/HTMLDialogElement.h>
#include <LibWeb/HTML/HTMLDocument.h>
#include <LibWeb/HTML/HTMLEmbedElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>
@ -593,6 +594,8 @@ void Document::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_showing_auto_popover_list);
visitor.visit(m_showing_hint_popover_list);
visitor.visit(m_popover_pointerdown_target);
visitor.visit(m_open_dialogs_list);
visitor.visit(m_dialog_pointerdown_target);
visitor.visit(m_console_client);
visitor.visit(m_editing_host_manager);
visitor.visit(m_local_storage_holder);

View file

@ -801,6 +801,11 @@ public:
void set_popover_pointerdown_target(GC::Ptr<HTML::HTMLElement> target) { m_popover_pointerdown_target = target; }
GC::Ptr<HTML::HTMLElement> popover_pointerdown_target() { return m_popover_pointerdown_target; }
Vector<GC::Ref<HTML::HTMLDialogElement>>& open_dialogs_list() { return m_open_dialogs_list; }
void set_dialog_pointerdown_target(GC::Ptr<HTML::HTMLDialogElement> target) { m_dialog_pointerdown_target = target; }
GC::Ptr<HTML::HTMLDialogElement> dialog_pointerdown_target() { return m_dialog_pointerdown_target; }
size_t transition_generation() const { return m_transition_generation; }
// Does document represent an embedded svg img
@ -1209,6 +1214,10 @@ private:
Vector<GC::Ref<HTML::HTMLElement>> m_showing_hint_popover_list;
GC::Ptr<HTML::HTMLElement> m_popover_pointerdown_target;
Vector<GC::Ref<HTML::HTMLDialogElement>> m_open_dialogs_list;
GC::Ptr<HTML::HTMLDialogElement> m_dialog_pointerdown_target;
// https://dom.spec.whatwg.org/#document-allow-declarative-shadow-roots
bool m_allow_declarative_shadow_roots { false };

View file

@ -45,6 +45,7 @@ namespace AttributeNames {
__ENUMERATE_HTML_ATTRIBUTE(class_, "class") \
__ENUMERATE_HTML_ATTRIBUTE(classid, "classid") \
__ENUMERATE_HTML_ATTRIBUTE(clear, "clear") \
__ENUMERATE_HTML_ATTRIBUTE(closedby, "closedby") \
__ENUMERATE_HTML_ATTRIBUTE(code, "code") \
__ENUMERATE_HTML_ATTRIBUTE(codebase, "codebase") \
__ENUMERATE_HTML_ATTRIBUTE(codetype, "codetype") \

View file

@ -17,6 +17,8 @@
#include <LibWeb/HTML/HTMLDialogElement.h>
#include <LibWeb/HTML/ToggleEvent.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/UIEvents/EventNames.h>
#include <LibWeb/UIEvents/PointerEvent.h>
namespace Web::HTML {
@ -131,8 +133,12 @@ WebIDL::ExceptionOr<void> HTMLDialogElement::show()
// 6. Add an open attribute to this, whose value is the empty string.
TRY(set_attribute(AttributeNames::open, {}));
// FIXME: 7. Assert: this's node document's open dialogs list does not contain this.
// FIXME: 8. Add this to this's node document's open dialogs list.
// 7. Assert: this's node document's open dialogs list does not contain this.
VERIFY(!m_document->open_dialogs_list().contains_slow(GC::Ref(*this)));
// 8. Add this to this's node document's open dialogs list.
m_document->open_dialogs_list().append(*this);
// 9. Set the dialog close watcher with this.
set_close_watcher();
// FIXME: 10. Set this's previously focused element to the focused element.
@ -225,8 +231,14 @@ WebIDL::ExceptionOr<void> HTMLDialogElement::show_a_modal_dialog(HTMLDialogEleme
// 12. Set is modal of subject to true.
subject.set_is_modal(true);
// FIXME: 13. Assert: subject's node document's open dialogs list does not contain subject.
// FIXME: 14. Add subject to subject's node document's open dialogs list.
// 13. Assert: subject's node document's open dialogs list does not contain subject.
// AD-HOC: This assertion is skipped because it fails if the open attribute was removed before calling showModal()
// See https://github.com/whatwg/html/issues/10953 and https://github.com/whatwg/html/pull/10954
// VERIFY(!subject.document().open_dialogs_list().contains_slow(GC::Ref(subject)));
// 14. Add subject to subject's node document's open dialogs list.
subject.document().open_dialogs_list().append(subject);
// FIXME: 15. Let subject's node document be blocked by the modal dialog subject.
// 16. If subject's node document's top layer does not already contain subject, then add an element to the top layer given subject.
@ -340,7 +352,8 @@ void HTMLDialogElement::close_the_dialog(Optional<String> result)
// 8. Set the is modal flag of subject to false.
set_is_modal(false);
// FIXME: 9. Remove subject from subject's node document's open dialogs list.
// 9. Remove subject from subject's node document's open dialogs list.
document().open_dialogs_list().remove_first_matching([this](auto other) { return other == this; });
// 10. If result is not null, then set the returnValue attribute to result.
if (result.has_value())
@ -478,4 +491,92 @@ void HTMLDialogElement::invoker_command_steps(DOM::Element& invoker, String& com
}
}
// https://html.spec.whatwg.org/multipage/interactive-elements.html#nearest-clicked-dialog
GC::Ptr<HTMLDialogElement> HTMLDialogElement::nearest_clicked_dialog(UIEvents::PointerEvent const& event, const GC::Ptr<DOM::Node> target)
{
// To find the nearest clicked dialog, given a PointerEvent event:
// 1. Let target be event's target.
// 2. If target is a dialog element, target has an open attribute, target's is modal is true, and event's clientX and clientY are outside the bounds of target, then return null.
if (auto const* target_dialog = as_if<HTMLDialogElement>(*target); target_dialog
&& target_dialog->has_attribute(AttributeNames::open)
&& target_dialog->is_modal()
&& !target_dialog->get_bounding_client_rect().to_type<double>().contains(event.client_x(), event.client_y()))
return {};
// 3. Let currentNode be target.
auto current_node = target;
// 4. While currentNode is not null:
while (current_node) {
// 1. If currentNode is a dialog element and currentNode has an open attribute, then return currentNode.
if (auto* current_dialog = as_if<HTMLDialogElement>(*current_node); current_dialog
&& current_dialog->has_attribute(AttributeNames::open))
return current_dialog;
// 2. Set currentNode to currentNode's parent in the flat tree.
current_node = current_node->shadow_including_first_ancestor_of_type<HTMLElement>();
}
// 5. Return null.
return {};
}
// https://html.spec.whatwg.org/multipage/interactive-elements.html#light-dismiss-open-dialogs
void HTMLDialogElement::light_dismiss_open_dialogs(UIEvents::PointerEvent const& event, const GC::Ptr<DOM::Node> target)
{
// To light dismiss open dialogs, given a PointerEvent event:
// 1. Assert: event's isTrusted attribute is true.
VERIFY(event.is_trusted());
// 2. Let document be event's target's node document.
// FIXME: The event's target hasn't been initialized yet, so it's passed as an argument
auto& document = target->document();
// 3. If document's open dialogs list is empty, then return.
if (document.open_dialogs_list().is_empty())
return;
// 4. Let ancestor be the result of running nearest clicked dialog given event.
auto const ancestor = nearest_clicked_dialog(event, target);
// 5. If event's type is "pointerdown", then set document's dialog pointerdown target to ancestor.
if (event.type() == UIEvents::EventNames::pointerdown)
document.set_dialog_pointerdown_target(ancestor);
// 6. If event's type is "pointerup", then:
if (event.type() == UIEvents::EventNames::pointerup) {
// 1. Let sameTarget be true if ancestor is document's dialog pointerdown target.
bool const same_target = ancestor == document.dialog_pointerdown_target();
// 2. Set document's dialog pointerdown target to null.
document.set_dialog_pointerdown_target({});
// 3. If sameTarget is false, then return.
if (!same_target)
return;
// 4. Let topmostDialog be the last element of document's open dialogs list.
auto const topmost_dialog = document.open_dialogs_list().last();
// 5. If ancestor is topmostDialog, then return.
if (ancestor == topmost_dialog)
return;
// 6. If topmostDialog's computed closed-by state is not Any, then return.
// FIXME: This should use the "computed closed-by state" algorithm.
auto closedby = topmost_dialog->attribute(AttributeNames::closedby);
if (!closedby.has_value() || !closedby.value().equals_ignoring_ascii_case("any"sv))
return;
// 7. Assert: topmostDialog's close watcher is not null.
VERIFY(topmost_dialog->m_close_watcher);
// 8. Request to close topmostDialog's close watcher with false.
topmost_dialog->request_close({});
}
}
}

View file

@ -33,6 +33,8 @@ public:
void close(Optional<String> return_value);
void request_close(Optional<String> return_value);
static void light_dismiss_open_dialogs(UIEvents::PointerEvent const&, GC::Ptr<DOM::Node>);
// https://www.w3.org/TR/html-aria/#el-dialog
virtual Optional<ARIA::Role> default_role() const override { return ARIA::Role::dialog; }
@ -56,6 +58,8 @@ private:
void set_close_watcher();
static GC::Ptr<HTMLDialogElement> nearest_clicked_dialog(UIEvents::PointerEvent const&, GC::Ptr<DOM::Node>);
String m_return_value;
bool m_is_modal { false };
Optional<String> m_request_close_return_value;

View file

@ -1,5 +1,12 @@
#import <HTML/HTMLElement.idl>
[MissingValueDefault=auto, InvalidValueDefault=auto]
enum ClosedByAttribute {
"any",
"closerequest",
"none"
};
// https://html.spec.whatwg.org/multipage/semantics.html#htmldialogelement
[Exposed=Window]
interface HTMLDialogElement : HTMLElement {
@ -8,7 +15,7 @@ interface HTMLDialogElement : HTMLElement {
[CEReactions, Reflect] attribute boolean open;
attribute DOMString returnValue;
[FIXME, CEReactions] attribute DOMString closedBy;
[CEReactions, Reflect=closedby, Enumerated=ClosedByAttribute] attribute DOMString closedBy;
[CEReactions] undefined show();
[CEReactions] undefined showModal();
[CEReactions] undefined close(optional DOMString returnValue);

View file

@ -12,6 +12,7 @@
#include <LibWeb/HTML/CloseWatcherManager.h>
#include <LibWeb/HTML/Focus.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/HTML/HTMLDialogElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/HTML/HTMLIFrameElement.h>
#include <LibWeb/HTML/HTMLImageElement.h>
@ -347,7 +348,8 @@ static void light_dismiss_activities(UIEvents::PointerEvent const& event, const
// 1. Run light dismiss open popovers with event.
HTML::HTMLElement::light_dismiss_open_popovers(event, target);
// FIXME: 2. Run light dismiss open dialogs with event.
// 2. Run light dismiss open dialogs with event.
HTML::HTMLDialogElement::light_dismiss_open_dialogs(event, target);
}
EventHandler::EventHandler(Badge<HTML::Navigable>, HTML::Navigable& navigable)

View file

@ -0,0 +1,3 @@
closerequest="Any" should respond to light dismissal
other dialogs should not respond to light dismissal
PASS

View file

@ -0,0 +1,80 @@
<!DOCTYPE html>
<script src="include.js"></script>
<p id="outside">Outside all dialogs</p>
<script>
failed = false;
assert = function (bool, message) {
if (!bool) {
println("FAIL: " + message);
failed = true;
}
}
click = function (element) {
const boundingRect = element.getBoundingClientRect();
const centerPoint = {
x: boundingRect.left + boundingRect.width / 2,
y: boundingRect.top + boundingRect.height / 2
};
internals.click(centerPoint.x, centerPoint.y);
};
test(() => {
println("closerequest=\"Any\" should respond to light dismissal");
["any", "AnY"].forEach((closedby) => {
const dialog = document.createElement("dialog");
dialog.closedBy = closedby;
document.body.appendChild(dialog);
assert(!dialog.open, "should be closed by default");
dialog.show();
assert(dialog.open, "should be open after show");
click(dialog);
assert(dialog.open, "should be open after clicking on dialog");
click(outside);
assert(!dialog.open, "should be closed after clicking outside dialog");
dialog.showModal();
assert(dialog.open, "should be open after showModal");
click(dialog);
assert(dialog.open, "should be open after clicking on modal dialog");
click(outside);
assert(!dialog.open, "should be closed after clicking outside modal dialog");
});
println("other dialogs should not respond to light dismissal");
["closerequest", "ClOsErEqUeSt", "none", "NoNe", "invalid", "", undefined].forEach((closedby) => {
const dialog = document.createElement("dialog");
dialog.closedBy = closedby;
document.body.appendChild(dialog);
assert(!dialog.open, "should be closed by default");
dialog.show();
assert(dialog.open, "should be open after show");
click(dialog);
assert(dialog.open, "should be open after clicking on dialog");
click(outside);
assert(dialog.open, "should be open after clicking outside dialog");
dialog.close();
dialog.showModal();
assert(dialog.open, "should be open after showModal");
click(dialog);
assert(dialog.open, "should be open after clicking on modal dialog");
click(outside);
assert(dialog.open, "should be open after clicking outside modal dialog");
});
if (!failed) println("PASS")
});
</script>