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 a919f424276..c86353f6b63 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,28 @@ public void setDrawerOpened(boolean drawerOpened) {
getElement().setProperty("drawerOpened", drawerOpened);
}
+ /**
+ * 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 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,
+ 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());
+ }
+}