From 34d8b2d8ac974e2afcf95636423d7a482b881b5f Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Feb 2026 13:56:38 +0000 Subject: [PATCH 1/2] feat: add signal bind methods to app-layout component --- .../vaadin-app-layout-flow/pom.xml | 6 + .../flow/component/applayout/AppLayout.java | 21 ++++ .../applayout/AppLayoutSignalTest.java | 115 ++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 vaadin-app-layout-flow-parent/vaadin-app-layout-flow/src/test/java/com/vaadin/flow/component/applayout/AppLayoutSignalTest.java diff --git a/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/pom.xml b/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/pom.xml index 79c55c048ea..754f865eb1e 100644 --- a/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/pom.xml +++ b/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/pom.xml @@ -39,6 +39,12 @@ vaadin-flow-components-base ${project.version} + + com.vaadin + vaadin-flow-components-test-util + ${project.version} + test + com.vaadin vaadin-icons-flow diff --git a/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/src/main/java/com/vaadin/flow/component/applayout/AppLayout.java b/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/src/main/java/com/vaadin/flow/component/applayout/AppLayout.java index 33da0631f04..cc5b12e12f6 100644 --- a/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/src/main/java/com/vaadin/flow/component/applayout/AppLayout.java +++ b/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/src/main/java/com/vaadin/flow/component/applayout/AppLayout.java @@ -30,8 +30,10 @@ import com.vaadin.flow.component.dependency.JsModule; import com.vaadin.flow.component.dependency.NpmPackage; import com.vaadin.flow.component.shared.SlotUtils; +import com.vaadin.flow.function.SerializableConsumer; import com.vaadin.flow.internal.JacksonUtils; import com.vaadin.flow.router.RouterLayout; +import com.vaadin.flow.signals.Signal; /** * App Layout is a component for building common application layouts. @@ -145,6 +147,25 @@ public void setDrawerOpened(boolean drawerOpened) { getElement().setProperty("drawerOpened", drawerOpened); } + /** + * Binds the drawer opened state to the given writable signal. The binding + * is two-way: signal changes push to the DOM property, and client-side + * property changes push back to the signal. + *

+ * While a signal is bound, any attempt to set the drawer opened state + * manually throws {@link com.vaadin.flow.signals.BindingActiveException}. + * + * @param signal + * the writable signal to bind, not {@code null} + * @since 25.1 + */ + public void bindDrawerOpened(Signal signal, + SerializableConsumer writeCallback) { + Objects.requireNonNull(signal, "Signal cannot be null"); + getElement().bindProperty("drawerOpened", + signal.map(v -> v == null ? Boolean.TRUE : v), writeCallback); + } + /** * Note: This property is controlled via CSS and can not be * changed directly. diff --git a/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/src/test/java/com/vaadin/flow/component/applayout/AppLayoutSignalTest.java b/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/src/test/java/com/vaadin/flow/component/applayout/AppLayoutSignalTest.java new file mode 100644 index 00000000000..76d1ff67657 --- /dev/null +++ b/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/src/test/java/com/vaadin/flow/component/applayout/AppLayoutSignalTest.java @@ -0,0 +1,115 @@ +/* + * 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.applayout; + +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.signals.BindingActiveException; +import com.vaadin.flow.signals.local.ValueSignal; +import com.vaadin.tests.AbstractSignalsUnitTest; + +public class AppLayoutSignalTest extends AbstractSignalsUnitTest { + + private AppLayout appLayout; + private ValueSignal signal; + + @Before + public void setup() { + appLayout = new AppLayout(); + signal = new ValueSignal<>(true); + } + + @After + public void tearDown() { + if (appLayout != null && appLayout.isAttached()) { + appLayout.removeFromParent(); + } + } + + @Test + public void bindDrawerOpened_signalBound_propertySync() { + appLayout.bindDrawerOpened(signal, signal::set); + UI.getCurrent().add(appLayout); + + Assert.assertTrue(appLayout.isDrawerOpened()); + + signal.set(false); + Assert.assertFalse(appLayout.isDrawerOpened()); + + signal.set(true); + Assert.assertTrue(appLayout.isDrawerOpened()); + } + + @Test + public void bindDrawerOpened_notAttached_noEffect() { + appLayout.bindDrawerOpened(signal, signal::set); + + boolean initial = appLayout.getElement().getProperty("drawerOpened", + true); + signal.set(false); + Assert.assertEquals(initial, + appLayout.getElement().getProperty("drawerOpened", true)); + } + + @Test + public void bindDrawerOpened_detachAndReattach() { + appLayout.bindDrawerOpened(signal, signal::set); + UI.getCurrent().add(appLayout); + + signal.set(false); + Assert.assertFalse(appLayout.isDrawerOpened()); + + appLayout.removeFromParent(); + signal.set(true); + Assert.assertFalse(appLayout.isDrawerOpened()); + + UI.getCurrent().add(appLayout); + Assert.assertTrue(appLayout.isDrawerOpened()); + } + + @Test(expected = BindingActiveException.class) + public void bindDrawerOpened_setWhileBound_throws() { + appLayout.bindDrawerOpened(signal, signal::set); + UI.getCurrent().add(appLayout); + + appLayout.setDrawerOpened(false); + } + + @Test(expected = BindingActiveException.class) + public void bindDrawerOpened_doubleBind_throws() { + appLayout.bindDrawerOpened(signal, signal::set); + var other = new ValueSignal<>(false); + appLayout.bindDrawerOpened(other, other::set); + } + + @Test(expected = NullPointerException.class) + public void bindDrawerOpened_nullSignal_throwsNPE() { + appLayout.bindDrawerOpened(null, null); + } + + @Test + public void bindDrawerOpened_nullDefault_defaultsToTrue() { + ValueSignal nullSignal = new ValueSignal<>(null); + appLayout.bindDrawerOpened(nullSignal, nullSignal::set); + UI.getCurrent().add(appLayout); + + Assert.assertTrue(appLayout.isDrawerOpened()); + } +} From 1315c39da1ff52dadd30a9ea949fa9468666822b Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Thu, 19 Feb 2026 17:05:28 +0000 Subject: [PATCH 2/2] fix: update bindDrawerOpened javadoc to document writeCallback parameter --- .../vaadin/flow/component/applayout/AppLayout.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/src/main/java/com/vaadin/flow/component/applayout/AppLayout.java b/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/src/main/java/com/vaadin/flow/component/applayout/AppLayout.java index cc5b12e12f6..2e1db3b0edc 100644 --- a/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/src/main/java/com/vaadin/flow/component/applayout/AppLayout.java +++ b/vaadin-app-layout-flow-parent/vaadin-app-layout-flow/src/main/java/com/vaadin/flow/component/applayout/AppLayout.java @@ -148,15 +148,18 @@ public void setDrawerOpened(boolean drawerOpened) { } /** - * Binds the drawer opened state to the given writable signal. The binding - * is two-way: signal changes push to the DOM property, and client-side - * property changes push back to the signal. + * Binds the drawer opened state to the given signal. The binding is + * two-way: signal changes push to the DOM property, and client-side + * property changes invoke the write callback. *

* While a signal is bound, any attempt to set the drawer opened state * manually throws {@link com.vaadin.flow.signals.BindingActiveException}. * * @param signal - * the writable signal to bind, not {@code null} + * the signal to bind, not {@code null} + * @param writeCallback + * the callback to propagate value changes back, or {@code null} + * for one-way binding * @since 25.1 */ public void bindDrawerOpened(Signal signal,