From 1f1884da545df4ec6fa0c13175694e0ef587b282 Mon Sep 17 00:00:00 2001 From: Gingeh <39150378+Gingeh@users.noreply.github.com> Date: Sun, 13 Apr 2025 18:58:40 +1000 Subject: [PATCH] LibWeb: Light dismiss popovers on click --- Libraries/LibWeb/DOM/Document.cpp | 18 +++ Libraries/LibWeb/DOM/Document.h | 6 + Libraries/LibWeb/HTML/HTMLElement.cpp | 131 ++++++++++++++++++ Libraries/LibWeb/HTML/HTMLElement.h | 5 + .../LibWeb/HTML/PopoverInvokerElement.cpp | 5 +- Libraries/LibWeb/HTML/PopoverInvokerElement.h | 4 +- Libraries/LibWeb/Page/EventHandler.cpp | 19 ++- .../popover/popover-light-dismiss.txt | 26 ++++ .../input/popover/popover-light-dismiss.html | 100 +++++++++++++ 9 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/popover/popover-light-dismiss.txt create mode 100644 Tests/LibWeb/Text/input/popover/popover-light-dismiss.html diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index e3a0fbf02bd..9c70ad391a3 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -592,6 +592,7 @@ void Document::visit_edges(Cell::Visitor& visitor) visitor.visit(m_top_layer_pending_removals); visitor.visit(m_showing_auto_popover_list); visitor.visit(m_showing_hint_popover_list); + visitor.visit(m_popover_pointerdown_target); visitor.visit(m_console_client); visitor.visit(m_editing_host_manager); visitor.visit(m_local_storage_holder); @@ -6117,6 +6118,23 @@ void Document::process_top_layer_removals() } } +// https://html.spec.whatwg.org/multipage/popover.html#topmost-auto-popover +GC::Ptr Document::topmost_auto_or_hint_popover() +{ + // To find the topmost auto or hint popover given a Document document, perform the following steps. They return an HTML element or null. + + // 1. If document's showing hint popover list is not empty, then return document's showing hint popover list's last element. + if (!m_showing_hint_popover_list.is_empty()) + return m_showing_hint_popover_list.last(); + + // 2. If document's showing auto popover list is not empty, then return document's showing auto popover list's last element. + if (!m_showing_auto_popover_list.is_empty()) + return m_showing_auto_popover_list.last(); + + // 3. Return null. + return {}; +} + void Document::set_needs_to_refresh_scroll_state(bool b) { if (auto* paintable = this->paintable()) diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index 09e78d237dc..ecce0f39cbf 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -796,6 +796,11 @@ public: Vector> const& showing_auto_popover_list() const { return m_showing_auto_popover_list; } Vector> const& showing_hint_popover_list() const { return m_showing_hint_popover_list; } + GC::Ptr topmost_auto_or_hint_popover(); + + void set_popover_pointerdown_target(GC::Ptr target) { m_popover_pointerdown_target = target; } + GC::Ptr popover_pointerdown_target() { return m_popover_pointerdown_target; } + size_t transition_generation() const { return m_transition_generation; } // Does document represent an embedded svg img @@ -1203,6 +1208,7 @@ private: Vector> m_showing_auto_popover_list; Vector> m_showing_hint_popover_list; + GC::Ptr m_popover_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/HTMLElement.cpp b/Libraries/LibWeb/HTML/HTMLElement.cpp index 589902eed53..2db8b2c2ebb 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLElement.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -1771,6 +1772,34 @@ GC::Ptr HTMLElement::nearest_inclusive_open_popover() return {}; } +// https://html.spec.whatwg.org/multipage/popover.html#nearest-inclusive-target-popover-for-invoker +GC::Ptr HTMLElement::nearest_inclusive_target_popover_for_invoker() +{ + // To find the nearest inclusive target popover for invoker given a Node node: + + // 1. Let currentNode be node. + auto* current_node = this; + + // 2. While currentNode is not null: + while (current_node) { + // 1. Let targetPopover be currentNode's popover target element. + auto target_popover = PopoverInvokerElement::get_the_popover_target_element(*current_node); + + // AD-HOC: This also allows hint popovers. + // See nearest_inclusive_open_popover above. + // 2. If targetPopover is not null and targetPopover's popover attribute is in the auto state and targetPopover's popover visibility state is showing, then return targetPopover. + if (target_popover) { + if (target_popover->popover().has_value() && target_popover->popover().value().is_one_of("auto", "hint") && target_popover->popover_visibility_state() == PopoverVisibilityState::Showing) + return target_popover; + } + + // 3. Set currentNode to currentNode's ancestor in the flat tree. + current_node = current_node->shadow_including_first_ancestor_of_type(); + } + + return {}; +} + // https://html.spec.whatwg.org/multipage/popover.html#queue-a-popover-toggle-event-task void HTMLElement::queue_a_popover_toggle_event_task(String old_state, String new_state) { @@ -1809,6 +1838,108 @@ void HTMLElement::queue_a_popover_toggle_event_task(String old_state, String new }; } +// https://html.spec.whatwg.org/multipage/popover.html#light-dismiss-open-popovers +void HTMLElement::light_dismiss_open_popovers(UIEvents::PointerEvent const& event, GC::Ptr const target) +{ + // To light dismiss open popovers, given a PointerEvent event: + + // 1. Assert: event's isTrusted attribute is true. + VERIFY(event.is_trusted()); + + // 2. Let target be event's target. + // FIXME: The event's target hasn't been initialized yet, so it's passed as an argument + + // 3. Let document be target's node document. + auto& document = target->document(); + + // 4. Let topmostPopover be the result of running topmost auto popover given document. + auto topmost_popover = document.topmost_auto_or_hint_popover(); + + // 5. If topmostPopover is null, then return. + if (!topmost_popover) + return; + + // 6. If event's type is "pointerdown", then: set document's popover pointerdown target to the result of running topmost clicked popover given target. + if (event.type() == UIEvents::EventNames::pointerdown) + document.set_popover_pointerdown_target(topmost_clicked_popover(target)); + + // 7. If event's type is "pointerup", then: + if (event.type() == UIEvents::EventNames::pointerup) { + // 1. Let ancestor be the result of running topmost clicked popover given target. + auto const ancestor = topmost_clicked_popover(target); + + // 2. Let sameTarget be true if ancestor is document's popover pointerdown target. + bool const same_target = ancestor == document.popover_pointerdown_target(); + + // 3. Set document's popover pointerdown target to null. + document.set_popover_pointerdown_target({}); + + // 4. If ancestor is null, then set ancestor to document. + Variant, GC::Ptr> ancestor_or_document = ancestor; + if (!ancestor) + ancestor_or_document = GC::Ptr(document); + + // 5. If sameTarget is true, then run hide all popovers until given ancestor, false, and true. + if (same_target) + hide_all_popovers_until(ancestor_or_document, FocusPreviousElement::No, FireEvents::Yes); + } +} + +// https://html.spec.whatwg.org/multipage/popover.html#get-the-popover-stack-position +size_t HTMLElement::popover_stack_position() +{ + // To get the popover stack position, given an HTML element popover: + + // 1. Let hintList be popover's node document's showing hint popover list. + auto const& hint_list = document().showing_hint_popover_list(); + + // 2. Let autoList be popover's node document's showing auto popover list. + auto const& auto_list = document().showing_auto_popover_list(); + + // 3. If popover is in hintList, then return the index of popover in hintList + the size of autoList + 1. + if (hint_list.contains_slow(GC::Ref(*this))) + return hint_list.find_first_index(GC::Ref(*this)).value() + auto_list.size() + 1; + + // 4. If popover is in autoList, then return the index of popover in autoList + 1. + if (auto_list.contains_slow(GC::Ref(*this))) + return auto_list.find_first_index(GC::Ref(*this)).value() + 1; + + // 5. Return 0. + return 0; +} + +// https://html.spec.whatwg.org/multipage/popover.html#topmost-clicked-popover +GC::Ptr HTMLElement::topmost_clicked_popover(GC::Ptr node) +{ + // To find the topmost clicked popover, given a Node node: + + GC::Ptr nearest_element = as_if(*node); + if (!nearest_element) + nearest_element = node->shadow_including_first_ancestor_of_type(); + + if (!nearest_element) + return {}; + + // 1. Let clickedPopover be the result of running nearest inclusive open popover given node. + auto clicked_popover = nearest_element->nearest_inclusive_open_popover(); + + // 2. Let invokerPopover be the result of running nearest inclusive target popover for invoker given node. + auto invoker_popover = nearest_element->nearest_inclusive_target_popover_for_invoker(); + + if (!clicked_popover) + return invoker_popover; + + if (!invoker_popover) + return clicked_popover; + + // 3. If the result of getting the popover stack position given clickedPopover is greater than the result of getting the popover stack position given invokerPopover, then return clickedPopover. + if (clicked_popover->popover_stack_position() > invoker_popover->popover_stack_position()) + return clicked_popover; + + // 4. Return invokerPopover. + return invoker_popover; +} + void HTMLElement::did_receive_focus() { if (!first_is_one_of(m_content_editable_state, ContentEditableState::True, ContentEditableState::PlaintextOnly)) diff --git a/Libraries/LibWeb/HTML/HTMLElement.h b/Libraries/LibWeb/HTML/HTMLElement.h index acf935f0461..a2bc1af9d06 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.h +++ b/Libraries/LibWeb/HTML/HTMLElement.h @@ -149,6 +149,8 @@ public: static void hide_all_popovers_until(Variant, GC::Ptr> endpoint, FocusPreviousElement focus_previous_element, FireEvents fire_events); static GC::Ptr topmost_popover_ancestor(GC::Ptr new_popover_or_top_layer_element, Vector> const& popover_list, GC::Ptr invoker, IsPopover is_popover); + static void light_dismiss_open_popovers(UIEvents::PointerEvent const&, GC::Ptr); + bool is_inert() const { return m_inert; } virtual bool is_valid_invoker_command(String&) { return false; } @@ -197,7 +199,10 @@ private: static Optional popover_value_to_state(Optional value); void hide_popover_stack_until(Vector> const& popover_list, FocusPreviousElement focus_previous_element, FireEvents fire_events); GC::Ptr nearest_inclusive_open_popover(); + GC::Ptr nearest_inclusive_target_popover_for_invoker(); static void close_entire_popover_list(Vector> const& popover_list, FocusPreviousElement focus_previous_element, FireEvents fire_events); + static GC::Ptr topmost_clicked_popover(GC::Ptr node); + size_t popover_stack_position(); // https://html.spec.whatwg.org/multipage/custom-elements.html#attached-internals GC::Ptr m_attached_internals; diff --git a/Libraries/LibWeb/HTML/PopoverInvokerElement.cpp b/Libraries/LibWeb/HTML/PopoverInvokerElement.cpp index 84883af97e5..8d7fd3a1f5f 100644 --- a/Libraries/LibWeb/HTML/PopoverInvokerElement.cpp +++ b/Libraries/LibWeb/HTML/PopoverInvokerElement.cpp @@ -78,8 +78,9 @@ GC::Ptr PopoverInvokerElement::get_the_popover_target_element(GC::R { // To get the popover target element given a Node node, perform the following steps. They return an HTML element or null. - auto const* form_associated_element = dynamic_cast(node.ptr()); - VERIFY(form_associated_element); + auto const* form_associated_element = as_if(*node); + if (!form_associated_element) + return {}; // 1. If node is not a button, then return null. if (!form_associated_element->is_button()) diff --git a/Libraries/LibWeb/HTML/PopoverInvokerElement.h b/Libraries/LibWeb/HTML/PopoverInvokerElement.h index 55269776d87..9deb1572676 100644 --- a/Libraries/LibWeb/HTML/PopoverInvokerElement.h +++ b/Libraries/LibWeb/HTML/PopoverInvokerElement.h @@ -21,6 +21,8 @@ public: void set_popover_target_element(GC::Ptr value) { m_popover_target_element = value; } + static GC::Ptr get_the_popover_target_element(GC::Ref node); + static void popover_target_activation_behaviour(GC::Ref node, GC::Ref event_target); protected: @@ -29,8 +31,6 @@ protected: private: GC::Ptr m_popover_target_element; - - static GC::Ptr get_the_popover_target_element(GC::Ref node); }; } diff --git a/Libraries/LibWeb/Page/EventHandler.cpp b/Libraries/LibWeb/Page/EventHandler.cpp index d998b7a5346..680fbb29f5e 100644 --- a/Libraries/LibWeb/Page/EventHandler.cpp +++ b/Libraries/LibWeb/Page/EventHandler.cpp @@ -339,6 +339,17 @@ static void set_user_selection(GC::Ptr anchor_node, unsigned anchor_o (void)selection->set_base_and_extent(*anchor_node, anchor_offset, *focus_node, focus_offset); } +// https://html.spec.whatwg.org/multipage/interactive-elements.html#run-light-dismiss-activities +static void light_dismiss_activities(UIEvents::PointerEvent const& event, const GC::Ptr target) +{ + // To run light dismiss activities, given a PointerEvent event: + + // 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. +} + EventHandler::EventHandler(Badge, HTML::Navigable& navigable) : m_navigable(navigable) , m_drag_and_drop_event_handler(make()) @@ -481,7 +492,9 @@ EventResult EventHandler::handle_mouseup(CSSPixelPoint viewport_position, CSSPix auto page_offset = compute_mouse_event_page_offset(viewport_position); auto offset = compute_mouse_event_offset(page_offset, *layout_node->first_paintable()); - node->dispatch_event(UIEvents::PointerEvent::create_from_platform_event(node->realm(), UIEvents::EventNames::pointerup, screen_position, page_offset, viewport_position, offset, {}, button, buttons, modifiers).release_value_but_fixme_should_propagate_errors()); + auto pointer_event = UIEvents::PointerEvent::create_from_platform_event(node->realm(), UIEvents::EventNames::pointerup, screen_position, page_offset, viewport_position, offset, {}, button, buttons, modifiers).release_value_but_fixme_should_propagate_errors(); + light_dismiss_activities(pointer_event, node); + node->dispatch_event(pointer_event); node->dispatch_event(UIEvents::MouseEvent::create_from_platform_event(node->realm(), UIEvents::EventNames::mouseup, screen_position, page_offset, viewport_position, offset, {}, button, buttons, modifiers).release_value_but_fixme_should_propagate_errors()); handled_event = EventResult::Handled; @@ -629,7 +642,9 @@ EventResult EventHandler::handle_mousedown(CSSPixelPoint viewport_position, CSSP m_mousedown_target = node.ptr(); auto page_offset = compute_mouse_event_page_offset(viewport_position); auto offset = compute_mouse_event_offset(page_offset, *layout_node->first_paintable()); - node->dispatch_event(UIEvents::PointerEvent::create_from_platform_event(node->realm(), UIEvents::EventNames::pointerdown, screen_position, page_offset, viewport_position, offset, {}, button, buttons, modifiers).release_value_but_fixme_should_propagate_errors()); + auto pointer_event = UIEvents::PointerEvent::create_from_platform_event(node->realm(), UIEvents::EventNames::pointerdown, screen_position, page_offset, viewport_position, offset, {}, button, buttons, modifiers).release_value_but_fixme_should_propagate_errors(); + light_dismiss_activities(pointer_event, node); + node->dispatch_event(pointer_event); node->dispatch_event(UIEvents::MouseEvent::create_from_platform_event(node->realm(), UIEvents::EventNames::mousedown, screen_position, page_offset, viewport_position, offset, {}, button, buttons, modifiers).release_value_but_fixme_should_propagate_errors()); } diff --git a/Tests/LibWeb/Text/expected/popover/popover-light-dismiss.txt b/Tests/LibWeb/Text/expected/popover/popover-light-dismiss.txt new file mode 100644 index 00000000000..233350a5ed1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/popover/popover-light-dismiss.txt @@ -0,0 +1,26 @@ +p1 open: false (should be false) +p2 open: false (should be false) +Opening p1 +p1 open: true (should be true) +p2 open: false (should be false) +Opening p2 +p1 open: true (should be true) +p2 open: true (should be true) +Clicking p2 +p1 open: true (should be true) +p2 open: true (should be true) +Clicking p1 +p1 open: true (should be true) +p2 open: false (should be false) +Clicking p1's show invoker +p1 open: true (should be true) +p2 open: false (should be false) +Clicking p1's toggle invoker +p1 open: false (should be false) +p2 open: false (should be false) +Re-opening both popovers +p1 open: true (should be true) +p2 open: true (should be true) +Clicking outside both popovers +p1 open: false (should be false) +p2 open: false (should be false) diff --git a/Tests/LibWeb/Text/input/popover/popover-light-dismiss.html b/Tests/LibWeb/Text/input/popover/popover-light-dismiss.html new file mode 100644 index 00000000000..3386293f8a6 --- /dev/null +++ b/Tests/LibWeb/Text/input/popover/popover-light-dismiss.html @@ -0,0 +1,100 @@ + + + + + + +Outside all popovers +
+ Inside popover 1 + + Inside popover 1 after button +
+ Inside popover 2 +
+
+ +