From e0f15e385c37921950531071ae05701087135c54 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Fri, 23 Jan 2026 15:59:53 +0200 Subject: [PATCH] feat: add signal support for Details component summary text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds signal binding support for the Details component's summary text property through new constructors and binding methods. Connected to vaadin/flow#23193 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../flow/component/details/Details.java | 139 +++++++++-- .../component/details/DetailsSignalTest.java | 222 ++++++++++++++++++ 2 files changed, 342 insertions(+), 19 deletions(-) create mode 100644 vaadin-details-flow-parent/vaadin-details-flow/src/test/java/com/vaadin/flow/component/details/DetailsSignalTest.java diff --git a/vaadin-details-flow-parent/vaadin-details-flow/src/main/java/com/vaadin/flow/component/details/Details.java b/vaadin-details-flow-parent/vaadin-details-flow/src/main/java/com/vaadin/flow/component/details/Details.java index 95fc88d4d1c..6e2a49c22f3 100644 --- a/vaadin-details-flow-parent/vaadin-details-flow/src/main/java/com/vaadin/flow/component/details/Details.java +++ b/vaadin-details-flow-parent/vaadin-details-flow/src/main/java/com/vaadin/flow/component/details/Details.java @@ -23,6 +23,7 @@ import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.HasComponents; import com.vaadin.flow.component.HasSize; +import com.vaadin.flow.component.SignalPropertySupport; import com.vaadin.flow.component.Synchronize; import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.dependency.JsModule; @@ -33,6 +34,7 @@ import com.vaadin.flow.component.shared.HasTooltip; import com.vaadin.flow.component.shared.SlotUtils; import com.vaadin.flow.shared.Registration; +import com.vaadin.signals.Signal; /** * Details is an expandable panel for showing and hiding content from the user @@ -61,6 +63,10 @@ public class Details extends Component implements HasComponents, HasSize, private final Component summaryContainer; private final Div contentContainer; + /** Signal support for the summary text property. */ + private final SignalPropertySupport summaryTextSupport = SignalPropertySupport + .create(this, this::updateSummaryText); + /** * Server-side component for the {@code } element. */ @@ -81,7 +87,7 @@ public Details() { SlotUtils.addToSlot(this, "summary", summaryContainer); if (getElement().getPropertyRaw("opened") == null) { - setOpened(false); + getElement().setProperty("opened", false); } getElement().addPropertyChangeListener("opened", event -> fireEvent( @@ -97,7 +103,10 @@ public Details() { */ public Details(String summary) { this(); - setSummaryText(summary); + if (summary == null) { + summary = ""; + } + updateSummaryText(summary); } /** @@ -109,7 +118,19 @@ public Details(String summary) { */ public Details(Component summary) { this(); - setSummary(summary); + updateSummary(summary); + } + + /** + * Initializes a new Details component with a summary text provided by a + * signal. + * + * @param summaryTextSignal + * the signal that provides the summary text + */ + public Details(Signal summaryTextSignal) { + this(); + summaryTextSupport.bind(summaryTextSignal); } /** @@ -125,8 +146,11 @@ public Details(Component summary) { */ public Details(String summary, Component content) { this(); - setSummaryText(summary); - add(content); + if (summary == null) { + summary = ""; + } + updateSummaryText(summary); + contentContainer.add(content); } /** @@ -142,8 +166,23 @@ public Details(String summary, Component content) { */ public Details(Component summary, Component content) { this(); - setSummary(summary); - add(content); + updateSummary(summary); + contentContainer.add(content); + } + + /** + * Initializes a new Details component with a summary text provided by a + * signal and content. + * + * @param summaryTextSignal + * the signal that provides the summary text. + * @param content + * the content component to add. + */ + public Details(Signal summaryTextSignal, Component content) { + this(); + summaryTextSupport.bind(summaryTextSignal); + contentContainer.add(content); } /** @@ -160,7 +199,7 @@ public Details(Component summary, Component content) { */ public Details(String summary, Component... components) { this(summary); - add(components); + contentContainer.add(components); } /** @@ -177,7 +216,22 @@ public Details(String summary, Component... components) { */ public Details(Component summary, Component... components) { this(summary); - add(components); + contentContainer.add(components); + } + + /** + * Initializes a new Details component with a summary text provided by a + * signal and optional content components. + * + * @param summaryTextSignal + * the signal that provides the summary text. + * @param components + * the content components to add. + */ + public Details(Signal summaryTextSignal, Component... components) { + this(); + summaryTextSupport.bind(summaryTextSignal); + contentContainer.add(components); } /** @@ -198,13 +252,7 @@ protected Component createSummaryContainer() { * any previously set summary */ public void setSummary(Component summary) { - summaryContainer.getElement().removeAllChildren(); - if (summary == null) { - return; - } - - this.summary = summary; - summaryContainer.getElement().appendChild(summary.getElement()); + updateSummary(summary); } /** @@ -218,14 +266,19 @@ public Component getSummary() { } /** - * Creates a text wrapper and sets a summary via - * {@link #setSummary(Component)} + * Sets the summary text of the details component. + * + * @param summary + * the summary text to set, or {@code null} for empty text + * @throws com.vaadin.signals.BindingActiveException + * if the summary text is currently bound to a signal + * @see #bindSummaryText(Signal) */ public void setSummaryText(String summary) { if (summary == null) { summary = ""; } - setSummary(new Span(summary)); + summaryTextSupport.set(summary); } /** @@ -236,6 +289,54 @@ public String getSummaryText() { return summary == null ? "" : summary.getElement().getText(); } + /** + * Updates the summary component. For internal use during initialization and + * signal updates. + */ + private void updateSummary(Component summary) { + summaryContainer.getElement().removeAllChildren(); + if (summary == null) { + return; + } + + this.summary = summary; + summaryContainer.getElement().appendChild(summary.getElement()); + } + + /** + * Updates the summary text when bound to a signal. + */ + private void updateSummaryText(String newText) { + if (summary == null || !(summary instanceof Span)) { + updateSummary(new Span(newText)); + } else { + summary.getElement().setText(newText); + } + } + + /** + * Binds a signal to update the summary text of the details component. + *

+ * When bound, the signal manages the summary text. Any previously set + * custom summary component will be replaced when the signal updates. + * + * @param signal + * the signal that provides the new summary text, or {@code null} + * to remove the binding. + */ + public void bindSummaryText(Signal signal) { + summaryTextSupport.bind(signal); + } + + /** + * Gets the summary text signal support instance for testing purposes. + * + * @return the summary text signal support + */ + SignalPropertySupport getSummaryTextSupport() { + return summaryTextSupport; + } + /** * Adds components to the content section * diff --git a/vaadin-details-flow-parent/vaadin-details-flow/src/test/java/com/vaadin/flow/component/details/DetailsSignalTest.java b/vaadin-details-flow-parent/vaadin-details-flow/src/test/java/com/vaadin/flow/component/details/DetailsSignalTest.java new file mode 100644 index 00000000000..4734c2f1fa0 --- /dev/null +++ b/vaadin-details-flow-parent/vaadin-details-flow/src/test/java/com/vaadin/flow/component/details/DetailsSignalTest.java @@ -0,0 +1,222 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.details; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.signals.BindingActiveException; +import com.vaadin.signals.Signal; +import com.vaadin.signals.ValueSignal; +import com.vaadin.tests.AbstractSignalsUnitTest; + +/** + * Unit tests for the Details component's signal bindings. + */ +public class DetailsSignalTest extends AbstractSignalsUnitTest { + + private Details details; + private ValueSignal summaryTextSignal; + private Signal computedSignal; + + @Before + public void setup() { + summaryTextSignal = new ValueSignal<>("Initial Summary"); + computedSignal = Signal + .computed(() -> summaryTextSignal.value() + " computed"); + } + + @After + public void tearDown() { + if (details != null && details.isAttached()) { + details.removeFromParent(); + } + } + + // A. Constructor Variant Tests + + @Test + public void summaryTextSignalCtor() { + details = new Details(summaryTextSignal); + UI.getCurrent().add(details); + assertSummaryTextSignalBindingActive(); + } + + @Test + public void summaryTextSignalWithContentCtor() { + Div content = new Div(); + content.setText("Content"); + details = new Details(summaryTextSignal, content); + UI.getCurrent().add(details); + assertSummaryTextSignalBindingActive(); + + // Verify content is added + Assert.assertEquals(1, details.getContent().count()); + Assert.assertEquals(content, details.getContent().findFirst().get()); + } + + @Test + public void summaryTextSignalWithMultipleComponentsCtor() { + Div content1 = new Div(); + content1.setText("Content 1"); + Div content2 = new Div(); + content2.setText("Content 2"); + Span content3 = new Span("Content 3"); + + details = new Details(summaryTextSignal, content1, content2, content3); + UI.getCurrent().add(details); + assertSummaryTextSignalBindingActive(); + + // Verify all components are added + Assert.assertEquals(3, details.getContent().count()); + } + + // B. Signal Binding Lifecycle Tests + + @Test + public void summaryTextSignal_notAttached() { + details = new Details(summaryTextSignal); + assertSummaryTextSignalBindingInactive(); + } + + @Test + public void summaryTextSignal_detachedAndAttached() { + details = new Details(summaryTextSignal); + UI.getCurrent().add(details); + details.removeFromParent(); + assertSummaryTextSignalBindingInactive(); + + UI.getCurrent().add(details); + assertSummaryTextSignalBindingActive(); + } + + @Test + public void summaryTextSignal_removeBinding() { + details = new Details(summaryTextSignal); + UI.getCurrent().add(details); + + details.bindSummaryText(null); + assertSummaryTextSignalBindingInactive(); + + details.setSummaryText("Manual text"); + Assert.assertEquals("Manual text", details.getSummaryText()); + } + + @Test(expected = BindingActiveException.class) + public void setSummaryTextWhileBound_throws() { + details = new Details(summaryTextSignal); + UI.getCurrent().add(details); + + details.setSummaryText("Attempt to set text"); + } + + // C. Signal Type Tests + + @Test + public void summaryTextComputedSignalCtor() { + details = new Details(computedSignal); + UI.getCurrent().add(details); + Assert.assertEquals("Initial Summary computed", + details.getSummaryText()); + + summaryTextSignal.value("Updated"); + Assert.assertEquals("Updated computed", details.getSummaryText()); + } + + @Test + public void summaryTextComputedSignal_removeBindingAndRebind() { + details = new Details(computedSignal); + UI.getCurrent().add(details); + + details.bindSummaryText(null); + details.bindSummaryText(summaryTextSignal); + assertSummaryTextSignalBindingActive(); + + details.bindSummaryText(null); + assertSummaryTextSignalBindingInactive(); + } + + // D. Constructor Delegation Test + + @Test + public void summaryTextSignalConstructors_useSignalSupport() { + // Test signal constructor variant 1 + details = new Details(summaryTextSignal); + UI.getCurrent().add(details); + Assert.assertEquals("Initial Summary", details.getSummaryText()); + summaryTextSignal.value("Changed"); + Assert.assertEquals("Changed", details.getSummaryText()); + details.removeFromParent(); + + // Test signal constructor variant 2 + details = new Details(summaryTextSignal, new Div()); + UI.getCurrent().add(details); + summaryTextSignal.value("Changed Again"); + Assert.assertEquals("Changed Again", details.getSummaryText()); + details.removeFromParent(); + + // Test signal constructor variant 3 + details = new Details(summaryTextSignal, new Div(), new Span()); + UI.getCurrent().add(details); + summaryTextSignal.value("Final Change"); + Assert.assertEquals("Final Change", details.getSummaryText()); + } + + // E. Content Management Test + + @Test + public void contentAddedWithSignalConstructor_signalStillWorks() { + Div content = new Div("Content"); + details = new Details(summaryTextSignal, content); + UI.getCurrent().add(details); + + // Verify content is accessible + Assert.assertEquals(1, details.getContent().count()); + Assert.assertEquals(content, details.getContent().findFirst().get()); + + // Verify signal binding still works + assertSummaryTextSignalBindingActive(); + + // Add more content + Span additionalContent = new Span("More content"); + details.add(additionalContent); + Assert.assertEquals(2, details.getContent().count()); + + // Verify signal binding still works after adding content + summaryTextSignal.value("After adding content"); + Assert.assertEquals("After adding content", details.getSummaryText()); + } + + // Helper Methods + + private void assertSummaryTextSignalBindingActive() { + summaryTextSignal.value("First Update"); + Assert.assertEquals("First Update", details.getSummaryText()); + summaryTextSignal.value("Second Update"); + Assert.assertEquals("Second Update", details.getSummaryText()); + } + + private void assertSummaryTextSignalBindingInactive() { + String currentText = details.getSummaryText(); + summaryTextSignal.value(currentText + " changed"); + Assert.assertEquals(currentText, details.getSummaryText()); + } +}