mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-29 12:19:54 +00:00
LibWeb: Implement popovertarget buttons
This commit is contained in:
parent
a1cf5271c2
commit
108f3a9aac
Notes:
github-actions[bot]
2025-01-30 22:49:42 +00:00
Author: https://github.com/Gingeh
Commit: 108f3a9aac
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3285
Reviewed-by: https://github.com/ADKaster ✅
Reviewed-by: https://github.com/AtkinsSJ
14 changed files with 188 additions and 14 deletions
|
@ -5864,10 +5864,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
|
||||
|
@ -5880,9 +5881,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
|
||||
|
@ -5892,10 +5896,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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<CSS::CountersSet const&> counters_set();
|
||||
CSS::CountersSet& ensure_counters_set();
|
||||
|
@ -502,6 +509,7 @@ private:
|
|||
Array<CSSPixelPoint, 3> 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 };
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
@ -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<DOM::Node>(*event.target()));
|
||||
}
|
||||
|
||||
bool HTMLButtonElement::is_focusable() const
|
||||
|
|
|
@ -131,6 +131,7 @@ public:
|
|||
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>);
|
||||
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);
|
||||
|
||||
|
@ -160,8 +161,6 @@ private:
|
|||
|
||||
GC::Ptr<DOM::NodeList> m_labels;
|
||||
|
||||
WebIDL::ExceptionOr<bool> check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr<DOM::Document>);
|
||||
|
||||
void queue_a_popover_toggle_event_task(String old_state, String new_state);
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/custom-elements.html#attached-internals
|
||||
|
|
|
@ -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;
|
||||
|
@ -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<DOM::Node>(*event.target()));
|
||||
}
|
||||
|
||||
bool HTMLInputElement::has_input_activation_behavior() const
|
||||
|
|
|
@ -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>
|
||||
|
@ -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)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#import <HTML/HTMLElement.idl>
|
||||
#import <HTML/HTMLFormElement.idl>
|
||||
#import <HTML/PopoverInvokerElement.idl>
|
||||
#import <HTML/ValidityState.idl>
|
||||
#import <FileAPI/FileList.idl>
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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<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 (as<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 (as<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, 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<HTMLElement>(*node)));
|
||||
}
|
||||
}
|
||||
|
||||
// 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());
|
||||
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<PopoverInvokerElement const*>(node.ptr());
|
||||
VERIFY(popover_invoker_element);
|
||||
GC::Ptr<HTMLElement> popover_element = as<HTMLElement>(popover_invoker_element->m_popover_target_element.ptr());
|
||||
if (!popover_element) {
|
||||
auto target_id = as<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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,12 +21,16 @@ public:
|
|||
|
||||
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);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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::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())
|
||||
|
@ -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<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();
|
||||
}
|
||||
|
|
27
Tests/LibWeb/Layout/expected/popovertarget-button.txt
Normal file
27
Tests/LibWeb/Layout/expected/popovertarget-button.txt
Normal file
|
@ -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
Tests/LibWeb/Layout/input/popovertarget-button.html
Normal file
12
Tests/LibWeb/Layout/input/popovertarget-button.html
Normal file
|
@ -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>
|
1
Tests/LibWeb/Text/expected/popover-crashes.txt
Normal file
1
Tests/LibWeb/Text/expected/popover-crashes.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Didn't crash when showing recently hidden popover
|
12
Tests/LibWeb/Text/input/popover-crashes.html
Normal file
12
Tests/LibWeb/Text/input/popover-crashes.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!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");
|
||||
});
|
||||
</script>
|
Loading…
Add table
Add a link
Reference in a new issue