diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 42267dcb6cc..e54f7492d67 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -5864,10 +5864,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 @@ -5880,9 +5881,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 @@ -5892,10 +5896,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 @@ -5904,11 +5909,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 8c4a25bc525..dbb5350aaee 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -388,9 +388,16 @@ public: 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(); @@ -502,6 +509,7 @@ private: 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 }; bool m_affected_by_has_pseudo_class_in_subject_position { false }; diff --git a/Libraries/LibWeb/HTML/HTMLButtonElement.cpp b/Libraries/LibWeb/HTML/HTMLButtonElement.cpp index 90e8e9207b8..dc16d6dee62 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.h b/Libraries/LibWeb/HTML/HTMLElement.h index b5d1d7f8d94..942a33d1f82 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.h +++ b/Libraries/LibWeb/HTML/HTMLElement.h @@ -131,6 +131,7 @@ public: 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); WebIDL::ExceptionOr show_popover(ThrowExceptions throw_exceptions, GC::Ptr invoker); WebIDL::ExceptionOr hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions); @@ -160,8 +161,6 @@ private: 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); // https://html.spec.whatwg.org/multipage/custom-elements.html#attached-internals diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index bc2c55365e2..ffedd98e8f5 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; @@ -2538,6 +2540,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: @@ -2546,6 +2549,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 76b45fec9ca..e2178a63830 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 4e7b46af29f..c308e5f36b7 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 fae92643af4..a2211316646 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,92 @@ 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 +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, and false. + if (popover->popover_visibility_state() == HTMLElement::PopoverVisibilityState::Showing) { + MUST(popover->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No)); + } + + // 7. Otherwise, if popover's popover visibility state is hidden and the result of running check popover validity given popover, false, false, and null 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))) { + 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 839fa170023..55269776d87 100644 --- a/Libraries/LibWeb/HTML/PopoverInvokerElement.h +++ b/Libraries/LibWeb/HTML/PopoverInvokerElement.h @@ -21,12 +21,16 @@ public: 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 a7d10f7d908..1f425adfcf8 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 00000000000..a93b32c6eda --- /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 00000000000..eb284037599 --- /dev/null +++ b/Tests/LibWeb/Text/expected/popover-crashes.txt @@ -0,0 +1 @@ +Didn't crash when showing recently hidden popover diff --git a/Tests/LibWeb/Text/input/popover-crashes.html b/Tests/LibWeb/Text/input/popover-crashes.html new file mode 100644 index 00000000000..a387b1bf779 --- /dev/null +++ b/Tests/LibWeb/Text/input/popover-crashes.html @@ -0,0 +1,12 @@ + + +
+