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 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()); + } +}