From 501f5d9424a80cb8deae28b1e95e7ec888dd9a21 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Sat, 21 Feb 2026 13:59:13 +0000 Subject: [PATCH 1/5] feat: Add Component.sizeSignal() for tracking component size via ResizeObserver Adds a lazily-initialized, read-only signal on Component that tracks the element's size using the browser's ResizeObserver API. A per-UI ComponentSizeObserver manages a single shared ResizeObserver instance and dispatches size updates to individual component signals. --- .../com/vaadin/flow/component/Component.java | 67 ++++++++++ .../java/com/vaadin/flow/component/UI.java | 1 + .../internal/ComponentSizeObserver.java | 121 ++++++++++++++++++ .../frontend/componentSizeObserver.js | 37 ++++++ .../component/ComponentSizeSignalTest.java | 107 ++++++++++++++++ 5 files changed, 333 insertions(+) create mode 100644 flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentSizeObserver.java create mode 100644 flow-server/src/main/resources/META-INF/frontend/componentSizeObserver.js create mode 100644 flow-server/src/test/java/com/vaadin/flow/component/ComponentSizeSignalTest.java diff --git a/flow-server/src/main/java/com/vaadin/flow/component/Component.java b/flow-server/src/main/java/com/vaadin/flow/component/Component.java index 7a98f1e82e9..afd89144a50 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/Component.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/Component.java @@ -28,6 +28,7 @@ import java.util.stream.Stream; import com.vaadin.flow.component.internal.ComponentMetaData; +import com.vaadin.flow.component.internal.ComponentSizeObserver; import com.vaadin.flow.component.internal.ComponentTracker; import com.vaadin.flow.component.template.Id; import com.vaadin.flow.dom.DomListenerRegistration; @@ -45,6 +46,7 @@ import com.vaadin.flow.shared.Registration; import com.vaadin.flow.signals.BindingActiveException; import com.vaadin.flow.signals.Signal; +import com.vaadin.flow.signals.local.ValueSignal; /** * A Component is a higher level abstraction of an {@link Element} or a @@ -62,6 +64,18 @@ public abstract class Component implements HasStyle, AttachNotifier, DetachNotifier { + /** + * Represents the size of a component as observed by the browser's + * {@code ResizeObserver} API. + * + * @param width + * the component width in pixels + * @param height + * the component height in pixels + */ + public record Size(int width, int height) implements Serializable { + } + /** * Encapsulates data required for mapping a new component instance to an * existing element. @@ -927,6 +941,59 @@ public T findAncestor(Class componentType) { return null; } + /** + * Returns a signal that tracks the current size of this component as + * observed by the browser's {@code ResizeObserver} API. + *

+ * The signal is lazily initialized on first access. It automatically starts + * observing when the component is attached and stops when detached. The + * initial value is {@code Size(0, 0)} until the browser reports the actual + * size. + *

+ * The returned signal is read-only. + * + * @return a read-only signal with the current component size + */ + @SuppressWarnings("unchecked") + public Signal sizeSignal() { + ValueSignal signal = (ValueSignal) ComponentUtil + .getData(this, Size.class.getName()); + if (signal == null) { + signal = new ValueSignal<>(new Size(0, 0)); + ComponentUtil.setData(this, Size.class.getName(), signal); + setupSizeObservation(signal); + } + return signal.asReadonly(); + } + + private void setupSizeObservation(ValueSignal signal) { + addAttachListener(attach -> { + UI ui = attach.getUI(); + ComponentSizeObserver.get(ui).observe(getElement(), signal); + + addDetachListener(detach -> { + if (!detach.getUI().isClosing()) { + ComponentSizeObserver.get(detach.getUI()).unobserve(signal); + } + detach.unregisterListener(); + }); + }); + + if (isAttached()) { + getUI().ifPresent(ui -> { + ComponentSizeObserver.get(ui).observe(getElement(), signal); + + addDetachListener(detach -> { + if (!detach.getUI().isClosing()) { + ComponentSizeObserver.get(detach.getUI()) + .unobserve(signal); + } + detach.unregisterListener(); + }); + }); + } + } + /** * Removes the component from its parent. */ diff --git a/flow-server/src/main/java/com/vaadin/flow/component/UI.java b/flow-server/src/main/java/com/vaadin/flow/component/UI.java index e0b1bfe81f2..9a60080977f 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/UI.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/UI.java @@ -109,6 +109,7 @@ * @since 1.0 */ @JsModule("@vaadin/common-frontend/ConnectionIndicator.js") +@JsModule("./componentSizeObserver.js") public class UI extends Component implements PollNotifier, HasComponents, RouterLayout { diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentSizeObserver.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentSizeObserver.java new file mode 100644 index 00000000000..688f032f772 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentSizeObserver.java @@ -0,0 +1,121 @@ +/* + * 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.internal; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import tools.jackson.databind.node.ObjectNode; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.dom.Element; +import com.vaadin.flow.signals.local.ValueSignal; + +/** + * Per-UI shared ResizeObserver manager that tracks component sizes using a + * single browser {@code ResizeObserver} instance. + *

+ * One instance is created per UI, lazily via {@link #get(UI)} when the first + * component's size is observed. A single browser {@code ResizeObserver} is used + * to track all observed elements, dispatching a custom + * {@code "vaadin-component-resize"} event on the UI element with aggregated + * size data. + *

+ * For internal use only. May be renamed or removed in a future release. + */ +public class ComponentSizeObserver implements Serializable { + + private static final String EVENT_NAME = "vaadin-component-resize"; + + private final Element uiElement; + private final Map> idToSignal = new HashMap<>(); + private final Map, Integer> signalToId = new HashMap<>(); + private int nextId = 0; + + /** + * Returns the ComponentSizeObserver for the given UI, creating it lazily. + * + * @param ui + * the UI to get the observer for + * @return the observer instance + */ + public static ComponentSizeObserver get(UI ui) { + ComponentSizeObserver observer = ComponentUtil.getData(ui, + ComponentSizeObserver.class); + if (observer == null) { + observer = new ComponentSizeObserver(ui); + ComponentUtil.setData(ui, ComponentSizeObserver.class, observer); + } + return observer; + } + + private ComponentSizeObserver(UI ui) { + this.uiElement = ui.getElement(); + + uiElement.executeJs( + "window.Vaadin.Flow.componentSizeObserver.init(this)"); + + uiElement.addEventListener(EVENT_NAME, event -> { + ObjectNode sizes = (ObjectNode) event.getEventData() + .get("event.sizes"); + for (String idStr : sizes.propertyNames()) { + int id = Integer.parseInt(idStr); + ValueSignal signal = idToSignal.get(id); + if (signal != null) { + ObjectNode size = (ObjectNode) sizes.get(idStr); + int w = size.get("w").intValue(); + int h = size.get("h").intValue(); + signal.set(new Component.Size(w, h)); + } + } + }).addEventData("event.sizes").debounce(100).allowInert(); + } + + /** + * Starts observing the given element and updates the signal with size + * changes. + * + * @param element + * the element to observe + * @param signal + * the signal to update + */ + public void observe(Element element, ValueSignal signal) { + int id = nextId++; + idToSignal.put(id, signal); + signalToId.put(signal, id); + + uiElement.executeJs( + "window.Vaadin.Flow.componentSizeObserver.observe(this, $0, $1)", + element, id); + } + + /** + * Stops observing the component associated with the given signal. + * + * @param signal + * the signal whose component should stop being observed + */ + public void unobserve(ValueSignal signal) { + Integer id = signalToId.remove(signal); + if (id != null) { + idToSignal.remove(id); + } + } +} diff --git a/flow-server/src/main/resources/META-INF/frontend/componentSizeObserver.js b/flow-server/src/main/resources/META-INF/frontend/componentSizeObserver.js new file mode 100644 index 00000000000..7e4528a8be1 --- /dev/null +++ b/flow-server/src/main/resources/META-INF/frontend/componentSizeObserver.js @@ -0,0 +1,37 @@ +window.Vaadin = window.Vaadin || {}; +window.Vaadin.Flow = window.Vaadin.Flow || {}; +window.Vaadin.Flow.componentSizeObserver = { + /** + * Creates a shared ResizeObserver on the given UI element. + * Size changes are dispatched as "vaadin-component-resize" + * custom events on the UI element. + */ + init: function (uiElement) { + uiElement._componentSizeObserver = new ResizeObserver(function (entries) { + var sizes = {}; + for (var i = 0; i < entries.length; i++) { + var entry = entries[i]; + if (entry.target.isConnected && entry.contentBoxSize) { + var id = entry.target._componentSizeId; + sizes[id] = { + w: Math.round(entry.contentRect.width), + h: Math.round(entry.contentRect.height) + }; + } + } + if (Object.keys(sizes).length > 0) { + var event = new Event('vaadin-component-resize'); + event.sizes = sizes; + uiElement.dispatchEvent(event); + } + }); + }, + + /** + * Starts observing the given element with the given numeric ID. + */ + observe: function (uiElement, element, id) { + element._componentSizeId = id; + uiElement._componentSizeObserver.observe(element); + } +}; diff --git a/flow-server/src/test/java/com/vaadin/flow/component/ComponentSizeSignalTest.java b/flow-server/src/test/java/com/vaadin/flow/component/ComponentSizeSignalTest.java new file mode 100644 index 00000000000..c8037b5b9af --- /dev/null +++ b/flow-server/src/test/java/com/vaadin/flow/component/ComponentSizeSignalTest.java @@ -0,0 +1,107 @@ +/* + * 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; + +import org.junit.jupiter.api.Test; +import tools.jackson.databind.node.ObjectNode; + +import com.vaadin.flow.dom.DomEvent; +import com.vaadin.flow.internal.JacksonUtils; +import com.vaadin.flow.internal.nodefeature.ElementListenerMap; +import com.vaadin.flow.shared.JsonConstants; +import com.vaadin.flow.signals.Signal; +import com.vaadin.flow.signals.local.ValueSignal; +import com.vaadin.tests.util.MockUI; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class ComponentSizeSignalTest { + + @Tag(Tag.DIV) + private static class TestDiv extends Component { + } + + @Test + void sizeSignal_isReadOnly() { + MockUI ui = new MockUI(); + TestDiv div = new TestDiv(); + ui.add(div); + + Signal signal = div.sizeSignal(); + assertFalse(signal instanceof ValueSignal, + "sizeSignal() should return a read-only signal"); + } + + @Test + void sizeSignal_defaultValue_isZeroByZero() { + MockUI ui = new MockUI(); + TestDiv div = new TestDiv(); + ui.add(div); + + Signal signal = div.sizeSignal(); + assertEquals(new Component.Size(0, 0), signal.get()); + } + + @Test + void sizeSignal_returnsSameInstance() { + MockUI ui = new MockUI(); + TestDiv div = new TestDiv(); + ui.add(div); + + Signal first = div.sizeSignal(); + Signal second = div.sizeSignal(); + // Both wrappers read from the same underlying ValueSignal + fireComponentResizeEvent(ui, 0, 640, 480); + assertEquals(new Component.Size(640, 480), first.get()); + assertEquals(new Component.Size(640, 480), second.get()); + } + + @Test + void sizeSignal_updatesOnResizeEvent() { + MockUI ui = new MockUI(); + TestDiv div = new TestDiv(); + ui.add(div); + + Signal signal = div.sizeSignal(); + assertEquals(new Component.Size(0, 0), signal.get()); + + fireComponentResizeEvent(ui, 0, 800, 600); + assertEquals(new Component.Size(800, 600), signal.get()); + + fireComponentResizeEvent(ui, 0, 1024, 768); + assertEquals(new Component.Size(1024, 768), signal.get()); + } + + private void fireComponentResizeEvent(MockUI ui, int componentId, int width, + int height) { + ObjectNode eventData = JacksonUtils.createObjectNode(); + + ObjectNode sizes = JacksonUtils.createObjectNode(); + ObjectNode sizeEntry = JacksonUtils.createObjectNode(); + sizeEntry.put("w", width); + sizeEntry.put("h", height); + sizes.set(String.valueOf(componentId), sizeEntry); + + eventData.set("event.sizes", sizes); + eventData.put(JsonConstants.EVENT_DATA_PHASE, + JsonConstants.EVENT_PHASE_TRAILING); + + ui.getElement().getNode().getFeature(ElementListenerMap.class) + .fireEvent(new DomEvent(ui.getElement(), + "vaadin-component-resize", eventData)); + } +} From 0c2006acd36c9b74992a651053d10ac500294e94 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Thu, 7 May 2026 11:04:58 +0000 Subject: [PATCH 2/5] refactor: load componentSizeObserver via flow-client bundle Move the componentSizeObserver script from a flow-server META-INF resource (loaded via @JsModule on UI) to a TypeScript file in flow-client/src/main/frontend, imported from Flow.ts. This matches the pattern already used by Geolocation and PageVisibility, so the script is bundled with the Flow client and no per-class @JsModule annotation is needed. --- .../main/frontend/ComponentSizeObserver.ts | 71 +++++++++++++++++++ flow-client/src/main/frontend/Flow.ts | 1 + .../java/com/vaadin/flow/component/UI.java | 1 - .../frontend/componentSizeObserver.js | 37 ---------- 4 files changed, 72 insertions(+), 38 deletions(-) create mode 100644 flow-client/src/main/frontend/ComponentSizeObserver.ts delete mode 100644 flow-server/src/main/resources/META-INF/frontend/componentSizeObserver.js diff --git a/flow-client/src/main/frontend/ComponentSizeObserver.ts b/flow-client/src/main/frontend/ComponentSizeObserver.ts new file mode 100644 index 00000000000..339cc580b55 --- /dev/null +++ b/flow-client/src/main/frontend/ComponentSizeObserver.ts @@ -0,0 +1,71 @@ +/* + * 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. + */ + +interface VaadinComponentSize { + w: number; + h: number; +} + +interface UiElementWithObserver extends HTMLElement { + _componentSizeObserver?: ResizeObserver; +} + +interface ObservedElement extends Element { + _componentSizeId?: number; +} + +const $wnd = window as any; +$wnd.Vaadin ??= {}; +$wnd.Vaadin.Flow ??= {}; +$wnd.Vaadin.Flow.componentSizeObserver = { + /** + * Creates a shared ResizeObserver on the given UI element. Size changes + * are dispatched as "vaadin-component-resize" custom events on the UI + * element, with a `sizes` property mapping component IDs to their new + * width and height. + */ + init(uiElement: UiElementWithObserver): void { + uiElement._componentSizeObserver = new ResizeObserver((entries) => { + const sizes: Record = {}; + for (const entry of entries) { + const target = entry.target as ObservedElement; + if (target.isConnected && entry.contentBoxSize && target._componentSizeId !== undefined) { + sizes[target._componentSizeId] = { + w: Math.round(entry.contentRect.width), + h: Math.round(entry.contentRect.height) + }; + } + } + if (Object.keys(sizes).length > 0) { + const event = new Event('vaadin-component-resize') as Event & { sizes: typeof sizes }; + event.sizes = sizes; + uiElement.dispatchEvent(event); + } + }); + }, + + /** + * Starts observing the given element with the given numeric ID. + */ + observe(uiElement: UiElementWithObserver, element: ObservedElement, id: number): void { + element._componentSizeId = id; + uiElement._componentSizeObserver?.observe(element); + } +}; + +// Empty export to ensure TypeScript emits this as an ES module, +// which is required for Vite to load it via import. +export {}; diff --git a/flow-client/src/main/frontend/Flow.ts b/flow-client/src/main/frontend/Flow.ts index 61cb614b28a..c5d9a601d83 100644 --- a/flow-client/src/main/frontend/Flow.ts +++ b/flow-client/src/main/frontend/Flow.ts @@ -4,6 +4,7 @@ import { type ConnectionStateChangeListener, type ConnectionStateStore } from '@vaadin/common-frontend'; +import './ComponentSizeObserver'; import './Geolocation'; import { currentVisibility } from './PageVisibility'; diff --git a/flow-server/src/main/java/com/vaadin/flow/component/UI.java b/flow-server/src/main/java/com/vaadin/flow/component/UI.java index 28c8a772767..e458f9744d1 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/UI.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/UI.java @@ -111,7 +111,6 @@ * @since 1.0 */ @JsModule("@vaadin/common-frontend/ConnectionIndicator.js") -@JsModule("./componentSizeObserver.js") public class UI extends Component implements PollNotifier, HasComponents, RouterLayout { diff --git a/flow-server/src/main/resources/META-INF/frontend/componentSizeObserver.js b/flow-server/src/main/resources/META-INF/frontend/componentSizeObserver.js deleted file mode 100644 index 7e4528a8be1..00000000000 --- a/flow-server/src/main/resources/META-INF/frontend/componentSizeObserver.js +++ /dev/null @@ -1,37 +0,0 @@ -window.Vaadin = window.Vaadin || {}; -window.Vaadin.Flow = window.Vaadin.Flow || {}; -window.Vaadin.Flow.componentSizeObserver = { - /** - * Creates a shared ResizeObserver on the given UI element. - * Size changes are dispatched as "vaadin-component-resize" - * custom events on the UI element. - */ - init: function (uiElement) { - uiElement._componentSizeObserver = new ResizeObserver(function (entries) { - var sizes = {}; - for (var i = 0; i < entries.length; i++) { - var entry = entries[i]; - if (entry.target.isConnected && entry.contentBoxSize) { - var id = entry.target._componentSizeId; - sizes[id] = { - w: Math.round(entry.contentRect.width), - h: Math.round(entry.contentRect.height) - }; - } - } - if (Object.keys(sizes).length > 0) { - var event = new Event('vaadin-component-resize'); - event.sizes = sizes; - uiElement.dispatchEvent(event); - } - }); - }, - - /** - * Starts observing the given element with the given numeric ID. - */ - observe: function (uiElement, element, id) { - element._componentSizeId = id; - uiElement._componentSizeObserver.observe(element); - } -}; From 2465811b4b3dadd2891fef2661cf290c3c8d5c31 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Thu, 7 May 2026 11:25:31 +0000 Subject: [PATCH 3/5] refactor: move sizeSignal() from Component to Element Per PR review feedback, move the low-level resize-observer API from Component to Element so that the prominent Component API is not polluted with this functionality and a future high-level responsive-layout API can be added on top of it without overlap. The signal is now stored in a small SizeSignalFeature node feature so subsequent calls on the same element return the same signal instance. ComponentSizeObserver is renamed to ElementSizeObserver and updated to deal in Element.Size. The JS side (window.Vaadin.Flow.componentSizeObserver and the vaadin-component-resize event) is unchanged. --- .../com/vaadin/flow/component/Component.java | 67 ----------------- ...Observer.java => ElementSizeObserver.java} | 39 +++++----- .../java/com/vaadin/flow/dom/Element.java | 61 +++++++++++++++ .../dom/impl/BasicElementStateProvider.java | 5 +- .../nodefeature/NodeFeatureRegistry.java | 2 + .../internal/nodefeature/NodeFeatures.java | 5 ++ .../nodefeature/SizeSignalFeature.java | 75 +++++++++++++++++++ .../vaadin/flow/dom/ElementJacksonTest.java | 3 + .../ElementSizeSignalTest.java} | 39 +++++----- .../java/com/vaadin/flow/dom/ElementTest.java | 3 + .../internal/nodefeature/NodeFeatureTest.java | 3 +- 11 files changed, 193 insertions(+), 109 deletions(-) rename flow-server/src/main/java/com/vaadin/flow/component/internal/{ComponentSizeObserver.java => ElementSizeObserver.java} (69%) create mode 100644 flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/SizeSignalFeature.java rename flow-server/src/test/java/com/vaadin/flow/{component/ComponentSizeSignalTest.java => dom/ElementSizeSignalTest.java} (69%) diff --git a/flow-server/src/main/java/com/vaadin/flow/component/Component.java b/flow-server/src/main/java/com/vaadin/flow/component/Component.java index afd89144a50..7a98f1e82e9 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/Component.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/Component.java @@ -28,7 +28,6 @@ import java.util.stream.Stream; import com.vaadin.flow.component.internal.ComponentMetaData; -import com.vaadin.flow.component.internal.ComponentSizeObserver; import com.vaadin.flow.component.internal.ComponentTracker; import com.vaadin.flow.component.template.Id; import com.vaadin.flow.dom.DomListenerRegistration; @@ -46,7 +45,6 @@ import com.vaadin.flow.shared.Registration; import com.vaadin.flow.signals.BindingActiveException; import com.vaadin.flow.signals.Signal; -import com.vaadin.flow.signals.local.ValueSignal; /** * A Component is a higher level abstraction of an {@link Element} or a @@ -64,18 +62,6 @@ public abstract class Component implements HasStyle, AttachNotifier, DetachNotifier { - /** - * Represents the size of a component as observed by the browser's - * {@code ResizeObserver} API. - * - * @param width - * the component width in pixels - * @param height - * the component height in pixels - */ - public record Size(int width, int height) implements Serializable { - } - /** * Encapsulates data required for mapping a new component instance to an * existing element. @@ -941,59 +927,6 @@ public T findAncestor(Class componentType) { return null; } - /** - * Returns a signal that tracks the current size of this component as - * observed by the browser's {@code ResizeObserver} API. - *

- * The signal is lazily initialized on first access. It automatically starts - * observing when the component is attached and stops when detached. The - * initial value is {@code Size(0, 0)} until the browser reports the actual - * size. - *

- * The returned signal is read-only. - * - * @return a read-only signal with the current component size - */ - @SuppressWarnings("unchecked") - public Signal sizeSignal() { - ValueSignal signal = (ValueSignal) ComponentUtil - .getData(this, Size.class.getName()); - if (signal == null) { - signal = new ValueSignal<>(new Size(0, 0)); - ComponentUtil.setData(this, Size.class.getName(), signal); - setupSizeObservation(signal); - } - return signal.asReadonly(); - } - - private void setupSizeObservation(ValueSignal signal) { - addAttachListener(attach -> { - UI ui = attach.getUI(); - ComponentSizeObserver.get(ui).observe(getElement(), signal); - - addDetachListener(detach -> { - if (!detach.getUI().isClosing()) { - ComponentSizeObserver.get(detach.getUI()).unobserve(signal); - } - detach.unregisterListener(); - }); - }); - - if (isAttached()) { - getUI().ifPresent(ui -> { - ComponentSizeObserver.get(ui).observe(getElement(), signal); - - addDetachListener(detach -> { - if (!detach.getUI().isClosing()) { - ComponentSizeObserver.get(detach.getUI()) - .unobserve(signal); - } - detach.unregisterListener(); - }); - }); - } - } - /** * Removes the component from its parent. */ diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentSizeObserver.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/ElementSizeObserver.java similarity index 69% rename from flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentSizeObserver.java rename to flow-server/src/main/java/com/vaadin/flow/component/internal/ElementSizeObserver.java index 688f032f772..8e354343963 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentSizeObserver.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/ElementSizeObserver.java @@ -21,51 +21,50 @@ import tools.jackson.databind.node.ObjectNode; -import com.vaadin.flow.component.Component; import com.vaadin.flow.component.ComponentUtil; import com.vaadin.flow.component.UI; import com.vaadin.flow.dom.Element; import com.vaadin.flow.signals.local.ValueSignal; /** - * Per-UI shared ResizeObserver manager that tracks component sizes using a - * single browser {@code ResizeObserver} instance. + * Per-UI shared ResizeObserver manager that tracks element sizes using a single + * browser {@code ResizeObserver} instance. *

* One instance is created per UI, lazily via {@link #get(UI)} when the first - * component's size is observed. A single browser {@code ResizeObserver} is used + * element's size is observed. A single browser {@code ResizeObserver} is used * to track all observed elements, dispatching a custom * {@code "vaadin-component-resize"} event on the UI element with aggregated * size data. *

* For internal use only. May be renamed or removed in a future release. */ -public class ComponentSizeObserver implements Serializable { +public class ElementSizeObserver implements Serializable { private static final String EVENT_NAME = "vaadin-component-resize"; private final Element uiElement; - private final Map> idToSignal = new HashMap<>(); - private final Map, Integer> signalToId = new HashMap<>(); + private final Map> idToSignal = new HashMap<>(); + private final Map, Integer> signalToId = new HashMap<>(); private int nextId = 0; /** - * Returns the ComponentSizeObserver for the given UI, creating it lazily. + * Returns the ElementSizeObserver for the given UI, creating it lazily. * * @param ui * the UI to get the observer for * @return the observer instance */ - public static ComponentSizeObserver get(UI ui) { - ComponentSizeObserver observer = ComponentUtil.getData(ui, - ComponentSizeObserver.class); + public static ElementSizeObserver get(UI ui) { + ElementSizeObserver observer = ComponentUtil.getData(ui, + ElementSizeObserver.class); if (observer == null) { - observer = new ComponentSizeObserver(ui); - ComponentUtil.setData(ui, ComponentSizeObserver.class, observer); + observer = new ElementSizeObserver(ui); + ComponentUtil.setData(ui, ElementSizeObserver.class, observer); } return observer; } - private ComponentSizeObserver(UI ui) { + private ElementSizeObserver(UI ui) { this.uiElement = ui.getElement(); uiElement.executeJs( @@ -76,12 +75,12 @@ private ComponentSizeObserver(UI ui) { .get("event.sizes"); for (String idStr : sizes.propertyNames()) { int id = Integer.parseInt(idStr); - ValueSignal signal = idToSignal.get(id); + ValueSignal signal = idToSignal.get(id); if (signal != null) { ObjectNode size = (ObjectNode) sizes.get(idStr); int w = size.get("w").intValue(); int h = size.get("h").intValue(); - signal.set(new Component.Size(w, h)); + signal.set(new Element.Size(w, h)); } } }).addEventData("event.sizes").debounce(100).allowInert(); @@ -96,7 +95,7 @@ private ComponentSizeObserver(UI ui) { * @param signal * the signal to update */ - public void observe(Element element, ValueSignal signal) { + public void observe(Element element, ValueSignal signal) { int id = nextId++; idToSignal.put(id, signal); signalToId.put(signal, id); @@ -107,12 +106,12 @@ public void observe(Element element, ValueSignal signal) { } /** - * Stops observing the component associated with the given signal. + * Stops observing the element associated with the given signal. * * @param signal - * the signal whose component should stop being observed + * the signal whose element should stop being observed */ - public void unobserve(ValueSignal signal) { + public void unobserve(ValueSignal signal) { Integer id = signalToId.remove(signal); if (id != null) { idToSignal.remove(id); diff --git a/flow-server/src/main/java/com/vaadin/flow/dom/Element.java b/flow-server/src/main/java/com/vaadin/flow/dom/Element.java index e993d166495..81c02f54b55 100644 --- a/flow-server/src/main/java/com/vaadin/flow/dom/Element.java +++ b/flow-server/src/main/java/com/vaadin/flow/dom/Element.java @@ -42,6 +42,8 @@ import com.vaadin.flow.component.ComponentUtil; import com.vaadin.flow.component.ScrollIntoViewOption; import com.vaadin.flow.component.ScrollOptions; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.internal.ElementSizeObserver; import com.vaadin.flow.component.internal.PendingJavaScriptInvocation; import com.vaadin.flow.component.internal.UIInternals.JavaScriptInvocation; import com.vaadin.flow.component.page.Page; @@ -54,7 +56,9 @@ import com.vaadin.flow.internal.JacksonUtils; import com.vaadin.flow.internal.JavaScriptSemantics; import com.vaadin.flow.internal.StateNode; +import com.vaadin.flow.internal.StateTree; import com.vaadin.flow.internal.nodefeature.SignalBindingFeature; +import com.vaadin.flow.internal.nodefeature.SizeSignalFeature; import com.vaadin.flow.internal.nodefeature.VirtualChildrenList; import com.vaadin.flow.server.AbstractStreamResource; import com.vaadin.flow.server.Command; @@ -64,6 +68,7 @@ import com.vaadin.flow.shared.Registration; import com.vaadin.flow.signals.BindingActiveException; import com.vaadin.flow.signals.Signal; +import com.vaadin.flow.signals.local.ValueSignal; /** * Represents an element in the DOM. @@ -75,6 +80,19 @@ * @since 1.0 */ public class Element extends Node { + + /** + * Represents the size of an element as observed by the browser's + * {@code ResizeObserver} API. + * + * @param width + * the element width in pixels + * @param height + * the element height in pixels + */ + public record Size(int width, int height) implements Serializable { + } + private static final String EVENT_TYPE_MUST_NOT_BE_NULL = "Event type must not be null"; static final String ATTRIBUTE_NAME_CANNOT_BE_NULL = "The attribute name cannot be null"; @@ -1651,6 +1669,49 @@ public void execute() { }); } + /** + * Returns a signal that tracks the current size of this element as observed + * by the browser's {@code ResizeObserver} API. + *

+ * The signal is lazily initialized on first access. It automatically starts + * observing when the element is attached and stops when detached. The + * initial value is {@code Size(0, 0)} until the browser reports the actual + * size. + *

+ * The returned signal is read-only. + * + * @return a read-only signal with the current element size + */ + public Signal sizeSignal() { + SizeSignalFeature feature = getNode() + .getFeature(SizeSignalFeature.class); + ValueSignal signal = feature.getOrCreateSignal(); + if (!feature.isObserverRegistered()) { + feature.markObserverRegistered(); + registerSizeObservation(signal); + } + return signal.asReadonly(); + } + + private void registerSizeObservation(ValueSignal signal) { + ElementAttachListener attachListener = attach -> { + UI ui = ((StateTree) getNode().getOwner()).getUI(); + ElementSizeObserver.get(ui).observe(this, signal); + + Registration[] detachReg = new Registration[1]; + detachReg[0] = addDetachListener(detach -> { + if (!ui.isClosing()) { + ElementSizeObserver.get(ui).unobserve(signal); + } + detachReg[0].remove(); + }); + }; + addAttachListener(attachListener); + if (getNode().isAttached()) { + attachListener.onAttach(new ElementAttachEvent(this)); + } + } + @Override public String toString() { return getOuterHTML(); diff --git a/flow-server/src/main/java/com/vaadin/flow/dom/impl/BasicElementStateProvider.java b/flow-server/src/main/java/com/vaadin/flow/dom/impl/BasicElementStateProvider.java index bd16e79968d..4c2892dbce7 100644 --- a/flow-server/src/main/java/com/vaadin/flow/dom/impl/BasicElementStateProvider.java +++ b/flow-server/src/main/java/com/vaadin/flow/dom/impl/BasicElementStateProvider.java @@ -56,6 +56,7 @@ import com.vaadin.flow.internal.nodefeature.ReturnChannelMap; import com.vaadin.flow.internal.nodefeature.ShadowRootData; import com.vaadin.flow.internal.nodefeature.SignalBindingFeature; +import com.vaadin.flow.internal.nodefeature.SizeSignalFeature; import com.vaadin.flow.internal.nodefeature.VirtualChildrenList; import com.vaadin.flow.server.AbstractStreamResource; import com.vaadin.flow.shared.Registration; @@ -90,8 +91,8 @@ public class BasicElementStateProvider extends AbstractNodeStateProvider { PolymerServerEventHandlers.class, ClientCallableHandlers.class, PolymerEventListenerMap.class, ShadowRootData.class, AttachExistingElementFeature.class, VirtualChildrenList.class, - ReturnChannelMap.class, InertData.class, - SignalBindingFeature.class }; + ReturnChannelMap.class, InertData.class, SignalBindingFeature.class, + SizeSignalFeature.class }; private BasicElementStateProvider() { // Not meant to be sub classed and only once instance should ever exist diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeFeatureRegistry.java b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeFeatureRegistry.java index a0cf2e794fe..0e6c0bd53cb 100644 --- a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeFeatureRegistry.java +++ b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeFeatureRegistry.java @@ -74,6 +74,8 @@ private NodeFeatureData( /* Signal binding features */ registerFeature(SignalBindingFeature.class, SignalBindingFeature::new, NodeFeatures.SIGNAL_BINDING); + registerFeature(SizeSignalFeature.class, SizeSignalFeature::new, + NodeFeatures.SIZE_SIGNAL); /* Common element features */ registerFeature(ElementChildrenList.class, ElementChildrenList::new, diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeFeatures.java b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeFeatures.java index 1378bb57e09..b181e71acd9 100644 --- a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeFeatures.java +++ b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeFeatures.java @@ -147,6 +147,11 @@ public final class NodeFeatures { */ public static final int SIGNAL_BINDING = 27; + /** + * Id for {@link SizeSignalFeature}. + */ + public static final int SIZE_SIGNAL = 28; + private NodeFeatures() { // Only static } diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/SizeSignalFeature.java b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/SizeSignalFeature.java new file mode 100644 index 00000000000..d8cfb9160fa --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/SizeSignalFeature.java @@ -0,0 +1,75 @@ +/* + * 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.internal.nodefeature; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import com.vaadin.flow.dom.Element; +import com.vaadin.flow.internal.StateNode; +import com.vaadin.flow.signals.local.ValueSignal; + +/** + * Holds the lazily-allocated {@link ValueSignal} that backs + * {@link Element#sizeSignal()} and tracks whether the per-UI resize observer + * has been wired up for the element. Used to keep the same signal instance + * across multiple {@code sizeSignal()} calls on a given element, and to ensure + * the attach/detach observer registration happens only once. + *

+ * For internal use only. May be renamed or removed in a future release. + */ +@NullMarked +public class SizeSignalFeature extends ServerSideFeature { + + private @Nullable ValueSignal signal; + private boolean observerRegistered; + + /** + * Creates a SizeSignalFeature for the given node. + * + * @param node + * the node which supports the feature + */ + public SizeSignalFeature(StateNode node) { + super(node); + } + + /** + * Returns the underlying value signal, allocating it on first access with a + * default value of {@code Size(0, 0)}. + */ + public ValueSignal getOrCreateSignal() { + if (signal == null) { + signal = new ValueSignal<>(new Element.Size(0, 0)); + } + return signal; + } + + /** + * Returns whether the observer registration listeners have already been + * installed for this element. + */ + public boolean isObserverRegistered() { + return observerRegistered; + } + + /** + * Marks the observer registration listeners as installed. + */ + public void markObserverRegistered() { + this.observerRegistered = true; + } +} diff --git a/flow-server/src/test/java/com/vaadin/flow/dom/ElementJacksonTest.java b/flow-server/src/test/java/com/vaadin/flow/dom/ElementJacksonTest.java index 5dcd7829ffd..00cdd988961 100644 --- a/flow-server/src/test/java/com/vaadin/flow/dom/ElementJacksonTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/dom/ElementJacksonTest.java @@ -178,6 +178,9 @@ void publicElementMethodsShouldReturnElement() { ignore.add("bindText"); ignore.add("bindVisible"); + // returns a read-only Signal + ignore.add("sizeSignal"); + // returns void ignore.add("flashClass"); diff --git a/flow-server/src/test/java/com/vaadin/flow/component/ComponentSizeSignalTest.java b/flow-server/src/test/java/com/vaadin/flow/dom/ElementSizeSignalTest.java similarity index 69% rename from flow-server/src/test/java/com/vaadin/flow/component/ComponentSizeSignalTest.java rename to flow-server/src/test/java/com/vaadin/flow/dom/ElementSizeSignalTest.java index c8037b5b9af..2812c39f47b 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/ComponentSizeSignalTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/dom/ElementSizeSignalTest.java @@ -13,12 +13,13 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.flow.component; +package com.vaadin.flow.dom; import org.junit.jupiter.api.Test; import tools.jackson.databind.node.ObjectNode; -import com.vaadin.flow.dom.DomEvent; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; import com.vaadin.flow.internal.JacksonUtils; import com.vaadin.flow.internal.nodefeature.ElementListenerMap; import com.vaadin.flow.shared.JsonConstants; @@ -29,7 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -class ComponentSizeSignalTest { +class ElementSizeSignalTest { @Tag(Tag.DIV) private static class TestDiv extends Component { @@ -41,7 +42,7 @@ void sizeSignal_isReadOnly() { TestDiv div = new TestDiv(); ui.add(div); - Signal signal = div.sizeSignal(); + Signal signal = div.getElement().sizeSignal(); assertFalse(signal instanceof ValueSignal, "sizeSignal() should return a read-only signal"); } @@ -52,8 +53,8 @@ void sizeSignal_defaultValue_isZeroByZero() { TestDiv div = new TestDiv(); ui.add(div); - Signal signal = div.sizeSignal(); - assertEquals(new Component.Size(0, 0), signal.get()); + Signal signal = div.getElement().sizeSignal(); + assertEquals(new Element.Size(0, 0), signal.peek()); } @Test @@ -62,12 +63,12 @@ void sizeSignal_returnsSameInstance() { TestDiv div = new TestDiv(); ui.add(div); - Signal first = div.sizeSignal(); - Signal second = div.sizeSignal(); + Signal first = div.getElement().sizeSignal(); + Signal second = div.getElement().sizeSignal(); // Both wrappers read from the same underlying ValueSignal - fireComponentResizeEvent(ui, 0, 640, 480); - assertEquals(new Component.Size(640, 480), first.get()); - assertEquals(new Component.Size(640, 480), second.get()); + fireElementResizeEvent(ui, 0, 640, 480); + assertEquals(new Element.Size(640, 480), first.peek()); + assertEquals(new Element.Size(640, 480), second.peek()); } @Test @@ -76,17 +77,17 @@ void sizeSignal_updatesOnResizeEvent() { TestDiv div = new TestDiv(); ui.add(div); - Signal signal = div.sizeSignal(); - assertEquals(new Component.Size(0, 0), signal.get()); + Signal signal = div.getElement().sizeSignal(); + assertEquals(new Element.Size(0, 0), signal.peek()); - fireComponentResizeEvent(ui, 0, 800, 600); - assertEquals(new Component.Size(800, 600), signal.get()); + fireElementResizeEvent(ui, 0, 800, 600); + assertEquals(new Element.Size(800, 600), signal.peek()); - fireComponentResizeEvent(ui, 0, 1024, 768); - assertEquals(new Component.Size(1024, 768), signal.get()); + fireElementResizeEvent(ui, 0, 1024, 768); + assertEquals(new Element.Size(1024, 768), signal.peek()); } - private void fireComponentResizeEvent(MockUI ui, int componentId, int width, + private void fireElementResizeEvent(MockUI ui, int elementId, int width, int height) { ObjectNode eventData = JacksonUtils.createObjectNode(); @@ -94,7 +95,7 @@ private void fireComponentResizeEvent(MockUI ui, int componentId, int width, ObjectNode sizeEntry = JacksonUtils.createObjectNode(); sizeEntry.put("w", width); sizeEntry.put("h", height); - sizes.set(String.valueOf(componentId), sizeEntry); + sizes.set(String.valueOf(elementId), sizeEntry); eventData.set("event.sizes", sizes); eventData.put(JsonConstants.EVENT_DATA_PHASE, diff --git a/flow-server/src/test/java/com/vaadin/flow/dom/ElementTest.java b/flow-server/src/test/java/com/vaadin/flow/dom/ElementTest.java index 3964ae29e07..62a81e36dc4 100644 --- a/flow-server/src/test/java/com/vaadin/flow/dom/ElementTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/dom/ElementTest.java @@ -180,6 +180,9 @@ void publicElementMethodsShouldReturnElement() { ignore.add("bindText"); ignore.add("bindVisible"); + // returns a read-only Signal + ignore.add("sizeSignal"); + // returns void ignore.add("flashClass"); diff --git a/flow-server/src/test/java/com/vaadin/flow/internal/nodefeature/NodeFeatureTest.java b/flow-server/src/test/java/com/vaadin/flow/internal/nodefeature/NodeFeatureTest.java index c46bd6b04dd..dcffe0c3fc6 100644 --- a/flow-server/src/test/java/com/vaadin/flow/internal/nodefeature/NodeFeatureTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/internal/nodefeature/NodeFeatureTest.java @@ -106,6 +106,7 @@ private static Map, Integer> buildExpectedIdMap() { expectedIds.put(InertData.class, NodeFeatures.INERT_DATA); expectedIds.put(SignalBindingFeature.class, NodeFeatures.SIGNAL_BINDING); + expectedIds.put(SizeSignalFeature.class, NodeFeatures.SIZE_SIGNAL); return expectedIds; } @@ -148,7 +149,7 @@ void priorityOrder() { BasicTypeValue.class, /* Signal binding feature */ - SignalBindingFeature.class, + SignalBindingFeature.class, SizeSignalFeature.class, /* Common element features */ ElementChildrenList.class, ElementPropertyMap.class, From 045ef851785a6fdfc8b4121043b4fb0c6c1a75e8 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Thu, 7 May 2026 11:31:50 +0000 Subject: [PATCH 4/5] refactor: extract Size to a reusable top-level record Move the Size record out of Element as nested type and into com.vaadin.flow.component.Size so the same record can back any future size-related signal (window, viewport, content, ...). Page.windowSizeSignal still returns WindowSize since that record is part of the released 25.1 API. --- .../java/com/vaadin/flow/component/Size.java | 29 +++++++++++++++++++ .../internal/ElementSizeObserver.java | 13 +++++---- .../java/com/vaadin/flow/dom/Element.java | 14 +-------- .../nodefeature/SizeSignalFeature.java | 7 +++-- .../flow/dom/ElementSizeSignalTest.java | 23 ++++++++------- 5 files changed, 53 insertions(+), 33 deletions(-) create mode 100644 flow-server/src/main/java/com/vaadin/flow/component/Size.java diff --git a/flow-server/src/main/java/com/vaadin/flow/component/Size.java b/flow-server/src/main/java/com/vaadin/flow/component/Size.java new file mode 100644 index 00000000000..10f87bc41f0 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/component/Size.java @@ -0,0 +1,29 @@ +/* + * 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; + +import java.io.Serializable; + +/** + * Represents a 2D pixel size with a width and a height. + * + * @param width + * the width in pixels + * @param height + * the height in pixels + */ +public record Size(int width, int height) implements Serializable { +} diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/ElementSizeObserver.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/ElementSizeObserver.java index 8e354343963..bc1c8fc77dc 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/internal/ElementSizeObserver.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/ElementSizeObserver.java @@ -22,6 +22,7 @@ import tools.jackson.databind.node.ObjectNode; import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.component.Size; import com.vaadin.flow.component.UI; import com.vaadin.flow.dom.Element; import com.vaadin.flow.signals.local.ValueSignal; @@ -43,8 +44,8 @@ public class ElementSizeObserver implements Serializable { private static final String EVENT_NAME = "vaadin-component-resize"; private final Element uiElement; - private final Map> idToSignal = new HashMap<>(); - private final Map, Integer> signalToId = new HashMap<>(); + private final Map> idToSignal = new HashMap<>(); + private final Map, Integer> signalToId = new HashMap<>(); private int nextId = 0; /** @@ -75,12 +76,12 @@ private ElementSizeObserver(UI ui) { .get("event.sizes"); for (String idStr : sizes.propertyNames()) { int id = Integer.parseInt(idStr); - ValueSignal signal = idToSignal.get(id); + ValueSignal signal = idToSignal.get(id); if (signal != null) { ObjectNode size = (ObjectNode) sizes.get(idStr); int w = size.get("w").intValue(); int h = size.get("h").intValue(); - signal.set(new Element.Size(w, h)); + signal.set(new Size(w, h)); } } }).addEventData("event.sizes").debounce(100).allowInert(); @@ -95,7 +96,7 @@ private ElementSizeObserver(UI ui) { * @param signal * the signal to update */ - public void observe(Element element, ValueSignal signal) { + public void observe(Element element, ValueSignal signal) { int id = nextId++; idToSignal.put(id, signal); signalToId.put(signal, id); @@ -111,7 +112,7 @@ public void observe(Element element, ValueSignal signal) { * @param signal * the signal whose element should stop being observed */ - public void unobserve(ValueSignal signal) { + public void unobserve(ValueSignal signal) { Integer id = signalToId.remove(signal); if (id != null) { idToSignal.remove(id); diff --git a/flow-server/src/main/java/com/vaadin/flow/dom/Element.java b/flow-server/src/main/java/com/vaadin/flow/dom/Element.java index 81c02f54b55..0c568ee7bfb 100644 --- a/flow-server/src/main/java/com/vaadin/flow/dom/Element.java +++ b/flow-server/src/main/java/com/vaadin/flow/dom/Element.java @@ -42,6 +42,7 @@ import com.vaadin.flow.component.ComponentUtil; import com.vaadin.flow.component.ScrollIntoViewOption; import com.vaadin.flow.component.ScrollOptions; +import com.vaadin.flow.component.Size; import com.vaadin.flow.component.UI; import com.vaadin.flow.component.internal.ElementSizeObserver; import com.vaadin.flow.component.internal.PendingJavaScriptInvocation; @@ -80,19 +81,6 @@ * @since 1.0 */ public class Element extends Node { - - /** - * Represents the size of an element as observed by the browser's - * {@code ResizeObserver} API. - * - * @param width - * the element width in pixels - * @param height - * the element height in pixels - */ - public record Size(int width, int height) implements Serializable { - } - private static final String EVENT_TYPE_MUST_NOT_BE_NULL = "Event type must not be null"; static final String ATTRIBUTE_NAME_CANNOT_BE_NULL = "The attribute name cannot be null"; diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/SizeSignalFeature.java b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/SizeSignalFeature.java index d8cfb9160fa..7214d43caae 100644 --- a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/SizeSignalFeature.java +++ b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/SizeSignalFeature.java @@ -18,6 +18,7 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import com.vaadin.flow.component.Size; import com.vaadin.flow.dom.Element; import com.vaadin.flow.internal.StateNode; import com.vaadin.flow.signals.local.ValueSignal; @@ -34,7 +35,7 @@ @NullMarked public class SizeSignalFeature extends ServerSideFeature { - private @Nullable ValueSignal signal; + private @Nullable ValueSignal signal; private boolean observerRegistered; /** @@ -51,9 +52,9 @@ public SizeSignalFeature(StateNode node) { * Returns the underlying value signal, allocating it on first access with a * default value of {@code Size(0, 0)}. */ - public ValueSignal getOrCreateSignal() { + public ValueSignal getOrCreateSignal() { if (signal == null) { - signal = new ValueSignal<>(new Element.Size(0, 0)); + signal = new ValueSignal<>(new Size(0, 0)); } return signal; } diff --git a/flow-server/src/test/java/com/vaadin/flow/dom/ElementSizeSignalTest.java b/flow-server/src/test/java/com/vaadin/flow/dom/ElementSizeSignalTest.java index 2812c39f47b..55e8b1b3f8a 100644 --- a/flow-server/src/test/java/com/vaadin/flow/dom/ElementSizeSignalTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/dom/ElementSizeSignalTest.java @@ -19,6 +19,7 @@ import tools.jackson.databind.node.ObjectNode; import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Size; import com.vaadin.flow.component.Tag; import com.vaadin.flow.internal.JacksonUtils; import com.vaadin.flow.internal.nodefeature.ElementListenerMap; @@ -42,7 +43,7 @@ void sizeSignal_isReadOnly() { TestDiv div = new TestDiv(); ui.add(div); - Signal signal = div.getElement().sizeSignal(); + Signal signal = div.getElement().sizeSignal(); assertFalse(signal instanceof ValueSignal, "sizeSignal() should return a read-only signal"); } @@ -53,8 +54,8 @@ void sizeSignal_defaultValue_isZeroByZero() { TestDiv div = new TestDiv(); ui.add(div); - Signal signal = div.getElement().sizeSignal(); - assertEquals(new Element.Size(0, 0), signal.peek()); + Signal signal = div.getElement().sizeSignal(); + assertEquals(new Size(0, 0), signal.peek()); } @Test @@ -63,12 +64,12 @@ void sizeSignal_returnsSameInstance() { TestDiv div = new TestDiv(); ui.add(div); - Signal first = div.getElement().sizeSignal(); - Signal second = div.getElement().sizeSignal(); + Signal first = div.getElement().sizeSignal(); + Signal second = div.getElement().sizeSignal(); // Both wrappers read from the same underlying ValueSignal fireElementResizeEvent(ui, 0, 640, 480); - assertEquals(new Element.Size(640, 480), first.peek()); - assertEquals(new Element.Size(640, 480), second.peek()); + assertEquals(new Size(640, 480), first.peek()); + assertEquals(new Size(640, 480), second.peek()); } @Test @@ -77,14 +78,14 @@ void sizeSignal_updatesOnResizeEvent() { TestDiv div = new TestDiv(); ui.add(div); - Signal signal = div.getElement().sizeSignal(); - assertEquals(new Element.Size(0, 0), signal.peek()); + Signal signal = div.getElement().sizeSignal(); + assertEquals(new Size(0, 0), signal.peek()); fireElementResizeEvent(ui, 0, 800, 600); - assertEquals(new Element.Size(800, 600), signal.peek()); + assertEquals(new Size(800, 600), signal.peek()); fireElementResizeEvent(ui, 0, 1024, 768); - assertEquals(new Element.Size(1024, 768), signal.peek()); + assertEquals(new Size(1024, 768), signal.peek()); } private void fireElementResizeEvent(MockUI ui, int elementId, int width, From 9e9eeefc8945a8a8b687a649f456b1f5c91a3504 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Thu, 7 May 2026 11:40:38 +0000 Subject: [PATCH 5/5] refactor: rename JS-side identifiers to elementSizeObserver Align the internal JS-side names with the Element-based public API: TS file ComponentSizeObserver -> ElementSizeObserver, global window.Vaadin.Flow.componentSizeObserver -> window.Vaadin.Flow.elementSizeObserver, DOM event vaadin-component-resize -> vaadin-element-resize, and the private element fields _componentSizeId / _componentSizeObserver -> _elementSizeId / _elementSizeObserver. All of these are internal, so no released API is affected. --- DESIGN_GUIDELINES.md | 2 +- ...SizeObserver.ts => ElementSizeObserver.ts} | 26 +++++++++---------- flow-client/src/main/frontend/Flow.ts | 2 +- .../internal/ElementSizeObserver.java | 12 ++++----- .../flow/dom/ElementSizeSignalTest.java | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) rename flow-client/src/main/frontend/{ComponentSizeObserver.ts => ElementSizeObserver.ts} (70%) diff --git a/DESIGN_GUIDELINES.md b/DESIGN_GUIDELINES.md index fd8d3f3bd13..f160ca28287 100644 --- a/DESIGN_GUIDELINES.md +++ b/DESIGN_GUIDELINES.md @@ -170,7 +170,7 @@ anything else: - **Global state and helper functions live under `window.Vaadin.Flow`** (e.g. `window.Vaadin.Flow.geolocation`, `window.Vaadin.Flow.pageVisibility`, - `window.Vaadin.Flow.componentSizeObserver`). Use annotations on + `window.Vaadin.Flow.elementSizeObserver`). Use annotations on `UI.java` for scripts that need to run globally. - **`init(element)` installers must be idempotent.** A facade may call `window.Vaadin.Flow.xxx.init(this)` more than once per UI element diff --git a/flow-client/src/main/frontend/ComponentSizeObserver.ts b/flow-client/src/main/frontend/ElementSizeObserver.ts similarity index 70% rename from flow-client/src/main/frontend/ComponentSizeObserver.ts rename to flow-client/src/main/frontend/ElementSizeObserver.ts index 339cc580b55..531eea83cf7 100644 --- a/flow-client/src/main/frontend/ComponentSizeObserver.ts +++ b/flow-client/src/main/frontend/ElementSizeObserver.ts @@ -14,43 +14,43 @@ * the License. */ -interface VaadinComponentSize { +interface VaadinElementSize { w: number; h: number; } interface UiElementWithObserver extends HTMLElement { - _componentSizeObserver?: ResizeObserver; + _elementSizeObserver?: ResizeObserver; } interface ObservedElement extends Element { - _componentSizeId?: number; + _elementSizeId?: number; } const $wnd = window as any; $wnd.Vaadin ??= {}; $wnd.Vaadin.Flow ??= {}; -$wnd.Vaadin.Flow.componentSizeObserver = { +$wnd.Vaadin.Flow.elementSizeObserver = { /** * Creates a shared ResizeObserver on the given UI element. Size changes - * are dispatched as "vaadin-component-resize" custom events on the UI - * element, with a `sizes` property mapping component IDs to their new + * are dispatched as "vaadin-element-resize" custom events on the UI + * element, with a `sizes` property mapping element IDs to their new * width and height. */ init(uiElement: UiElementWithObserver): void { - uiElement._componentSizeObserver = new ResizeObserver((entries) => { - const sizes: Record = {}; + uiElement._elementSizeObserver = new ResizeObserver((entries) => { + const sizes: Record = {}; for (const entry of entries) { const target = entry.target as ObservedElement; - if (target.isConnected && entry.contentBoxSize && target._componentSizeId !== undefined) { - sizes[target._componentSizeId] = { + if (target.isConnected && entry.contentBoxSize && target._elementSizeId !== undefined) { + sizes[target._elementSizeId] = { w: Math.round(entry.contentRect.width), h: Math.round(entry.contentRect.height) }; } } if (Object.keys(sizes).length > 0) { - const event = new Event('vaadin-component-resize') as Event & { sizes: typeof sizes }; + const event = new Event('vaadin-element-resize') as Event & { sizes: typeof sizes }; event.sizes = sizes; uiElement.dispatchEvent(event); } @@ -61,8 +61,8 @@ $wnd.Vaadin.Flow.componentSizeObserver = { * Starts observing the given element with the given numeric ID. */ observe(uiElement: UiElementWithObserver, element: ObservedElement, id: number): void { - element._componentSizeId = id; - uiElement._componentSizeObserver?.observe(element); + element._elementSizeId = id; + uiElement._elementSizeObserver?.observe(element); } }; diff --git a/flow-client/src/main/frontend/Flow.ts b/flow-client/src/main/frontend/Flow.ts index c5d9a601d83..98ad5713680 100644 --- a/flow-client/src/main/frontend/Flow.ts +++ b/flow-client/src/main/frontend/Flow.ts @@ -4,7 +4,7 @@ import { type ConnectionStateChangeListener, type ConnectionStateStore } from '@vaadin/common-frontend'; -import './ComponentSizeObserver'; +import './ElementSizeObserver'; import './Geolocation'; import { currentVisibility } from './PageVisibility'; diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/ElementSizeObserver.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/ElementSizeObserver.java index bc1c8fc77dc..7eab8cde1e8 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/internal/ElementSizeObserver.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/ElementSizeObserver.java @@ -34,14 +34,14 @@ * One instance is created per UI, lazily via {@link #get(UI)} when the first * element's size is observed. A single browser {@code ResizeObserver} is used * to track all observed elements, dispatching a custom - * {@code "vaadin-component-resize"} event on the UI element with aggregated - * size data. + * {@code "vaadin-element-resize"} event on the UI element with aggregated size + * data. *

* For internal use only. May be renamed or removed in a future release. */ public class ElementSizeObserver implements Serializable { - private static final String EVENT_NAME = "vaadin-component-resize"; + private static final String EVENT_NAME = "vaadin-element-resize"; private final Element uiElement; private final Map> idToSignal = new HashMap<>(); @@ -68,8 +68,8 @@ public static ElementSizeObserver get(UI ui) { private ElementSizeObserver(UI ui) { this.uiElement = ui.getElement(); - uiElement.executeJs( - "window.Vaadin.Flow.componentSizeObserver.init(this)"); + uiElement + .executeJs("window.Vaadin.Flow.elementSizeObserver.init(this)"); uiElement.addEventListener(EVENT_NAME, event -> { ObjectNode sizes = (ObjectNode) event.getEventData() @@ -102,7 +102,7 @@ public void observe(Element element, ValueSignal signal) { signalToId.put(signal, id); uiElement.executeJs( - "window.Vaadin.Flow.componentSizeObserver.observe(this, $0, $1)", + "window.Vaadin.Flow.elementSizeObserver.observe(this, $0, $1)", element, id); } diff --git a/flow-server/src/test/java/com/vaadin/flow/dom/ElementSizeSignalTest.java b/flow-server/src/test/java/com/vaadin/flow/dom/ElementSizeSignalTest.java index 55e8b1b3f8a..8aa28493611 100644 --- a/flow-server/src/test/java/com/vaadin/flow/dom/ElementSizeSignalTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/dom/ElementSizeSignalTest.java @@ -104,6 +104,6 @@ private void fireElementResizeEvent(MockUI ui, int elementId, int width, ui.getElement().getNode().getFeature(ElementListenerMap.class) .fireEvent(new DomEvent(ui.getElement(), - "vaadin-component-resize", eventData)); + "vaadin-element-resize", eventData)); } }