From 91e4fb248b8c060002f547ddc5518b92ee5ff2eb Mon Sep 17 00:00:00 2001 From: Gingeh <39150378+Gingeh@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:23:09 +1100 Subject: [PATCH] LibWeb: Hide unrelated popovers when showing popovers Also hides decendant popovers when hiding. Also hides unrelated popovers when showing dialogs. --- Libraries/LibWeb/DOM/Document.cpp | 2 + Libraries/LibWeb/DOM/Document.h | 10 + Libraries/LibWeb/HTML/HTMLDialogElement.cpp | 44 +- Libraries/LibWeb/HTML/HTMLElement.cpp | 457 ++++++++++++++++-- Libraries/LibWeb/HTML/HTMLElement.h | 14 + .../popover/auto-hides-other-popovers.txt | 2 + .../popover/hint-only-hides-hint-popovers.txt | 3 + .../{ => popover}/popover-crashes.txt | 0 .../popover-invoker-attributes.txt | 0 .../popover/popover-invoker-is-ancestor.txt | 2 + .../popover/auto-hides-other-popovers.html | 41 ++ .../hint-only-hides-hint-popovers.html | 39 ++ .../input/{ => popover}/popover-crashes.html | 2 +- .../popover-invoker-attributes.html | 0 .../popover/popover-invoker-is-ancestor.html | 47 ++ 15 files changed, 607 insertions(+), 56 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/popover/auto-hides-other-popovers.txt create mode 100644 Tests/LibWeb/Text/expected/popover/hint-only-hides-hint-popovers.txt rename Tests/LibWeb/Text/expected/{ => popover}/popover-crashes.txt (100%) rename Tests/LibWeb/Text/expected/{HTML => popover}/popover-invoker-attributes.txt (100%) create mode 100644 Tests/LibWeb/Text/expected/popover/popover-invoker-is-ancestor.txt create mode 100644 Tests/LibWeb/Text/input/popover/auto-hides-other-popovers.html create mode 100644 Tests/LibWeb/Text/input/popover/hint-only-hides-hint-popovers.html rename Tests/LibWeb/Text/input/{ => popover}/popover-crashes.html (94%) rename Tests/LibWeb/Text/input/{HTML => popover}/popover-invoker-attributes.html (100%) create mode 100644 Tests/LibWeb/Text/input/popover/popover-invoker-is-ancestor.html diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index bd7e0951c07..8001ac1c700 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -574,6 +574,8 @@ void Document::visit_edges(Cell::Visitor& visitor) visitor.visit(m_top_layer_elements); visitor.visit(m_top_layer_pending_removals); + visitor.visit(m_showing_auto_popover_list); + visitor.visit(m_showing_hint_popover_list); 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 5416b277df2..4fe28bd9611 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -712,6 +712,13 @@ public: OrderedHashTable> const& top_layer_elements() const { return m_top_layer_elements; } + // AD-HOC: These lists are managed dynamically instead of being generated as needed. + // Spec issue: https://github.com/whatwg/html/issues/11007 + Vector>& showing_auto_popover_list() { return m_showing_auto_popover_list; } + Vector>& showing_hint_popover_list() { return m_showing_hint_popover_list; } + 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; } + size_t transition_generation() const { return m_transition_generation; } // Does document represent an embedded svg img @@ -1101,6 +1108,9 @@ private: OrderedHashTable> m_top_layer_elements; OrderedHashTable> m_top_layer_pending_removals; + Vector> m_showing_auto_popover_list; + Vector> m_showing_hint_popover_list; + // https://dom.spec.whatwg.org/#document-allow-declarative-shadow-roots bool m_allow_declarative_shadow_roots { false }; diff --git a/Libraries/LibWeb/HTML/HTMLDialogElement.cpp b/Libraries/LibWeb/HTML/HTMLDialogElement.cpp index 3d2bc69814e..a10a0cc36b0 100644 --- a/Libraries/LibWeb/HTML/HTMLDialogElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLDialogElement.cpp @@ -135,11 +135,23 @@ WebIDL::ExceptionOr HTMLDialogElement::show() // 9. Set the dialog close watcher with this. set_close_watcher(); // FIXME: 10. Set this's previously focused element to the focused element. - // FIXME: 11. Let document be this's node document. - // FIXME: 12. Let hideUntil be the result of running topmost popover ancestor given this, document's showing hint popover list, null, and false. - // FIXME: 13. If hideUntil is null, then set hideUntil to the result of running topmost popover ancestor given this, document's showing auto popover list, null, and false. - // FIXME: 14. If hideUntil is null, then set hideUntil to document. - // FIXME: 15. Run hide all popovers until given hideUntil, false, and true. + + // 11. Let document be this's node document. + auto document = m_document; + + // 12. Let hideUntil be the result of running topmost popover ancestor given this, document's showing hint popover list, null, and false. + Variant, GC::Ptr> hide_until = topmost_popover_ancestor(this, document->showing_hint_popover_list(), nullptr, IsPopover::No); + + // 13. If hideUntil is null, then set hideUntil to the result of running topmost popover ancestor given this, document's showing auto popover list, null, and false. + if (!hide_until.get>()) + hide_until = topmost_popover_ancestor(this, document->showing_auto_popover_list(), nullptr, IsPopover::No); + + // 14. If hideUntil is null, then set hideUntil to document. + if (!hide_until.get>()) + hide_until = document; + + // 15. Run hide all popovers until given hideUntil, false, and true. + hide_all_popovers_until(hide_until, FocusPreviousElement::No, FireEvents::Yes); // 16. Run the dialog focusing steps given this. run_dialog_focusing_steps(); @@ -224,11 +236,23 @@ WebIDL::ExceptionOr HTMLDialogElement::show_a_modal_dialog(HTMLDialogEleme subject.set_close_watcher(); // FIXME: 18. Set subject's previously focused element to the focused element. - // FIXME: 19. Let document be subject's node document. - // FIXME: 20. Let hideUntil be the result of running topmost popover ancestor given subject, document's showing hint popover list, null, and false. - // FIXME: 21. If hideUntil is null, then set hideUntil to the result of running topmost popover ancestor given subject, document's showing auto popover list, null, and false. - // FIXME: 22. If hideUntil is null, then set hideUntil to document. - // FIXME: 23. Run hide all popovers until given hideUntil, false, and true. + + // 19. Let document be subject's node document. + auto& document = subject.document(); + + // 20. Let hideUntil be the result of running topmost popover ancestor given subject, document's showing hint popover list, null, and false. + Variant, GC::Ptr> hide_until = topmost_popover_ancestor(subject, document.showing_hint_popover_list(), nullptr, IsPopover::No); + + // 21. If hideUntil is null, then set hideUntil to the result of running topmost popover ancestor given subject, document's showing auto popover list, null, and false. + if (!hide_until.get>()) + hide_until = topmost_popover_ancestor(subject, document.showing_auto_popover_list(), nullptr, IsPopover::No); + + // 22. If hideUntil is null, then set hideUntil to document. + if (!hide_until.get>()) + hide_until = GC::Ptr(document); + + // 23. Run hide all popovers until given hideUntil, false, and true. + hide_all_popovers_until(hide_until, FocusPreviousElement::No, FireEvents::Yes); // 24. Run the dialog focusing steps given subject. subject.run_dialog_focusing_steps(); diff --git a/Libraries/LibWeb/HTML/HTMLElement.cpp b/Libraries/LibWeb/HTML/HTMLElement.cpp index 0dbfc95f2ea..db33dcae809 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLElement.cpp @@ -1038,7 +1038,8 @@ Optional HTMLElement::popover_value_to_state(Optional value) if (value.value().is_empty() || value.value().equals_ignoring_ascii_case("auto"sv)) return "auto"_string; - // FIXME: This should reflect the hint value too. + if (value.value().equals_ignoring_ascii_case("hint"sv)) + return "hint"_string; return "manual"_string; } @@ -1146,7 +1147,8 @@ WebIDL::ExceptionOr HTMLElement::show_popover(ThrowExceptions throw_except // 5. Let nestedShow be element's popover showing or hiding. auto nested_show = m_popover_showing_or_hiding; - // FIXME: 6. Let fireEvents be the boolean negation of nestedShow. + // 6. Let fireEvents be the boolean negation of nestedShow. + FireEvents fire_events = nested_show ? FireEvents::No : FireEvents::Yes; // 7. Set element's popover showing or hiding to true. m_popover_showing_or_hiding = true; @@ -1174,48 +1176,130 @@ WebIDL::ExceptionOr HTMLElement::show_popover(ThrowExceptions throw_except return {}; } - // FIXME: 11. Let shouldRestoreFocus be false. + // 11. Let shouldRestoreFocus be false. + auto should_restore_focus = FocusPreviousElement::No; // 12. Let originalType be the current state of element's popover attribute. auto original_type = popover(); - // FIXME: 13. Let stackToAppendTo be null. + // 13. Let stackToAppendTo be null. + enum class StackToAppendTo : u8 { + Null, + Auto, + Hint, + }; + StackToAppendTo stack_to_append_to = StackToAppendTo::Null; - // FIXME: 14. Let autoAncestor be the result of running the topmost popover ancestor algorithm given element, document's showing auto popover list, invoker, and true. + // 16. If originalType is the auto state, then: + if (original_type == "auto"sv) { + // 1. Run close entire popover list given document's showing hint popover list, shouldRestoreFocus, and fireEvents. + close_entire_popover_list(document.showing_hint_popover_list(), should_restore_focus, fire_events); - // FIXME: 15. Let hintAncestor be the result of running the topmost popover ancestor algorithm given element, document's showing hint popover list, invoker, and true. - // FIXME: 16. If originalType is the auto state, then: - // FIXME: 16.1. Run close entire popover list given document's showing hint popover list, shouldRestoreFocus, and fireEvents. - // FIXME: 16.2. Let ancestor be the result of running the topmost popover ancestor algorithm given element, document's showing auto popover list, invoker, and true. - // FIXME: 16.3. If ancestor is null, then set ancestor to document. - // FIXME: 16.4. Run hide all popovers until given ancestor, shouldRestoreFocus, and fireEvents. - // FIXME: 16.5. Set stackToAppendTo to "auto". - // FIXME: 17. If originalType is the hint state, then: - // FIXME: 17.1. If hintAncestor is not null, then: - // FIXME: 17.1.1. Run hide all popovers until given hintAncestor, shouldRestoreFocus, and fireEvents. - // FIXME: 17.1.2. Set stackToAppendTo to "hint". - // FIXME: 17.2. Otherwise: - // FIXME: 17.2.1. Run close entire popover list given document's showing hint popover list, shouldRestoreFocus, and fireEvents. - // FIXME: 17.2.2. If autoAncestor is not null, then: - // FIXME: 17.2.2.1. Run hide all popovers until given autoAncestor, shouldRestoreFocus, and fireEvents. - // FIXME: 17.2.2.2. Set stackToAppendTo to "auto". - // FIXME: 17.3. Otherwise, set stackToAppendTo to "hint". - // 18. If originalType is auto or FIXME: hint, then: - if (original_type.has_value() && (original_type.value() == "auto"sv)) { - // FIXME: 18.1. Assert: stackToAppendTo is not null. - // FIXME: 18.2. If originalType is not equal to the value of element's popover attribute, then: - // FIXME: 18.2.1. If throwExceptions is true, then throw a "InvalidStateError" DOMException. - // FIXME: 18.2.2. Return. - // FIXME: 18.3. If the result of running check popover validity given element, false, throwExceptions, document, and false is false, then run cleanupShowingFlag and return. - // FIXME: 18.4. If the result of running topmost auto or hint popover on document is null, then set shouldRestoreFocus to true. - // FIXME: 18.5. If stackToAppendTo is "auto": - // FIXME: 18.5.1. Assert: document's showing auto popover list does not contain element. - // FIXME: 18.5.2. Set element's opened in popover mode to "auto". + // 2. Let ancestor be the result of running the topmost popover ancestor algorithm given element, document's showing auto popover list, invoker, and true. + Variant, GC::Ptr> ancestor = topmost_popover_ancestor(this, document.showing_auto_popover_list(), invoker, IsPopover::Yes); + + // 3. If ancestor is null, then set ancestor to document. + if (!ancestor.get>()) + ancestor = GC::Ptr(document); + + // 4. Run hide all popovers until given ancestor, shouldRestoreFocus, and fireEvents. + hide_all_popovers_until(ancestor, should_restore_focus, fire_events); + + // 5. Set stackToAppendTo to "auto". + stack_to_append_to = StackToAppendTo::Auto; + } + + // 17. If originalType is the hint state, then: + if (original_type == "hint"sv) { + + // AD-HOC: Steps 14 and 15 have been moved here to avoid hitting the `popover != manual` assertion in the topmost popover ancestor algorithm. + // Spec issue: https://github.com/whatwg/html/issues/10988. + // 14. Let autoAncestor be the result of running the topmost popover ancestor algorithm given element, document's showing auto popover list, invoker, and true. + auto auto_ancestor = topmost_popover_ancestor(this, document.showing_auto_popover_list(), invoker, IsPopover::Yes); + + // 15. Let hintAncestor be the result of running the topmost popover ancestor algorithm given element, document's showing hint popover list, invoker, and true. + auto hint_ancestor = topmost_popover_ancestor(this, document.showing_hint_popover_list(), invoker, IsPopover::Yes); + + // 1. If hintAncestor is not null, then: + if (hint_ancestor) { + // 1. Run hide all popovers until given hintAncestor, shouldRestoreFocus, and fireEvents. + hide_all_popovers_until(hint_ancestor, should_restore_focus, fire_events); + + // 2. Set stackToAppendTo to "hint". + stack_to_append_to = StackToAppendTo::Hint; + } + // 2. Otherwise: + else { + // 1. Run close entire popover list given document's showing hint popover list, shouldRestoreFocus, and fireEvents. + close_entire_popover_list(document.showing_hint_popover_list(), should_restore_focus, fire_events); + + // 2. If autoAncestor is not null, then: + if (auto_ancestor) { + // 1. Run hide all popovers until given autoAncestor, shouldRestoreFocus, and fireEvents. + hide_all_popovers_until(auto_ancestor, should_restore_focus, fire_events); + + // 2. Set stackToAppendTo to "auto". + stack_to_append_to = StackToAppendTo::Auto; + } + // 3. Otherwise, set stackToAppendTo to "hint". + else { + stack_to_append_to = StackToAppendTo::Hint; + } + } + } + + // 18. If originalType is auto or hint, then: + if (original_type.has_value() && original_type.value().is_one_of("auto", "hint")) { + // 1. Assert: stackToAppendTo is not null. + VERIFY(stack_to_append_to != StackToAppendTo::Null); + + // 2. If originalType is not equal to the value of element's popover attribute, then: + if (original_type != popover()) { + // 1. If throwExceptions is true, then throw a "InvalidStateError" DOMException. + if (throw_exceptions == ThrowExceptions::Yes) + return WebIDL::InvalidStateError::create(realm(), "Element is not in a valid state to show a popover"_string); + + // 2. Return. + return {}; + } + + // 3. If the result of running check popover validity given element, false, throwExceptions, document, and false is false, then run cleanupShowingFlag and return. + if (!TRY(check_popover_validity(ExpectedToBeShowing::No, throw_exceptions, document, IgnoreDomState::No))) { + cleanup_showing_flag(); + return {}; + } + + // FIXME: 4. If the result of running topmost auto or hint popover on document is null, then set shouldRestoreFocus to true. + + // 5. If stackToAppendTo is "auto": + if (stack_to_append_to == StackToAppendTo::Auto) { + // 1. Assert: document's showing auto popover list does not contain element. + VERIFY(!document.showing_auto_popover_list().contains_slow(GC::Ref(*this))); + + // AD-HOC: Append element to the document's showing auto popover list. + // Spec issue: https://github.com/whatwg/html/issues/11007 + document.showing_auto_popover_list().append(*this); + + // 2. Set element's opened in popover mode to "auto". + m_opened_in_popover_mode = "auto"_string; + } // Otherwise: - // FIXME: 1. Assert: stackToAppendTo is "hint". - // FIXME: 2. Assert: document's showing hint popover list does not contain element. - // FIXME: 3. Set element's opened in popover mode to "hint". - // 18.6. Set element's popover close watcher to the result of establishing a close watcher given element's relevant global object, with: + else { + // 1. Assert: stackToAppendTo is "hint". + VERIFY(stack_to_append_to == StackToAppendTo::Hint); + + // 2. Assert: document's showing hint popover list does not contain element. + VERIFY(!document.showing_hint_popover_list().contains_slow(GC::Ref(*this))); + + // AD-HOC: Append element to the document's showing hint popover list. + // Spec issue: https://github.com/whatwg/html/issues/11007 + document.showing_hint_popover_list().append(*this); + + // 3. Set element's opened in popover mode to "hint". + m_opened_in_popover_mode = "hint"_string; + } + + // 6. Set element's popover close watcher to the result of establishing a close watcher given element's relevant global object, with: m_popover_close_watcher = CloseWatcher::establish(*document.window()); // - cancelAction being to return true. // We simply don't add an event listener for the cancel action. @@ -1261,7 +1345,7 @@ WebIDL::ExceptionOr HTMLElement::hide_popover_for_bindings() // https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm // https://whatpr.org/html/9457/popover.html#hide-popover-algorithm -WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement, FireEvents fire_events, ThrowExceptions throw_exceptions, IgnoreDomState ignore_dom_state) +WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions, IgnoreDomState ignore_dom_state) { // 1. If the result of running check popover validity given element, true, throwExceptions, null and ignoreDomState is false, then return. if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr, ignore_dom_state))) @@ -1294,12 +1378,22 @@ WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement, FireEv } }; - // 7. If element's popover attribute is in the auto state FIXME: or the hint state, then: - if (popover().has_value() && popover().value() == "auto"sv) { - // FIXME: 7.1. Run hide all popovers until given element, focusPreviousElement, and fireEvents. - // FIXME: 7.2. If the result of running check popover validity given element, true, throwExceptions, and ignoreDomState is false, then run cleanupSteps and return. + // AD-HOC: This implementation checks "opened in popover mode" instead of the current popover state. + // Spec issue: https://github.com/whatwg/html/issues/10996. + // 7. If element's popover attribute is in the auto state or the hint state, then: + if (m_opened_in_popover_mode.has_value() && m_opened_in_popover_mode.value().is_one_of("auto", "hint")) { + // 7.1. Run hide all popovers until given element, focusPreviousElement, and fireEvents. + hide_all_popovers_until(GC::Ptr(this), focus_previous_element, fire_events); + + // 7.2. If the result of running check popover validity given element, true, throwExceptions, and ignoreDomState is false, then run cleanupSteps and return. + if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr, ignore_dom_state))) { + cleanup_steps(); + return {}; + } } - // FIXME: 8. Let autoPopoverListContainsElement be true if document's showing auto popover list's last item is element, otherwise false. + // 8. Let autoPopoverListContainsElement be true if document's showing auto popover list's last item is element, otherwise false. + auto const& showing_popovers = document.showing_auto_popover_list(); + bool auto_popover_list_contains_element = !showing_popovers.is_empty() && showing_popovers.last() == this; // 9. Set element's popover invoker to null. m_popover_invoker = nullptr; @@ -1312,7 +1406,9 @@ WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement, FireEv event_init.new_state = "closed"_string; dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init))); - // FIXME: 10.2. If autoPopoverListContainsElement is true and document's showing auto popover list's last item is not element, then run hide all popovers until given element, focusPreviousElement, and false. + // 10.2. If autoPopoverListContainsElement is true and document's showing auto popover list's last item is not element, then run hide all popovers until given element, focusPreviousElement, and false. + if (auto_popover_list_contains_element && (showing_popovers.is_empty() || showing_popovers.last() != this)) + hide_all_popovers_until(GC::Ptr(this), focus_previous_element, FireEvents::No); // 10.3. If the result of running check popover validity given element, true, throwExceptions, null, and ignoreDomState is false, then run cleanupSteps and return. if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr, ignore_dom_state))) { @@ -1326,7 +1422,33 @@ WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement, FireEv document.remove_an_element_from_the_top_layer_immediately(*this); } - // FIXME: 12. Set element's opened in popover mode to null. + // AD-HOC: The following block of code is all ad-hoc. + // Spec issue: https://github.com/whatwg/html/issues/11007 + + // If element's opened in popover mode is "auto" or "hint": + if (m_opened_in_popover_mode.has_value() && m_opened_in_popover_mode.value().is_one_of("auto", "hint")) { + // If document's showing hint popover list's last item is element: + auto& hint_popovers = document.showing_hint_popover_list(); + if (!hint_popovers.is_empty() && hint_popovers.last() == this) { + // Assert: element's opened in popover mode is "hint". + VERIFY(m_opened_in_popover_mode == "hint"sv); + + // Remove the last item from document's showing hint popover list. + hint_popovers.remove(hint_popovers.size() - 1); + } + // Otherwise: + else { + // Assert: document's showing auto popover list's last item is element. + auto& auto_popovers = document.showing_auto_popover_list(); + VERIFY(!auto_popovers.is_empty() && auto_popovers.last() == this); + + // Remove the last item from document's showing auto popover list. + auto_popovers.remove(auto_popovers.size() - 1); + } + } + + // 12. Set element's opened in popover mode to null. + m_opened_in_popover_mode = {}; // 13. Set element's popover visibility state to hidden. m_popover_visibility_state = PopoverVisibilityState::Hidden; @@ -1384,6 +1506,251 @@ WebIDL::ExceptionOr HTMLElement::toggle_popover(TogglePopoverOptionsOrForc return popover_visibility_state() == PopoverVisibilityState::Showing; } +// AD-HOC: This implementation checks "opened in popover mode" instead of the current popover state. +// Spec issue: https://github.com/whatwg/html/issues/10996. +// https://html.spec.whatwg.org/multipage/popover.html#hide-all-popovers-until +void HTMLElement::hide_all_popovers_until(Variant, GC::Ptr> endpoint, FocusPreviousElement focus_previous_element, FireEvents fire_events) +{ + // To hide all popovers until, given an HTML element or Document endpoint, a boolean focusPreviousElement, and a boolean fireEvents: + + // 1. If endpoint is an HTML element and endpoint is not in the popover showing state, then return. + if (endpoint.has>() && endpoint.get>()->popover_visibility_state() != PopoverVisibilityState::Showing) + return; + + // 2. Let document be endpoint's node document. + auto const* document = endpoint.visit([](auto endpoint) { return &endpoint->document(); }); + + // 3. Assert: endpoint is a Document or endpoint's popover visibility state is showing. + VERIFY(endpoint.has>() || endpoint.get>()->popover_visibility_state() == PopoverVisibilityState::Showing); + + // 4. Assert: endpoint is a Document or endpoint's popover attribute is in the auto state or endpoint's popover attribute is in the hint state. + VERIFY(endpoint.has>() || endpoint.get>()->m_opened_in_popover_mode->is_one_of("auto", "hint")); + + // 5. If endpoint is a Document: + if (endpoint.has>()) { + // 1. Run close entire popover list given document's showing hint popover list, focusPreviousElement, and fireEvents. + close_entire_popover_list(document->showing_hint_popover_list(), focus_previous_element, fire_events); + + // 2. Run close entire popover list given document's showing auto popover list, focusPreviousElement, and fireEvents. + close_entire_popover_list(document->showing_auto_popover_list(), focus_previous_element, fire_events); + + // 3. Return. + return; + } + + // 6. If document's showing hint popover list contains endpoint: + auto endpoint_element = endpoint.get>(); + if (document->showing_hint_popover_list().contains_slow(GC::Ref(*endpoint_element))) { + // 1. Assert: endpoint's popover attribute is in the hint state. + VERIFY(endpoint_element->m_opened_in_popover_mode == "hint"sv); + + // 2. Run hide popover stack until given endpoint, document's showing hint popover list, focusPreviousElement, and fireEvents. + endpoint_element->hide_popover_stack_until(document->showing_hint_popover_list(), focus_previous_element, fire_events); + + // 3. Return. + return; + } + + // 7. Run close entire popover list given document's showing hint popover list, focusPreviousElement, and fireEvents. + close_entire_popover_list(document->showing_hint_popover_list(), focus_previous_element, fire_events); + + // 8. If document's showing auto popover list does not contain endpoint, then return. + if (!document->showing_auto_popover_list().contains_slow(GC::Ref(*endpoint_element))) + return; + + // 9. Run hide popover stack until given endpoint, document's showing auto popover list, focusPreviousElement, and fireEvents. + endpoint_element->hide_popover_stack_until(document->showing_auto_popover_list(), focus_previous_element, fire_events); +} + +// https://html.spec.whatwg.org/multipage/popover.html#hide-popover-stack-until +void HTMLElement::hide_popover_stack_until(Vector> const& popover_list, FocusPreviousElement focus_previous_element, FireEvents fire_events) +{ + // To hide popover stack until, given an HTML element endpoint, a list popoverList, a boolean focusPreviousElement, and a boolean fireEvents: + + // 1. Let repeatingHide be false. + bool repeating_hide = false; + + // 2. Perform the following steps at least once: + do { + // 1. Let lastToHide be null. + GC::Ptr last_to_hide; + + // 2. For each popover in popoverList: + // AD-HOC: This needs to be iterated in reverse because step 4 hides items in reverse. + for (auto const& popover : popover_list.in_reverse()) { + // 1. If popover is endpoint, then break. + if (popover == this) + break; + + // 2. Set lastToHide to popover. + last_to_hide = popover; + } + + // 3. If lastToHide is null, then return. + if (!last_to_hide) + return; + + // 4. While lastToHide's popover visibility state is showing: + while (last_to_hide->popover_visibility_state() == PopoverVisibilityState::Showing) { + // 1. Assert: popoverList is not empty. + VERIFY(!popover_list.is_empty()); + + // 2. Run the hide popover algorithm given the last item in popoverList, focusPreviousElement, fireEvents, and false. + MUST(popover_list.last()->hide_popover(focus_previous_element, fire_events, ThrowExceptions::No, IgnoreDomState::No)); + } + + // 5. Assert: repeatingHide is false or popoverList's last item is endpoint. + VERIFY(!repeating_hide || popover_list.last() == this); + + // 6. Set repeatingHide to true if popoverList contains endpoint and popoverList's last item is not endpoint, otherwise false. + repeating_hide = popover_list.contains_slow(GC::Ref(*this)) && popover_list.last() != this; + + // 7. If repeatingHide is true, then set fireEvents to false. + if (repeating_hide) + fire_events = FireEvents::No; + + } while (repeating_hide); + // and keep performing them while repeatingHide is true. +} + +// https://html.spec.whatwg.org/multipage/popover.html#close-entire-popover-list +void HTMLElement::close_entire_popover_list(Vector> const& popover_list, FocusPreviousElement focus_previous_element, FireEvents fire_events) +{ + // To close entire popover list given a list popoverList, a boolean focusPreviousElement, and a boolean fireEvents: + + // FIXME: If an event handler opens a new popover then this could be an infinite loop. + // 1. While popoverList is not empty: + while (!popover_list.is_empty()) { + // 1. Run the hide popover algorithm given popoverList's last item, focusPreviousElement, fireEvents, and false. + MUST(popover_list.last()->hide_popover(focus_previous_element, fire_events, ThrowExceptions::No, IgnoreDomState::No)); + } +} + +// https://html.spec.whatwg.org/multipage/popover.html#topmost-popover-ancestor +GC::Ptr HTMLElement::topmost_popover_ancestor(GC::Ptr new_popover_or_top_layer_element, Vector> const& popover_list, GC::Ptr invoker, IsPopover is_popover) +{ + // To find the topmost popover ancestor, given a Node newPopoverOrTopLayerElement, a list popoverList, an HTML element or null invoker, and a boolean isPopover, perform the following steps. They return an HTML element or null. + + // 1. If isPopover is true: + auto* new_popover = as_if(*new_popover_or_top_layer_element); + if (is_popover == IsPopover::Yes) { + // 1. Assert: newPopoverOrTopLayerElement is an HTML element. + VERIFY(new_popover); + + // 2. Assert: newPopoverOrTopLayerElement's popover attribute is not in the no popover state or the manual state. + VERIFY(!new_popover->popover().has_value() || new_popover->popover().value() != "manual"sv); + + // 3. Assert: newPopoverOrTopLayerElement's popover visibility state is not in the popover showing state. + VERIFY(new_popover->popover_visibility_state() != PopoverVisibilityState::Showing); + } + // 2. Otherwise: + else { + // 1. Assert: invoker is null. + VERIFY(!invoker); + } + + // 3. Let popoverPositions be an empty ordered map. + OrderedHashMap, int> popover_positions; + + // 4. Let index be 0. + int index = 0; + + // 5. For each popover of popoverList: + for (auto const& popover : popover_list) { + // 1. Set popoverPositions[popover] to index. + popover_positions.set(*popover, index); + + // 2. Increment index by 1. + index++; + } + + // 6. If isPopover is true, then set popoverPositions[newPopoverOrTopLayerElement] to index. + if (is_popover == IsPopover::Yes) + popover_positions.set(*new_popover, index); + + // 7. Increment index by 1. + index++; + + // 8. Let topmostPopoverAncestor be null. + GC::Ptr topmost_popover_ancestor; + + // 9. Let checkAncestor be an algorithm which performs the following steps given candidate: + auto check_ancestor = [&](auto candidate) { + // 1. If candidate is null, then return. + if (!candidate) + return; + + // 2. Let okNesting be false. + bool ok_nesting = false; + + // 3. Let candidateAncestor be null. + GC::Ptr candidate_ancestor; + + // 4. While okNesting is false: + while (!ok_nesting) { + // 1. Set candidateAncestor to the result of running nearest inclusive open popover given candidate. + candidate_ancestor = candidate->nearest_inclusive_open_popover(); + + // 2. If candidateAncestor is null or popoverPositions does not contain candidateAncestor, then return. + if (!candidate_ancestor || !popover_positions.contains(*candidate_ancestor)) + return; + + // 3. Assert: candidateAncestor's popover attribute is not in the manual or none state. + VERIFY(!candidate_ancestor->popover().has_value() || candidate_ancestor->popover().value() != "manual"sv); + + // AD-HOC: This also checks if isPopover is false. + // Spec issue: https://github.com/whatwg/html/issues/11008. + // 4. Set okNesting to true if newPopoverOrTopLayerElement's popover attribute is in the hint state or candidateAncestor's popover attribute is in the auto state. + if (is_popover == IsPopover::No || new_popover->popover() == "hint"sv || candidate_ancestor->popover() == "auto"sv) + ok_nesting = true; + + // 5. If okNesting is false, then set candidate to candidateAncestor's parent in the flat tree. + if (!ok_nesting) + candidate = candidate_ancestor->shadow_including_first_ancestor_of_type(); + } + + // 5. Let candidatePosition be popoverPositions[candidateAncestor]. + auto candidate_position = popover_positions.get(*candidate_ancestor).value(); + + // 6. If topmostPopoverAncestor is null or popoverPositions[topmostPopoverAncestor] is less than candidatePosition, then set topmostPopoverAncestor to candidateAncestor. + if (!topmost_popover_ancestor || popover_positions.get(*topmost_popover_ancestor).value() < candidate_position) + topmost_popover_ancestor = candidate_ancestor; + }; + + // 10. Run checkAncestor given newPopoverOrTopLayerElement's parent node within the flat tree. + check_ancestor(new_popover_or_top_layer_element->shadow_including_first_ancestor_of_type()); + + // 11. Run checkAncestor given invoker. + check_ancestor(invoker.ptr()); + + // 12. Return topmostPopoverAncestor. + return topmost_popover_ancestor; +} + +// https://html.spec.whatwg.org/multipage/popover.html#nearest-inclusive-open-popover +GC::Ptr HTMLElement::nearest_inclusive_open_popover() +{ + // To find the nearest inclusive open popover given a Node node, perform the following steps. They return an HTML element or null. + + // 1. Let currentNode be node. + auto* current_node = this; + + // 2. While currentNode is not null: + while (current_node) { + // AD-HOC: This also allows hint popovers. + // Spec issue: https://github.com/whatwg/html/issues/11008. + // 1. If currentNode's popover attribute is in the auto state and currentNode's popover visibility state is showing, then return currentNode. + if (current_node->popover().has_value() && current_node->popover().value().is_one_of("auto", "hint") && current_node->popover_visibility_state() == PopoverVisibilityState::Showing) + return current_node; + + // 2. Set currentNode to currentNode's parent in the flat tree. + current_node = current_node->shadow_including_first_ancestor_of_type(); + } + + // 3. Return null. + 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) { diff --git a/Libraries/LibWeb/HTML/HTMLElement.h b/Libraries/LibWeb/HTML/HTMLElement.h index 7043c9e870d..1053e05b6d5 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.h +++ b/Libraries/LibWeb/HTML/HTMLElement.h @@ -65,6 +65,11 @@ enum class IgnoreDomState { No, }; +enum class IsPopover { + Yes, + No, +}; + class HTMLElement : public DOM::Element , public HTML::GlobalEventHandlers @@ -123,6 +128,7 @@ public: WebIDL::ExceptionOr set_popover(Optional value); Optional popover() const; + Optional opened_in_popover_mode() const { return m_opened_in_popover_mode; } virtual void removed_from(Node* old_parent, Node& old_root) override; @@ -140,6 +146,9 @@ public: WebIDL::ExceptionOr show_popover(ThrowExceptions throw_exceptions, GC::Ptr invoker); WebIDL::ExceptionOr hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions, IgnoreDomState ignore_dom_state); + 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); + protected: HTMLElement(DOM::Document&, DOM::QualifiedName); @@ -178,6 +187,9 @@ private: void queue_a_popover_toggle_event_task(String old_state, String new_state); 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(); + static void close_entire_popover_list(Vector> const& popover_list, FocusPreviousElement focus_previous_element, FireEvents fire_events); // https://html.spec.whatwg.org/multipage/custom-elements.html#attached-internals GC::Ptr m_attached_internals; @@ -204,6 +216,8 @@ private: // https://html.spec.whatwg.org/multipage/popover.html#popover-close-watcher GC::Ptr m_popover_close_watcher; + + Optional m_opened_in_popover_mode; }; } diff --git a/Tests/LibWeb/Text/expected/popover/auto-hides-other-popovers.txt b/Tests/LibWeb/Text/expected/popover/auto-hides-other-popovers.txt new file mode 100644 index 00000000000..38e03525155 --- /dev/null +++ b/Tests/LibWeb/Text/expected/popover/auto-hides-other-popovers.txt @@ -0,0 +1,2 @@ +PASS +PASS diff --git a/Tests/LibWeb/Text/expected/popover/hint-only-hides-hint-popovers.txt b/Tests/LibWeb/Text/expected/popover/hint-only-hides-hint-popovers.txt new file mode 100644 index 00000000000..1f514b58545 --- /dev/null +++ b/Tests/LibWeb/Text/expected/popover/hint-only-hides-hint-popovers.txt @@ -0,0 +1,3 @@ +PASS +PASS +PASS diff --git a/Tests/LibWeb/Text/expected/popover-crashes.txt b/Tests/LibWeb/Text/expected/popover/popover-crashes.txt similarity index 100% rename from Tests/LibWeb/Text/expected/popover-crashes.txt rename to Tests/LibWeb/Text/expected/popover/popover-crashes.txt diff --git a/Tests/LibWeb/Text/expected/HTML/popover-invoker-attributes.txt b/Tests/LibWeb/Text/expected/popover/popover-invoker-attributes.txt similarity index 100% rename from Tests/LibWeb/Text/expected/HTML/popover-invoker-attributes.txt rename to Tests/LibWeb/Text/expected/popover/popover-invoker-attributes.txt diff --git a/Tests/LibWeb/Text/expected/popover/popover-invoker-is-ancestor.txt b/Tests/LibWeb/Text/expected/popover/popover-invoker-is-ancestor.txt new file mode 100644 index 00000000000..38e03525155 --- /dev/null +++ b/Tests/LibWeb/Text/expected/popover/popover-invoker-is-ancestor.txt @@ -0,0 +1,2 @@ +PASS +PASS diff --git a/Tests/LibWeb/Text/input/popover/auto-hides-other-popovers.html b/Tests/LibWeb/Text/input/popover/auto-hides-other-popovers.html new file mode 100644 index 00000000000..9edb568dbdf --- /dev/null +++ b/Tests/LibWeb/Text/input/popover/auto-hides-other-popovers.html @@ -0,0 +1,41 @@ + + + + + + +
+
+
+
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/popover/hint-only-hides-hint-popovers.html b/Tests/LibWeb/Text/input/popover/hint-only-hides-hint-popovers.html new file mode 100644 index 00000000000..496d08fa882 --- /dev/null +++ b/Tests/LibWeb/Text/input/popover/hint-only-hides-hint-popovers.html @@ -0,0 +1,39 @@ + + + + + +
+
+
+ + diff --git a/Tests/LibWeb/Text/input/popover-crashes.html b/Tests/LibWeb/Text/input/popover/popover-crashes.html similarity index 94% rename from Tests/LibWeb/Text/input/popover-crashes.html rename to Tests/LibWeb/Text/input/popover/popover-crashes.html index 7e268cb13d6..f1ae3bfb7b0 100644 --- a/Tests/LibWeb/Text/input/popover-crashes.html +++ b/Tests/LibWeb/Text/input/popover/popover-crashes.html @@ -1,5 +1,5 @@ - +
+ + + + +
+ + +
+
+ +
+
+
+ +