aboutsummaryrefslogtreecommitdiffstats
path: root/components/script/dom/htmldetailselement.rs
diff options
context:
space:
mode:
authorSimon Wülker <simon.wuelker@arcor.de>2025-02-25 12:56:36 +0100
committerGitHub <noreply@github.com>2025-02-25 11:56:36 +0000
commit754b117011b1e035fabbcaa9cd0797e3c9242c8c (patch)
tree265a7749857fc4f67ca80891a21524c1f4b7da7e /components/script/dom/htmldetailselement.rs
parentcceff779288dc25c507e782aac19b25decfbeb83 (diff)
downloadservo-754b117011b1e035fabbcaa9cd0797e3c9242c8c.tar.gz
servo-754b117011b1e035fabbcaa9cd0797e3c9242c8c.zip
Allow the `<details>` element to be opened and closed (#35261)
* Implement the <summary> element Signed-off-by: Simon Wülker <simon.wuelker@arcor.de> * Implement UA shadow root for <details> Signed-off-by: Simon Wülker <simon.wuelker@arcor.de> * Invalidate style when display is opened or closed Signed-off-by: Simon Wülker <simon.wuelker@arcor.de> * Fix /_mozilla/mozilla/duplicated_scroll_ids.html This test previously assumed that <details> elements would not be rendered. Signed-off-by: Simon Wülker <simon.wuelker@arcor.de> * Implement implicit summary elements Signed-off-by: Simon Wülker <simon.wuelker@arcor.de> * Update WPT expectations Signed-off-by: Simon Wülker <simon.wuelker@arcor.de> * Remove test for duplicated scroll IDs See https://github.com/servo/servo/pull/35261#discussion_r1969328725 for reasoning. Signed-off-by: Simon Wülker <simon.wuelker@arcor.de> * Use Iterator::find to find implicit summary element Signed-off-by: Simon Wülker <simon.wuelker@arcor.de> --------- Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
Diffstat (limited to 'components/script/dom/htmldetailselement.rs')
-rw-r--r--components/script/dom/htmldetailselement.rs180
1 files changed, 175 insertions, 5 deletions
diff --git a/components/script/dom/htmldetailselement.rs b/components/script/dom/htmldetailselement.rs
index 00fb8aa21f4..04451cf2ae2 100644
--- a/components/script/dom/htmldetailselement.rs
+++ b/components/script/dom/htmldetailselement.rs
@@ -2,29 +2,58 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
-use std::cell::Cell;
+use std::cell::{Cell, Ref};
use dom_struct::dom_struct;
use html5ever::{local_name, LocalName, Prefix};
use js::rust::HandleObject;
use crate::dom::attr::Attr;
+use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::HTMLDetailsElementBinding::HTMLDetailsElementMethods;
+use crate::dom::bindings::codegen::Bindings::HTMLSlotElementBinding::HTMLSlotElement_Binding::HTMLSlotElementMethods;
+use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
+use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{
+ ShadowRootMode, SlotAssignmentMode,
+};
+use crate::dom::bindings::codegen::UnionTypes::ElementOrText;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::refcounted::Trusted;
-use crate::dom::bindings::root::DomRoot;
+use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::document::Document;
-use crate::dom::element::AttributeMutation;
+use crate::dom::element::{AttributeMutation, Element};
use crate::dom::eventtarget::EventTarget;
use crate::dom::htmlelement::HTMLElement;
-use crate::dom::node::{Node, NodeDamage, NodeTraits};
+use crate::dom::htmlslotelement::HTMLSlotElement;
+use crate::dom::node::{BindContext, ChildrenMutation, Node, NodeDamage, NodeTraits};
+use crate::dom::shadowroot::IsUserAgentWidget;
+use crate::dom::text::Text;
use crate::dom::virtualmethods::VirtualMethods;
use crate::script_runtime::CanGc;
+/// The summary that should be presented if no `<summary>` element is present
+const DEFAULT_SUMMARY: &str = "Details";
+
+/// Holds handles to all slots in the UA shadow tree
+///
+/// The composition of the tree is described in
+/// <https://html.spec.whatwg.org/multipage/#the-details-and-summary-elements>
+#[derive(Clone, JSTraceable, MallocSizeOf)]
+#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
+struct ShadowTree {
+ summary: Dom<HTMLSlotElement>,
+ descendants: Dom<HTMLSlotElement>,
+ /// The summary that is displayed if no other summary exists
+ implicit_summary: Dom<HTMLElement>,
+}
+
#[dom_struct]
pub(crate) struct HTMLDetailsElement {
htmlelement: HTMLElement,
toggle_counter: Cell<u32>,
+
+ /// Represents the UA widget for the details element
+ shadow_tree: DomRefCell<Option<ShadowTree>>,
}
impl HTMLDetailsElement {
@@ -36,6 +65,7 @@ impl HTMLDetailsElement {
HTMLDetailsElement {
htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
toggle_counter: Cell::new(0),
+ shadow_tree: Default::default(),
}
}
@@ -60,6 +90,131 @@ impl HTMLDetailsElement {
pub(crate) fn toggle(&self) {
self.SetOpen(!self.Open());
}
+
+ fn shadow_tree(&self, can_gc: CanGc) -> Ref<'_, ShadowTree> {
+ if !self.upcast::<Element>().is_shadow_host() {
+ self.create_shadow_tree(can_gc);
+ }
+
+ Ref::filter_map(self.shadow_tree.borrow(), Option::as_ref)
+ .ok()
+ .expect("UA shadow tree was not created")
+ }
+
+ fn create_shadow_tree(&self, can_gc: CanGc) {
+ let document = self.owner_document();
+ let root = self
+ .upcast::<Element>()
+ .attach_shadow(
+ IsUserAgentWidget::Yes,
+ ShadowRootMode::Closed,
+ false,
+ SlotAssignmentMode::Manual,
+ can_gc,
+ )
+ .expect("Attaching UA shadow root failed");
+
+ let summary = HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc);
+ root.upcast::<Node>()
+ .AppendChild(summary.upcast::<Node>())
+ .unwrap();
+
+ let fallback_summary =
+ HTMLElement::new(local_name!("summary"), None, &document, None, can_gc);
+ fallback_summary
+ .upcast::<Node>()
+ .SetTextContent(Some(DEFAULT_SUMMARY.into()), can_gc);
+ summary
+ .upcast::<Node>()
+ .AppendChild(fallback_summary.upcast::<Node>())
+ .unwrap();
+
+ let descendants = HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc);
+ root.upcast::<Node>()
+ .AppendChild(descendants.upcast::<Node>())
+ .unwrap();
+
+ let _ = self.shadow_tree.borrow_mut().insert(ShadowTree {
+ summary: summary.as_traced(),
+ descendants: descendants.as_traced(),
+ implicit_summary: fallback_summary.as_traced(),
+ });
+ self.upcast::<Node>()
+ .dirty(crate::dom::node::NodeDamage::OtherNodeDamage);
+ }
+
+ pub(crate) fn find_corresponding_summary_element(&self) -> Option<DomRoot<HTMLElement>> {
+ self.upcast::<Node>()
+ .children()
+ .filter_map(DomRoot::downcast::<HTMLElement>)
+ .find(|html_element| {
+ html_element.upcast::<Element>().local_name() == &local_name!("summary")
+ })
+ }
+
+ fn update_shadow_tree_contents(&self, can_gc: CanGc) {
+ let shadow_tree = self.shadow_tree(can_gc);
+
+ if let Some(summary) = self.find_corresponding_summary_element() {
+ shadow_tree
+ .summary
+ .Assign(vec![ElementOrText::Element(DomRoot::upcast(summary))]);
+ }
+
+ let mut slottable_children = vec![];
+ for child in self.upcast::<Node>().children() {
+ if let Some(element) = child.downcast::<Element>() {
+ if element.local_name() == &local_name!("summary") {
+ continue;
+ }
+
+ slottable_children.push(ElementOrText::Element(DomRoot::from_ref(element)));
+ }
+
+ if let Some(text) = child.downcast::<Text>() {
+ slottable_children.push(ElementOrText::Text(DomRoot::from_ref(text)));
+ }
+ }
+ shadow_tree.descendants.Assign(slottable_children);
+
+ self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
+ }
+
+ fn update_shadow_tree_styles(&self, can_gc: CanGc) {
+ let shadow_tree = self.shadow_tree(can_gc);
+
+ let value = if self.Open() {
+ "display: block;"
+ } else {
+ // TODO: This should be "display: block; content-visibility: hidden;",
+ // but servo does not support content-visibility yet
+ "display: none;"
+ };
+ shadow_tree
+ .descendants
+ .upcast::<Element>()
+ .set_string_attribute(&local_name!("style"), value.into(), can_gc);
+
+ // Manually update the list item style of the implicit summary element.
+ // Unlike the other summaries, this summary is in the shadow tree and
+ // can't be styled with UA sheets
+ let implicit_summary_list_item_style = if self.Open() {
+ "disclosure-open"
+ } else {
+ "disclosure-closed"
+ };
+ let implicit_summary_style = format!(
+ "display: list-item;
+ counter-increment: list-item 0;
+ list-style: {implicit_summary_list_item_style} inside;"
+ );
+ shadow_tree
+ .implicit_summary
+ .upcast::<Element>()
+ .set_string_attribute(&local_name!("style"), implicit_summary_style.into(), can_gc);
+
+ self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
+ }
}
impl HTMLDetailsElementMethods<crate::DomTypeHolder> for HTMLDetailsElement {
@@ -79,6 +234,8 @@ impl VirtualMethods for HTMLDetailsElement {
self.super_type().unwrap().attribute_mutated(attr, mutation);
if attr.local_name() == &local_name!("open") {
+ self.update_shadow_tree_styles(CanGc::note());
+
let counter = self.toggle_counter.get() + 1;
self.toggle_counter.set(counter);
@@ -92,7 +249,20 @@ impl VirtualMethods for HTMLDetailsElement {
this.upcast::<EventTarget>().fire_event(atom!("toggle"), CanGc::note());
}
}));
- self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage)
+ self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
}
}
+
+ fn children_changed(&self, mutation: &ChildrenMutation) {
+ self.super_type().unwrap().children_changed(mutation);
+
+ self.update_shadow_tree_contents(CanGc::note());
+ }
+
+ fn bind_to_tree(&self, context: &BindContext) {
+ self.super_type().unwrap().bind_to_tree(context);
+
+ self.update_shadow_tree_contents(CanGc::note());
+ self.update_shadow_tree_styles(CanGc::note());
+ }
}