From 59a4203cf021a6f23f24a2dd7f353c970b4120ed Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 15 Jan 2025 14:39:05 -0500 Subject: [PATCH] LibWeb: Implement the exclusive
accordion This is a relatively new feature which allows naming
groups to ensure only one
element in that group is opened at a time. --- Libraries/LibWeb/HTML/HTMLDetailsElement.cpp | 92 ++++++++++++++++++- Libraries/LibWeb/HTML/HTMLDetailsElement.h | 4 +- .../Text/expected/HTML/details-name.txt | 7 ++ .../LibWeb/Text/input/HTML/details-name.html | 56 +++++++++++ 4 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/HTML/details-name.txt create mode 100644 Tests/LibWeb/Text/input/HTML/details-name.html diff --git a/Libraries/LibWeb/HTML/HTMLDetailsElement.cpp b/Libraries/LibWeb/HTML/HTMLDetailsElement.cpp index fa03cdafa53..7227d623032 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 075d428876a..8dd57f2e580 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 00000000000..3508bf2b746 --- /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 00000000000..5805918a7ae --- /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 +
+ +