diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index e48a7f52028ea..b169e0ba02403 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -5814,10 +5814,11 @@ void Document::add_an_element_to_the_top_layer(GC::Ref element) // 3. Append el to doc’s top layer. m_top_layer_elements.set(element); - element->set_in_top_layer(true); // FIXME: 4. At the UA !important cascade origin, add a rule targeting el containing an overlay: auto declaration. + element->set_rendered_in_top_layer(true); + element->set_needs_style_update(true); } // https://drafts.csswg.org/css-position-4/#request-an-element-to-be-removed-from-the-top-layer @@ -5830,9 +5831,12 @@ void Document::request_an_element_to_be_remove_from_the_top_layer(GC::Refset_rendered_in_top_layer(false); + element->set_needs_style_update(true); // 4. Append el to doc’s pending top layer removals. m_top_layer_pending_removals.set(element); + element->set_in_top_layer(false); } // https://drafts.csswg.org/css-position-4/#remove-an-element-from-the-top-layer-immediately @@ -5842,10 +5846,11 @@ void Document::remove_an_element_from_the_top_layer_immediately(GC::Ref // 2. Remove el from doc’s top layer and pending top layer removals. m_top_layer_elements.remove(element); - element->set_in_top_layer(false); // FIXME: 3. Remove the UA !important overlay: auto rule targeting el, if it exists. + element->set_rendered_in_top_layer(false); + element->set_needs_style_update(true); } // https://drafts.csswg.org/css-position-4/#process-top-layer-removals @@ -5854,11 +5859,10 @@ void Document::process_top_layer_removals() // 1. For each element el in doc’s pending top layer removals: if el’s computed value of overlay is none, or el is // not rendered, remove el from doc’s top layer and pending top layer removals. for (auto& element : m_top_layer_pending_removals) { - // FIXME: Check overlay property - if (!element->paintable()) { + // FIXME: Implement overlay property + if (true || !element->paintable()) { m_top_layer_elements.remove(element); m_top_layer_pending_removals.remove(element); - element->set_in_top_layer(false); } } } diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 3c785a79924fd..d83ff096e1fe4 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -388,9 +388,16 @@ class Element return nullptr; } + // An element el is in the top layer if el is contained in its node document’s top layer + // but not contained in its node document’s pending top layer removals. void set_in_top_layer(bool in_top_layer) { m_in_top_layer = in_top_layer; } bool in_top_layer() const { return m_in_top_layer; } + // An element el is rendered in the top layer if el is contained in its node document’s top layer, + // FIXME: and el has overlay: auto. + void set_rendered_in_top_layer(bool rendered_in_top_layer) { m_rendered_in_top_layer = rendered_in_top_layer; } + bool rendered_in_top_layer() const { return m_rendered_in_top_layer; } + bool has_non_empty_counters_set() const { return m_counters_set; } Optional counters_set(); CSS::CountersSet& ensure_counters_set(); @@ -499,6 +506,7 @@ class Element Array m_scroll_offset; bool m_in_top_layer { false }; + bool m_rendered_in_top_layer { false }; bool m_style_uses_css_custom_properties { false }; OwnPtr m_counters_set; diff --git a/Libraries/LibWeb/HTML/HTMLButtonElement.cpp b/Libraries/LibWeb/HTML/HTMLButtonElement.cpp index 90e8e9207b87e..dc16d6dee62e1 100644 --- a/Libraries/LibWeb/HTML/HTMLButtonElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLButtonElement.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -115,7 +116,9 @@ void HTMLButtonElement::activation_behavior(DOM::Event const& event) } } - // 4. FIXME: Run the popover target attribute activation behavior given element. + // 4. Run the popover target attribute activation behavior given element and event's target. + if (event.target() && event.target()->is_dom_node()) + PopoverInvokerElement::popover_target_activation_behaviour(*this, as(*event.target())); } bool HTMLButtonElement::is_focusable() const diff --git a/Libraries/LibWeb/HTML/HTMLElement.cpp b/Libraries/LibWeb/HTML/HTMLElement.cpp index 9bb2c619af56d..52ac5b3748146 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLElement.cpp @@ -619,6 +619,27 @@ void HTMLElement::attribute_changed(FlyString const& name, Optional cons } ENUMERATE_GLOBAL_EVENT_HANDLERS(__ENUMERATE) #undef __ENUMERATE + + [&]() { + // https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute:concept-element-attributes-change-ext + // https://whatpr.org/html/9457/popover.html#the-popover-attribute:concept-element-attributes-change-ext + // The following attribute change steps, given element, localName, oldValue, value, and namespace, are used for all HTML elements: + + // 1. If namespace is not null, then return. + if (namespace_.has_value()) + return; + + // 2. If localName is not popover, then return. + if (name != HTML::AttributeNames::popover) + return; + + // 3. If element's popover visibility state is in the showing state + // and oldValue and value are in different states, + // then run the hide popover algorithm given element, true, true, false, and true. + if (m_popover_visibility_state == PopoverVisibilityState::Showing + && popover_value_to_state(old_value) != popover_value_to_state(value)) + MUST(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::Yes)); + }(); } WebIDL::ExceptionOr HTMLElement::cloned(Web::DOM::Node& copy, bool clone_children) const @@ -957,13 +978,8 @@ WebIDL::ExceptionOr> HTMLElement::attach_internals() return { internals }; } -// https://html.spec.whatwg.org/multipage/popover.html#dom-popover -Optional HTMLElement::popover() const +Optional HTMLElement::popover_value_to_state(Optional value) { - // FIXME: This should probably be `Reflect` in the IDL. - // The popover IDL attribute must reflect the popover attribute, limited to only known values. - auto value = get_attribute(HTML::AttributeNames::popover); - if (!value.has_value()) return {}; @@ -975,6 +991,15 @@ Optional HTMLElement::popover() const return "manual"_string; } +// https://html.spec.whatwg.org/multipage/popover.html#dom-popover +Optional HTMLElement::popover() const +{ + // FIXME: This should probably be `Reflect` in the IDL. + // The popover IDL attribute must reflect the popover attribute, limited to only known values. + auto value = get_attribute(HTML::AttributeNames::popover); + return popover_value_to_state(value); +} + // https://html.spec.whatwg.org/multipage/popover.html#dom-popover WebIDL::ExceptionOr HTMLElement::set_popover(Optional value) { @@ -997,10 +1022,11 @@ void HTMLElement::adjust_computed_style(CSS::ComputedProperties& 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) +// https://whatpr.org/html/9457/popover.html#check-popover-validity +WebIDL::ExceptionOr HTMLElement::check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr expected_document, IgnoreDomState ignore_dom_state) { - // 1. If element's popover attribute is in the no popover state, then: - if (!popover().has_value()) { + // 1. If ignoreDomState is false and element's popover attribute is in the no popover state, then: + if (ignore_dom_state == IgnoreDomState::No && !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); @@ -1017,15 +1043,19 @@ WebIDL::ExceptionOr HTMLElement::check_popover_validity(ExpectedToBeShowin } // 3. If any of the following are true: - // - element is not connected; + // - ignoreDomState is false and element is not connected; // - element's node document is not fully active; - // - expectedDocument is not null and element's node document is not expectedDocument; + // - ignoreDomState is false and 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) && as(*this).is_modal())) { + + if ((ignore_dom_state == IgnoreDomState::No && !is_connected()) + || !document().is_fully_active() + || (ignore_dom_state == IgnoreDomState::No && expected_document && &document() != expected_document) + || (is(*this) && as(*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; @@ -1045,10 +1075,11 @@ WebIDL::ExceptionOr HTMLElement::show_popover_for_bindings(ShowPopoverOpti } // https://html.spec.whatwg.org/multipage/popover.html#show-popover +// https://whatpr.org/html/9457/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))) + // 1. If the result of running check popover validity given element, false, throwExceptions, null and false is false, then return. + if (!TRY(check_popover_validity(ExpectedToBeShowing::No, throw_exceptions, nullptr, IgnoreDomState::No))) return {}; // 2. Let document be element's node document. @@ -1085,8 +1116,8 @@ WebIDL::ExceptionOr HTMLElement::show_popover(ThrowExceptions throw_except return {}; } - // 10. If the result of running check popover validity given element, false, throwExceptions, and document is false, then run cleanupShowingFlag and return. - if (!TRY(check_popover_validity(ExpectedToBeShowing::No, throw_exceptions, nullptr))) { + // 10. 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 {}; } @@ -1123,7 +1154,7 @@ WebIDL::ExceptionOr HTMLElement::show_popover(ThrowExceptions throw_except // 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, and document is false, then run cleanupShowingFlag and 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. @@ -1139,7 +1170,7 @@ WebIDL::ExceptionOr HTMLElement::show_popover(ThrowExceptions throw_except // - closeAction being to hide a popover given element, true, true, and false. auto close_callback_function = JS::NativeFunction::create( realm(), [this](JS::VM&) { - MUST(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No)); + MUST(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No)); return JS::js_undefined(); }, @@ -1168,17 +1199,19 @@ WebIDL::ExceptionOr HTMLElement::show_popover(ThrowExceptions throw_except } // https://html.spec.whatwg.org/multipage/popover.html#dom-hidepopover +// https://whatpr.org/html/9457/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); + // The hidePopover() method steps are to run the hide popover algorithm given this, true, true, true, and false. + return hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes, IgnoreDomState::No); } // https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm -WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement, FireEvents fire_events, ThrowExceptions throw_exceptions) +// 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) { - // 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))) + // 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))) return {}; // 2. Let document be element's node document. @@ -1211,7 +1244,7 @@ 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, and throwExceptions is false, then run cleanupSteps and return. + // FIXME: 7.2. If the result of running check popover validity given element, true, throwExceptions, and ignoreDomState 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. @@ -1228,8 +1261,8 @@ WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement, FireEv // 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.3. If the result of running check popover validity given element, true, throwExceptions, and null is false, then run cleanupSteps and return. - if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr))) { + // 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))) { cleanup_steps(); return {}; } @@ -1262,6 +1295,7 @@ WebIDL::ExceptionOr HTMLElement::hide_popover(FocusPreviousElement, FireEv } // https://html.spec.whatwg.org/multipage/popover.html#dom-togglepopover +// https://whatpr.org/html/9457/popover.html#dom-togglepopover WebIDL::ExceptionOr HTMLElement::toggle_popover(TogglePopoverOptionsOrForceBoolean const& options) { // 1. Let force be null. @@ -1280,9 +1314,9 @@ WebIDL::ExceptionOr HTMLElement::toggle_popover(TogglePopoverOptionsOrForc 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. + // 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, true, and false. if (popover_visibility_state() == PopoverVisibilityState::Showing && (!force.has_value() || !force.value())) - TRY(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes)); + TRY(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes, IgnoreDomState::No)); // 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)); @@ -1290,8 +1324,8 @@ WebIDL::ExceptionOr HTMLElement::toggle_popover(TogglePopoverOptionsOrForc 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)); + // 7.2 Run check popover validity given expectedToBeShowing, true, null, and false. + TRY(check_popover_validity(expected_to_be_showing, ThrowExceptions::Yes, nullptr, IgnoreDomState::No)); } // 8. Return true if this's popover visibility state is showing; otherwise false. return popover_visibility_state() == PopoverVisibilityState::Showing; @@ -1368,9 +1402,10 @@ void HTMLElement::removed_from(Node* old_parent, Node& old_root) { Element::removed_from(old_parent, old_root); - // If removedNode's popover attribute is not in the no popover state, then run the hide popover algorithm given removedNode, false, false, and false. + // https://whatpr.org/html/9457/infrastructure.html#dom-trees:concept-node-remove-ext + // If removedNode's popover attribute is not in the no popover state, then run the hide popover algorithm given removedNode, false, false, false, and true. if (popover().has_value()) - MUST(hide_popover(FocusPreviousElement::No, FireEvents::No, ThrowExceptions::No)); + MUST(hide_popover(FocusPreviousElement::No, FireEvents::No, ThrowExceptions::No, IgnoreDomState::Yes)); } // https://html.spec.whatwg.org/multipage/interaction.html#dom-accesskeylabel diff --git a/Libraries/LibWeb/HTML/HTMLElement.h b/Libraries/LibWeb/HTML/HTMLElement.h index b5d1d7f8d9450..e373f84ac5659 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.h +++ b/Libraries/LibWeb/HTML/HTMLElement.h @@ -60,6 +60,11 @@ enum class ExpectedToBeShowing { No, }; +enum class IgnoreDomState { + Yes, + No, +}; + class HTMLElement : public DOM::Element , public HTML::GlobalEventHandlers @@ -131,8 +136,9 @@ class HTMLElement WebIDL::ExceptionOr hide_popover_for_bindings(); WebIDL::ExceptionOr toggle_popover(TogglePopoverOptionsOrForceBoolean const&); + WebIDL::ExceptionOr check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr, IgnoreDomState ignore_dom_state); 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 hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions, IgnoreDomState ignore_dom_state); protected: HTMLElement(DOM::Document&, DOM::QualifiedName); @@ -160,10 +166,10 @@ class HTMLElement GC::Ptr m_labels; - WebIDL::ExceptionOr check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr); - void queue_a_popover_toggle_event_task(String old_state, String new_state); + static Optional popover_value_to_state(Optional value); + // https://html.spec.whatwg.org/multipage/custom-elements.html#attached-internals GC::Ptr m_attached_internals; diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index e4bfe296f1f6b..f2259a0811b49 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -1261,8 +1261,10 @@ void HTMLInputElement::did_lose_focus() commit_pending_changes(); } -void HTMLInputElement::form_associated_element_attribute_changed(FlyString const& name, Optional const& value, Optional const&) +void HTMLInputElement::form_associated_element_attribute_changed(FlyString const& name, Optional const& value, Optional const& namespace_) { + PopoverInvokerElement::associated_attribute_changed(name, value, namespace_); + if (name == HTML::AttributeNames::checked) { // https://html.spec.whatwg.org/multipage/input.html#the-input-element:concept-input-checked-dirty-2 // When the checked content attribute is added, if the control does not have dirty checkedness, the user agent must set the checkedness of the element to true; @@ -2536,6 +2538,7 @@ bool HTMLInputElement::has_activation_behavior() const return true; } +// https://html.spec.whatwg.org/multipage/input.html#the-input-element:activation-behaviour void HTMLInputElement::activation_behavior(DOM::Event const& event) { // The activation behavior for input elements are these steps: @@ -2544,6 +2547,10 @@ void HTMLInputElement::activation_behavior(DOM::Event const& event) // 2. Run this element's input activation behavior, if any, and do nothing otherwise. run_input_activation_behavior(event).release_value_but_fixme_should_propagate_errors(); + + // 3. Run the popover target attribute activation behavior given element and event's target. + if (event.target() && event.target()->is_dom_node()) + PopoverInvokerElement::popover_target_activation_behaviour(*this, as(*event.target())); } bool HTMLInputElement::has_input_activation_behavior() const diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.h b/Libraries/LibWeb/HTML/HTMLInputElement.h index 76b45fec9ca0e..e2178a63830cf 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -50,7 +51,8 @@ namespace Web::HTML { class HTMLInputElement final : public HTMLElement , public FormAssociatedTextControlElement - , public Layout::ImageProvider { + , public Layout::ImageProvider + , public PopoverInvokerElement { WEB_PLATFORM_OBJECT(HTMLInputElement, HTMLElement); GC_DECLARE_ALLOCATOR(HTMLInputElement); FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLInputElement) diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.idl b/Libraries/LibWeb/HTML/HTMLInputElement.idl index 4e7b46af29f38..c308e5f36b721 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.idl +++ b/Libraries/LibWeb/HTML/HTMLInputElement.idl @@ -1,5 +1,6 @@ #import #import +#import #import #import @@ -73,4 +74,4 @@ interface HTMLInputElement : HTMLElement { [CEReactions, Reflect] attribute DOMString align; [CEReactions, Reflect=usemap] attribute DOMString useMap; }; -// FIXME: HTMLInputElement includes PopoverInvokerElement; +HTMLInputElement includes PopoverInvokerElement; diff --git a/Libraries/LibWeb/HTML/PopoverInvokerElement.cpp b/Libraries/LibWeb/HTML/PopoverInvokerElement.cpp index fae92643af46e..84883af97e5b5 100644 --- a/Libraries/LibWeb/HTML/PopoverInvokerElement.cpp +++ b/Libraries/LibWeb/HTML/PopoverInvokerElement.cpp @@ -4,8 +4,12 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include +#include #include +#include +#include #include namespace Web::HTML { @@ -29,4 +33,93 @@ void PopoverInvokerElement::visit_edges(JS::Cell::Visitor& visitor) visitor.visit(m_popover_target_element); } +// https://html.spec.whatwg.org/multipage/popover.html#popover-target-attribute-activation-behavior +// https://whatpr.org/html/9457/popover.html#popover-target-attribute-activation-behavior +void PopoverInvokerElement::popover_target_activation_behaviour(GC::Ref node, GC::Ref event_target) +{ + // To run the popover target attribute activation behavior given a Node node and a Node eventTarget: + + // 1. Let popover be node's popover target element. + auto popover = PopoverInvokerElement::get_the_popover_target_element(node); + + // 2. If popover is null, then return. + if (!popover) + return; + + // 3. If eventTarget is a shadow-including inclusive descendant of popover and popover is a shadow-including descendant of node, then return. + if (event_target->is_shadow_including_inclusive_descendant_of(*popover) + && popover->is_shadow_including_descendant_of(node)) + return; + + // 4. If node's popovertargetaction attribute is in the show state and popover's popover visibility state is showing, then return. + if (as(*node).get_attribute_value(HTML::AttributeNames::popovertargetaction).equals_ignoring_ascii_case("show"sv) + && popover->popover_visibility_state() == HTMLElement::PopoverVisibilityState::Showing) + return; + + // 5. If node's popovertargetaction attribute is in the hide state and popover's popover visibility state is hidden, then return. + if (as(*node).get_attribute_value(HTML::AttributeNames::popovertargetaction).equals_ignoring_ascii_case("hide"sv) + && popover->popover_visibility_state() == HTMLElement::PopoverVisibilityState::Hidden) + return; + + // 6. If popover's popover visibility state is showing, then run the hide popover algorithm given popover, true, true, false, and false. + if (popover->popover_visibility_state() == HTMLElement::PopoverVisibilityState::Showing) { + MUST(popover->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No)); + } + + // 7. Otherwise, if popover's popover visibility state is hidden and the result of running check popover validity given popover, false, false, null, and false is true, then run show popover given popover, false, and node. + else if (popover->popover_visibility_state() == HTMLElement::PopoverVisibilityState::Hidden + && MUST(popover->check_popover_validity(ExpectedToBeShowing::No, ThrowExceptions::No, nullptr, IgnoreDomState::No))) { + MUST(popover->show_popover(ThrowExceptions::No, as(*node))); + } +} + +// https://html.spec.whatwg.org/multipage/popover.html#popover-target-element +GC::Ptr PopoverInvokerElement::get_the_popover_target_element(GC::Ref node) +{ + // 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); + + // 1. If node is not a button, then return null. + if (!form_associated_element->is_button()) + return {}; + + // 2. If node is disabled, then return null. + if (!form_associated_element->enabled()) + return {}; + + // 3. If node has a form owner and node is a submit button, then return null. + if (form_associated_element->form() != nullptr && form_associated_element->is_submit_button()) + return {}; + + // 4. Let popoverElement be the result of running node's get the popovertarget-associated element. + auto const* popover_invoker_element = dynamic_cast(node.ptr()); + VERIFY(popover_invoker_element); + GC::Ptr popover_element = as(popover_invoker_element->m_popover_target_element.ptr()); + if (!popover_element) { + auto target_id = as(*node).attribute("popovertarget"_fly_string); + if (target_id.has_value()) { + node->root().for_each_in_inclusive_subtree_of_type([&](auto& candidate) { + if (candidate.attribute(HTML::AttributeNames::id) == target_id.value()) { + popover_element = &candidate; + return TraversalDecision::Break; + } + return TraversalDecision::Continue; + }); + } + } + + // 5. If popoverElement is null, then return null. + if (!popover_element) + return {}; + + // 6. If popoverElement's popover attribute is in the no popover state, then return null. + if (!popover_element->popover().has_value()) + return {}; + + // 7. Return popoverElement. + return popover_element; +} + } diff --git a/Libraries/LibWeb/HTML/PopoverInvokerElement.h b/Libraries/LibWeb/HTML/PopoverInvokerElement.h index 839fa1700231c..55269776d878b 100644 --- a/Libraries/LibWeb/HTML/PopoverInvokerElement.h +++ b/Libraries/LibWeb/HTML/PopoverInvokerElement.h @@ -21,12 +21,16 @@ class PopoverInvokerElement { void set_popover_target_element(GC::Ptr value) { m_popover_target_element = value; } + static void popover_target_activation_behaviour(GC::Ref node, GC::Ref event_target); + protected: void visit_edges(JS::Cell::Visitor&); void associated_attribute_changed(FlyString const& name, Optional const& value, Optional const& namespace_); private: GC::Ptr m_popover_target_element; + + static GC::Ptr get_the_popover_target_element(GC::Ref node); }; } diff --git a/Libraries/LibWeb/Layout/TreeBuilder.cpp b/Libraries/LibWeb/Layout/TreeBuilder.cpp index a7d10f7d908e6..1f425adfcf869 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.cpp +++ b/Libraries/LibWeb/Layout/TreeBuilder.cpp @@ -459,7 +459,7 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& if (dom_node.is_element()) { auto& element = static_cast(dom_node); - if (element.in_top_layer() && !context.layout_top_layer) + if (element.rendered_in_top_layer() && !context.layout_top_layer) return; } if (dom_node.is_element()) @@ -595,8 +595,10 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& // Elements in the top layer do not lay out normally based on their position in the document; instead they // generate boxes as if they were siblings of the root element. TemporaryChange layout_mask(context.layout_top_layer, true); - for (auto const& top_layer_element : document.top_layer_elements()) - update_layout_tree(top_layer_element, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No); + for (auto const& top_layer_element : document.top_layer_elements()) { + if (top_layer_element->rendered_in_top_layer()) + update_layout_tree(top_layer_element, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No); + } } pop_parent(); } diff --git a/Tests/LibWeb/Layout/expected/popovertarget-button.txt b/Tests/LibWeb/Layout/expected/popovertarget-button.txt new file mode 100644 index 0000000000000..a93b32c6edaa2 --- /dev/null +++ b/Tests/LibWeb/Layout/expected/popovertarget-button.txt @@ -0,0 +1,27 @@ +Viewport <#document> at (0,0) content-size 800x600 children: not-inline + BlockContainer at (0,0) content-size 800x600 [BFC] children: not-inline + BlockContainer at (8,8) content-size 784x17 children: inline + frag 0 from BlockContainer start: 0, length: 0, rect: [13,19 0x0] baseline: 4 + BlockContainer at (13,19) content-size 0x0 inline-block [BFC] children: not-inline + BlockContainer <(anonymous)> at (13,19) content-size 0x0 flex-container(column) [FFC] children: not-inline + BlockContainer <(anonymous)> at (13,19) content-size 0x0 [BFC] children: not-inline + TextNode <#text> + TextNode <#text> + TextNode <#text> + BlockContainer at (358.84375,291.5) content-size 82.3125x17 positioned [BFC] children: inline + TextNode <#text> + InlineNode + frag 0 from TextNode start: 0, length: 10, rect: [358.84375,291.5 82.3125x17] baseline: 13.296875 + "I'm a node" + TextNode <#text> + TextNode <#text> + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x600] + PaintableWithLines (BlockContainer) [8,8 784x17] + PaintableWithLines (BlockContainer +
+ I'm a node +
+ + diff --git a/Tests/LibWeb/Text/expected/popover-crashes.txt b/Tests/LibWeb/Text/expected/popover-crashes.txt new file mode 100644 index 0000000000000..52f2729b493f8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/popover-crashes.txt @@ -0,0 +1,3 @@ +Didn't crash when showing recently hidden popover +Didn't crash when removing visible popover +Didn't crash when removing popover with changed attribute diff --git a/Tests/LibWeb/Text/input/popover-crashes.html b/Tests/LibWeb/Text/input/popover-crashes.html new file mode 100644 index 0000000000000..7e268cb13d6b4 --- /dev/null +++ b/Tests/LibWeb/Text/input/popover-crashes.html @@ -0,0 +1,20 @@ + + +
+
+