LibWeb: Forward non-synthetic pseudo-elt accesses to relevant element

This adds a new class `ElementReferencePseudoElement` which forwards
any accesses to the referenced element for "element-reference" pseudo
elements.

This allows us to, for instance:
 - Access the underlying `ComputedProperties` in getComputedStyle, which
   includes inline style.
 - Store and access `CustomPropertyData`
 - Access the layout node for "resolved value" computation
 - Apply animations from `update_animated_style_if_needed`

We register these with the originating element when calling
`set_associated_shadow_host_pseudo_element`, this requires us to append
the element to the tree beforehand since we need to be able to get the
shadow host.
This commit is contained in:
Callum Law 2026-05-15 21:16:25 +12:00 committed by Sam Atkins
parent 507380d8c5
commit 199e82fc73
18 changed files with 210 additions and 17 deletions

View file

@ -3568,6 +3568,21 @@ void Element::set_computed_properties(Optional<CSS::PseudoElement> pseudo_elemen
computed_properties_changed();
}
void Element::set_associated_shadow_host_pseudo_element(CSS::PseudoElement type)
{
VERIFY(CSS::is_element_reference_pseudo_element(type));
auto& root = this->root();
VERIFY(is<ShadowRoot>(root));
auto& shadow_root = as<ShadowRoot>(root);
VERIFY(shadow_root.host());
shadow_root.host()->register_element_reference_pseudo_element(type, *this);
m_associated_shadow_host_pseudo_element = type;
}
Optional<SyntheticPseudoElement&> Element::get_synthetic_pseudo_element(CSS::PseudoElement type) const
{
VERIFY(is_synthetic_pseudo_element(type));
@ -3596,6 +3611,25 @@ Optional<PseudoElement&> Element::get_pseudo_element(CSS::PseudoElement type) co
return *(pseudo_element.value());
}
void Element::register_element_reference_pseudo_element(CSS::PseudoElement type, GC::Ref<Element> element)
{
VERIFY(CSS::is_element_reference_pseudo_element(type));
if (!m_pseudo_element_data)
m_pseudo_element_data = make<PseudoElementData>();
m_pseudo_element_data->set(type, heap().allocate<ElementReferencePseudoElement>(element));
}
void Element::clear_element_reference_pseudo_elements()
{
if (!m_pseudo_element_data)
return;
for (auto i = to_underlying(CSS::first_element_reference_pseudo_element); i <= to_underlying(CSS::last_element_reference_pseudo_element); ++i)
m_pseudo_element_data->remove(static_cast<CSS::PseudoElement>(i));
}
SyntheticPseudoElement& Element::ensure_synthetic_pseudo_element(CSS::PseudoElement type) const
{
if (!m_pseudo_element_data)

View file

@ -194,7 +194,7 @@ public:
CSS::RequiredInvalidationAfterStyleChange recompute_inherited_style();
Optional<CSS::PseudoElement> associated_shadow_host_pseudo_element() const { return m_associated_shadow_host_pseudo_element; }
void set_associated_shadow_host_pseudo_element(Optional<CSS::PseudoElement> pseudo_element) { m_associated_shadow_host_pseudo_element = move(pseudo_element); }
void set_associated_shadow_host_pseudo_element(CSS::PseudoElement pseudo_element);
GC::Ptr<Layout::NodeWithStyle> layout_node();
GC::Ptr<Layout::NodeWithStyle const> layout_node() const;
@ -662,6 +662,7 @@ protected:
GC::Ptr<HTML::CustomElementDefinition> custom_element_definition() const { return m_custom_element_definition; }
void play_or_cancel_animations_after_display_property_change();
void clear_element_reference_pseudo_elements();
private:
FlyString make_html_uppercased_qualified_name() const;
@ -700,6 +701,7 @@ private:
using PseudoElementData = HashMap<CSS::PseudoElement, GC::Ref<PseudoElement>>;
mutable OwnPtr<PseudoElementData> m_pseudo_element_data;
void register_element_reference_pseudo_element(CSS::PseudoElement type, GC::Ref<Element> element);
SyntheticPseudoElement& ensure_synthetic_pseudo_element(CSS::PseudoElement) const;
void clear_synthetic_pseudo_element_layout_nodes();

View file

@ -5,6 +5,7 @@
*/
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/PseudoElement.h>
#include <LibWeb/Layout/Node.h>
@ -13,6 +14,7 @@ namespace Web::DOM {
GC_DEFINE_ALLOCATOR(PseudoElement);
GC_DEFINE_ALLOCATOR(SyntheticPseudoElement);
GC_DEFINE_ALLOCATOR(SyntheticPseudoElementTreeNode);
GC_DEFINE_ALLOCATOR(ElementReferencePseudoElement);
void SyntheticPseudoElement::visit_edges(JS::Cell::Visitor& visitor)
{
@ -43,4 +45,29 @@ void SyntheticPseudoElement::set_counters_set(OwnPtr<CSS::CountersSet>&& counter
m_counters_set = move(counters_set);
}
GC::Ptr<Layout::NodeWithStyle> ElementReferencePseudoElement::layout_node() const
{
return m_referenced_element->layout_node();
}
GC::Ptr<Layout::NodeWithStyle> ElementReferencePseudoElement::unsafe_layout_node() const
{
return m_referenced_element->unsafe_layout_node();
}
GC::Ptr<CSS::ComputedProperties> ElementReferencePseudoElement::computed_properties() const
{
return m_referenced_element->computed_properties({});
}
RefPtr<CSS::CustomPropertyData const> ElementReferencePseudoElement::custom_property_data() const
{
return m_referenced_element->custom_property_data({});
}
void ElementReferencePseudoElement::set_custom_property_data(RefPtr<CSS::CustomPropertyData const> value)
{
m_referenced_element->set_custom_property_data({}, move(value));
}
}

View file

@ -78,4 +78,32 @@ protected:
}
};
class WEB_API ElementReferencePseudoElement : public PseudoElement {
GC_CELL(ElementReferencePseudoElement, PseudoElement);
GC_DECLARE_ALLOCATOR(ElementReferencePseudoElement);
ElementReferencePseudoElement(GC::Ref<Element> referenced_element)
: m_referenced_element(referenced_element)
{
}
GC::Ptr<Layout::NodeWithStyle> layout_node() const override;
GC::Ptr<Layout::NodeWithStyle> unsafe_layout_node() const override;
GC::Ptr<CSS::ComputedProperties> computed_properties() const override;
RefPtr<CSS::CustomPropertyData const> custom_property_data() const override;
void set_custom_property_data(RefPtr<CSS::CustomPropertyData const> value) override;
protected:
virtual void visit_edges(JS::Cell::Visitor& visitor) override
{
Base::visit_edges(visitor);
visitor.visit(m_referenced_element);
}
private:
GC::Ref<Element> m_referenced_element;
};
}

View file

@ -242,8 +242,8 @@ WebIDL::ExceptionOr<void> HTMLDetailsElement::create_shadow_tree_if_needed()
// The second child element is a slot that is expected to take the details element's remaining descendants, if any.
auto descendants_slot = TRY(DOM::create_element(document(), HTML::TagNames::slot, Namespace::HTML));
descendants_slot->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::DetailsContent);
MUST(shadow_root->append_child(descendants_slot));
descendants_slot->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::DetailsContent);
// The third child element is either a link or style element with the following styles for the default summary:
auto style = TRY(DOM::create_element(document(), HTML::TagNames::style, Namespace::HTML));

View file

@ -1170,8 +1170,8 @@ void HTMLInputElement::create_text_input_shadow_tree()
MUST(m_inner_text_element->append_child(*m_text_node));
m_placeholder_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
m_placeholder_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::Placeholder);
MUST(element->append_child(*m_placeholder_element));
m_placeholder_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::Placeholder);
m_placeholder_text_node = realm().create<DOM::Text>(document(), Utf16String::from_utf8(placeholder()));
MUST(m_placeholder_element->append_child(*m_placeholder_text_node));
@ -1295,6 +1295,7 @@ void HTMLInputElement::create_file_input_shadow_tree()
shadow_root->set_user_agent_internal(true);
m_file_button = DOM::create_element(document(), HTML::TagNames::button, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
MUST(shadow_root->append_child(*m_file_button));
m_file_button->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::FileSelectorButton);
m_file_label = DOM::create_element(document(), HTML::TagNames::label, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
@ -1311,7 +1312,6 @@ void HTMLInputElement::create_file_input_shadow_tree()
update_file_input_shadow_tree();
MUST(shadow_root->append_child(*m_file_button));
MUST(shadow_root->append_child(*m_file_label));
set_shadow_root(shadow_root);
@ -1342,16 +1342,16 @@ void HTMLInputElement::create_range_input_shadow_tree()
set_shadow_root(shadow_root);
m_slider_runnable_track = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
m_slider_runnable_track->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderTrack);
MUST(shadow_root->append_child(*m_slider_runnable_track));
m_slider_runnable_track->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderTrack);
m_slider_progress_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
m_slider_progress_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderFill);
MUST(m_slider_runnable_track->append_child(*m_slider_progress_element));
m_slider_progress_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderFill);
m_slider_thumb = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
m_slider_thumb->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderThumb);
MUST(m_slider_runnable_track->append_child(*m_slider_thumb));
m_slider_thumb->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderThumb);
update_slider_shadow_tree_elements();
@ -1620,6 +1620,7 @@ void HTMLInputElement::type_attribute_changed(TypeAttributeState old_state, Type
// 4. Update the element's rendering and behavior to the new state's.
m_type = new_state;
clear_element_reference_pseudo_elements();
set_shadow_root(nullptr);
create_shadow_tree_if_needed();

View file

@ -187,12 +187,12 @@ void HTMLMeterElement::create_shadow_tree_if_needed()
set_shadow_root(shadow_root);
auto meter_bar_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
meter_bar_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderTrack);
MUST(shadow_root->append_child(*meter_bar_element));
meter_bar_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderTrack);
m_meter_value_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
m_meter_value_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderFill);
MUST(meter_bar_element->append_child(*m_meter_value_element));
m_meter_value_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderFill);
update_meter_value_element();
}

View file

@ -112,12 +112,12 @@ void HTMLProgressElement::create_shadow_tree_if_needed()
set_shadow_root(shadow_root);
auto progress_bar_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
progress_bar_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderTrack);
MUST(shadow_root->append_child(*progress_bar_element));
progress_bar_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderTrack);
m_progress_value_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
m_progress_value_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderFill);
MUST(progress_bar_element->append_child(*m_progress_value_element));
m_progress_value_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::SliderFill);
update_progress_value_element();
}

View file

@ -358,8 +358,8 @@ void HTMLTextAreaElement::create_shadow_tree_if_needed()
MUST(m_inner_text_element->append_child(*m_text_node));
m_placeholder_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
m_placeholder_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::Placeholder);
MUST(element->append_child(*m_placeholder_element));
m_placeholder_element->set_associated_shadow_host_pseudo_element(CSS::PseudoElement::Placeholder);
m_placeholder_text_node = realm().create<DOM::Text>(document(), Utf16String::from_utf8(get_attribute_value(HTML::AttributeNames::placeholder)));
MUST(m_placeholder_element->append_child(*m_placeholder_text_node));

View file

@ -0,0 +1,12 @@
<!doctype html>
<style>
details::details-content {
background-color: green;
width: 100px;
height: 100px;
}
</style>
<details open>
<summary></summary>
<div></div>
</details>

View file

@ -0,0 +1,31 @@
<!doctype html>
<html class="reftest-wait">
<link rel="match" href="../expected/web-animation-details-content-pseudo-element-ref.html" />
<style>
details::details-content {
display: block;
background-color: red;
width: 100px;
height: 100px;
}
</style>
<details open id="foo">
<summary></summary>
<div></div>
</details>
<script>
getComputedStyle(foo, "::details-content").width;
const animation = foo.animate(
{ backgroundColor: ["green", "green"] },
{ duration: 1000, pseudoElement: "::details-content" }
);
animation.currentTime = 500;
animation.pause();
requestAnimationFrame(() => {
document.documentElement.classList.remove("reftest-wait");
});
</script>
</html>

View file

@ -0,0 +1,2 @@
details-content open content-visibility: visible
details-content closed content-visibility: hidden

View file

@ -0,0 +1,2 @@
before change: rgb(0, 128, 0)
after change: rgb(128, 128, 128)

View file

@ -1,5 +1,5 @@
color: rgb(0, 0, 0)
opacity: 1
font-weight: 400
inherited color: rgb(0, 0, 0)
nested color: rgb(0, 0, 0)
color: rgb(0, 0, 255)
opacity: 0.5
font-weight: 900
inherited color: rgb(0, 128, 0)
nested color: rgb(0, 0, 255)

View file

@ -0,0 +1 @@
input placeholder 'letter-spacing: 0': normal

View file

@ -0,0 +1,25 @@
<!doctype html>
<details id="details" open>
<summary>summary</summary>
</details>
<script src="../include.js"></script>
<script>
test(() => {
// Force a layout update
getComputedStyle(details, "::details-content").width;
println(
`details-content open content-visibility: ${
getComputedStyle(details, "::details-content").contentVisibility
}`
);
details.removeAttribute("open");
println(
`details-content closed content-visibility: ${
getComputedStyle(details, "::details-content").contentVisibility
}`
);
});
</script>

View file

@ -0,0 +1,15 @@
<!doctype html>
<style>
input[type="text"]::placeholder {
color: green;
}
</style>
<input id="target" type="text" placeholder="placeholder" />
<script src="../include.js"></script>
<script>
test(() => {
println(`before change: ${getComputedStyle(target, "::placeholder").color}`);
target.type = "file";
println(`after change: ${getComputedStyle(target, "::placeholder").color}`);
});
</script>

View file

@ -0,0 +1,13 @@
<!doctype html>
<style>
input::placeholder {
letter-spacing: 0;
}
</style>
<input id="input" placeholder="placeholder" />
<script src="../include.js"></script>
<script>
test(() => {
println(`input placeholder 'letter-spacing: 0': ${getComputedStyle(input, "::placeholder").letterSpacing}`);
});
</script>