Skip to content

Commit c7ae20b

Browse files
authored
Merge branch 'main' into edler-san-patch-2
2 parents dd4556b + dc9b402 commit c7ae20b

File tree

30 files changed

+3540
-54
lines changed

30 files changed

+3540
-54
lines changed

flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeDetails.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public void setContent(Component content) {
193193
*
194194
* @return whether details are expanded or collapsed
195195
*/
196-
@Synchronize(property = "open", value = "toggle")
196+
@Synchronize(property = "open", value = "toggle", allowInert = true)
197197
public boolean isOpen() {
198198
return getElement().getProperty("open", false);
199199
}

flow-server/src/main/java/com/vaadin/flow/component/Component.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.vaadin.flow.component.internal.ComponentMetaData;
3232
import com.vaadin.flow.component.internal.ComponentTracker;
3333
import com.vaadin.flow.component.template.Id;
34+
import com.vaadin.flow.dom.DomListenerRegistration;
3435
import com.vaadin.flow.dom.Element;
3536
import com.vaadin.flow.dom.ElementUtil;
3637
import com.vaadin.flow.dom.PropertyChangeListener;
@@ -178,9 +179,13 @@ private void addSynchronizedProperty(
178179
throw new IllegalArgumentException(getClass().getName()
179180
+ ": event type must not be null for @Synchronize annotation");
180181
}
181-
element.addPropertyChangeListener(info.getProperty(), eventType,
182-
NOOP_PROPERTY_LISTENER)
182+
DomListenerRegistration propertyListener = element
183+
.addPropertyChangeListener(info.getProperty(), eventType,
184+
NOOP_PROPERTY_LISTENER)
183185
.setDisabledUpdateMode(info.getUpdateMode());
186+
if (info.getAllowInert()) {
187+
propertyListener.allowInert();
188+
}
184189
});
185190
}
186191

flow-server/src/main/java/com/vaadin/flow/component/Synchronize.java

+9
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,13 @@
7171
* @return the property update mode for disabled element
7272
*/
7373
DisabledUpdateMode allowUpdates() default DisabledUpdateMode.ONLY_WHEN_ENABLED;
74+
75+
/**
76+
* Makes this property able to synchronize even when the related node is
77+
* inert.
78+
*
79+
* @return {@code true} to allow inert synchronization, {@code false} to
80+
* disallow. Defaults to {@code false}.
81+
*/
82+
boolean allowInert() default false;
7483
}

flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentMetaData.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,15 @@ public boolean isEmpty() {
9090
public static class SynchronizedPropertyInfo {
9191
private final String property;
9292
private final DisabledUpdateMode mode;
93+
private final boolean allowInert;
9394
private final String[] eventNames;
9495

9596
SynchronizedPropertyInfo(String property, String[] eventNames,
96-
DisabledUpdateMode mode) {
97+
DisabledUpdateMode mode, boolean allowInert) {
9798
this.property = property;
9899
this.eventNames = eventNames;
99100
this.mode = mode;
101+
this.allowInert = allowInert;
100102
}
101103

102104
public String getProperty() {
@@ -110,6 +112,10 @@ public Stream<String> getEventNames() {
110112
public DisabledUpdateMode getUpdateMode() {
111113
return mode;
112114
}
115+
116+
public boolean getAllowInert() {
117+
return allowInert;
118+
}
113119
}
114120

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

264270
String[] eventNames = annotation.value();
265-
infos.put(method.getName(), new SynchronizedPropertyInfo(
266-
propertyName, eventNames, annotation.allowUpdates()));
271+
infos.put(method.getName(),
272+
new SynchronizedPropertyInfo(propertyName, eventNames,
273+
annotation.allowUpdates(),
274+
annotation.allowInert()));
267275
}
268276
}
269277
}

flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/ElementListenerMap.java

+21
Original file line numberDiff line numberDiff line change
@@ -512,4 +512,25 @@ public DisabledUpdateMode getPropertySynchronizationMode(
512512
.reduce(DisabledUpdateMode::mostPermissive).orElse(null);
513513
}
514514

515+
/**
516+
* Returns {@code true} if any listener for the given property has
517+
* allowInert enabled. Note that this means that enabling allowInert for any
518+
* listener for a certain property will effectively allow it for all
519+
* listeners for said property.
520+
*
521+
* @param propertyName
522+
* the property name to check, not <code>null</code>
523+
* @return {@code true} if allowInert is enabled for any listener for the
524+
* given property, {@code false otherwise}
525+
*/
526+
public boolean hasAllowInertForProperty(String propertyName) {
527+
assert propertyName != null;
528+
529+
if (listeners == null) {
530+
return false;
531+
}
532+
return listeners.values().stream().flatMap(List::stream)
533+
.filter(wrapper -> wrapper.isPropertySynchronized(propertyName))
534+
.anyMatch(wrapper -> wrapper.allowInert);
535+
}
515536
}

flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java

+31
Original file line numberDiff line numberDiff line change
@@ -1628,6 +1628,12 @@ protected static Collection<Element> getStylesheetTags(
16281628
* Gives a links for referencing the custom theme stylesheet files
16291629
* (typically styles.css or document.css), which are served in express build
16301630
* mode by static file server directly from frontend/themes folder.
1631+
* <p>
1632+
* </p>
1633+
* This method does not verify that the style sheet exists, so it may end up
1634+
* at runtime with broken links. Use
1635+
* {@link #getStylesheetLinks(VaadinContext, String, File)} if you want only
1636+
* links for existing files to be returned.
16311637
*
16321638
* @param context
16331639
* the vaadin context
@@ -1637,7 +1643,32 @@ protected static Collection<Element> getStylesheetTags(
16371643
*/
16381644
protected static Collection<String> getStylesheetLinks(
16391645
VaadinContext context, String fileName) {
1646+
return getStylesheetLinks(context, fileName, null);
1647+
}
1648+
1649+
/**
1650+
* Gives a links for referencing the custom theme stylesheet files
1651+
* (typically styles.css or document.css), which are served in express build
1652+
* mode by static file server directly from frontend/themes folder.
1653+
* <p>
1654+
* </p>
1655+
* This method return links only for existing style sheet files.
1656+
*
1657+
* @param context
1658+
* the vaadin context
1659+
* @param fileName
1660+
* the stylesheet file name to add a reference to
1661+
* @param frontendDirectory
1662+
* the directory where project's frontend files are located.
1663+
*
1664+
* @return the collection of links to be added to the page
1665+
*/
1666+
protected static Collection<String> getStylesheetLinks(
1667+
VaadinContext context, String fileName, File frontendDirectory) {
16401668
return ThemeUtils.getActiveThemes(context).stream()
1669+
.filter(theme -> frontendDirectory == null
1670+
|| ThemeUtils.getThemeFolder(frontendDirectory, theme)
1671+
.toPath().resolve(fileName).toFile().exists())
16411672
.map(theme -> ThemeUtils.getThemeFilePath(theme, fileName))
16421673
.toList();
16431674
}

flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentBootstrapHandler.java

+8-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.vaadin.flow.server.communication;
1717

1818
import java.io.BufferedWriter;
19+
import java.io.File;
1920
import java.io.IOException;
2021
import java.io.OutputStreamWriter;
2122
import java.io.Writer;
@@ -429,11 +430,16 @@ protected void writeBootstrapPage(String contentType,
429430
.forEach(element -> ElementUtil.fromJsoup(element)
430431
.ifPresent(elementsForShadows::add));
431432

433+
File frontendDirectory = FrontendUtils
434+
.getProjectFrontendDir(config);
435+
432436
// Add document.css link to the document
433-
BootstrapHandler.getStylesheetLinks(context, "document.css")
437+
BootstrapHandler
438+
.getStylesheetLinks(context, "document.css",
439+
frontendDirectory)
434440
.forEach(link -> UI.getCurrent().getPage().executeJs(
435441
BootstrapHandler.SCRIPT_TEMPLATE_FOR_STYLESHEET_LINK_TAG,
436-
link));
442+
modifyPath(serviceUrl, link)));
437443
}
438444

439445
WebComponentConfigurationRegistry

flow-server/src/main/java/com/vaadin/flow/server/communication/rpc/AbstractRpcInvocationHandler.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,15 @@ public Optional<Runnable> handle(UI ui, JsonObject invocationJson) {
6666
if (node.isInactive()) {
6767
logHandlingIgnoredMessage(node, "inactive (disabled or invisible)");
6868
return Optional.empty();
69-
} else if (!allowInert(ui, invocationJson) && node.isInert()) {
70-
logHandlingIgnoredMessage(node, "inert");
71-
return Optional.empty();
69+
} else if (node.isInert()) {
70+
if (allowInert(ui, invocationJson)) {
71+
// Allow handling of RPC request if any listener for the event
72+
// type or the synchronized property have enabled allowInert.
73+
return handleNode(node, invocationJson);
74+
} else {
75+
logHandlingIgnoredMessage(node, "inert");
76+
return Optional.empty();
77+
}
7278
} else {
7379
return handleNode(node, invocationJson);
7480
}

flow-server/src/main/java/com/vaadin/flow/server/communication/rpc/MapSyncRpcHandler.java

+16
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.slf4j.LoggerFactory;
2525

2626
import com.vaadin.flow.component.Component;
27+
import com.vaadin.flow.component.UI;
2728
import com.vaadin.flow.dom.DisabledUpdateMode;
2829
import com.vaadin.flow.dom.Element;
2930
import com.vaadin.flow.internal.JsonCodec;
@@ -121,6 +122,21 @@ protected Optional<Runnable> handleNode(StateNode node,
121122
return Optional.empty();
122123
}
123124

125+
@Override
126+
protected boolean allowInert(UI ui, JsonObject invocationJson) {
127+
StateNode node = ui.getInternals().getStateTree()
128+
.getNodeById(getNodeId(invocationJson));
129+
if (node != null && node.hasFeature(ElementListenerMap.class)) {
130+
ElementListenerMap listenerMap = node
131+
.getFeature(ElementListenerMap.class);
132+
return invocationJson.hasKey(JsonConstants.RPC_PROPERTY)
133+
&& listenerMap.hasAllowInertForProperty(invocationJson
134+
.getString(JsonConstants.RPC_PROPERTY));
135+
} else {
136+
return super.allowInert(ui, invocationJson);
137+
}
138+
}
139+
124140
private Optional<Runnable> enqueuePropertyUpdate(StateNode node,
125141
JsonObject invocationJson, String property) {
126142
Serializable value = JsonCodec.decodeWithoutTypeInfo(

flow-server/src/main/resources/com/vaadin/flow/server/frontend/dependencies/vite/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"license": "Apache-2.0",
1010
"dependencies": {},
1111
"devDependencies": {
12-
"vite": "6.2.0",
12+
"vite": "6.2.2",
1313
"@vitejs/plugin-react": "4.3.4",
1414
"@preact/signals-react-transform": "0.5.1",
1515
"@rollup/plugin-replace": "6.0.2",

flow-tests/test-express-build/test-embedding-express-build/src/main/frontend/themes/embedded-theme/theme.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"parent": "parent-theme",
23
"documentCss": ["@fortawesome/fontawesome-free/css/all.css"],
34
"assets": {
45
"@fortawesome/fontawesome-free": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/*
2+
Parent theme added only to ensure that exported web components do not add
3+
style sheet link to the document when the theme does not provide a document.css
4+
file
5+
*/

flow-tests/test-express-build/test-embedding-express-build/src/test/java/com/vaadin/flow/webcomponent/ApplicationThemeComponentIT.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.vaadin.flow.webcomponent;
1717

18+
import java.net.URI;
1819
import java.util.List;
1920

2021
import org.junit.Assert;
@@ -222,8 +223,12 @@ public void documentCssLinkAddedToHead() {
222223
final List<WebElement> links = documentHead
223224
.findElements(By.tagName("link"));
224225
Assert.assertEquals(1, links.size());
225-
Assert.assertTrue(links.get(0).getAttribute("href")
226+
String documentCssURL = links.get(0).getAttribute("href");
227+
Assert.assertTrue(documentCssURL
226228
.contains("VAADIN/themes/embedded-theme/document.css"));
229+
URI documentCssURI = URI.create(documentCssURL);
230+
Assert.assertTrue("document.css URL should be absolute, but was "
231+
+ documentCssURL, documentCssURI.isAbsolute());
227232
}
228233

229234
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.vaadin.flow.uitest.ui;
18+
19+
import com.vaadin.flow.component.ClickEvent;
20+
import com.vaadin.flow.component.Component;
21+
import com.vaadin.flow.component.ComponentEventListener;
22+
import com.vaadin.flow.component.Text;
23+
import com.vaadin.flow.component.UI;
24+
import com.vaadin.flow.component.html.Div;
25+
import com.vaadin.flow.component.html.NativeButton;
26+
import com.vaadin.flow.component.html.NativeDetails;
27+
import com.vaadin.flow.component.html.Span;
28+
import com.vaadin.flow.router.Route;
29+
30+
@Route(value = "com.vaadin.flow.uitest.ui.AllowInertSynchronizedPropertyView")
31+
public class AllowInertSynchronizedPropertyView extends AbstractDivView {
32+
33+
public static final String OPEN_MODAL_BUTTON = "modal-dialog-button";
34+
public static final String READ_NATIVE_DETAILS_STATE_BUTTON = "read-native-details-state-button";
35+
public static final String NATIVE_DETAILS_STATE = "native-details-state";
36+
public static final String NATIVE_DETAILS_SUMMARY = "native-details-summary";
37+
38+
private NativeDetails nativeDetails;
39+
private Span state;
40+
41+
@Override
42+
protected void onShow() {
43+
add(createOpenDialogButton(OPEN_MODAL_BUTTON));
44+
45+
nativeDetails = new NativeDetails();
46+
add(nativeDetails);
47+
48+
Span summary = new Span("Native details summary");
49+
summary.setId(NATIVE_DETAILS_SUMMARY);
50+
nativeDetails.setSummary(summary);
51+
52+
state = new Span("unknown");
53+
state.setId(NATIVE_DETAILS_STATE);
54+
add(state);
55+
}
56+
57+
private Component createOpenDialogButton(String id) {
58+
final NativeButton button = createButton("Open modal dialog",
59+
event -> new Dialog().open());
60+
button.setId(id);
61+
return button;
62+
}
63+
64+
private NativeButton createButton(String caption,
65+
ComponentEventListener<ClickEvent<NativeButton>> listener) {
66+
final NativeButton button = new NativeButton();
67+
button.setText(caption);
68+
button.addClickListener(listener);
69+
button.getStyle().set("border", "1px solid black");
70+
button.setWidth("100px");
71+
return button;
72+
}
73+
74+
public class Dialog extends Div {
75+
76+
public Dialog() {
77+
final NativeButton readNativeDetailsStateButton = new NativeButton(
78+
"Read Native Details State", event -> {
79+
if (nativeDetails.isOpen()) {
80+
state.setText("opened");
81+
} else {
82+
state.setText("closed");
83+
}
84+
});
85+
readNativeDetailsStateButton
86+
.setId(READ_NATIVE_DETAILS_STATE_BUTTON);
87+
88+
add(new Text("A modal dialog"), readNativeDetailsStateButton);
89+
90+
getStyle().set("position", "fixed").set("inset", "50% 50%")
91+
.set("border", "1px solid black");
92+
}
93+
94+
public void open() {
95+
final UI ui = UI.getCurrent();
96+
ui.addModal(this);
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)