Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add allowInert support for synchronized properties (#21119) (CP: 24.6) #21131

Merged
merged 1 commit into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public void setContent(Component content) {
*
* @return whether details are expanded or collapsed
*/
@Synchronize(property = "open", value = "toggle")
@Synchronize(property = "open", value = "toggle", allowInert = true)
public boolean isOpen() {
return getElement().getProperty("open", false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.vaadin.flow.component.internal.ComponentMetaData;
import com.vaadin.flow.component.internal.ComponentTracker;
import com.vaadin.flow.component.template.Id;
import com.vaadin.flow.dom.DomListenerRegistration;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.dom.ElementUtil;
import com.vaadin.flow.dom.PropertyChangeListener;
Expand Down Expand Up @@ -178,9 +179,13 @@ private void addSynchronizedProperty(
throw new IllegalArgumentException(getClass().getName()
+ ": event type must not be null for @Synchronize annotation");
}
element.addPropertyChangeListener(info.getProperty(), eventType,
NOOP_PROPERTY_LISTENER)
DomListenerRegistration propertyListener = element
.addPropertyChangeListener(info.getProperty(), eventType,
NOOP_PROPERTY_LISTENER)
.setDisabledUpdateMode(info.getUpdateMode());
if (info.getAllowInert()) {
propertyListener.allowInert();
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,13 @@
* @return the property update mode for disabled element
*/
DisabledUpdateMode allowUpdates() default DisabledUpdateMode.ONLY_WHEN_ENABLED;

/**
* Makes this property able to synchronize even when the related node is
* inert.
*
* @return {@code true} to allow inert synchronization, {@code false} to
* disallow. Defaults to {@code false}.
*/
boolean allowInert() default false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,15 @@ public boolean isEmpty() {
public static class SynchronizedPropertyInfo {
private final String property;
private final DisabledUpdateMode mode;
private final boolean allowInert;
private final String[] eventNames;

SynchronizedPropertyInfo(String property, String[] eventNames,
DisabledUpdateMode mode) {
DisabledUpdateMode mode, boolean allowInert) {
this.property = property;
this.eventNames = eventNames;
this.mode = mode;
this.allowInert = allowInert;
}

public String getProperty() {
Expand All @@ -110,6 +112,10 @@ public Stream<String> getEventNames() {
public DisabledUpdateMode getUpdateMode() {
return mode;
}

public boolean getAllowInert() {
return allowInert;
}
}

private final Collection<SynchronizedPropertyInfo> synchronizedProperties;
Expand Down Expand Up @@ -262,8 +268,10 @@ private static void doCollectSynchronizedProperties(Class<?> clazz,
}

String[] eventNames = annotation.value();
infos.put(method.getName(), new SynchronizedPropertyInfo(
propertyName, eventNames, annotation.allowUpdates()));
infos.put(method.getName(),
new SynchronizedPropertyInfo(propertyName, eventNames,
annotation.allowUpdates(),
annotation.allowInert()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -512,4 +512,25 @@ public DisabledUpdateMode getPropertySynchronizationMode(
.reduce(DisabledUpdateMode::mostPermissive).orElse(null);
}

/**
* Returns {@code true} if any listener for the given property has
* allowInert enabled. Note that this means that enabling allowInert for any
* listener for a certain property will effectively allow it for all
* listeners for said property.
*
* @param propertyName
* the property name to check, not <code>null</code>
* @return {@code true} if allowInert is enabled for any listener for the
* given property, {@code false otherwise}
*/
public boolean hasAllowInertForProperty(String propertyName) {
assert propertyName != null;

if (listeners == null) {
return false;
}
return listeners.values().stream().flatMap(List::stream)
.filter(wrapper -> wrapper.isPropertySynchronized(propertyName))
.anyMatch(wrapper -> wrapper.allowInert);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,15 @@ public Optional<Runnable> handle(UI ui, JsonObject invocationJson) {
if (node.isInactive()) {
logHandlingIgnoredMessage(node, "inactive (disabled or invisible)");
return Optional.empty();
} else if (!allowInert(ui, invocationJson) && node.isInert()) {
logHandlingIgnoredMessage(node, "inert");
return Optional.empty();
} else if (node.isInert()) {
if (allowInert(ui, invocationJson)) {
// Allow handling of RPC request if any listener for the event
// type or the synchronized property have enabled allowInert.
return handleNode(node, invocationJson);
} else {
logHandlingIgnoredMessage(node, "inert");
return Optional.empty();
}
} else {
return handleNode(node, invocationJson);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.dom.DisabledUpdateMode;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.internal.JsonCodec;
Expand Down Expand Up @@ -123,6 +124,21 @@ protected Optional<Runnable> handleNode(StateNode node,
return Optional.empty();
}

@Override
protected boolean allowInert(UI ui, JsonObject invocationJson) {
StateNode node = ui.getInternals().getStateTree()
.getNodeById(getNodeId(invocationJson));
if (node != null && node.hasFeature(ElementListenerMap.class)) {
ElementListenerMap listenerMap = node
.getFeature(ElementListenerMap.class);
return invocationJson.hasKey(JsonConstants.RPC_PROPERTY)
&& listenerMap.hasAllowInertForProperty(invocationJson
.getString(JsonConstants.RPC_PROPERTY));
} else {
return super.allowInert(ui, invocationJson);
}
}

private Optional<Runnable> enqueuePropertyUpdate(StateNode node,
JsonObject invocationJson, Class<? extends NodeFeature> feature,
String property) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2000-2025 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.uitest.ui;

import com.vaadin.flow.component.ClickEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.NativeButton;
import com.vaadin.flow.component.html.NativeDetails;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.router.Route;

@Route(value = "com.vaadin.flow.uitest.ui.AllowInertSynchronizedPropertyView")
public class AllowInertSynchronizedPropertyView extends AbstractDivView {

public static final String OPEN_MODAL_BUTTON = "modal-dialog-button";
public static final String READ_NATIVE_DETAILS_STATE_BUTTON = "read-native-details-state-button";
public static final String NATIVE_DETAILS_STATE = "native-details-state";
public static final String NATIVE_DETAILS_SUMMARY = "native-details-summary";

private NativeDetails nativeDetails;
private Span state;

@Override
protected void onShow() {
add(createOpenDialogButton(OPEN_MODAL_BUTTON));

nativeDetails = new NativeDetails();
add(nativeDetails);

Span summary = new Span("Native details summary");
summary.setId(NATIVE_DETAILS_SUMMARY);
nativeDetails.setSummary(summary);

state = new Span("unknown");
state.setId(NATIVE_DETAILS_STATE);
add(state);
}

private Component createOpenDialogButton(String id) {
final NativeButton button = createButton("Open modal dialog",
event -> new Dialog().open());
button.setId(id);
return button;
}

private NativeButton createButton(String caption,
ComponentEventListener<ClickEvent<NativeButton>> listener) {
final NativeButton button = new NativeButton();
button.setText(caption);
button.addClickListener(listener);
button.getStyle().set("border", "1px solid black");
button.setWidth("100px");
return button;
}

public class Dialog extends Div {

public Dialog() {
final NativeButton readNativeDetailsStateButton = new NativeButton(
"Read Native Details State", event -> {
if (nativeDetails.isOpen()) {
state.setText("opened");
} else {
state.setText("closed");
}
});
readNativeDetailsStateButton
.setId(READ_NATIVE_DETAILS_STATE_BUTTON);

add(new Text("A modal dialog"), readNativeDetailsStateButton);

getStyle().set("position", "fixed").set("inset", "50% 50%")
.set("border", "1px solid black");
}

public void open() {
final UI ui = UI.getCurrent();
ui.addModal(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.vaadin.flow.uitest.ui;

import org.junit.Assert;
import org.junit.Test;
import org.openqa.selenium.By;

import com.vaadin.flow.component.html.testbench.NativeButtonElement;
import com.vaadin.flow.component.html.testbench.NativeDetailsElement;
import com.vaadin.flow.component.html.testbench.SpanElement;
import com.vaadin.flow.testutil.ChromeBrowserTest;
import com.vaadin.testbench.TestBenchElement;

public class AllowInertSynchronizedPropertyIT extends ChromeBrowserTest {

private TestBenchElement modalDialogButton;

@Override
protected void open(String... parameters) {
super.open(parameters);
modalDialogButton = $(NativeButtonElement.class)
.id(ModalDialogView.OPEN_MODAL_BUTTON);
}

@Test
public void modalDialogOpened_toggleNativeDetailsVisibility_allowInertSynchronizedPropertyShouldChange() {
open();

modalDialogButton.click();

$(NativeButtonElement.class).id(
AllowInertSynchronizedPropertyView.READ_NATIVE_DETAILS_STATE_BUTTON)
.click();

Assert.assertEquals("closed", getStateText());

$(NativeDetailsElement.class).first().findElement(By
.id(AllowInertSynchronizedPropertyView.NATIVE_DETAILS_SUMMARY))
.click();
$(NativeButtonElement.class).id(
AllowInertSynchronizedPropertyView.READ_NATIVE_DETAILS_STATE_BUTTON)
.click();

Assert.assertEquals("opened", getStateText());
}

private String getStateText() {
return $(SpanElement.class)
.id(AllowInertSynchronizedPropertyView.NATIVE_DETAILS_STATE)
.getText();
}
}