From 6fd4d19c4c26180d2c02daae0d8cc2cde8a60e97 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Sun, 15 Mar 2026 10:24:32 +0000 Subject: [PATCH 1/4] Add sizeSignal to MissingAPI and simplify UseCase11View Extract ResizeObserver setup into a reusable MissingAPI.sizeSignal() method that mirrors the future Component.sizeSignal() API (vaadin/flow#23618). UseCase11View now uses this instead of manual JS setup/teardown. --- src/main/java/com/example/MissingAPI.java | 89 ++++++++++++++ .../com/example/usecase11/UseCase11View.java | 110 +++++------------- 2 files changed, 118 insertions(+), 81 deletions(-) diff --git a/src/main/java/com/example/MissingAPI.java b/src/main/java/com/example/MissingAPI.java index d7d1115a..8d0ba389 100644 --- a/src/main/java/com/example/MissingAPI.java +++ b/src/main/java/com/example/MissingAPI.java @@ -1,14 +1,18 @@ package com.example; +import java.io.Serializable; import java.util.List; import java.util.stream.Stream; +import com.vaadin.flow.component.Component; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.tabs.TabSheet; import com.vaadin.flow.component.virtuallist.VirtualList; +import com.vaadin.flow.dom.DomListenerRegistration; import com.vaadin.flow.function.SerializableConsumer; import com.vaadin.flow.signals.Signal; import com.vaadin.flow.signals.local.ListSignal; +import com.vaadin.flow.signals.local.ValueSignal; /** * Temporary helper class providing static methods for Signal-based component @@ -147,4 +151,89 @@ public static void tabsSyncSelectedIndex(TabSheet tabs, public static Stream getValues(ListSignal signal) { return signal.get().stream().map(Signal::get); } + + /** + * A component's size in pixels, mirroring the future + * {@code Component.Size} API from vaadin/flow#23618. + */ + public record ComponentSize(int width, int height) implements Serializable { + } + + private static final String RESIZE_EVENT = "component-resize"; + + private static final String SETUP_RESIZE_OBSERVER_JS = """ + const target = $0; + const resizeObserver = new ResizeObserver(entries => { + for (let entry of entries) { + const width = Math.floor(entry.contentRect.width); + const height = Math.floor(entry.contentRect.height); + const event = new CustomEvent('%s', { + detail: { width: width, height: height } + }); + target.dispatchEvent(event); + } + }); + resizeObserver.observe(target); + target._resizeObserver = resizeObserver; + const rect = target.getBoundingClientRect(); + const event = new CustomEvent('%s', { + detail: { width: Math.floor(rect.width), height: Math.floor(rect.height) } + }); + target.dispatchEvent(event); + """.formatted(RESIZE_EVENT, RESIZE_EVENT); + + private static final String CLEANUP_RESIZE_OBSERVER_JS = """ + const target = $0; + if (target && target._resizeObserver) { + target._resizeObserver.disconnect(); + delete target._resizeObserver; + } + """; + + /** + * Creates a read-only signal that tracks the size of the given component's + * element using a ResizeObserver, with an initial size of (0, 0). + * + * @see #sizeSignal(Component, ComponentSize) + */ + public static Signal sizeSignal(Component component) { + return sizeSignal(component, new ComponentSize(0, 0)); + } + + /** + * Creates a read-only signal that tracks the size of the given component's + * element using a ResizeObserver. + *

+ * This mirrors the future {@code Component.sizeSignal()} API from + * vaadin/flow#23618. When that PR merges, replace + * {@code MissingAPI.sizeSignal(component)} with + * {@code component.sizeSignal()}. + */ + public static Signal sizeSignal(Component component, + ComponentSize initialSize) { + ValueSignal signal = new ValueSignal<>(initialSize); + + component.addAttachListener(attachEvent -> { + DomListenerRegistration reg = component.getElement() + .addEventListener(RESIZE_EVENT, event -> { + int width = (int) event.getEventData() + .get("event.detail.width").asDouble(); + int height = (int) event.getEventData() + .get("event.detail.height").asDouble(); + signal.set(new ComponentSize(width, height)); + }).addEventData("event.detail.width") + .addEventData("event.detail.height"); + + component.getElement().executeJs(SETUP_RESIZE_OBSERVER_JS, + component.getElement()); + + component.addDetachListener(detachEvent -> { + reg.remove(); + component.getElement().executeJs( + CLEANUP_RESIZE_OBSERVER_JS, component.getElement()); + }); + }); + + return signal.asReadonly(); + } } diff --git a/src/main/java/com/example/usecase11/UseCase11View.java b/src/main/java/com/example/usecase11/UseCase11View.java index cf676445..7cd7d9ea 100644 --- a/src/main/java/com/example/usecase11/UseCase11View.java +++ b/src/main/java/com/example/usecase11/UseCase11View.java @@ -2,12 +2,10 @@ import jakarta.annotation.security.PermitAll; -import java.util.UUID; - +import com.example.MissingAPI; +import com.example.MissingAPI.ComponentSize; import com.example.views.MainLayout; -import com.vaadin.flow.component.AttachEvent; -import com.vaadin.flow.component.ClientCallable; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.H2; @@ -19,7 +17,6 @@ import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.signals.Signal; -import com.vaadin.flow.signals.local.ValueSignal; /** * Use Case 11: Responsive Layout with Container Size Signal @@ -40,25 +37,13 @@ @PermitAll public class UseCase11View extends VerticalLayout { - public record ContainerSize(int width, int height) { - public boolean isSmall() { - return width < 400; // Small breakpoint - } - - public boolean isMedium() { - return width >= 400 && width < 700; // Medium breakpoint - } - - public boolean isLarge() { - return width >= 700; // Large breakpoint - } - } + private static final int SMALL_BREAKPOINT = 400; + private static final int LARGE_BREAKPOINT = 700; - private final ValueSignal containerSizeSignal = new ValueSignal<>( - new ContainerSize(600, 400)); - private final Signal isSmall = containerSizeSignal.map(ContainerSize::isSmall); - private final Signal isMedium = containerSizeSignal.map(ContainerSize::isMedium); - private final Signal isLarge = containerSizeSignal.map(ContainerSize::isLarge); + private final Signal containerSizeSignal; + private final Signal isSmall; + private final Signal isMedium; + private final Signal isLarge; private Div responsiveContent; @@ -67,6 +52,22 @@ public UseCase11View() { setPadding(true); setSizeFull(); + // Create responsive content container first so we can set up the size + // signal before building other panels that depend on it + responsiveContent = new Div(); + responsiveContent.getStyle().set("padding", "1em") + .set("height", "100%").set("overflow-y", "auto") + .set("background-color", "#ffffff"); + containerSizeSignal = MissingAPI.sizeSignal(responsiveContent, + new ComponentSize(600, 400)); + isSmall = containerSizeSignal + .map(size -> size.width() < SMALL_BREAKPOINT); + isMedium = containerSizeSignal.map(size -> size + .width() >= SMALL_BREAKPOINT + && size.width() < LARGE_BREAKPOINT); + isLarge = containerSizeSignal + .map(size -> size.width() >= LARGE_BREAKPOINT); + H2 title = new H2( "Use Case 11: Responsive Content in Resizable Container"); @@ -85,8 +86,8 @@ public UseCase11View() { // Left side: Static info panel Div infoPanel = createInfoPanel(); - // Right side: Responsive content area - responsiveContent = createResponsiveContent(); + // Right side: Populate responsive content + populateResponsiveContent(responsiveContent); splitLayout.addToPrimary(responsiveContent); splitLayout.addToSecondary(infoPanel); @@ -126,11 +127,11 @@ private Div createInfoPanel() { .set("padding", "1em").set("border-radius", "4px") .set("margin", "1em 0"); - Paragraph widthPara = new Paragraph(() -> "Width: " + containerSizeSignal.get().width + "px"); + Paragraph widthPara = new Paragraph(() -> "Width: " + containerSizeSignal.get().width() + "px"); widthPara.getStyle().set("font-family", "monospace").set("margin", "0.25em 0"); - Paragraph heightPara = new Paragraph(() -> "Height: " + containerSizeSignal.get().height + "px"); + Paragraph heightPara = new Paragraph(() -> "Height: " + containerSizeSignal.get().height() + "px"); heightPara.getStyle().set("font-family", "monospace").set("margin", "0.25em 0"); @@ -150,11 +151,7 @@ private Div createInfoPanel() { return panel; } - private Div createResponsiveContent() { - Div container = new Div(); - container.getStyle().set("padding", "1em").set("height", "100%") - .set("overflow-y", "auto").set("background-color", "#ffffff"); - + private void populateResponsiveContent(Div container) { // Small width content Div smallContent = createSection("📱 Small Width Layout", "This is the mobile view (width < 400px). Navigation is stacked vertically, " @@ -182,7 +179,6 @@ private Div createResponsiveContent() { container.add(smallContent, mediumContent, largeContent, cardGridTitle, cardGrid); - return container; } private Div createSection(String title, String content, @@ -236,52 +232,4 @@ private Component createResponsiveCardGrid() { return gridContainer; } - @Override - protected void onAttach(AttachEvent attachEvent) { - super.onAttach(attachEvent); - - String cleanupKey = UUID.randomUUID().toString(); - - // Set up ResizeObserver to monitor the responsive content area - String script = """ - const targetElement = $0; - const resizeObserver = new ResizeObserver(entries => { - for (let entry of entries) { - const width = Math.floor(entry.contentRect.width); - const height = Math.floor(entry.contentRect.height); - this.$server.updateContainerSize(width, height); - } - }); - resizeObserver.observe(targetElement); - - // Store observer for cleanup - window[$1] = resizeObserver; - - // Report initial size - const rect = targetElement.getBoundingClientRect(); - this.$server.updateContainerSize(Math.floor(rect.width), Math.floor(rect.height)); - """; - - getElement().executeJs(script, responsiveContent, cleanupKey); - - addDetachListener(detach -> { - detach.unregisterListener(); - - String cleanupScript = """ - if (window[$0]) { - window[$0].disconnect(); - delete window[$0]; - } - """; - detach.getUI().getPage().executeJs(cleanupScript, cleanupKey); - }); - } - - /** - * Called from JavaScript when the container is resized - */ - @ClientCallable - public void updateContainerSize(int width, int height) { - containerSizeSignal.set(new ContainerSize(width, height)); - } } From 86fe2f7c3187b0ca8d37f4882a0979ff73a34465 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Sun, 15 Mar 2026 11:45:33 +0000 Subject: [PATCH 2/4] Remove sizeSignal initialSize overload to match the PR API sizeSignal() now only takes a Component, matching the future Component.sizeSignal() API. Initial size is always (0, 0) until the ResizeObserver reports from the browser. --- src/main/java/com/example/MissingAPI.java | 16 +++------------- .../com/example/usecase11/UseCase11View.java | 3 +-- .../com/example/usecase11/UseCase11ViewTest.java | 11 +++++------ 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/example/MissingAPI.java b/src/main/java/com/example/MissingAPI.java index 8d0ba389..7aad1e89 100644 --- a/src/main/java/com/example/MissingAPI.java +++ b/src/main/java/com/example/MissingAPI.java @@ -190,16 +190,6 @@ public record ComponentSize(int width, int height) implements Serializable { } """; - /** - * Creates a read-only signal that tracks the size of the given component's - * element using a ResizeObserver, with an initial size of (0, 0). - * - * @see #sizeSignal(Component, ComponentSize) - */ - public static Signal sizeSignal(Component component) { - return sizeSignal(component, new ComponentSize(0, 0)); - } - /** * Creates a read-only signal that tracks the size of the given component's * element using a ResizeObserver. @@ -209,9 +199,9 @@ public static Signal sizeSignal(Component component) { * {@code MissingAPI.sizeSignal(component)} with * {@code component.sizeSignal()}. */ - public static Signal sizeSignal(Component component, - ComponentSize initialSize) { - ValueSignal signal = new ValueSignal<>(initialSize); + public static Signal sizeSignal(Component component) { + ValueSignal signal = new ValueSignal<>( + new ComponentSize(0, 0)); component.addAttachListener(attachEvent -> { DomListenerRegistration reg = component.getElement() diff --git a/src/main/java/com/example/usecase11/UseCase11View.java b/src/main/java/com/example/usecase11/UseCase11View.java index 7cd7d9ea..6cb92968 100644 --- a/src/main/java/com/example/usecase11/UseCase11View.java +++ b/src/main/java/com/example/usecase11/UseCase11View.java @@ -58,8 +58,7 @@ public UseCase11View() { responsiveContent.getStyle().set("padding", "1em") .set("height", "100%").set("overflow-y", "auto") .set("background-color", "#ffffff"); - containerSizeSignal = MissingAPI.sizeSignal(responsiveContent, - new ComponentSize(600, 400)); + containerSizeSignal = MissingAPI.sizeSignal(responsiveContent); isSmall = containerSizeSignal .map(size -> size.width() < SMALL_BREAKPOINT); isMedium = containerSizeSignal.map(size -> size diff --git a/src/test/java/com/example/usecase11/UseCase11ViewTest.java b/src/test/java/com/example/usecase11/UseCase11ViewTest.java index a1bd9c07..d82f9522 100644 --- a/src/test/java/com/example/usecase11/UseCase11ViewTest.java +++ b/src/test/java/com/example/usecase11/UseCase11ViewTest.java @@ -25,19 +25,18 @@ void viewRendersWithSplitLayout() { } @Test - void initialContainerSizeIsMedium() { + void initialContainerSizeIsSmall() { navigate(UseCase11View.class); runPendingSignalsTasks(); - // Default container size is 600x400 which is medium (400-700px) - // The medium layout section should be visible, small and large should - // not + // Initial size from sizeSignal is (0, 0) which is small (< 400px) + // until the ResizeObserver reports the actual size from the browser assertTrue( $view(H3.class).all().stream().anyMatch(h -> h.getText() != null - && h.getText().contains("Medium Width Layout"))); + && h.getText().contains("Small Width Layout"))); assertFalse( $view(H3.class).all().stream().anyMatch(h -> h.getText() != null - && h.getText().contains("Small Width Layout"))); + && h.getText().contains("Medium Width Layout"))); assertFalse( $view(H3.class).all().stream().anyMatch(h -> h.getText() != null && h.getText().contains("Large Width Layout"))); From 1908d1a6f3e210d4aa479df8107921914d85f195 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Sun, 15 Mar 2026 12:20:23 +0000 Subject: [PATCH 3/4] Fix sizeSignal detach cleanup to use page JS execution Store ResizeObserver on window[uuid] instead of the element so cleanup can run via detachEvent.getUI().getPage().executeJs() after the component is already detached. --- src/main/java/com/example/MissingAPI.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/MissingAPI.java b/src/main/java/com/example/MissingAPI.java index 7aad1e89..9feded81 100644 --- a/src/main/java/com/example/MissingAPI.java +++ b/src/main/java/com/example/MissingAPI.java @@ -2,6 +2,7 @@ import java.io.Serializable; import java.util.List; +import java.util.UUID; import java.util.stream.Stream; import com.vaadin.flow.component.Component; @@ -174,7 +175,7 @@ public record ComponentSize(int width, int height) implements Serializable { } }); resizeObserver.observe(target); - target._resizeObserver = resizeObserver; + window[$1] = resizeObserver; const rect = target.getBoundingClientRect(); const event = new CustomEvent('%s', { detail: { width: Math.floor(rect.width), height: Math.floor(rect.height) } @@ -183,10 +184,9 @@ public record ComponentSize(int width, int height) implements Serializable { """.formatted(RESIZE_EVENT, RESIZE_EVENT); private static final String CLEANUP_RESIZE_OBSERVER_JS = """ - const target = $0; - if (target && target._resizeObserver) { - target._resizeObserver.disconnect(); - delete target._resizeObserver; + if (window[$0]) { + window[$0].disconnect(); + delete window[$0]; } """; @@ -204,6 +204,8 @@ public static Signal sizeSignal(Component component) { new ComponentSize(0, 0)); component.addAttachListener(attachEvent -> { + String cleanupKey = UUID.randomUUID().toString(); + DomListenerRegistration reg = component.getElement() .addEventListener(RESIZE_EVENT, event -> { int width = (int) event.getEventData() @@ -215,12 +217,13 @@ public static Signal sizeSignal(Component component) { .addEventData("event.detail.height"); component.getElement().executeJs(SETUP_RESIZE_OBSERVER_JS, - component.getElement()); + component.getElement(), cleanupKey); component.addDetachListener(detachEvent -> { + detachEvent.unregisterListener(); reg.remove(); - component.getElement().executeJs( - CLEANUP_RESIZE_OBSERVER_JS, component.getElement()); + detachEvent.getUI().getPage().executeJs( + CLEANUP_RESIZE_OBSERVER_JS, cleanupKey); }); }); From 55aae55ed1e4a79abc8ac1416a80ce68e6b1668f Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 16 Mar 2026 11:51:05 +0000 Subject: [PATCH 4/4] Address PR review: use this in JS and getEventDetail for deserialization Use implicit this from Element.executeJs instead of passing the element as $0. Simplify event handling with getEventDetail(ComponentSize.class) instead of manual field extraction. --- src/main/java/com/example/MissingAPI.java | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/example/MissingAPI.java b/src/main/java/com/example/MissingAPI.java index 9feded81..7c9f561c 100644 --- a/src/main/java/com/example/MissingAPI.java +++ b/src/main/java/com/example/MissingAPI.java @@ -163,7 +163,6 @@ public record ComponentSize(int width, int height) implements Serializable { private static final String RESIZE_EVENT = "component-resize"; private static final String SETUP_RESIZE_OBSERVER_JS = """ - const target = $0; const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { const width = Math.floor(entry.contentRect.width); @@ -171,16 +170,16 @@ public record ComponentSize(int width, int height) implements Serializable { const event = new CustomEvent('%s', { detail: { width: width, height: height } }); - target.dispatchEvent(event); + this.dispatchEvent(event); } }); - resizeObserver.observe(target); - window[$1] = resizeObserver; - const rect = target.getBoundingClientRect(); + resizeObserver.observe(this); + window[$0] = resizeObserver; + const rect = this.getBoundingClientRect(); const event = new CustomEvent('%s', { detail: { width: Math.floor(rect.width), height: Math.floor(rect.height) } }); - target.dispatchEvent(event); + this.dispatchEvent(event); """.formatted(RESIZE_EVENT, RESIZE_EVENT); private static final String CLEANUP_RESIZE_OBSERVER_JS = """ @@ -208,16 +207,12 @@ public static Signal sizeSignal(Component component) { DomListenerRegistration reg = component.getElement() .addEventListener(RESIZE_EVENT, event -> { - int width = (int) event.getEventData() - .get("event.detail.width").asDouble(); - int height = (int) event.getEventData() - .get("event.detail.height").asDouble(); - signal.set(new ComponentSize(width, height)); - }).addEventData("event.detail.width") - .addEventData("event.detail.height"); + signal.set(event.getEventDetail( + ComponentSize.class)); + }); component.getElement().executeJs(SETUP_RESIZE_OBSERVER_JS, - component.getElement(), cleanupKey); + cleanupKey); component.addDetachListener(detachEvent -> { detachEvent.unregisterListener();