Skip to content

Commit

Permalink
LibWeb: Implement popovertarget buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
Gingeh committed Jan 18, 2025
1 parent 1e72b35 commit 0ab8e2a
Show file tree
Hide file tree
Showing 14 changed files with 192 additions and 14 deletions.
14 changes: 9 additions & 5 deletions Libraries/LibWeb/DOM/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5748,10 +5748,11 @@ void Document::add_an_element_to_the_top_layer(GC::Ref<Element> 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
Expand All @@ -5764,9 +5765,12 @@ void Document::request_an_element_to_be_remove_from_the_top_layer(GC::Ref<Elemen
return;

// FIXME: 3. Remove the UA !important overlay: auto rule targeting el.
element->set_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
Expand All @@ -5776,10 +5780,11 @@ void Document::remove_an_element_from_the_top_layer_immediately(GC::Ref<Element>

// 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
Expand All @@ -5788,11 +5793,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);
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions Libraries/LibWeb/DOM/Element.h
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,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<CSS::CountersSet const&> counters_set();
CSS::CountersSet& ensure_counters_set();
Expand Down Expand Up @@ -485,6 +492,7 @@ class Element
Array<CSSPixelPoint, 3> m_scroll_offset;

bool m_in_top_layer { false };
bool m_rendered_in_top_layer { false };

OwnPtr<CSS::CountersSet> m_counters_set;

Expand Down
5 changes: 4 additions & 1 deletion Libraries/LibWeb/HTML/HTMLButtonElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <LibWeb/Bindings/HTMLButtonElementPrototype.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/HTML/HTMLButtonElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>

Expand Down Expand Up @@ -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, verify_cast<DOM::Node>(*event.target()));
}

bool HTMLButtonElement::is_focusable() const
Expand Down
3 changes: 1 addition & 2 deletions Libraries/LibWeb/HTML/HTMLElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ class HTMLElement
WebIDL::ExceptionOr<void> hide_popover_for_bindings();
WebIDL::ExceptionOr<bool> toggle_popover(TogglePopoverOptionsOrForceBoolean const&);

WebIDL::ExceptionOr<bool> check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr<DOM::Document>, IgnoreDomState ignore_dom_state);
WebIDL::ExceptionOr<void> show_popover(ThrowExceptions throw_exceptions, GC::Ptr<HTMLElement> invoker);
WebIDL::ExceptionOr<void> hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions, IgnoreDomState ignore_dom_state);

Expand Down Expand Up @@ -162,8 +163,6 @@ class HTMLElement

GC::Ptr<DOM::NodeList> m_labels;

WebIDL::ExceptionOr<bool> check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr<DOM::Document>, IgnoreDomState ignore_dom_state);

void queue_a_popover_toggle_event_task(String old_state, String new_state);

// https://html.spec.whatwg.org/multipage/custom-elements.html#attached-internals
Expand Down
9 changes: 8 additions & 1 deletion Libraries/LibWeb/HTML/HTMLInputElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1261,8 +1261,10 @@ void HTMLInputElement::did_lose_focus()
commit_pending_changes();
}

void HTMLInputElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> const&)
void HTMLInputElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> 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;
Expand Down Expand Up @@ -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:
Expand All @@ -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, verify_cast<DOM::Node>(*event.target()));
}

bool HTMLInputElement::has_input_activation_behavior() const
Expand Down
4 changes: 3 additions & 1 deletion Libraries/LibWeb/HTML/HTMLInputElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/HTML/PopoverInvokerElement.h>
#include <LibWeb/Layout/ImageProvider.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/Types.h>
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion Libraries/LibWeb/HTML/HTMLInputElement.idl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import <HTML/HTMLElement.idl>
#import <HTML/HTMLFormElement.idl>
#import <HTML/PopoverInvokerElement.idl>
#import <HTML/ValidityState.idl>
#import <FileAPI/FileList.idl>

Expand Down Expand Up @@ -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;
93 changes: 93 additions & 0 deletions Libraries/LibWeb/HTML/PopoverInvokerElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/HTML/AttributeNames.h>
#include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/HTML/PopoverInvokerElement.h>

namespace Web::HTML {
Expand All @@ -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
void PopoverInvokerElement::popover_target_activation_behaviour(GC::Ref<DOM::Node> node, GC::Ref<DOM::Node> 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 (verify_cast<DOM::Element>(*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 (verify_cast<DOM::Element>(*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) {
popover->hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No).release_value_but_fixme_should_propagate_errors();
}

// 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
&& popover->check_popover_validity(ExpectedToBeShowing::No, ThrowExceptions::No, nullptr, IgnoreDomState::No).release_value_but_fixme_should_propagate_errors()) {
popover->show_popover(ThrowExceptions::No, verify_cast<HTMLElement>(*node)).release_value_but_fixme_should_propagate_errors();
}
}

// https://html.spec.whatwg.org/multipage/popover.html#popover-target-element
GC::Ptr<HTMLElement> PopoverInvokerElement::get_the_popover_target_element(GC::Ref<DOM::Node> 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<FormAssociatedElement const*>(node.ptr());
if (!form_associated_element)
return {};

// 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<PopoverInvokerElement const*>(node.ptr());
GC::Ptr<HTMLElement> popover_element = verify_cast<HTMLElement>(popover_invoker_element->m_popover_target_element.ptr());

if (!popover_element) {
auto target_id = verify_cast<HTMLElement>(*node).attribute("popovertarget"_fly_string);
if (target_id.has_value()) {
node->root().for_each_in_inclusive_subtree_of_type<HTMLElement>([&](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;
}

}
4 changes: 4 additions & 0 deletions Libraries/LibWeb/HTML/PopoverInvokerElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ class PopoverInvokerElement {

void set_popover_target_element(GC::Ptr<DOM::Element> value) { m_popover_target_element = value; }

static void popover_target_activation_behaviour(GC::Ref<DOM::Node> node, GC::Ref<DOM::Node> event_target);

protected:
void visit_edges(JS::Cell::Visitor&);
void associated_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> const& namespace_);

private:
GC::Ptr<DOM::Element> m_popover_target_element;

static GC::Ptr<HTMLElement> get_the_popover_target_element(GC::Ref<DOM::Node> node);
};

}
8 changes: 5 additions & 3 deletions Libraries/LibWeb/Layout/TreeBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&

if (dom_node.is_element()) {
auto& element = static_cast<DOM::Element&>(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())
Expand Down Expand Up @@ -448,8 +448,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<bool> 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();
}
Expand Down
27 changes: 27 additions & 0 deletions Tests/LibWeb/Layout/expected/popovertarget-button.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (0,0) content-size 800x600 [BFC] children: not-inline
BlockContainer <body> at (8,8) content-size 784x17 children: inline
frag 0 from BlockContainer start: 0, length: 0, rect: [13,19 0x0] baseline: 4
BlockContainer <button#button> 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 <div#pop> at (358.84375,291.5) content-size 82.3125x17 positioned [BFC] children: inline
TextNode <#text>
InlineNode <span>
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<HTML>) [0,0 800x600]
PaintableWithLines (BlockContainer<BODY>) [8,8 784x17]
PaintableWithLines (BlockContainer<BUTTON>#button) [8,17 10x4]
PaintableWithLines (BlockContainer(anonymous)) [13,19 0x0]
PaintableWithLines (BlockContainer(anonymous)) [13,19 0x0]
PaintableWithLines (BlockContainer<DIV>#pop) [351.84375,284.5 96.3125x31]
PaintableWithLines (InlineNode<SPAN>)
TextPaintable (TextNode<#text>)
12 changes: 12 additions & 0 deletions Tests/LibWeb/Layout/input/popovertarget-button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<button popovertarget="pop" id="button"></button>
<div popover id="pop">
<span>I'm a node</span>
</div>

<script>
document.addEventListener("DOMContentLoaded", () => {
let button = document.getElementById('button');
const rect = button.getBoundingClientRect();
internals.click(rect.x + rect.width / 2, rect.y + rect.height / 2);
});
</script>
2 changes: 2 additions & 0 deletions Tests/LibWeb/Text/expected/popover-crashes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Didn't crash when showing recently hidden popover
Didn't crash when removing visible popover
14 changes: 14 additions & 0 deletions Tests/LibWeb/Text/input/popover-crashes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<script src="include.js"></script>
<div popover id="pop"></div>
<script>
test(() => {
const pop = document.getElementById("pop");
pop.showPopover();
pop.hidePopover();
pop.showPopover();
println("Didn't crash when showing recently hidden popover");
pop.remove();
println("Didn't crash when removing visible popover")
});
</script>

0 comments on commit 0ab8e2a

Please sign in to comment.