From eb1c60f37b4e3e01a261882d7af5296a40e3b500 Mon Sep 17 00:00:00 2001 From: Luke Warlow Date: Thu, 5 Dec 2024 23:24:24 +0000 Subject: [PATCH] LibWeb: Implement popover methods Implements basics of showPopover, hidePopover and togglePopover. --- Libraries/LibWeb/CSS/PseudoClasses.json | 3 + Libraries/LibWeb/CSS/SelectorEngine.cpp | 10 + Libraries/LibWeb/HTML/HTMLDialogElement.cpp | 8 +- Libraries/LibWeb/HTML/HTMLElement.cpp | 238 ++++++++++++++++++ Libraries/LibWeb/HTML/HTMLElement.h | 56 +++++ Libraries/LibWeb/HTML/HTMLElement.idl | 16 +- .../popovers/popover-active-document.txt | 6 + .../html/semantics/popovers/popover-types.txt | 6 + .../html/semantics/popovers/togglePopover.txt | 9 + .../popovers/popover-active-document.html | 17 ++ .../semantics/popovers/popover-types.html | 37 +++ .../semantics/popovers/togglePopover.html | 79 ++++++ 12 files changed, 480 insertions(+), 5 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/popover-active-document.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/popover-types.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/togglePopover.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/popover-active-document.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/popover-types.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/togglePopover.html diff --git a/Libraries/LibWeb/CSS/PseudoClasses.json b/Libraries/LibWeb/CSS/PseudoClasses.json index 7fc5b8bd7a5..0fb9d81e4b4 100644 --- a/Libraries/LibWeb/CSS/PseudoClasses.json +++ b/Libraries/LibWeb/CSS/PseudoClasses.json @@ -101,6 +101,9 @@ "open": { "argument": "" }, + "popover-open": { + "argument": "" + }, "paused": { "argument": "" }, diff --git a/Libraries/LibWeb/CSS/SelectorEngine.cpp b/Libraries/LibWeb/CSS/SelectorEngine.cpp index 93a1061a0e7..fb08103f660 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.cpp +++ b/Libraries/LibWeb/CSS/SelectorEngine.cpp @@ -738,6 +738,16 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla // FIXME: fullscreen elements are also modal. return false; } + case CSS::PseudoClass::PopoverOpen: { + // https://html.spec.whatwg.org/#selector-popover-open + // The :popover-open pseudo-class is defined to match any HTML element whose popover attribute is not in the no popover state and whose popover visibility state is showing. + if (is(element) && element.has_attribute(HTML::AttributeNames::popover)) { + auto& html_element = static_cast(element); + return html_element.popover_visibility_state() == HTML::HTMLElement::PopoverVisibilityState::Showing; + } + + return false; + } } return false; diff --git a/Libraries/LibWeb/HTML/HTMLDialogElement.cpp b/Libraries/LibWeb/HTML/HTMLDialogElement.cpp index 851ef196ffa..83067ec9d34 100644 --- a/Libraries/LibWeb/HTML/HTMLDialogElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLDialogElement.cpp @@ -163,7 +163,9 @@ WebIDL::ExceptionOr HTMLDialogElement::show_modal() if (!is_connected()) return WebIDL::InvalidStateError::create(realm(), "Dialog not connected"_string); - // FIXME: 5. If this is in the popover showing state, then throw an "InvalidStateError" DOMException. + // 5. If this is in the popover showing state, then throw an "InvalidStateError" DOMException. + if (popover_visibility_state() == PopoverVisibilityState::Showing) + return WebIDL::InvalidStateError::create(realm(), "Dialog already open as popover"_string); // 6. If the result of firing an event named beforetoggle, using ToggleEvent, // with the cancelable attribute initialized to true, the oldState attribute initialized to "closed", @@ -185,7 +187,9 @@ WebIDL::ExceptionOr HTMLDialogElement::show_modal() if (!is_connected()) return {}; - // FIXME: 9. If this is in the popover showing state, then return. + // 9. If this is in the popover showing state, then return. + if (popover_visibility_state() == PopoverVisibilityState::Showing) + return {}; // 10. Queue a dialog toggle event task given subject, "closed", and "open". queue_a_dialog_toggle_event_task("closed"_string, "open"_string); diff --git a/Libraries/LibWeb/HTML/HTMLElement.cpp b/Libraries/LibWeb/HTML/HTMLElement.cpp index 97a82fb24cc..c7d1f0fdf37 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLElement.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +63,7 @@ void HTMLElement::visit_edges(Cell::Visitor& visitor) HTMLOrSVGElement::visit_edges(visitor); visitor.visit(m_labels); visitor.visit(m_attached_internals); + visitor.visit(m_popover_invoker); } // https://html.spec.whatwg.org/multipage/dom.html#dom-dir @@ -925,6 +927,242 @@ void HTMLElement::adjust_computed_style(CSS::StyleProperties& style) } } +// https://html.spec.whatwg.org/multipage/popover.html#check-popover-validity +WebIDL::ExceptionOr HTMLElement::check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr expected_document) +{ + // 1. If element's popover attribute is in the no popover state, then: + if (!popover().has_value()) { + // 1.1. If throwExceptions is true, then throw a "NotSupportedError" DOMException. + if (throw_exceptions == ThrowExceptions::Yes) + return WebIDL::NotSupportedError::create(realm(), "Element is not a popover"_string); + // 1.2. Return false. + return false; + } + + // 2. If any of the following are true: + // - expectedToBeShowing is true and element's popover visibility state is not showing; or + // - expectedToBeShowing is false and element's popover visibility state is not hidden, + if ((expected_to_be_showing == ExpectedToBeShowing::Yes && m_popover_visibility_state != PopoverVisibilityState::Showing) || (expected_to_be_showing == ExpectedToBeShowing::No && m_popover_visibility_state != PopoverVisibilityState::Hidden)) { + // then return false. + return false; + } + + // 3. If any of the following are true: + // - element is not connected; + // - element's node document is not fully active; + // - expectedDocument is not null and element's node document is not expectedDocument; + // - element is a dialog element and its is modal flage is set to true; or + // - FIXME: element's fullscreen flag is set, + // then: + // 3.1 If throwExceptions is true, then throw an "InvalidStateError" DOMException. + // 3.2 Return false. + if (!is_connected() || !document().is_fully_active() || (expected_document && &document() != expected_document) || (is(*this) && verify_cast(*this).is_modal())) { + if (throw_exceptions == ThrowExceptions::Yes) + return WebIDL::InvalidStateError::create(realm(), "Element is not in a valid state to show a popover"_string); + return false; + } + + // 4. Return true. + return true; +} + +// https://html.spec.whatwg.org/multipage/popover.html#dom-showpopover +WebIDL::ExceptionOr HTMLElement::show_popover_for_bindings(ShowPopoverOptions const& options) +{ + // 1. Let invoker be options["source"] if it exists; otherwise, null. + auto invoker = options.source; + // 2. Run show popover given this, true, and invoker. + return show_popover(ThrowExceptions::Yes, invoker); +} + +// https://html.spec.whatwg.org/multipage/popover.html#show-popover +WebIDL::ExceptionOr HTMLElement::show_popover(ThrowExceptions throw_exceptions, GC::Ptr invoker) +{ + // 1. If the result of running check popover validity given element, false, throwExceptions, and null is false, then return. + if (!TRY(check_popover_validity(ExpectedToBeShowing::No, throw_exceptions, nullptr))) + return {}; + + // 2. Let document be element's node document. + auto& document = this->document(); + + // 3. Assert: element's popover invoker is null. + VERIFY(!m_popover_invoker); + + // 4. Assert: element is not in document's top layer. + VERIFY(!in_top_layer()); + + // 5. Let nestedShow be element's popover showing or hiding. + auto nested_show = m_popover_showing_or_hiding; + + // 6. Set element's popover showing or hiding to true. + m_popover_showing_or_hiding = true; + + // 7. Let cleanupShowingFlag be the following steps: + auto cleanup_showing_flag = GC::create_function(this->heap(), [&nested_show, this] { + // 7.1. If nestedShow is false, then set element's popover showing or hiding to false. + if (!nested_show) + m_popover_showing_or_hiding = false; + }); + + // FIXME: 8. If the result of firing an event named beforetoggle, using ToggleEvent, with the cancelable attribute initialized to true, the oldState attribute initialized to "closed", and the newState attribute initialized to "open" at element is false, then run cleanupShowingFlag and return. + + // FIXME: 9. If the result of running check popover validity given element, false, throwExceptions, and document is false, then run cleanupShowingFlag and return. + + // 10. Let shouldRestoreFocus be false. + bool should_restore_focus = false; + + // 11. If element's popover attribute is in the auto state, then: + if (popover().has_value() && popover().value() == "auto"sv) { + // FIXME: 11.1. Let originalType be the value of element's popover attribute. + // FIXME: 11.2. Let ancestor be the result of running the topmost popover ancestor algorithm given element, invoker, and true. + // FIXME: 11.3. If ancestor is null, then set ancestor to document. + // FIXME: 11.4. Run hide all popovers until given ancestor, false, and not nestedShow. + // FIXME: 11.5. If originalType is not equal to the value of element's popover attribute, then throw a "InvalidStateError" DOMException. + // FIXME: 11.6. If the result of running check popover validity given element, false, throwExceptions, and document is false, then run cleanupShowingFlag and return. + // FIXME: 11.7. If the result of running topmost auto popover on document is null, then set shouldRestoreFocus to true. + // FIXME: 11.8. Set element's popover close watcher to the result of establishing a close watcher given element's relevant global object, with: + // - cancelAction being to return true. + // - closeAction being to hide a popover given element, true, true, and false. + } + + // FIXME: 12. Set element's previously focused element to null. + // FIXME: 13. Let originallyFocusedElement be document's focused area of the document's DOM anchor. + + // 14. Add an element to the top layer given element. + document.add_an_element_to_the_top_layer(*this); + // 15. Set element's popover visibility state to showing. + m_popover_visibility_state = PopoverVisibilityState::Showing; + // 16. Set element's popover invoker to invoker. + m_popover_invoker = invoker; + + // FIXME: 17. Set element's implicit anchor element to invoker. + + // FIXME: 18. Run the popover focusing steps given element. + + // 19. If shouldRestoreFocus is true and element's popover attribute is not in the no popover state + if (should_restore_focus && popover().has_value()) { + // FIXME: then set element's previously focused element to originallyFocusedElement. + } + + // FIXME: 20. Queue a popover toggle event task given element, "closed", and "open". + // 21. Run cleanupShowingFlag. + cleanup_showing_flag->function()(); + + return {}; +} + +// https://html.spec.whatwg.org/multipage/popover.html#dom-hidepopover +WebIDL::ExceptionOr HTMLElement::hide_popover_for_bindings() +{ + // The hidePopover() method steps are to run the hide popover algorithm given this, true, true, and true. + return hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes); +} + +// https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm +WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement, FireEvents fire_events, ThrowExceptions throw_exceptions) +{ + // 1. If the result of running check popover validity given element, true, throwExceptions, and null is false, then return. + if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr))) + return {}; + + // 2. Let document be element's node document. + auto& document = this->document(); + + // 3. Let nestedHide be element's popover showing or hiding. + auto nested_hide = m_popover_showing_or_hiding; + + // 4. Set element's popover showing or hiding to true. + m_popover_showing_or_hiding = true; + + // 5. If nestedHide is true, then set fireEvents to false. + if (nested_hide) + fire_events = FireEvents::No; + + // 6. Let cleanupSteps be the following steps: + auto cleanup_steps = GC::create_function(this->heap(), [&nested_hide, this] { + // 6.1. If nestedHide is false, then set element's popover showing or hiding to false. + if (nested_hide) + m_popover_showing_or_hiding = false; + // FIXME: 6.2. If element's popover close watcher is not null, then: + // FIXME: 6.2.1. Destroy element's popover close watcher. + // FIXME: 6.2.2. Set element's popover close watcher to null. + }); + + // 7. If element's popover attribute is in the auto 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, and throwExceptions is false, then run cleanupSteps and return. + } + // FIXME: 8. Let autoPopoverListContainsElement be true if document's showing auto popover list's last item is element, otherwise false. + + // 9. Set element's popover invoker to null. + m_popover_invoker = nullptr; + + // 10. If fireEvents is true: + if (fire_events == FireEvents::Yes) { + // FIXME: 10.1. Fire an event named beforetoggle, using ToggleEvent, with the oldState attribute initialized to "open" and the newState attribute initialized to "closed" at element. + // 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. + // FIXME: 10.3. If the result of running check popover validity given element, true, throwExceptions, and null is false, then run cleanupSteps and return. + // 10.4. Request an element to be removed from the top layer given element. + document.request_an_element_to_be_remove_from_the_top_layer(*this); + } else { + // 11. Otherwise, remove an element from the top layer immediately given element. + document.remove_an_element_from_the_top_layer_immediately(*this); + } + + // 12. Set element's popover visibility state to hidden. + m_popover_visibility_state = PopoverVisibilityState::Hidden; + + // FIXME: 13. If fireEvents is true, then queue a popover toggle event task given element, "open", and "closed". + + // FIXME: 14. Let previouslyFocusedElement be element's previously focused element. + + // FIXME: 15. If previouslyFocusedElement is not null, then: + // FIXME: 15.1. Set element's previously focused element to null. + // FIXME: 15.2. If focusPreviousElement is true and document's focused area of the document's DOM anchor is a shadow-including inclusive descendant of element, then run the focusing steps for previouslyFocusedElement; the viewport should not be scrolled by doing this step. + + // 16. Run cleanupSteps. + cleanup_steps->function()(); + + return {}; +} + +// https://html.spec.whatwg.org/multipage/popover.html#dom-togglepopover +WebIDL::ExceptionOr HTMLElement::toggle_popover(TogglePopoverOptionsOrForceBoolean const& options) +{ + // 1. Let force be null. + Optional force; + GC::Ptr invoker; + + // 2. If options is a boolean, set force to options. + options.visit( + [&force](bool forceBool) { + force = forceBool; + }, + [&force, &invoker](TogglePopoverOptions options) { + // 3. Otherwise, if options["force"] exists, set force to options["force"]. + force = options.force; + // 4. Let invoker be options["source"] if it exists; otherwise, null. + invoker = options.source; + }); + + // 5. If this's popover visibility state is showing, and force is null or false, then run the hide popover algorithm given this, true, true, and true. + if (popover_visibility_state() == PopoverVisibilityState::Showing && (!force.has_value() || !force.value())) + TRY(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes)); + // 6. Otherwise, if force is not present or true, then run show popover given this true, and invoker. + else if (!force.has_value() || force.value()) + TRY(show_popover(ThrowExceptions::Yes, invoker)); + // 7. Otherwise: + else { + // 7.1 Let expectedToBeShowing be true if this's popover visibility state is showing; otherwise false. + ExpectedToBeShowing expected_to_be_showing = popover_visibility_state() == PopoverVisibilityState::Showing ? ExpectedToBeShowing::Yes : ExpectedToBeShowing::No; + // 7.2 Run check popover validity given expectedToBeShowing, true, and null. + TRY(check_popover_validity(expected_to_be_showing, ThrowExceptions::Yes, nullptr)); + } + // 8. Return true if this's popover visibility state is showing; otherwise false. + return popover_visibility_state() == PopoverVisibilityState::Showing; +} + void HTMLElement::did_receive_focus() { if (m_content_editable_state != ContentEditableState::True) diff --git a/Libraries/LibWeb/HTML/HTMLElement.h b/Libraries/LibWeb/HTML/HTMLElement.h index 9f106f54952..77c67f50558 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.h +++ b/Libraries/LibWeb/HTML/HTMLElement.h @@ -28,6 +28,36 @@ enum class ContentEditableState { Inherit, }; +struct ShowPopoverOptions { + GC::Ptr source; +}; + +struct TogglePopoverOptions : public ShowPopoverOptions { + Optional force {}; +}; + +using TogglePopoverOptionsOrForceBoolean = Variant; + +enum class ThrowExceptions { + Yes, + No, +}; + +enum class FocusPreviousElement { + Yes, + No, +}; + +enum class FireEvents { + Yes, + No, +}; + +enum class ExpectedToBeShowing { + Yes, + No, +}; + class HTMLElement : public DOM::Element , public HTML::GlobalEventHandlers @@ -85,6 +115,21 @@ public: WebIDL::ExceptionOr set_popover(Optional value); Optional popover() const; + enum class PopoverVisibilityState { + Hidden, + Showing, + }; + PopoverVisibilityState popover_visibility_state() const { return m_popover_visibility_state; } + + WebIDL::ExceptionOr show_popover_for_bindings(ShowPopoverOptions const& = {}); + WebIDL::ExceptionOr hide_popover_for_bindings(); + WebIDL::ExceptionOr toggle_popover(TogglePopoverOptionsOrForceBoolean const&); + + WebIDL::ExceptionOr show_popover(ThrowExceptions throw_exceptions, GC::Ptr invoker); + WebIDL::ExceptionOr hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions); + + WebIDL::ExceptionOr check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr); + protected: HTMLElement(DOM::Document&, DOM::QualifiedName); @@ -119,6 +164,17 @@ private: // https://html.spec.whatwg.org/multipage/interaction.html#click-in-progress-flag bool m_click_in_progress { false }; + + // Popover API + + // https://html.spec.whatwg.org/multipage/popover.html#popover-visibility-state + PopoverVisibilityState m_popover_visibility_state { PopoverVisibilityState::Hidden }; + + // https://html.spec.whatwg.org/multipage/popover.html#popover-invoker + GC::Ptr m_popover_invoker; + + // https://html.spec.whatwg.org/multipage/popover.html#popover-showing-or-hiding + bool m_popover_showing_or_hiding { false }; }; } diff --git a/Libraries/LibWeb/HTML/HTMLElement.idl b/Libraries/LibWeb/HTML/HTMLElement.idl index 71c0a47984f..3d81cbc3c69 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.idl +++ b/Libraries/LibWeb/HTML/HTMLElement.idl @@ -34,9 +34,9 @@ interface HTMLElement : Element { ElementInternals attachInternals(); // The popover API - [FIXME] undefined showPopover(); - [FIXME] undefined hidePopover(); - [FIXME] boolean togglePopover(optional boolean force); + [ImplementedAs=show_popover_for_bindings] undefined showPopover(optional ShowPopoverOptions options = {}); + [ImplementedAs=hide_popover_for_bindings] undefined hidePopover(); + boolean togglePopover(optional (TogglePopoverOptions or boolean) options = {}); [CEReactions] attribute DOMString? popover; // https://drafts.csswg.org/cssom-view/#extensions-to-the-htmlelement-interface @@ -48,6 +48,16 @@ interface HTMLElement : Element { }; +// https://html.spec.whatwg.org/multipage/dom.html#showpopoveroptions +dictionary ShowPopoverOptions { + HTMLElement source; +}; + +// https://html.spec.whatwg.org/multipage/dom.html#togglepopoveroptions +dictionary TogglePopoverOptions : ShowPopoverOptions { + boolean force; +}; + HTMLElement includes GlobalEventHandlers; HTMLElement includes ElementContentEditable; HTMLElement includes HTMLOrSVGElement; diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/popover-active-document.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/popover-active-document.txt new file mode 100644 index 00000000000..3ec5826cd39 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/popover-active-document.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass showPopover should throw when the document isn't active \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/popover-types.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/popover-types.txt new file mode 100644 index 00000000000..5fc56baf9cb --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/popover-types.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass manuals do not close popovers \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/togglePopover.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/togglePopover.txt new file mode 100644 index 00000000000..e5c8b8d1c57 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/popovers/togglePopover.txt @@ -0,0 +1,9 @@ +Harness status: OK + +Found 3 tests + +2 Pass +1 Fail +Pass togglePopover should toggle the popover and return true or false as specified. +Fail togglePopover's return value should reflect what the end state is, not just the force parameter. +Pass togglePopover should throw an exception when there is no popover attribute. \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/popover-active-document.html b/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/popover-active-document.html new file mode 100644 index 00000000000..b682d787b5b --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/popover-active-document.html @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/popover-types.html b/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/popover-types.html new file mode 100644 index 00000000000..8d98d803d8a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/popover-types.html @@ -0,0 +1,37 @@ + + + + + + + +
+
Popover
+
Async
+
Async
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/togglePopover.html b/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/togglePopover.html new file mode 100644 index 00000000000..4e552963e66 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/popovers/togglePopover.html @@ -0,0 +1,79 @@ + + + + + + +
popover
+
popover
+ +