diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 9c70ad391a3..2d6081614f9 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -97,6 +97,7 @@ #include #include #include +#include #include #include #include @@ -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); diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index ecce0f39cbf..921daaa507f 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -801,6 +801,11 @@ public: void set_popover_pointerdown_target(GC::Ptr target) { m_popover_pointerdown_target = target; } GC::Ptr popover_pointerdown_target() { return m_popover_pointerdown_target; } + Vector>& open_dialogs_list() { return m_open_dialogs_list; } + + void set_dialog_pointerdown_target(GC::Ptr target) { m_dialog_pointerdown_target = target; } + GC::Ptr 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> m_showing_hint_popover_list; GC::Ptr m_popover_pointerdown_target; + + Vector> m_open_dialogs_list; + GC::Ptr m_dialog_pointerdown_target; + // https://dom.spec.whatwg.org/#document-allow-declarative-shadow-roots bool m_allow_declarative_shadow_roots { false }; diff --git a/Libraries/LibWeb/HTML/AttributeNames.h b/Libraries/LibWeb/HTML/AttributeNames.h index 3e27c7d122d..f4f18060541 100644 --- a/Libraries/LibWeb/HTML/AttributeNames.h +++ b/Libraries/LibWeb/HTML/AttributeNames.h @@ -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") \ diff --git a/Libraries/LibWeb/HTML/HTMLDialogElement.cpp b/Libraries/LibWeb/HTML/HTMLDialogElement.cpp index 9594e90fc50..3c00398230d 100644 --- a/Libraries/LibWeb/HTML/HTMLDialogElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLDialogElement.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include namespace Web::HTML { @@ -131,8 +133,12 @@ WebIDL::ExceptionOr 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 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 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::nearest_clicked_dialog(UIEvents::PointerEvent const& event, const GC::Ptr 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(*target); target_dialog + && target_dialog->has_attribute(AttributeNames::open) + && target_dialog->is_modal() + && !target_dialog->get_bounding_client_rect().to_type().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(*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(); + } + + // 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 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({}); + } +} + } diff --git a/Libraries/LibWeb/HTML/HTMLDialogElement.h b/Libraries/LibWeb/HTML/HTMLDialogElement.h index 5a8a1b81013..d66fce75eb5 100644 --- a/Libraries/LibWeb/HTML/HTMLDialogElement.h +++ b/Libraries/LibWeb/HTML/HTMLDialogElement.h @@ -33,6 +33,8 @@ public: void close(Optional return_value); void request_close(Optional return_value); + static void light_dismiss_open_dialogs(UIEvents::PointerEvent const&, GC::Ptr); + // https://www.w3.org/TR/html-aria/#el-dialog virtual Optional default_role() const override { return ARIA::Role::dialog; } @@ -56,6 +58,8 @@ private: void set_close_watcher(); + static GC::Ptr nearest_clicked_dialog(UIEvents::PointerEvent const&, GC::Ptr); + String m_return_value; bool m_is_modal { false }; Optional m_request_close_return_value; diff --git a/Libraries/LibWeb/HTML/HTMLDialogElement.idl b/Libraries/LibWeb/HTML/HTMLDialogElement.idl index 751c498997c..a195a6b8d39 100644 --- a/Libraries/LibWeb/HTML/HTMLDialogElement.idl +++ b/Libraries/LibWeb/HTML/HTMLDialogElement.idl @@ -1,5 +1,12 @@ #import +[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); diff --git a/Libraries/LibWeb/Page/EventHandler.cpp b/Libraries/LibWeb/Page/EventHandler.cpp index 680fbb29f5e..8b851dc288e 100644 --- a/Libraries/LibWeb/Page/EventHandler.cpp +++ b/Libraries/LibWeb/Page/EventHandler.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -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& navigable) diff --git a/Tests/LibWeb/Text/expected/dialog-light-dismiss.txt b/Tests/LibWeb/Text/expected/dialog-light-dismiss.txt new file mode 100644 index 00000000000..c6835b015e9 --- /dev/null +++ b/Tests/LibWeb/Text/expected/dialog-light-dismiss.txt @@ -0,0 +1,3 @@ +closerequest="Any" should respond to light dismissal +other dialogs should not respond to light dismissal +PASS diff --git a/Tests/LibWeb/Text/input/dialog-light-dismiss.html b/Tests/LibWeb/Text/input/dialog-light-dismiss.html new file mode 100644 index 00000000000..52c511c05f6 --- /dev/null +++ b/Tests/LibWeb/Text/input/dialog-light-dismiss.html @@ -0,0 +1,80 @@ + + + +

Outside all dialogs

+