diff options
author | Simon Wülker <simon.wuelker@arcor.de> | 2025-02-25 12:56:36 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-02-25 11:56:36 +0000 |
commit | 754b117011b1e035fabbcaa9cd0797e3c9242c8c (patch) | |
tree | 265a7749857fc4f67ca80891a21524c1f4b7da7e /components/script/dom/htmldetailselement.rs | |
parent | cceff779288dc25c507e782aac19b25decfbeb83 (diff) | |
download | servo-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.rs | 180 |
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()); + } } |