Skip to content
Open
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
2 changes: 1 addition & 1 deletion DESIGN_GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions flow-client/src/main/frontend/ElementSizeObserver.ts
Original file line number Diff line number Diff line change
@@ -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 VaadinElementSize {
w: number;
h: number;
}

interface UiElementWithObserver extends HTMLElement {
_elementSizeObserver?: ResizeObserver;
}

interface ObservedElement extends Element {
_elementSizeId?: number;
}

const $wnd = window as any;
$wnd.Vaadin ??= {};
$wnd.Vaadin.Flow ??= {};
$wnd.Vaadin.Flow.elementSizeObserver = {
/**
* Creates a shared ResizeObserver on the given UI element. Size changes
* 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._elementSizeObserver = new ResizeObserver((entries) => {
const sizes: Record<number, VaadinElementSize> = {};
for (const entry of entries) {
const target = entry.target as ObservedElement;
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-element-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._elementSizeId = id;
uiElement._elementSizeObserver?.observe(element);
}
};

// Empty export to ensure TypeScript emits this as an ES module,
// which is required for Vite to load it via import.
export {};
1 change: 1 addition & 0 deletions flow-client/src/main/frontend/Flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type ConnectionStateChangeListener,
type ConnectionStateStore
} from '@vaadin/common-frontend';
import './ElementSizeObserver';
import './Geolocation';
import { currentVisibility } from './PageVisibility';

Expand Down
29 changes: 29 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/component/Size.java
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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.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;

/**
* Per-UI shared ResizeObserver manager that tracks element sizes using a single
* browser {@code ResizeObserver} instance.
* <p>
* 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-element-resize"} event on the UI element with aggregated size
* data.
* <p>
* 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-element-resize";

private final Element uiElement;
private final Map<Integer, ValueSignal<Size>> idToSignal = new HashMap<>();
private final Map<ValueSignal<Size>, Integer> signalToId = new HashMap<>();
private int nextId = 0;

/**
* 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 ElementSizeObserver get(UI ui) {
ElementSizeObserver observer = ComponentUtil.getData(ui,
ElementSizeObserver.class);
if (observer == null) {
observer = new ElementSizeObserver(ui);
ComponentUtil.setData(ui, ElementSizeObserver.class, observer);
}
return observer;
}

private ElementSizeObserver(UI ui) {
this.uiElement = ui.getElement();

uiElement
.executeJs("window.Vaadin.Flow.elementSizeObserver.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<Size> 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 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<Size> signal) {
int id = nextId++;
idToSignal.put(id, signal);
signalToId.put(signal, id);

uiElement.executeJs(
"window.Vaadin.Flow.elementSizeObserver.observe(this, $0, $1)",
element, id);
}

/**
* Stops observing the element associated with the given signal.
*
* @param signal
* the signal whose element should stop being observed
*/
public void unobserve(ValueSignal<Size> signal) {
Integer id = signalToId.remove(signal);
if (id != null) {
idToSignal.remove(id);
}
}
}
49 changes: 49 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/dom/Element.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
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;
import com.vaadin.flow.component.internal.UIInternals.JavaScriptInvocation;
import com.vaadin.flow.component.page.Page;
Expand All @@ -54,7 +57,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;
Expand All @@ -64,6 +69,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.
Expand Down Expand Up @@ -1651,6 +1657,49 @@ public void execute() {
});
}

/**
* Returns a signal that tracks the current size of this element as observed
* by the browser's {@code ResizeObserver} API.
* <p>
* 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.
* <p>
* The returned signal is read-only.
*
* @return a read-only signal with the current element size
*/
public Signal<Size> sizeSignal() {
SizeSignalFeature feature = getNode()
.getFeature(SizeSignalFeature.class);
ValueSignal<Size> signal = feature.getOrCreateSignal();
if (!feature.isObserverRegistered()) {
feature.markObserverRegistered();
registerSizeObservation(signal);
}
return signal.asReadonly();
}

private void registerSizeObservation(ValueSignal<Size> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ private <T extends NodeFeature> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading
Loading