diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index 14688cf0c02..70ec416635c 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -62,9 +62,6 @@ use xml5ever::serialize::TraversalScope::{ ChildrenOnly as XmlChildrenOnly, IncludeNode as XmlIncludeNode, }; -use super::customelementregistry::is_valid_custom_element_name; -use super::htmltablecolelement::{HTMLTableColElement, HTMLTableColElementLayoutHelpers}; -use super::intersectionobserver::{IntersectionObserver, IntersectionObserverRegistration}; use crate::dom::activation::Activatable; use crate::dom::attr::{Attr, AttrHelpersForLayout}; use crate::dom::bindings::cell::{ref_filter_map, DomRefCell, Ref, RefMut}; @@ -94,7 +91,8 @@ use crate::dom::bindings::xmlname::{ use crate::dom::characterdata::CharacterData; use crate::dom::create::create_element; use crate::dom::customelementregistry::{ - CallbackReaction, CustomElementDefinition, CustomElementReaction, CustomElementState, + is_valid_custom_element_name, CallbackReaction, CustomElementDefinition, CustomElementReaction, + CustomElementState, }; use crate::dom::document::{ determine_policy_for_token, Document, LayoutDocumentHelpers, ReflowTriggerCondition, @@ -128,6 +126,7 @@ use crate::dom::htmlselectelement::HTMLSelectElement; use crate::dom::htmlslotelement::{HTMLSlotElement, Slottable}; use crate::dom::htmlstyleelement::HTMLStyleElement; use crate::dom::htmltablecellelement::{HTMLTableCellElement, HTMLTableCellElementLayoutHelpers}; +use crate::dom::htmltablecolelement::{HTMLTableColElement, HTMLTableColElementLayoutHelpers}; use crate::dom::htmltableelement::{HTMLTableElement, HTMLTableElementLayoutHelpers}; use crate::dom::htmltablerowelement::{HTMLTableRowElement, HTMLTableRowElementLayoutHelpers}; use crate::dom::htmltablesectionelement::{ @@ -136,6 +135,7 @@ use crate::dom::htmltablesectionelement::{ use crate::dom::htmltemplateelement::HTMLTemplateElement; use crate::dom::htmltextareaelement::{HTMLTextAreaElement, LayoutHTMLTextAreaElementHelpers}; use crate::dom::htmlvideoelement::{HTMLVideoElement, LayoutHTMLVideoElementHelpers}; +use crate::dom::intersectionobserver::{IntersectionObserver, IntersectionObserverRegistration}; use crate::dom::mutationobserver::{Mutation, MutationObserver}; use crate::dom::namednodemap::NamedNodeMap; use crate::dom::node::{ 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 `` 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 +/// +#[derive(Clone, JSTraceable, MallocSizeOf)] +#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] +struct ShadowTree { + summary: Dom, + descendants: Dom, + /// The summary that is displayed if no other summary exists + implicit_summary: Dom, +} + #[dom_struct] pub(crate) struct HTMLDetailsElement { htmlelement: HTMLElement, toggle_counter: Cell, + + /// Represents the UA widget for the details element + shadow_tree: DomRefCell>, } 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::().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::() + .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::() + .AppendChild(summary.upcast::()) + .unwrap(); + + let fallback_summary = + HTMLElement::new(local_name!("summary"), None, &document, None, can_gc); + fallback_summary + .upcast::() + .SetTextContent(Some(DEFAULT_SUMMARY.into()), can_gc); + summary + .upcast::() + .AppendChild(fallback_summary.upcast::()) + .unwrap(); + + let descendants = HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc); + root.upcast::() + .AppendChild(descendants.upcast::()) + .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::() + .dirty(crate::dom::node::NodeDamage::OtherNodeDamage); + } + + pub(crate) fn find_corresponding_summary_element(&self) -> Option> { + self.upcast::() + .children() + .filter_map(DomRoot::downcast::) + .find(|html_element| { + html_element.upcast::().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::().children() { + if let Some(element) = child.downcast::() { + if element.local_name() == &local_name!("summary") { + continue; + } + + slottable_children.push(ElementOrText::Element(DomRoot::from_ref(element))); + } + + if let Some(text) = child.downcast::() { + slottable_children.push(ElementOrText::Text(DomRoot::from_ref(text))); + } + } + shadow_tree.descendants.Assign(slottable_children); + + self.upcast::().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::() + .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::() + .set_string_attribute(&local_name!("style"), implicit_summary_style.into(), can_gc); + + self.upcast::().dirty(NodeDamage::OtherNodeDamage); + } } impl HTMLDetailsElementMethods 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::().fire_event(atom!("toggle"), CanGc::note()); } })); - self.upcast::().dirty(NodeDamage::OtherNodeDamage) + self.upcast::().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()); + } } diff --git a/components/script/dom/htmlelement.rs b/components/script/dom/htmlelement.rs index 32b746413cc..33e83a5db04 100644 --- a/components/script/dom/htmlelement.rs +++ b/components/script/dom/htmlelement.rs @@ -23,6 +23,7 @@ use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::{ use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLLabelElementBinding::HTMLLabelElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; +use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRoot_Binding::ShadowRootMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; @@ -48,6 +49,7 @@ use crate::dom::htmlinputelement::{HTMLInputElement, InputType}; use crate::dom::htmllabelelement::HTMLLabelElement; use crate::dom::htmltextareaelement::HTMLTextAreaElement; use crate::dom::node::{BindContext, Node, NodeTraits, ShadowIncluding, UnbindContext}; +use crate::dom::shadowroot::ShadowRoot; use crate::dom::text::Text; use crate::dom::virtualmethods::VirtualMethods; use crate::script_runtime::CanGc; @@ -914,45 +916,64 @@ impl HTMLElement { // https://html.spec.whatwg.org/multipage/#the-summary-element:activation-behaviour pub(crate) fn summary_activation_behavior(&self) { - // Step 1 - if !self.is_summary_for_its_parent_details() { + debug_assert!(self.as_element().local_name() == &local_name!("summary")); + + // Step 1. If this summary element is not the summary for its parent details, then return. + if !self.is_a_summary_for_its_parent_details() { return; } - // Step 2 - let parent_details = self.upcast::().GetParentNode().unwrap(); + // Step 2. Let parent be this summary element's parent. + let parent = if self.is_implicit_summary_element() { + DomRoot::downcast::(self.containing_shadow_root().unwrap().Host()) + .unwrap() + } else { + self.upcast::() + .GetParentNode() + .and_then(DomRoot::downcast::) + .unwrap() + }; - // Step 3 - parent_details - .downcast::() - .unwrap() - .toggle(); + // Step 3. If the open attribute is present on parent, then remove it. + // Otherwise, set parent's open attribute to the empty string. + parent.toggle(); } - // https://html.spec.whatwg.org/multipage/#summary-for-its-parent-details - fn is_summary_for_its_parent_details(&self) -> bool { - // Step 1 - let summary_node = self.upcast::(); - if !summary_node.has_parent() { + /// + fn is_a_summary_for_its_parent_details(&self) -> bool { + if self.is_implicit_summary_element() { + return true; + } + + // Step 1. If this summary element has no parent, then return false. + // Step 2. Let parent be this summary element's parent. + let Some(parent) = self.upcast::().GetParentNode() else { return false; - } + }; - // Step 2 - let parent = &summary_node.GetParentNode().unwrap(); - - // Step 3 - if !parent.is::() { + // Step 3. If parent is not a details element, then return false. + let Some(details) = parent.downcast::() else { return false; - } + }; - // Step 4 & 5 - let first_summary_element = parent - .child_elements() - .find(|el| el.local_name() == &local_name!("summary")); - match first_summary_element { - Some(first_summary) => &*first_summary == self.as_element(), - None => false, - } + // Step 4. If parent's first summary element child is not this summary + // element, then return false. + // Step 5. Return true. + details + .find_corresponding_summary_element() + .is_some_and(|summary| &*summary == self.upcast()) + } + + /// Whether or not this is an implicitly generated `` + /// element for a UA `
` shadow tree + fn is_implicit_summary_element(&self) -> bool { + // Note that non-implicit summary elements are not actually inside + // the UA shadow tree, they're only assigned to a slot inside it. + // Therefore they don't cause false positives here + self.containing_shadow_root() + .as_deref() + .map(ShadowRoot::Host) + .is_some_and(|host| host.is::()) } /// @@ -1173,6 +1194,7 @@ impl Activatable for HTMLElement { self.summary_activation_behavior(); } } + // Form-associated custom elements are the same interface type as // normal HTMLElements, so HTMLElement needs to have the FormControl trait // even though it's usually more specific trait implementations, like the diff --git a/resources/servo.css b/resources/servo.css index c7e891ec58a..2426c1b8f20 100644 --- a/resources/servo.css +++ b/resources/servo.css @@ -327,4 +327,17 @@ meter:-moz-meter-sub-optimum div { } meter:-moz-meter-sub-sub-optimum div { background: linear-gradient(#f77, #f77, #fcc 20%, #d44 45%, #d44 55%); +} + +/* https://html.spec.whatwg.org/#the-details-and-summary-elements */ +details, summary { + display: block; +} +details > summary:first-of-type { + display: list-item; + counter-increment: list-item 0; + list-style: disclosure-closed inside; +} +details[open] > summary:first-of-type { + list-style-type: disclosure-open; } \ No newline at end of file diff --git a/tests/wpt/meta/css/selectors/selectors-4/details-open-pseudo-003.html.ini b/tests/wpt/meta/css/selectors/selectors-4/details-open-pseudo-003.html.ini deleted file mode 100644 index 86c4dff2666..00000000000 --- a/tests/wpt/meta/css/selectors/selectors-4/details-open-pseudo-003.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[details-open-pseudo-003.html] - expected: FAIL diff --git a/tests/wpt/meta/html/dom/elements/the-innertext-and-outertext-properties/getter.html.ini b/tests/wpt/meta/html/dom/elements/the-innertext-and-outertext-properties/getter.html.ini index 182d7e1d4c5..37d45a806a3 100644 --- a/tests/wpt/meta/html/dom/elements/the-innertext-and-outertext-properties/getter.html.ini +++ b/tests/wpt/meta/html/dom/elements/the-innertext-and-outertext-properties/getter.html.ini @@ -25,3 +25,6 @@ [::first-line styles applied ("
abc")] expected: FAIL + + [opened
content shown ("
abc123")] + expected: FAIL diff --git a/tests/wpt/meta/html/rendering/the-details-element/details-display.html.ini b/tests/wpt/meta/html/rendering/the-details-element/details-display.html.ini index 31d0648af61..288a0776a08 100644 --- a/tests/wpt/meta/html/rendering/the-details-element/details-display.html.ini +++ b/tests/wpt/meta/html/rendering/the-details-element/details-display.html.ini @@ -1,6 +1,3 @@ [details-display.html] - [default display of first summary child of details is list-item] - expected: FAIL - [display of details element can be changed] expected: FAIL diff --git a/tests/wpt/meta/html/rendering/the-details-element/details-revert.html.ini b/tests/wpt/meta/html/rendering/the-details-element/details-revert.html.ini deleted file mode 100644 index 261501482db..00000000000 --- a/tests/wpt/meta/html/rendering/the-details-element/details-revert.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[details-revert.html] - expected: FAIL diff --git a/tests/wpt/meta/html/rendering/the-details-element/details-summary-display-inline-001.html.ini b/tests/wpt/meta/html/rendering/the-details-element/details-summary-display-inline-001.html.ini new file mode 100644 index 00000000000..595d38ebd04 --- /dev/null +++ b/tests/wpt/meta/html/rendering/the-details-element/details-summary-display-inline-001.html.ini @@ -0,0 +1,2 @@ +[details-summary-display-inline-001.html] + expected: FAIL diff --git a/tests/wpt/meta/html/rendering/the-details-element/details-summary-display-inline-002.html.ini b/tests/wpt/meta/html/rendering/the-details-element/details-summary-display-inline-002.html.ini deleted file mode 100644 index 70ac0b2409d..00000000000 --- a/tests/wpt/meta/html/rendering/the-details-element/details-summary-display-inline-002.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[details-summary-display-inline-002.html] - expected: FAIL diff --git a/tests/wpt/meta/html/rendering/the-details-element/summary-display-list-item-001.html.ini b/tests/wpt/meta/html/rendering/the-details-element/summary-display-list-item-001.html.ini deleted file mode 100644 index 772c1a86fc2..00000000000 --- a/tests/wpt/meta/html/rendering/the-details-element/summary-display-list-item-001.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[summary-display-list-item-001.html] - expected: FAIL diff --git a/tests/wpt/meta/html/rendering/the-details-element/summary-text-decoration.html.ini b/tests/wpt/meta/html/rendering/the-details-element/summary-text-decoration.html.ini deleted file mode 100644 index 1f62daa6741..00000000000 --- a/tests/wpt/meta/html/rendering/the-details-element/summary-text-decoration.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[summary-text-decoration.html] - expected: FAIL diff --git a/tests/wpt/meta/html/semantics/interactive-elements/the-details-element/closed-details-layout-apis.tentative.html.ini b/tests/wpt/meta/html/semantics/interactive-elements/the-details-element/closed-details-layout-apis.tentative.html.ini new file mode 100644 index 00000000000..c09547dad07 --- /dev/null +++ b/tests/wpt/meta/html/semantics/interactive-elements/the-details-element/closed-details-layout-apis.tentative.html.ini @@ -0,0 +1,3 @@ +[closed-details-layout-apis.tentative.html] + [Verifies the layout results of elements inside a closed
based on the usage of content-visibility:hidden.] + expected: FAIL diff --git a/tests/wpt/meta/html/semantics/interactive-elements/the-details-element/details-add-summary.html.ini b/tests/wpt/meta/html/semantics/interactive-elements/the-details-element/details-add-summary.html.ini deleted file mode 100644 index 96c661de0e2..00000000000 --- a/tests/wpt/meta/html/semantics/interactive-elements/the-details-element/details-add-summary.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[details-add-summary.html] - expected: FAIL diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index 7dadc6247d2..7f63b5e2079 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -7581,19 +7581,6 @@ {} ] ], - "duplicated_scroll_ids.html": [ - "a0ac8e578ddb63efa9aa673285a38c67c4ba6c2b", - [ - null, - [ - [ - "/_mozilla/mozilla/duplicated_scroll_ids_ref.html", - "==" - ] - ], - {} - ] - ], "font-element-comma-separated.html": [ "db7e13f2ca3db0ebd3c610c3c25b052749b85e30", [ @@ -10463,10 +10450,6 @@ "3e68ff395f5475e2b618147f270117f576a5b7bd", [] ], - "duplicated_scroll_ids_ref.html": [ - "6783d72a6629f4938df8126dc5114d936eaaa48f", - [] - ], "font-element-comma-separated-ref.html": [ "97efe2b83d5f78bdac0d4aa951b63342fb1fa1cf", [] diff --git a/tests/wpt/mozilla/tests/mozilla/duplicated_scroll_ids.html b/tests/wpt/mozilla/tests/mozilla/duplicated_scroll_ids.html deleted file mode 100644 index a0ac8e578dd..00000000000 --- a/tests/wpt/mozilla/tests/mozilla/duplicated_scroll_ids.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Ensure that content which produces duplicate scroll ids does not panic - - -
-
- - - diff --git a/tests/wpt/mozilla/tests/mozilla/duplicated_scroll_ids_ref.html b/tests/wpt/mozilla/tests/mozilla/duplicated_scroll_ids_ref.html deleted file mode 100644 index 6783d72a662..00000000000 --- a/tests/wpt/mozilla/tests/mozilla/duplicated_scroll_ids_ref.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - Ensure that content which produces duplicate scroll ids does not panic - - -
- - -