diff --git a/Libraries/LibWeb/HTML/HTMLDetailsElement.cpp b/Libraries/LibWeb/HTML/HTMLDetailsElement.cpp
index fa03cdafa5..7227d62303 100644
--- a/Libraries/LibWeb/HTML/HTMLDetailsElement.cpp
+++ b/Libraries/LibWeb/HTML/HTMLDetailsElement.cpp
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
- * Copyright (c) 2023, Tim Flynn
+ * Copyright (c) 2023-2025, Tim Flynn
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -42,8 +42,12 @@ void HTMLDetailsElement::initialize(JS::Realm& realm)
WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLDetailsElement);
}
+// https://html.spec.whatwg.org/multipage/interactive-elements.html#the-details-element:html-element-insertion-steps
void HTMLDetailsElement::inserted()
{
+ // 1. Ensure details exclusivity by closing the given element if needed given insertedNode.
+ ensure_details_exclusivity_by_closing_the_given_element_if_needed();
+
create_shadow_tree_if_needed().release_value_but_fixme_should_propagate_errors();
update_shadow_tree_slots();
}
@@ -64,7 +68,8 @@ void HTMLDetailsElement::attribute_changed(FlyString const& local_name, Optional
// 2. If localName is name, then ensure details exclusivity by closing the given element if needed given element.
if (local_name == HTML::AttributeNames::name) {
- // FIXME: Implement the exclusivity steps.
+ ensure_details_exclusivity_by_closing_the_given_element_if_needed();
+ update_shadow_tree_style();
}
// 3. If localName is open, then:
@@ -85,7 +90,7 @@ void HTMLDetailsElement::attribute_changed(FlyString const& local_name, Optional
// 2. If oldValue is null and value is not null, then ensure details exclusivity by closing other elements if
// needed given element.
if (!old_value.has_value() && value.has_value()) {
- // FIXME: Implement the exclusivity steps.
+ ensure_details_exclusivity_by_closing_other_elements_if_needed();
}
update_shadow_tree_style();
@@ -136,6 +141,87 @@ void HTMLDetailsElement::queue_a_details_toggle_event_task(String old_state, Str
};
}
+// https://html.spec.whatwg.org/multipage/interactive-elements.html#details-name-group
+template
+void for_each_element_in_details_name_group(HTMLDetailsElement& details, FlyString const& name, Callback&& callback)
+{
+ // The details name group that contains a details element a also contains all the other details elements b that
+ // fulfill all of the following conditions:
+ auto name_group_contains_element = [&](auto const& element) {
+ // 1. Both a and b are in the same tree.
+ // NOTE: This is true due to the way we iterate the tree below.
+
+ // 2. They both have a name attribute, their name attributes are not the empty string, and the value of a's name
+ // attribute equals the value of b's name attribute.
+ return element.attribute(HTML::AttributeNames::name) == name;
+ };
+
+ details.root().for_each_in_inclusive_subtree_of_type([&](HTMLDetailsElement& candidate) {
+ if (&details != &candidate && name_group_contains_element(candidate))
+ return callback(candidate);
+ return TraversalDecision::Continue;
+ });
+}
+
+// https://html.spec.whatwg.org/multipage/interactive-elements.html#ensure-details-exclusivity-by-closing-other-elements-if-needed
+void HTMLDetailsElement::ensure_details_exclusivity_by_closing_other_elements_if_needed()
+{
+ // 1. Assert: element has an open attribute.
+ VERIFY(has_attribute(HTML::AttributeNames::open));
+
+ // 2. If element does not have a name attribute, or its name attribute is the empty string, then return.
+ auto name = attribute(HTML::AttributeNames::name);
+ if (!name.has_value() || name->is_empty())
+ return;
+
+ // 3. Let groupMembers be a list of elements, containing all elements in element's details name group except for
+ // element, in tree order.
+ // 4. For each element otherElement of groupMembers:
+ for_each_element_in_details_name_group(*this, *name, [&](HTMLDetailsElement& other_element) {
+ // 1. If the open attribute is set on otherElement, then:
+ if (other_element.has_attribute(HTML::AttributeNames::open)) {
+ // 1. Assert: otherElement is the only element in groupMembers that has the open attribute set.
+
+ // 2. Remove the open attribute on otherElement.
+ other_element.remove_attribute(HTML::AttributeNames::open);
+
+ // 3. Break.
+ return TraversalDecision::Break;
+ }
+
+ return TraversalDecision::Continue;
+ });
+}
+
+// https://html.spec.whatwg.org/multipage/interactive-elements.html#ensure-details-exclusivity-by-closing-the-given-element-if-needed
+void HTMLDetailsElement::ensure_details_exclusivity_by_closing_the_given_element_if_needed()
+{
+ // 1. If element does not have an open attribute, then return.
+ if (!has_attribute(HTML::AttributeNames::open))
+ return;
+
+ // 2. If element does not have a name attribute, or its name attribute is the empty string, then return.
+ auto name = attribute(HTML::AttributeNames::name);
+ if (!name.has_value() || name->is_empty())
+ return;
+
+ // 3. Let groupMembers be a list of elements, containing all elements in element's details name group except for
+ // element, in tree order.
+ // 4. For each element otherElement of groupMembers:
+ for_each_element_in_details_name_group(*this, *name, [&](HTMLDetailsElement const& other_element) {
+ // 1. If the open attribute is set on otherElement, then:
+ if (other_element.has_attribute(HTML::AttributeNames::open)) {
+ // 1. Remove the open attribute on element.
+ remove_attribute(HTML::AttributeNames::open);
+
+ // 2. Break.
+ return TraversalDecision::Break;
+ }
+
+ return TraversalDecision::Continue;
+ });
+}
+
// https://html.spec.whatwg.org/#the-details-and-summary-elements
WebIDL::ExceptionOr HTMLDetailsElement::create_shadow_tree_if_needed()
{
diff --git a/Libraries/LibWeb/HTML/HTMLDetailsElement.h b/Libraries/LibWeb/HTML/HTMLDetailsElement.h
index 075d428876..8dd57f2e58 100644
--- a/Libraries/LibWeb/HTML/HTMLDetailsElement.h
+++ b/Libraries/LibWeb/HTML/HTMLDetailsElement.h
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
- * Copyright (c) 2023, Tim Flynn
+ * Copyright (c) 2023-2025, Tim Flynn
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -37,6 +37,8 @@ private:
virtual void attribute_changed(FlyString const& local_name, Optional const& old_value, Optional const& value, Optional const& namespace_) override;
void queue_a_details_toggle_event_task(String old_state, String new_state);
+ void ensure_details_exclusivity_by_closing_other_elements_if_needed();
+ void ensure_details_exclusivity_by_closing_the_given_element_if_needed();
WebIDL::ExceptionOr create_shadow_tree_if_needed();
void update_shadow_tree_slots();
diff --git a/Tests/LibWeb/Text/expected/HTML/details-name.txt b/Tests/LibWeb/Text/expected/HTML/details-name.txt
new file mode 100644
index 0000000000..3508bf2b74
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/HTML/details-name.txt
@@ -0,0 +1,7 @@
+details0=✗ details1=✗ details2=✗ details3=✗ details4=✗
+details0=✓ details1=✗ details2=✗ details3=✗ details4=✗
+details0=✗ details1=✓ details2=✗ details3=✗ details4=✗
+details0=✗ details1=✗ details2=✓ details3=✗ details4=✗
+details0=✗ details1=✗ details2=✓ details3=✓ details4=✗
+details0=✗ details1=✗ details2=✓ details3=✓ details4=✓
+details0=✓ details1=✗ details2=✗ details3=✓ details4=✓
diff --git a/Tests/LibWeb/Text/input/HTML/details-name.html b/Tests/LibWeb/Text/input/HTML/details-name.html
new file mode 100644
index 0000000000..5805918a7a
--- /dev/null
+++ b/Tests/LibWeb/Text/input/HTML/details-name.html
@@ -0,0 +1,56 @@
+
+ Summary 0
+ Contents 0
+
+
+ Summary 1
+ Contents 1
+
+
+ Summary 2
+ Contents 2
+
+
+ Summary 3
+ Contents 3
+
+
+ Summary 4
+ Contents 4
+
+
+