Fix details children slotting and apply summary

Signed-off-by: stevennovaryo <steven.novaryo@gmail.com>
This commit is contained in:
stevennovaryo 2025-05-23 16:53:54 +08:00
parent a824b2781f
commit 9d469fc917
2 changed files with 74 additions and 49 deletions

View file

@ -8,11 +8,11 @@ slot[id="internal-contents-slot"] {
display: block;
}
summary[id="internal-fallback-summary"] {
:host summary:first-of-type {
display: list-item;
counter-increment: list-item 0;
list-style: disclosure-closed inside;
}
:host([open]) summary[id="internal-fallback-summary"] {
:host([open]) summary:first-of-type {
list-style-type: disclosure-open;
}

View file

@ -45,8 +45,9 @@ const DEFAULT_SUMMARY: &str = "Details";
#[derive(Clone, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct ShadowTree {
summary: Dom<HTMLSlotElement>,
descendants: Dom<HTMLSlotElement>,
summary_slot: Dom<HTMLSlotElement>,
descendants_slot: Dom<HTMLSlotElement>,
fallback_summary: Dom<HTMLElement>,
}
#[dom_struct]
@ -103,6 +104,7 @@ impl HTMLDetailsElement {
.expect("UA shadow tree was not created")
}
/// <https://html.spec.whatwg.org/multipage/rendering.html#the-details-and-summary-elements>
fn create_shadow_tree(&self, can_gc: CanGc) {
let document = self.owner_document();
let root = self
@ -118,6 +120,50 @@ impl HTMLDetailsElement {
)
.expect("Attaching UA shadow root failed");
// > The details element is expected to have an internal shadow tree with three child elements:
// > 1. The first child element is a slot that is expected to take the details element's first summary
// > element child, if any.
let summary_slot = HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc);
summary_slot.upcast::<Element>().set_attribute(
&local_name!("id"),
AttrValue::from_atomic("internal-summary-slot".to_owned()),
can_gc,
);
root.upcast::<Node>()
.AppendChild(summary_slot.upcast::<Node>(), can_gc)
.unwrap();
// > This element has a single child summary element called the default summary which has
// > text content that is implementation-defined (and probably locale-specific).
let fallback_summary =
HTMLElement::new(local_name!("summary"), None, &document, None, can_gc);
fallback_summary.upcast::<Element>().set_attribute(
&local_name!("id"),
AttrValue::from_atomic("internal-fallback-summary".to_owned()),
can_gc,
);
fallback_summary
.upcast::<Node>()
.SetTextContent(Some(DEFAULT_SUMMARY.into()), can_gc);
summary_slot
.upcast::<Node>()
.AppendChild(fallback_summary.upcast::<Node>(), can_gc)
.unwrap();
// > 2. The second child element is a slot that is expected to take the details element's
// > remaining descendants, if any. This element has no contents.
let descendants_slot =
HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc);
descendants_slot.upcast::<Element>().set_attribute(
&local_name!("id"),
AttrValue::from_atomic("internal-contents-slot".to_owned()),
can_gc,
);
root.upcast::<Node>()
.AppendChild(descendants_slot.upcast::<Node>(), can_gc)
.unwrap();
// > 3. The third child element is either a link or style element with the following styles for the default summary:
let link_element = HTMLLinkElement::new(
local_name!("link"),
None,
@ -135,6 +181,8 @@ impl HTMLDetailsElement {
.AppendChild(link_element.upcast::<Node>(), can_gc)
.unwrap();
// TODO(stevennovaryo): We need a mechanism for parsing and storing a stylesheet as
// we shouldn't reparse this each time we access the element.
let details_stylesheet = parse_details_stylesheet(
link_element
.upcast::<Node>()
@ -143,45 +191,11 @@ impl HTMLDetailsElement {
);
link_element.set_stylesheet(details_stylesheet.unwrap());
let summary_slot = HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc);
summary_slot.upcast::<Element>().set_attribute(
&local_name!("id"),
AttrValue::from_atomic("internal-summary-slot".to_owned()),
can_gc,
);
root.upcast::<Node>()
.AppendChild(summary_slot.upcast::<Node>(), can_gc)
.unwrap();
let fallback_summary =
HTMLElement::new(local_name!("summary"), None, &document, None, can_gc);
fallback_summary.upcast::<Element>().set_attribute(
&local_name!("id"),
AttrValue::from_atomic("internal-fallback-summary".to_owned()),
can_gc,
);
fallback_summary
.upcast::<Node>()
.SetTextContent(Some(DEFAULT_SUMMARY.into()), can_gc);
summary_slot
.upcast::<Node>()
.AppendChild(fallback_summary.upcast::<Node>(), can_gc)
.unwrap();
let descendants_slot =
HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc);
descendants_slot.upcast::<Element>().set_attribute(
&local_name!("id"),
AttrValue::from_atomic("internal-contents-slot".to_owned()),
can_gc,
);
root.upcast::<Node>()
.AppendChild(descendants_slot.upcast::<Node>(), can_gc)
.unwrap();
let _ = self.shadow_tree.borrow_mut().insert(ShadowTree {
summary: summary_slot.as_traced(),
descendants: descendants_slot.as_traced(),
summary_slot: summary_slot.as_traced(),
descendants_slot: descendants_slot.as_traced(),
fallback_summary: fallback_summary.as_traced(),
});
self.upcast::<Node>()
.dirty(crate::dom::node::NodeDamage::OtherNodeDamage);
@ -199,16 +213,18 @@ impl HTMLDetailsElement {
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 discovered_first_summary_element = false;
let mut slottable_children = vec![];
// Assign all children to its corresponding slots.
for child in self.upcast::<Node>().children() {
if let Some(element) = child.downcast::<Element>() {
if element.local_name() == &local_name!("summary") {
// Assign the first summary element to the summary slot.
if element.local_name() == &local_name!("summary") && !discovered_first_summary_element {
shadow_tree
.summary_slot
.Assign(vec![ElementOrText::Element(DomRoot::from_ref(element))]);
discovered_first_summary_element = true;
continue;
}
@ -219,7 +235,16 @@ impl HTMLDetailsElement {
slottable_children.push(ElementOrText::Text(DomRoot::from_ref(text)));
}
}
shadow_tree.descendants.Assign(slottable_children);
shadow_tree.descendants_slot.Assign(slottable_children);
// If there are no summary element, assign fallback summary instead.
if !discovered_first_summary_element {
shadow_tree
.summary_slot
.Assign(vec![ElementOrText::Element(DomRoot::upcast(
shadow_tree.fallback_summary.as_rooted()
))]);
}
}
}