properties) {
+ addClassName("source-code-viewer");
+ addClassName("has-code-viewer-gutter");
+
codeViewer = new Element("code-viewer");
- getElement().appendChild(codeViewer);
- getElement().getStyle().set("overflow", "auto");
- getElement().getStyle().set("display", "flex");
- codeViewer.getStyle().set("flex-grow", "1");
+
+ Div codeViewerWrapper = new Div();
+ codeViewerWrapper.addClassName("source-code-viewer-codeviewer-wrapper");
+ codeViewerWrapper.getElement().appendChild(codeViewer);
+
+ // Non-scrolling overlay so the buttons stay pinned while the code scrolls
+ buttonsWrapper = new Div();
+ buttonsWrapper.addClassName("source-code-viewer-buttons-wrapper");
+
+ add(codeViewerWrapper, buttonsWrapper);
+
setProperties(properties);
- addAttachListener(ev -> fetchContents(url, language));
+ addAttachListener(ev -> {
+ fetchContents(url, language);
+ observeScrollbar();
+ });
+ }
+
+ /**
+ * Adds the overlay controls (show, hide, flip and rotate) pinned over the source code.
+ *
+ * The buttons do not change this viewer directly. Instead, each one dispatches a bubbling DOM
+ * event that is expected to be handled by an enclosing layout, which is what actually collapses,
+ * repositions or reorients the source panel:
+ *
+ * - show / hide dispatch {@code source-collapse-changed} (carrying {@code detail.collapsed});
+ *
- flip dispatches {@code source-flip};
+ *
- rotate dispatches {@code source-rotate}.
+ *
+ * The buttons are therefore only useful when this viewer is placed inside a layout that listens
+ * for those events and coordinates the response (see {@link TabbedDemo}); otherwise they emit
+ * events that nothing consumes.
+ *
+ * The controls are rendered by the {@code source-code-viewer-buttons} client-side web component,
+ * which dispatches the events locally on click; the enclosing layout (see {@link TabbedDemo})
+ * handles them through Vaadin server-side listeners. This method is idempotent: the component is
+ * added only once.
+ */
+ public void withButtons() {
+ if (buttonsWrapper.getElement().getChildCount() == 0) {
+ buttonsWrapper.getElement().appendChild(new Element("source-code-viewer-buttons"));
+ }
+ }
+
+ /**
+ * Observes the scrollable wrapper. Whenever the vertical scrollbar appears or disappears, sets (or
+ * clears) the {@code --code-viewer-gutter} custom property on the nearest ancestor (or self)
+ * carrying the {@code has-code-viewer-gutter} class. Whenever the wrapper collapses below 24px in
+ * width or 10px in height, sets {@code --source-code-viewer-show-button-display} so the show
+ * button becomes visible (and clears it otherwise).
+ */
+ private void observeScrollbar() {
+ getElement().executeJs(
+ """
+ const root = this;
+ const wrapper = root.querySelector('.source-code-viewer-codeviewer-wrapper');
+ if (!wrapper) return;
+ root.__scrollbarObserver?.disconnect();
+ root.__scrollbarMutation?.disconnect();
+ let hasScrollbar = null;
+ const update = () => {
+ if (wrapper.offsetWidth < 24 || wrapper.offsetHeight < 10) {
+ root.style.setProperty('--source-code-viewer-show-button-display', 'block');
+ } else {
+ root.style.removeProperty('--source-code-viewer-show-button-display');
+ }
+ const current = wrapper.scrollHeight > wrapper.clientHeight;
+ if (current === hasScrollbar) return;
+ hasScrollbar = current;
+ let target = root;
+ while (target && !target.classList.contains('has-code-viewer-gutter')) {
+ target = target.parentElement;
+ }
+ if (target) {
+ if (current) {
+ const scrollbarWidth = wrapper.offsetWidth - wrapper.clientWidth;
+ target.style.setProperty('--code-viewer-gutter', scrollbarWidth + 'px');
+ } else {
+ target.style.removeProperty('--code-viewer-gutter');
+ }
+ }
+ };
+ let frame = 0;
+ const scheduleUpdate = () => {
+ if (frame) return;
+ frame = requestAnimationFrame(() => { frame = 0; update(); });
+ };
+ const resizeObserver = new ResizeObserver(scheduleUpdate);
+ resizeObserver.observe(wrapper);
+ root.__scrollbarObserver = resizeObserver;
+ const codeViewer = root.querySelector('code-viewer');
+ if (codeViewer) {
+ const mutationObserver = new MutationObserver(scheduleUpdate);
+ mutationObserver.observe(codeViewer, {childList: true, subtree: true});
+ root.__scrollbarMutation = mutationObserver;
+ }
+ update();
+ """);
}
public void fetchContents(String url, String language) {
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
index 1f7a148..25aee66 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
@@ -48,7 +48,7 @@ public SplitLayoutDemo(Component demo, List tabs) {
properties.put("vaadin", VaadinVersion.getVaadinVersion());
properties.put("flow", Version.getFullVersion());
- code = new MultiSourceCodeViewer(tabs, properties);
+ code = new MultiSourceCodeViewer(tabs, properties).withButtons();
this.demo = demo;
setSourcePosition(code.getSourcePosition());
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
index f0d433f..0a09281 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
@@ -77,9 +77,6 @@ public class TabbedDemo extends VerticalLayout implements RouterLayout {
private EnhancedRouteTabs tabs;
private HorizontalLayout footer;
private SplitLayoutDemo currentLayout;
- private Checkbox orientationCB;
- private Checkbox codeCB;
- private Checkbox codePositionCB;
private Checkbox themeCB;
private Orientation splitOrientation;
private Button helperButton;
@@ -93,28 +90,14 @@ public TabbedDemo() {
tabs = new EnhancedRouteTabs();
- // Footer
- orientationCB = new Checkbox("Toggle Orientation");
- orientationCB.setValue(true);
- orientationCB.addClassName("smallcheckbox");
- orientationCB.addValueChangeListener(ev -> {
- if (ev.isFromClient()) {
- toggleSplitterOrientation(ev.isFromClient());
- }
- });
- codeCB = new Checkbox("Show Source Code");
- codeCB.setValue(true);
- codeCB.addClassName("smallcheckbox");
- codeCB.addValueChangeListener(
- ev -> fireSourceCollapseChangedEvent(!ev.getValue(), ev.isFromClient()));
+ // The source controls live inside SourceCodeViewer and signal across components
+ // via bubbling DOM events; collapse carries data, so it uses SourceCollapseChangedEvent.
addSourceCollapseListener(ev -> {
sourceCollapsed = ev.isCollapsed();
updateSplitterPosition();
});
- codePositionCB = new Checkbox("Toggle Code Position");
- codePositionCB.setValue(true);
- codePositionCB.addClassName("smallcheckbox");
- codePositionCB.addValueChangeListener(ev -> toggleSourcePosition(ev.isFromClient()));
+ getElement().addEventListener("source-flip", ev -> toggleSourcePosition(true));
+ getElement().addEventListener("source-rotate", ev -> toggleSplitterOrientation(true));
themeCB = new Checkbox("Dark Theme");
themeCB.setValue(false);
themeCB.addClassName("smallcheckbox");
@@ -126,7 +109,7 @@ public TabbedDemo() {
footer = new HorizontalLayout();
footer.setWidthFull();
footer.setJustifyContentMode(JustifyContentMode.END);
- footer.add(codeCB, codePositionCB, orientationCB, themeCB);
+ footer.add(themeCB);
footer.setClassName("demo-footer");
Package pkg = this.getClass().getPackage();
@@ -348,8 +331,6 @@ private void updateSplitterPosition() {
} else {
currentLayout.showSourceCode();
}
- orientationCB.setEnabled(!sourceCollapsed);
- codePositionCB.setEnabled(!sourceCollapsed);
}
}
@@ -359,8 +340,7 @@ private void updateSplitterPosition() {
* @param visible {@code true} to make the source code visible, {@code false} otherwise
*/
public void setSourceVisible(boolean visible) {
- codeCB.setValue(visible);
- codePositionCB.setVisible(visible);
+ fireSourceCollapseChangedEvent(!visible, false);
}
/**
@@ -428,7 +408,6 @@ private void setOrientation(Orientation orientation, boolean fromClient) {
currentLayout.setOrientation(orientation);
currentLayout.setSplitterPosition(50);
}
- orientationCB.setValue(Orientation.HORIZONTAL.equals(orientation));
fireOrientationChangedEvent(orientation, fromClient);
}
@@ -525,9 +504,6 @@ private static Stream collectThemeChangeObservers(Component
private void updateFooterButtonsVisibility() {
boolean hasSourceCode = currentLayout != null;
ComponentUtil.fireEvent(this, new TabbedDemoSourceEvent(this, hasSourceCode));
- orientationCB.setVisible(hasSourceCode);
- codeCB.setVisible(hasSourceCode);
- codePositionCB.setVisible(hasSourceCode);
}
/**
@@ -546,8 +522,10 @@ protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
getUI().ifPresent(ui -> ui.getPage().retrieveExtendedClientDetails(receiver -> {
boolean mobile = receiver.getBodyClientWidth() <= MOBILE_DEVICE_BREAKPOINT_WIDTH;
- codeCB.setValue(!sourceCollapsed && !mobile);
- codePositionCB.setValue(!sourceCollapsed && !mobile);
+ boolean shouldCollapse = sourceCollapsed || mobile;
+ if (shouldCollapse != sourceCollapsed) {
+ fireSourceCollapseChangedEvent(shouldCollapse, false);
+ }
boolean portraitOrientation = receiver.getBodyClientHeight() > receiver.getBodyClientWidth();
adjustSplitOrientation(portraitOrientation);
diff --git a/base/src/main/resources/META-INF/resources/frontend/source-code-viewer-buttons.ts b/base/src/main/resources/META-INF/resources/frontend/source-code-viewer-buttons.ts
new file mode 100644
index 0000000..030b82a
--- /dev/null
+++ b/base/src/main/resources/META-INF/resources/frontend/source-code-viewer-buttons.ts
@@ -0,0 +1,66 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
+import {html, LitElement} from 'lit';
+import {customElement} from 'lit/decorators.js';
+import '@vaadin/button/vaadin-button.js';
+import './commons-demo-iconset.js';
+
+/**
+ * Overlay controls pinned over the source code. Each button dispatches a bubbling DOM event that is
+ * handled by an enclosing layout (see TabbedDemo), which is what actually collapses, repositions or
+ * reorients the source panel. Firing the events on the client avoids a server roundtrip on click.
+ */
+@customElement("source-code-viewer-buttons")
+export class SourceCodeViewerButtons extends LitElement {
+
+ // Render in light DOM so the shared stylesheet (shared-styles.css) styles the buttons.
+ createRenderRoot() {
+ return this;
+ }
+
+ private fire(type: string, detail?: any) {
+ this.dispatchEvent(new CustomEvent(type, {bubbles: true, detail}));
+ }
+
+ render() {
+ return html`
+ this.fire('source-collapse-changed', {collapsed: false})}>
+
+
+ this.fire('source-collapse-changed', {collapsed: true})}>
+
+
+ this.fire('source-flip')}>
+
+
+ this.fire('source-rotate')}>
+
+
+ `;
+ }
+}
diff --git a/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css b/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css
index 6fef4f7..b0e87cd 100644
--- a/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css
+++ b/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css
@@ -51,5 +51,122 @@ code-highlighter code {
cursor: pointer;
}
+.source-code-viewer {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ overflow: hidden;
+}
+
+.source-code-viewer-codeviewer-wrapper {
+ display: flex;
+ flex-grow: 1;
+ overflow: auto;
+}
+
+.source-code-viewer-codeviewer-wrapper > code-viewer {
+ flex-grow: 1;
+}
+
+.source-code-viewer-buttons-wrapper {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+}
+
+source-code-viewer-buttons {
+ position: absolute;
+ z-index: 1;
+ right: calc(0.25rem + var(--code-viewer-gutter, 0px));
+ top: 0.25rem;
+ display: flex;
+ gap: var(--lumo-space-xs, 0.25rem);
+ container-type: inline-size;
+ width: 100%;
+ justify-content: end;
+ pointer-events: none;
+}
+
+vaadin-button.source-code-viewer-button {
+ color: color-mix(in srgb, #f8f8f2 70%, transparent);
+ pointer-events: auto;
+ background: transparent;
+ display: none;
+ padding: 0;
+}
+
+vaadin-button.source-code-viewer-button vaadin-icon {
+ --vaadin-icon-size: 20px;
+ padding: 0.125em;
+}
+
+@container style(--lumo-size-m) {
+ vaadin-button.source-code-viewer-button vaadin-icon {
+ padding: 0.25em;
+ }
+}
+
+
+vaadin-button.source-code-viewer-show-button {
+ display: var(--source-code-viewer-show-button-display, none);
+ color: var(--lumo-body-text-color, var(--vaadin-text-color));
+ position: fixed;
+}
+
+[orientation="horizontal"] [slot="primary"] vaadin-button.source-code-viewer-show-button {
+ right: 0;
+}
+
+[orientation="horizontal"] [slot="secondary"] vaadin-button.source-code-viewer-show-button {
+ right: 0.5rem;
+}
+
+[orientation="vertical"] [slot="primary"] vaadin-button.source-code-viewer-show-button {
+ top: 0.5rem;
+}
+
+[orientation="vertical"] [slot="secondary"] vaadin-button.source-code-viewer-show-button {
+ top: 0;
+}
+
+/* Rotate the icons to match the layout disposition. */
+[orientation="horizontal"] [slot="secondary"] vaadin-button.source-code-viewer-button{
+ transform: rotate(0deg);
+ &.source-code-viewer-rotate-button { transform: scaleX(-1) rotate(90deg); }
+}
+
+[orientation="horizontal"] [slot="primary"] vaadin-button.source-code-viewer-button {
+ transform: rotate(180deg);
+ &.source-code-viewer-rotate-button { transform: rotate(0deg); }
+}
+
+[orientation="vertical"] [slot="secondary"] vaadin-button.source-code-viewer-button {
+ transform: rotate(90deg);
+ &.source-code-viewer-rotate-button { transform: rotate(0deg); }
+}
+
+[orientation="vertical"] [slot="primary"] vaadin-button.source-code-viewer-button {
+ transform: rotate(270deg);
+ &.source-code-viewer-rotate-button { transform: scaleX(-1) rotate(90deg); }
+}
+
+@container (min-width: 82px) {
+ vaadin-button.source-code-viewer-flip-button {
+ display: block;
+ }
+}
+
+@container (min-width: 53px) {
+ vaadin-button.source-code-viewer-rotate-button {
+ display: block;
+ }
+}
+
+@container (min-width: 24px) {
+ vaadin-button.source-code-viewer-hide-button {
+ display: block;
+ }
+}
+
.commons-demo-split-layout { overflow: hidden }
-.commons-demo-split-layout > [slot] { overflow: auto; }
\ No newline at end of file
+.commons-demo-split-layout > [slot] { overflow: auto; }
From 65995ade81cbc2d61e5974c339010ebea1f6a55f Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Wed, 1 Jul 2026 15:06:23 -0300
Subject: [PATCH 08/18] refactor: remove splitOrientation variable in
TabbedDemo
---
.../vaadin/addons/demo/TabbedDemo.java | 22 +++++++++++--------
1 file changed, 13 insertions(+), 9 deletions(-)
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
index 0a09281..14ca2b2 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
@@ -78,7 +78,6 @@ public class TabbedDemo extends VerticalLayout implements RouterLayout {
private HorizontalLayout footer;
private SplitLayoutDemo currentLayout;
private Checkbox themeCB;
- private Orientation splitOrientation;
private Button helperButton;
private DemoHelperViewer demoHelperViewer;
@@ -223,6 +222,11 @@ public void showRouterLayoutContent(HasElement content) {
createSourceCodeTab(demo.getClass(), demoSource).ifPresent(sourceTabs::add);
}
+ Orientation splitOrientation = null;
+ if (currentLayout != null) {
+ splitOrientation = currentLayout.getOrientation();
+ }
+
if (!sourceTabs.isEmpty()) {
currentLayout = new SplitLayoutDemo(demo, sourceTabs);
if (currentLayout.isEmpty()) {
@@ -376,6 +380,8 @@ private void toggleSplitterOrientation(boolean fromClient) {
if (currentLayout == null) {
return;
}
+
+ Orientation splitOrientation = getOrientation();
if (Orientation.HORIZONTAL.equals(splitOrientation)) {
splitOrientation = Orientation.VERTICAL;
} else {
@@ -403,12 +409,11 @@ public void setOrientation(Orientation orientation) {
}
private void setOrientation(Orientation orientation, boolean fromClient) {
- splitOrientation = orientation;
- if (currentLayout != null) {
- currentLayout.setOrientation(orientation);
- currentLayout.setSplitterPosition(50);
+ if (currentLayout != null && orientation != getOrientation()) {
+ currentLayout.setOrientation(orientation);
+ currentLayout.showSourceCode();
+ fireOrientationChangedEvent(orientation, fromClient);
}
- fireOrientationChangedEvent(orientation, fromClient);
}
/**
@@ -534,11 +539,10 @@ protected void onAttach(AttachEvent attachEvent) {
private void adjustSplitOrientation(boolean portraitOrientation) {
if (portraitOrientation) {
- splitOrientation = Orientation.VERTICAL;
+ setOrientation(Orientation.VERTICAL);
} else {
- splitOrientation = Orientation.HORIZONTAL;
+ setOrientation(Orientation.HORIZONTAL);
}
- setOrientation(splitOrientation);
}
/**
From 425ea034fd3ee794a836c89df370bb6ae76a3e10 Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Wed, 1 Jul 2026 14:58:02 -0300
Subject: [PATCH 09/18] refactor: remove setSplitterPosition(int) from
SplitLayoutDemo
---
.../vaadin/addons/demo/SplitLayoutDemo.java | 4 ----
.../vaadin/addons/demo/TabbedDemo.java | 22 ++++++++-----------
2 files changed, 9 insertions(+), 17 deletions(-)
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
index 25aee66..b4b446f 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
@@ -93,10 +93,6 @@ public Orientation getOrientation() {
return getContent().getOrientation();
}
- public void setSplitterPosition(int pos) {
- getContent().setSplitterPosition(pos);
- }
-
public void setSizeFull() {
getContent().setSizeFull();
}
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
index 14ca2b2..528b597 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
@@ -78,6 +78,7 @@ public class TabbedDemo extends VerticalLayout implements RouterLayout {
private HorizontalLayout footer;
private SplitLayoutDemo currentLayout;
private Checkbox themeCB;
+ private Orientation splitOrientation;
private Button helperButton;
private DemoHelperViewer demoHelperViewer;
@@ -222,11 +223,6 @@ public void showRouterLayoutContent(HasElement content) {
createSourceCodeTab(demo.getClass(), demoSource).ifPresent(sourceTabs::add);
}
- Orientation splitOrientation = null;
- if (currentLayout != null) {
- splitOrientation = currentLayout.getOrientation();
- }
-
if (!sourceTabs.isEmpty()) {
currentLayout = new SplitLayoutDemo(demo, sourceTabs);
if (currentLayout.isEmpty()) {
@@ -380,8 +376,6 @@ private void toggleSplitterOrientation(boolean fromClient) {
if (currentLayout == null) {
return;
}
-
- Orientation splitOrientation = getOrientation();
if (Orientation.HORIZONTAL.equals(splitOrientation)) {
splitOrientation = Orientation.VERTICAL;
} else {
@@ -409,11 +403,12 @@ public void setOrientation(Orientation orientation) {
}
private void setOrientation(Orientation orientation, boolean fromClient) {
- if (currentLayout != null && orientation != getOrientation()) {
- currentLayout.setOrientation(orientation);
- currentLayout.showSourceCode();
- fireOrientationChangedEvent(orientation, fromClient);
+ splitOrientation = orientation;
+ if (currentLayout != null) {
+ currentLayout.setOrientation(orientation);
+ currentLayout.showSourceCode();
}
+ fireOrientationChangedEvent(orientation, fromClient);
}
/**
@@ -539,10 +534,11 @@ protected void onAttach(AttachEvent attachEvent) {
private void adjustSplitOrientation(boolean portraitOrientation) {
if (portraitOrientation) {
- setOrientation(Orientation.VERTICAL);
+ splitOrientation = Orientation.VERTICAL;
} else {
- setOrientation(Orientation.HORIZONTAL);
+ splitOrientation = Orientation.HORIZONTAL;
}
+ setOrientation(splitOrientation);
}
/**
From 50d9b6f1de0ab2bbbde8558d4c1e58b3c40dc2ca Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Wed, 1 Jul 2026 15:18:04 -0300
Subject: [PATCH 10/18] refactor: unify hide/show source methods in
SplitLayoutDemo
---
.../vaadin/addons/demo/SplitLayoutDemo.java | 24 ++++++++--------
.../vaadin/addons/demo/TabbedDemo.java | 28 +++++++++----------
2 files changed, 26 insertions(+), 26 deletions(-)
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
index b4b446f..d093d54 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
@@ -97,18 +97,18 @@ public void setSizeFull() {
getContent().setSizeFull();
}
- public void showSourceCode() {
- getContent().setSplitterPosition(50);
- }
-
- public void hideSourceCode() {
- switch (sourcePosition) {
- case PRIMARY:
- getContent().setSplitterPosition(0);
- break;
- case SECONDARY:
- getContent().setSplitterPosition(100);
- break;
+ public void setSourceCollapsed(boolean collapsed) {
+ if (!collapsed) {
+ getContent().setSplitterPosition(50);
+ } else {
+ switch (sourcePosition) {
+ case PRIMARY:
+ getContent().setSplitterPosition(0);
+ break;
+ case SECONDARY:
+ getContent().setSplitterPosition(100);
+ break;
+ }
}
}
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
index 528b597..7fe34f9 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
@@ -78,7 +78,6 @@ public class TabbedDemo extends VerticalLayout implements RouterLayout {
private HorizontalLayout footer;
private SplitLayoutDemo currentLayout;
private Checkbox themeCB;
- private Orientation splitOrientation;
private Button helperButton;
private DemoHelperViewer demoHelperViewer;
@@ -223,6 +222,11 @@ public void showRouterLayoutContent(HasElement content) {
createSourceCodeTab(demo.getClass(), demoSource).ifPresent(sourceTabs::add);
}
+ Orientation splitOrientation = null;
+ if (currentLayout != null) {
+ splitOrientation = currentLayout.getOrientation();
+ }
+
if (!sourceTabs.isEmpty()) {
currentLayout = new SplitLayoutDemo(demo, sourceTabs);
if (currentLayout.isEmpty()) {
@@ -326,11 +330,7 @@ public void removeRouterLayoutContent(HasElement oldContent) {
private void updateSplitterPosition() {
if (currentLayout != null) {
- if (sourceCollapsed) {
- currentLayout.hideSourceCode();
- } else {
- currentLayout.showSourceCode();
- }
+ currentLayout.setSourceCollapsed(sourceCollapsed);
}
}
@@ -376,6 +376,8 @@ private void toggleSplitterOrientation(boolean fromClient) {
if (currentLayout == null) {
return;
}
+
+ Orientation splitOrientation = getOrientation();
if (Orientation.HORIZONTAL.equals(splitOrientation)) {
splitOrientation = Orientation.VERTICAL;
} else {
@@ -403,12 +405,11 @@ public void setOrientation(Orientation orientation) {
}
private void setOrientation(Orientation orientation, boolean fromClient) {
- splitOrientation = orientation;
- if (currentLayout != null) {
- currentLayout.setOrientation(orientation);
- currentLayout.showSourceCode();
+ if (currentLayout != null && orientation != getOrientation()) {
+ currentLayout.setOrientation(orientation);
+ currentLayout.setSourceCollapsed(false);
+ fireOrientationChangedEvent(orientation, fromClient);
}
- fireOrientationChangedEvent(orientation, fromClient);
}
/**
@@ -534,11 +535,10 @@ protected void onAttach(AttachEvent attachEvent) {
private void adjustSplitOrientation(boolean portraitOrientation) {
if (portraitOrientation) {
- splitOrientation = Orientation.VERTICAL;
+ setOrientation(Orientation.VERTICAL);
} else {
- splitOrientation = Orientation.HORIZONTAL;
+ setOrientation(Orientation.HORIZONTAL);
}
- setOrientation(splitOrientation);
}
/**
From 7531d6177035d35f4c2c342ef09a4dbcbbd39fa7 Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Wed, 15 Jul 2026 11:52:49 -0300
Subject: [PATCH 11/18] refactor: force use of executeJs(String,
Serializable...)
---
.../java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
index 7fe34f9..358b861 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
@@ -45,6 +45,7 @@
import com.vaadin.flow.router.RouterLayout;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.Version;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -480,7 +481,7 @@ public static void setColorScheme(Component component, @NonNull ColorScheme colo
e.style.colorScheme = $0;
""";
- element.executeJs(script, theme);
+ element.executeJs(script, new Serializable[] {theme});
collectThemeChangeObservers(component).forEach(observer -> observer.onThemeChange(theme));
observeThemeChange(component);
From ea06ae9009717e59d5e4fa1a589423bdec6109b6 Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Wed, 15 Jul 2026 10:38:57 -0300
Subject: [PATCH 12/18] feat: make DemoSource inheritable
---
.../java/com/flowingcode/vaadin/addons/demo/DemoSource.java | 2 ++
.../java/com/flowingcode/vaadin/addons/demo/DemoSources.java | 2 ++
2 files changed, 4 insertions(+)
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java
index 1ea0b84..999f9db 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java
@@ -22,6 +22,7 @@
import com.flowingcode.vaadin.addons.GithubBranch;
import com.flowingcode.vaadin.addons.GithubLink;
import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -44,6 +45,7 @@
@Repeatable(DemoSources.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
+@Inherited
public @interface DemoSource {
static final String DEFAULT_VALUE = "__DEFAULT__";
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSources.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSources.java
index 741aeb4..41a1ebe 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSources.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSources.java
@@ -20,12 +20,14 @@
package com.flowingcode.vaadin.addons.demo;
import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
+@Inherited
public @interface DemoSources {
DemoSource[] value();
From 7d334e612a4fbf3da6e8ba3fd4bc4740c8b47e69 Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Tue, 7 Jul 2026 23:01:50 -0300
Subject: [PATCH 13/18] test: add tests for the new button layout
---
base/pom.xml | 19 +-
.../addons/demo/it/OverlayCallables.java | 51 +++++
.../addons/demo/it/OverlayControlsIT.java | 189 ++++++++++++++++++
.../vaadin/addons/demo/it/OverlayView.java | 103 ++++++++++
.../addons/demo/it/TabbedDemoUIUnitTest.java | 171 ++++++++++++++++
.../addons/demo/it/ViewInitializerImpl.java | 37 ++++
.../addons/demo/test/SplitLayoutDemoTest.java | 179 +++++++++++++++++
...adin.flow.server.VaadinServiceInitListener | 1 +
8 files changed, 747 insertions(+), 3 deletions(-)
create mode 100644 base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayCallables.java
create mode 100644 base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayControlsIT.java
create mode 100644 base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayView.java
create mode 100644 base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java
create mode 100644 base/src/test/java/com/flowingcode/vaadin/addons/demo/it/ViewInitializerImpl.java
create mode 100644 base/src/test/java/com/flowingcode/vaadin/addons/demo/test/SplitLayoutDemoTest.java
create mode 100644 base/src/test/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener
diff --git a/base/pom.xml b/base/pom.xml
index d2e3426..0508fdd 100644
--- a/base/pom.xml
+++ b/base/pom.xml
@@ -75,6 +75,12 @@
vaadin-core
true
+
+
+ com.vaadin
+ vaadin
+ test
+
org.projectlombok
@@ -99,13 +105,13 @@
com.flowingcode.vaadin
json-migration-helper
- 0.9.2
+ 0.9.5
junit
junit
- 4.11
+ 4.13.2
test
@@ -126,7 +132,14 @@
-
+
+
+ com.flowingcode.vaadin.test
+ testbench-rpc
+ 1.5.0
+ test
+
+
io.github.bonigarcia
webdrivermanager
diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayCallables.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayCallables.java
new file mode 100644
index 0000000..aa0b15b
--- /dev/null
+++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayCallables.java
@@ -0,0 +1,51 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.demo.it;
+
+import com.flowingcode.vaadin.addons.demo.SourcePosition;
+import com.flowingcode.vaadin.testbench.rpc.RmiCallable;
+import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation;
+
+/**
+ * testbench-rpc callable contract used by the level-E integration tests to read the server-side
+ * state coordinated by {@code TabbedDemo} directly from the browser test, instead of scraping DOM
+ * status elements. Implemented (with {@code @ClientCallable}) by {@link OverlayView}.
+ */
+public interface OverlayCallables extends RmiCallable {
+
+ /** Current split orientation, {@code HORIZONTAL} or {@code VERTICAL}. */
+ Orientation orientation();
+
+ /** Whether the source panel is currently collapsed (latest collapse event). */
+ boolean sourceCollapsed();
+
+ /** Latest source position, {@code PRIMARY} or {@code SECONDARY} (null before any change). */
+ SourcePosition sourcePosition();
+
+ /** {@code isFromClient()} of the most recent {@code SourceCollapseChangedEvent}. */
+ boolean lastCollapseFromClient();
+
+ /** {@code isFromClient()} of the most recent {@code SourcePositionChangedEvent}. */
+ boolean lastPositionFromClient();
+
+ /** {@code isFromClient()} of the most recent {@code OrientationChangedEvent}. */
+ boolean lastOrientationFromClient();
+
+}
diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayControlsIT.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayControlsIT.java
new file mode 100644
index 0000000..d783911
--- /dev/null
+++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayControlsIT.java
@@ -0,0 +1,189 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.demo.it;
+
+import static com.vaadin.flow.component.splitlayout.SplitLayout.Orientation.HORIZONTAL;
+import static com.vaadin.flow.component.splitlayout.SplitLayout.Orientation.VERTICAL;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import com.flowingcode.vaadin.testbench.rpc.HasRpcSupport;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Dimension;
+import org.openqa.selenium.WebElement;
+
+/**
+ * Integration tests for the source-code viewer overlay controls. Each test clicks a rendered
+ * overlay button and then asserts the effect on both the DOM (CSS-driven visibility, slot
+ * placement, orientation attribute) and the server-side state, the latter read over testbench-rpc
+ * through {@link OverlayCallables} — see {@link OverlayView} for the {@code @ClientCallable}
+ * implementation.
+ *
+ *
+ * The unit ({@code SplitLayoutDemoTest}) and UI-unit ({@code TabbedDemoUIUnitTest}) layers cover
+ * the mechanics and the programmatic ({@code fromClient=false}) paths; these tests add what only a
+ * real browser can show: CSS visibility, real geometry, and the genuine {@code fromClient=true}
+ * origin of a rendered button click.
+ */
+public class OverlayControlsIT extends AbstractViewTest implements HasRpcSupport {
+
+ private final OverlayCallables $server = createCallableProxy(OverlayCallables.class);
+
+ public OverlayControlsIT() {
+ super(null);
+ }
+
+ /** Landscape viewport (deterministic HORIZONTAL, non-collapsed baseline) + navigate. */
+ private void open() {
+ getDriver().manage().window().setSize(new Dimension(1200, 800));
+ getDriver().get(getURL("it/rpc-overlay"));
+ getCommandExecutor().waitForVaadin();
+ getDriver().findElement(By.id("content"));
+ waitUntil(d -> hasElement("vaadin-button.source-code-viewer-hide-button"));
+ }
+
+ private WebElement button(String name) {
+ return getDriver()
+ .findElement(By.cssSelector("vaadin-button.source-code-viewer-" + name + "-button"));
+ }
+
+ private void click(String name) {
+ button(name).click();
+ getCommandExecutor().waitForVaadin();
+ }
+
+ private boolean hasElement(String cssSelector) {
+ return !getDriver().findElements(By.cssSelector(cssSelector)).isEmpty();
+ }
+
+ private String orientationAttribute() {
+ return getDriver().findElement(By.cssSelector("vaadin-split-layout")).getAttribute("orientation");
+ }
+
+ /** Rendered pixel width of the source panel (the slotted {@code code-viewer}). */
+ private int sourcePanelWidth() {
+ // the slot that contains the source-code-viewer
+ String xpath = "//vaadin-split-layout/*[div[contains(@class, 'source-code-viewer')]]";
+ return getDriver().findElement(By.xpath(xpath)).getSize().getWidth();
+ }
+
+ /** Rendered pixel width of the whole split layout. */
+ private int splitLayoutWidth() {
+ return getDriver().findElement(By.cssSelector("vaadin-split-layout")).getSize().getWidth();
+ }
+
+ /** IT-BTN-01 — the action buttons are visible, the show button hidden, at a normal split. */
+ @Test
+ public void overlayButtonsVisibility() {
+ open();
+ assertTrue(button("hide").isDisplayed());
+ assertTrue(button("flip").isDisplayed());
+ assertTrue(button("rotate").isDisplayed());
+ assertFalse(button("show").isDisplayed());
+ }
+
+ /** IT-COL-01 — clicking Hide collapses the panel, reveals Show, and is client-originated. */
+ @Test
+ public void clickingHideCollapsesAndRevealsShow() {
+ open();
+ assertFalse($server.sourceCollapsed());
+
+ click("hide");
+
+ assertTrue($server.sourceCollapsed());
+ assertTrue($server.lastCollapseFromClient());
+ waitUntil(d -> button("show").isDisplayed());
+ // real geometry: the collapsed source panel measures ~0px wide
+ waitUntil(d -> sourcePanelWidth() < 5);
+ assertTrue("collapsed source panel should measure ~0px but was " + sourcePanelWidth(),
+ sourcePanelWidth() < 5);
+ }
+
+ /** IT-COL-02 — clicking Show restores the panel. */
+ @Test
+ public void clickingShowRestoresPanel() {
+ open();
+ click("hide");
+ waitUntil(d -> button("show").isDisplayed());
+
+ click("show");
+
+ assertFalse($server.sourceCollapsed());
+ waitUntil(d -> button("hide").isDisplayed());
+ assertFalse(button("show").isDisplayed());
+ // real geometry: the splitter returns to ~50, so the source panel spans ~half the layout
+ waitUntil(d -> {
+ int total = splitLayoutWidth();
+ return total > 0 && Math.abs(sourcePanelWidth() - total / 2.0) < total * 0.1;
+ });
+ double ratio = (double) sourcePanelWidth() / splitLayoutWidth();
+ assertTrue("restored source panel should span ~50% of the layout but was " + ratio,
+ ratio > 0.4 && ratio < 0.6);
+ }
+
+ /** IT-FLIP-01 — clicking Flip swaps the source viewer between slots (client-originated). */
+ @Test
+ public void clickingFlipSwapsSlots() {
+ open();
+ assertTrue(hasElement("[slot='secondary'] code-viewer"));
+ assertFalse(hasElement("[slot='primary'] code-viewer"));
+
+ click("flip");
+
+ waitUntil(d -> hasElement("[slot='primary'] code-viewer"));
+ assertTrue($server.lastPositionFromClient());
+
+ click("flip");
+ waitUntil(d -> hasElement("[slot='secondary'] code-viewer"));
+ }
+
+ /** IT-ROT-01 — clicking Rotate toggles the split orientation (client-originated). */
+ @Test
+ public void clickingRotateTogglesOrientation() {
+ open();
+ assertEquals(HORIZONTAL, $server.orientation());
+ assertEquals("horizontal", orientationAttribute());
+
+ click("rotate");
+
+ assertEquals(VERTICAL, $server.orientation());
+ waitUntil(d -> "vertical".equals(orientationAttribute()));
+ assertTrue($server.lastOrientationFromClient());
+
+ click("rotate");
+ assertEquals(HORIZONTAL, $server.orientation());
+ }
+
+ /**
+ * IT-VIS-01 (optional) — at the collapsed extreme the overlay container falls below every
+ * {@code @container} threshold, so only the show button remains visible.
+ */
+ @Test
+ public void collapsedPanelHidesActionButtons() {
+ open();
+ click("hide");
+ waitUntil(d -> button("show").isDisplayed());
+
+ assertFalse(button("hide").isDisplayed());
+ assertFalse(button("flip").isDisplayed());
+ assertFalse(button("rotate").isDisplayed());
+ }
+}
diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayView.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayView.java
new file mode 100644
index 0000000..4515dcb
--- /dev/null
+++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayView.java
@@ -0,0 +1,103 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.demo.it;
+
+import com.flowingcode.vaadin.addons.GithubLink;
+import com.flowingcode.vaadin.addons.demo.DemoSource;
+import com.flowingcode.vaadin.addons.demo.SourcePosition;
+import com.flowingcode.vaadin.addons.demo.TabbedDemo;
+import com.flowingcode.vaadin.jsonmigration.InstrumentedRoute;
+import com.flowingcode.vaadin.jsonmigration.JsonMigration;
+import com.flowingcode.vaadin.jsonmigration.LegacyClientCallable;
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.Span;
+import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation;
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+@SuppressWarnings("serial")
+@GithubLink("https://github.com/FlowingCode/CommonsDemo")
+public class OverlayView extends TabbedDemo implements OverlayCallables {
+
+ private boolean collapsed;
+ private boolean collapseFromClient;
+ private SourcePosition position;
+ private boolean positionFromClient;
+ private boolean orientationFromClient;
+
+ public OverlayView() {
+ addDemo(JsonMigration.instrumentClass(RpcOverlayDemo.class));
+ addSourceCollapseListener(ev -> {
+ collapsed = ev.isCollapsed();
+ collapseFromClient = ev.isFromClient();
+ });
+ addSourcePositionChangedListener(ev -> {
+ position = ev.getSourcePosition();
+ positionFromClient = ev.isFromClient();
+ });
+ addOrientationChangedListener(ev -> orientationFromClient = ev.isFromClient());
+ }
+
+ @Override
+ @LegacyClientCallable
+ public JsonValue $call(JsonObject invocation) {
+ return OverlayCallables.super.$call(invocation);
+ }
+
+ @Override
+ public Orientation orientation() {
+ return getOrientation();
+ }
+
+ @Override
+ public boolean sourceCollapsed() {
+ return collapsed;
+ }
+
+ @Override
+ public SourcePosition sourcePosition() {
+ return position;
+ }
+
+ @Override
+ public boolean lastCollapseFromClient() {
+ return collapseFromClient;
+ }
+
+ @Override
+ public boolean lastPositionFromClient() {
+ return positionFromClient;
+ }
+
+ @Override
+ public boolean lastOrientationFromClient() {
+ return orientationFromClient;
+ }
+
+ /** Single source-bearing demo, so the overlay controls are rendered. */
+ @DemoSource(clazz = OverlayView.class)
+ @InstrumentedRoute(value = "it/rpc-overlay", layout = OverlayView.class)
+ public static class RpcOverlayDemo extends Div {
+
+ public RpcOverlayDemo() {
+ add(new Span("RpcOverlayDemo"));
+ }
+ }
+}
diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java
new file mode 100644
index 0000000..d1e1053
--- /dev/null
+++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java
@@ -0,0 +1,171 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.demo.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import com.flowingcode.vaadin.addons.demo.SourcePosition;
+import com.flowingcode.vaadin.addons.demo.events.OrientationChangedEvent;
+import com.flowingcode.vaadin.addons.demo.events.SourceCollapseChangedEvent;
+import com.flowingcode.vaadin.addons.demo.events.SourcePositionChangedEvent;
+import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewMultiSource;
+import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewNoSource;
+import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewSingleSource;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation;
+import com.vaadin.flow.dom.Element;
+import com.vaadin.testbench.unit.UIUnit4Test;
+import com.vaadin.testbench.unit.ViewPackages;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+
+@ViewPackages(classes = TabbedDemoView.class)
+public class TabbedDemoUIUnitTest extends UIUnit4Test {
+
+ private static final String TAG_CODE_VIEWER = "code-viewer";
+ private static final String TAG_SOURCE_CODE_VIEWER_BUTTONS = "source-code-viewer-buttons";
+
+ private static TabbedDemoView openWithSource() {
+ TabbedDemoView demo = new TabbedDemoView();
+ demo.showRouterLayoutContent(new TabbedDemoViewSingleSource());
+ return demo;
+ }
+
+ // --- the overlay is present exactly when the demo has source ----------------------
+
+ @Test
+ public void overlayPresentForSingleSource() {
+ TabbedDemoView demo = openWithSource();
+ assertEquals(1, count(demo.getElement(), TAG_SOURCE_CODE_VIEWER_BUTTONS));
+ assertEquals(1, count(demo.getElement(), TAG_CODE_VIEWER));
+ }
+
+ @Test
+ public void overlayPresentOnceForMultiSource() {
+ TabbedDemoView demo = new TabbedDemoView();
+ demo.showRouterLayoutContent(new TabbedDemoViewMultiSource());
+ assertEquals(1, count(demo.getElement(), TAG_SOURCE_CODE_VIEWER_BUTTONS));
+ assertEquals(1, count(demo.getElement(), TAG_CODE_VIEWER));
+ }
+
+ @Test
+ public void noOverlayWhenDemoHasNoSource() {
+ TabbedDemoView demo = new TabbedDemoView();
+ demo.showRouterLayoutContent(new TabbedDemoViewNoSource());
+ assertEquals(0, count(demo.getElement(), TAG_SOURCE_CODE_VIEWER_BUTTONS));
+ assertEquals(0, count(demo.getElement(), TAG_CODE_VIEWER));
+ }
+
+ // --- setSourceVisible collapses/expands and fires the event (fromClient=false) ----
+
+ @Test
+ public void setSourceVisibleFiresCollapseEvent() {
+ TabbedDemoView demo = openWithSource();
+ List events = new ArrayList<>();
+ demo.addSourceCollapseListener(events::add);
+
+ demo.setSourceVisible(false);
+ assertEquals(1, events.size());
+ assertTrue(events.get(0).isCollapsed());
+ assertFalse(events.get(0).isFromClient());
+
+ demo.setSourceVisible(true);
+ assertEquals(2, events.size());
+ assertFalse(events.get(1).isCollapsed());
+ assertFalse(events.get(1).isFromClient());
+ }
+
+ // --- toggleSourcePosition toggles the position and fires the event ---------------
+
+ @Test
+ public void toggleSourcePositionFiresEventAndToggles() {
+ TabbedDemoView demo = openWithSource();
+ List events = new ArrayList<>();
+ demo.addSourcePositionChangedListener(events::add);
+
+ demo.toggleSourcePosition();
+ assertEquals(1, events.size());
+ // Single source defaults to SECONDARY, so the first toggle yields PRIMARY.
+ assertEquals(SourcePosition.PRIMARY, events.get(0).getSourcePosition());
+ assertFalse(events.get(0).isFromClient());
+
+ demo.toggleSourcePosition();
+ assertEquals(2, events.size());
+ assertEquals(SourcePosition.SECONDARY, events.get(1).getSourcePosition());
+ assertNotEquals(events.get(0).getSourcePosition(), events.get(1).getSourcePosition());
+ }
+
+ // --- setOrientation changes state and fires the event; same value is a no-op ------
+
+ @Test
+ public void setOrientationChangesStateAndFiresEvent() {
+ TabbedDemoView demo = openWithSource();
+ assertEquals(Orientation.HORIZONTAL, demo.getOrientation());
+ List events = new ArrayList<>();
+ demo.addOrientationChangedListener(events::add);
+
+ demo.setOrientation(Orientation.VERTICAL);
+
+ assertEquals(Orientation.VERTICAL, demo.getOrientation());
+ assertEquals(1, events.size());
+ assertEquals(Orientation.VERTICAL, events.get(0).getOrientation());
+ assertFalse(events.get(0).isFromClient());
+ }
+
+ @Test
+ public void settingCurrentOrientationIsANoOp() {
+ TabbedDemoView demo = openWithSource();
+ List events = new ArrayList<>();
+ demo.addOrientationChangedListener(events::add);
+
+ demo.setOrientation(Orientation.HORIZONTAL); // already horizontal
+
+ assertEquals(Orientation.HORIZONTAL, demo.getOrientation());
+ assertTrue(events.isEmpty());
+ }
+
+ // --- orientation carries over to the next demo ------------------------------------
+
+ @Test
+ public void orientationPersistsAcrossContentChange() {
+ TabbedDemoView demo = new TabbedDemoView();
+ Component first = new TabbedDemoViewSingleSource();
+ demo.showRouterLayoutContent(first);
+ demo.setOrientation(Orientation.VERTICAL);
+ assertEquals(Orientation.VERTICAL, demo.getOrientation());
+
+ demo.removeRouterLayoutContent(first);
+ demo.showRouterLayoutContent(new TabbedDemoViewMultiSource());
+
+ assertEquals(Orientation.VERTICAL, demo.getOrientation());
+ }
+
+ /** Counts elements with the given tag in the subtree rooted at {@code root} (inclusive). */
+ private static long count(Element root, String tag) {
+ long self = tag.equals(root.getTag()) ? 1 : 0;
+ return self + root.getChildren()
+ .filter(e -> !e.isTextNode())
+ .mapToLong(child -> count(child, tag)).sum();
+ }
+
+}
diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/ViewInitializerImpl.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/ViewInitializerImpl.java
new file mode 100644
index 0000000..5de5e57
--- /dev/null
+++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/ViewInitializerImpl.java
@@ -0,0 +1,37 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.demo.it;
+import com.flowingcode.vaadin.jsonmigration.InstrumentationViewInitializer;
+import com.vaadin.flow.server.ServiceInitEvent;
+
+/**
+ * Service initializer for instrumented test views.
+ *
+ * @author Javier Godoy / Flowing Code
+ */
+@SuppressWarnings("serial")
+public class ViewInitializerImpl extends InstrumentationViewInitializer {
+
+ @Override
+ public void serviceInit(ServiceInitEvent event) {
+ registerInstrumentedRoute(OverlayView.RpcOverlayDemo.class);
+ }
+
+}
diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/test/SplitLayoutDemoTest.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/test/SplitLayoutDemoTest.java
new file mode 100644
index 0000000..8499bb5
--- /dev/null
+++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/test/SplitLayoutDemoTest.java
@@ -0,0 +1,179 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
+package com.flowingcode.vaadin.addons.demo.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import com.flowingcode.vaadin.addons.demo.SourceCodeTab;
+import com.flowingcode.vaadin.addons.demo.SourcePosition;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.splitlayout.SplitLayout;
+import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.List;
+import lombok.SneakyThrows;
+import lombok.experimental.ExtensionMethod;
+import org.junit.Test;
+
+@ExtensionMethod(value = Reflect.class, suppressBaseMethods = true)
+public class SplitLayoutDemoTest {
+
+ private Div demo;
+
+ private Component build(SourcePosition position) {
+ demo = new Div();
+ demo.setId("content");
+ return Reflect.newSplitLayoutDemo(demo,
+ List.of(new SourceCodeTab("src/test/java/Demo.java", position)));
+ }
+
+ // --- collapse resolves to the correct splitter position -----------------------
+
+ @Test
+ public void collapseWithSecondarySourceMovesSplitterToFullPrimary() {
+ Component layout = build(SourcePosition.SECONDARY);
+
+ layout.setSourceCollapsed(false);
+ assertEquals(50.0, splitterPosition(layout), 0.0);
+
+ layout.setSourceCollapsed(true);
+ assertEquals(100.0, splitterPosition(layout), 0.0);
+
+ layout.setSourceCollapsed(false);
+ assertEquals(50.0, splitterPosition(layout), 0.0);
+ }
+
+ @Test
+ public void collapseWithPrimarySourceMovesSplitterToFullSecondary() {
+ Component layout = build(SourcePosition.PRIMARY);
+
+ layout.setSourceCollapsed(true);
+ assertEquals(0.0, splitterPosition(layout), 0.0);
+
+ layout.setSourceCollapsed(false);
+ assertEquals(50.0, splitterPosition(layout), 0.0);
+ }
+
+ // --- source position swaps slot occupants and toggles cleanly -----------------
+
+ @Test
+ public void sourcePositionSwapsSlotsAndToggles() {
+ Component layout = build(SourcePosition.SECONDARY);
+
+ // Default SECONDARY: demo in primary, source viewer in secondary.
+ assertSame(demo, layout.getContent().getPrimaryComponent());
+ assertNotSame(demo, layout.getContent().getSecondaryComponent());
+ Component code = layout.getContent().getSecondaryComponent();
+
+ layout.setSourcePosition(SourcePosition.PRIMARY);
+ assertSame(code, layout.getContent().getPrimaryComponent());
+ assertSame(demo, layout.getContent().getSecondaryComponent());
+ assertEquals(SourcePosition.PRIMARY, layout.getSourcePosition());
+
+ layout.setSourcePosition(SourcePosition.SECONDARY);
+ assertSame(demo, layout.getContent().getPrimaryComponent());
+ assertSame(code, layout.getContent().getSecondaryComponent());
+ assertEquals(SourcePosition.SECONDARY, layout.getSourcePosition());
+ }
+
+ // --- orientation set/get round-trips ------------------------------------------
+
+ @Test
+ public void orientationRoundTrips() {
+ Component layout = build(SourcePosition.SECONDARY);
+ assertEquals(Orientation.HORIZONTAL, layout.getOrientation());
+
+ layout.setOrientation(Orientation.VERTICAL);
+ assertEquals(Orientation.VERTICAL, layout.getOrientation());
+
+ layout.setOrientation(Orientation.HORIZONTAL);
+ assertEquals(Orientation.HORIZONTAL, layout.getOrientation());
+ }
+
+ private static double splitterPosition(Component layout) {
+ Double position = layout.getContent().getSplitterPosition();
+ assertNotNull("splitter position was never set", position);
+ return position;
+ }
+
+}
+
+
+/**
+ * Reflection accessors for the package-private {@code SplitLayoutDemo} and the protected
+ * {@code Composite.getContent()}, one static method per real method. Surfaced as Lombok
+ * {@code @ExtensionMethod}s so the test can drive the layout with real-API-looking call sites while
+ * the actual dispatch goes through reflection.
+ */
+final class Reflect {
+
+ private Reflect() {}
+
+ @SneakyThrows
+ static Component newSplitLayoutDemo(Component demo, List tabs) {
+ Class> type = Class.forName("com.flowingcode.vaadin.addons.demo.SplitLayoutDemo");
+ Constructor> constructor = type.getDeclaredConstructor(Component.class, List.class);
+ constructor.setAccessible(true);
+ return (Component) constructor.newInstance(demo, tabs);
+ }
+
+ public static void setSourceCollapsed(Component layout, boolean collapsed) {
+ call(layout, "setSourceCollapsed", new Class>[] {boolean.class}, collapsed);
+ }
+
+ public static void setSourcePosition(Component layout, SourcePosition position) {
+ call(layout, "setSourcePosition", new Class>[] {SourcePosition.class}, position);
+ }
+
+ public static void setOrientation(Component layout, Orientation orientation) {
+ call(layout, "setOrientation", new Class>[] {Orientation.class}, orientation);
+ }
+
+ public static Orientation getOrientation(Component layout) {
+ return (Orientation) call(layout, "getOrientation", new Class>[0]);
+ }
+
+ public static SourcePosition getSourcePosition(Component layout) {
+ return (SourcePosition) call(layout, "getSourcePosition", new Class>[0]);
+ }
+
+ public static SplitLayout getContent(Component layout) {
+ return (SplitLayout) call(layout, "getContent", new Class>[0]);
+ }
+
+ /** Locates {@code name(paramTypes)} on {@code target} or a supertype and invokes it. */
+ @SneakyThrows
+ private static Object call(Object target, String name, Class>[] paramTypes, Object... args) {
+ for (Class> c = target.getClass(); c != null; c = c.getSuperclass()) {
+ try {
+ Method method = c.getDeclaredMethod(name, paramTypes);
+ method.setAccessible(true);
+ return method.invoke(target, args);
+ } catch (NoSuchMethodException declaredInSupertype) {
+ // keep walking up the hierarchy
+ }
+ }
+ throw new NoSuchMethodException(target.getClass().getName() + "#" + name);
+ }
+}
diff --git a/base/src/test/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener b/base/src/test/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener
new file mode 100644
index 0000000..2d597cf
--- /dev/null
+++ b/base/src/test/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener
@@ -0,0 +1 @@
+com.flowingcode.vaadin.addons.demo.it.ViewInitializerImpl
\ No newline at end of file
From cdaea11ad91dcbe5e9afeceaa05768d812e96226 Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Thu, 16 Jul 2026 15:16:25 -0300
Subject: [PATCH 14/18] feat: preserve source position when toggling views
---
.../vaadin/addons/demo/DemoSource.java | 9 +++++--
.../addons/demo/MultiSourceCodeViewer.java | 2 +-
.../vaadin/addons/demo/SourcePosition.java | 6 +++--
.../vaadin/addons/demo/SplitLayoutDemo.java | 2 +-
.../vaadin/addons/demo/TabbedDemo.java | 27 ++++++++++++++++++-
.../addons/demo/it/TabbedDemoUIUnitTest.java | 23 ++++++++++++++++
.../vaadin/addons/demo/it/TabbedDemoView.java | 6 +++++
7 files changed, 68 insertions(+), 7 deletions(-)
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java
index 999f9db..1a999ae 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java
@@ -76,8 +76,13 @@
*/
String language() default DEFAULT_VALUE;
- /** Source code position in the layout */
- SourcePosition sourcePosition() default SourcePosition.SECONDARY;
+ /**
+ * Source code position in the layout. Defaults to {@link SourcePosition#DEFAULT}, which adopts the
+ * position carried over from the previously shown demo, or {@link SourcePosition#SECONDARY} if
+ * there is none. An explicit {@link SourcePosition#PRIMARY} or {@link SourcePosition#SECONDARY}
+ * always takes precedence over the carried-over position.
+ */
+ SourcePosition sourcePosition() default SourcePosition.DEFAULT;
String condition() default "";
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java
index 11baff5..ba42e3c 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java
@@ -162,7 +162,7 @@ public SourcePosition getSourcePosition() {
if (selectedTab != null) {
return (SourcePosition) ComponentUtil.getData(selectedTab, DATA_POSITION);
} else {
- return SourcePosition.SECONDARY;
+ return SourcePosition.DEFAULT;
}
}
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SourcePosition.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SourcePosition.java
index fa96948..ce05c21 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SourcePosition.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SourcePosition.java
@@ -21,15 +21,17 @@
public enum SourcePosition {
- PRIMARY, SECONDARY;
+ PRIMARY, SECONDARY, DEFAULT;
public SourcePosition toggle() {
switch (this) {
case SECONDARY:
+ case DEFAULT:
return PRIMARY;
case PRIMARY:
- default:
return SECONDARY;
+ default:
+ return this;
}
}
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
index d093d54..0b4bae6 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
@@ -106,8 +106,8 @@ public void setSourceCollapsed(boolean collapsed) {
getContent().setSplitterPosition(0);
break;
case SECONDARY:
+ default:
getContent().setSplitterPosition(100);
- break;
}
}
}
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
index 358b861..04dfbbf 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
@@ -224,8 +224,10 @@ public void showRouterLayoutContent(HasElement content) {
}
Orientation splitOrientation = null;
+ SourcePosition previousPosition = null;
if (currentLayout != null) {
splitOrientation = currentLayout.getOrientation();
+ previousPosition = currentLayout.getSourcePosition();
}
if (!sourceTabs.isEmpty()) {
@@ -238,6 +240,13 @@ public void showRouterLayoutContent(HasElement content) {
if (currentLayout != null) {
content = currentLayout;
+
+ // A DEFAULT position is soft: it adopts the position carried over from the previously shown
+ // demo (SECONDARY if there is none). An explicit PRIMARY/SECONDARY always wins.
+ if (currentLayout.getSourcePosition() == SourcePosition.DEFAULT) {
+ currentLayout.setSourcePosition(resolveDefaultPosition(previousPosition));
+ }
+
if (splitOrientation != null) {
setOrientation(splitOrientation);
updateSplitterPosition();
@@ -369,10 +378,26 @@ public void setSourcePosition(SourcePosition sourcePosition) {
private void setSourcePosition(SourcePosition sourcePosition, boolean fromClient) {
if (currentLayout != null) {
currentLayout.setSourcePosition(sourcePosition);
- fireSourcePositionChangedEvent(sourcePosition, fromClient);
+ SourcePosition resolvedPosition = sourcePosition == SourcePosition.DEFAULT
+ ? resolveDefaultPosition(currentLayout.getSourcePosition())
+ : sourcePosition;
+ fireSourcePositionChangedEvent(resolvedPosition, fromClient);
}
}
+ /**
+ * Resolves a {@link SourcePosition#DEFAULT} position against the position carried over from the
+ * previously shown demo.
+ *
+ * @param previous the carried-over position, or {@code null} if there is none
+ * @return {@code previous} when it is a concrete position, otherwise {@link SourcePosition#SECONDARY}
+ */
+ private static SourcePosition resolveDefaultPosition(SourcePosition previous) {
+ return (previous == null || previous == SourcePosition.DEFAULT)
+ ? SourcePosition.SECONDARY
+ : previous;
+ }
+
private void toggleSplitterOrientation(boolean fromClient) {
if (currentLayout == null) {
return;
diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java
index d1e1053..721a613 100644
--- a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java
+++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java
@@ -29,6 +29,7 @@
import com.flowingcode.vaadin.addons.demo.events.SourcePositionChangedEvent;
import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewMultiSource;
import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewNoSource;
+import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewPrimarySource;
import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewSingleSource;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation;
@@ -115,6 +116,28 @@ public void toggleSourcePositionFiresEventAndToggles() {
assertNotEquals(events.get(0).getSourcePosition(), events.get(1).getSourcePosition());
}
+ // --- a DEFAULT position inherits the position carried over from the previous demo -
+
+ @Test
+ public void defaultPositionInheritsFromPreviousDemo() {
+ TabbedDemoView demo = new TabbedDemoView();
+ Component first = new TabbedDemoViewPrimarySource();
+ demo.showRouterLayoutContent(first);
+
+ // TabbedDemoViewSingleSource has no explicit position (DEFAULT), so it should adopt
+ // the PRIMARY position carried over from the previous demo.
+ demo.removeRouterLayoutContent(first);
+ demo.showRouterLayoutContent(new TabbedDemoViewSingleSource());
+
+ List events = new ArrayList<>();
+ demo.addSourcePositionChangedListener(events::add);
+
+ demo.toggleSourcePosition();
+ assertEquals(1, events.size());
+ // Inherited PRIMARY, so the first toggle yields SECONDARY.
+ assertEquals(SourcePosition.SECONDARY, events.get(0).getSourcePosition());
+ }
+
// --- setOrientation changes state and fires the event; same value is a no-op ------
@Test
diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoView.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoView.java
index 0aebc70..8d919f4 100644
--- a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoView.java
+++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoView.java
@@ -22,6 +22,7 @@
import com.flowingcode.vaadin.addons.GithubLink;
import com.flowingcode.vaadin.addons.demo.AdHocDemo;
import com.flowingcode.vaadin.addons.demo.DemoSource;
+import com.flowingcode.vaadin.addons.demo.SourcePosition;
import com.flowingcode.vaadin.addons.demo.TabbedDemo;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
@@ -34,6 +35,7 @@ public class TabbedDemoView extends TabbedDemo {
public TabbedDemoView() {
addDemo(TabbedDemoViewNoSource.class);
addDemo(TabbedDemoViewSingleSource.class);
+ addDemo(TabbedDemoViewPrimarySource.class);
addDemo(TabbedDemoViewMultiSource.class);
addDemo(TabbedDemoViewConditionalTrue.class);
addDemo(TabbedDemoViewConditionalFalse.class);
@@ -52,6 +54,10 @@ public static class TabbedDemoViewNoSource extends AbstractDemoView { }
@Route(value = "it/tabbed-demo-single-source", layout = TabbedDemoView.class)
public static class TabbedDemoViewSingleSource extends AbstractDemoView { }
+ @DemoSource(clazz = TabbedDemoView.class, sourcePosition = SourcePosition.PRIMARY)
+ @Route(value = "it/tabbed-demo-primary-source", layout = TabbedDemoView.class)
+ public static class TabbedDemoViewPrimarySource extends AbstractDemoView { }
+
@DemoSource(clazz = TabbedDemoView.class)
@DemoSource(clazz = AdHocDemo.class)
@Route(value = "it/tabbed-demo-multi-source", layout = TabbedDemoView.class)
From c3bba6eeee010704d816c09f474b1f57facc14fc Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Fri, 17 Jul 2026 12:21:38 -0300
Subject: [PATCH 15/18] fix: keep source collapsed when swapping source
position
When the source is collapsed, swapping its side did not touch the
splitter, so the collapsed pane inherited the previous side's
percentage (e.g. showing the source full and hiding the demo instead of
staying collapsed). Reapply the collapsed splitter position for the new
side.
---
.../java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java | 3 +++
1 file changed, 3 insertions(+)
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
index 04dfbbf..8bf1652 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
@@ -378,6 +378,9 @@ public void setSourcePosition(SourcePosition sourcePosition) {
private void setSourcePosition(SourcePosition sourcePosition, boolean fromClient) {
if (currentLayout != null) {
currentLayout.setSourcePosition(sourcePosition);
+ if (sourceCollapsed) {
+ updateSplitterPosition();
+ }
SourcePosition resolvedPosition = sourcePosition == SourcePosition.DEFAULT
? resolveDefaultPosition(currentLayout.getSourcePosition())
: sourcePosition;
From 0d9f7ac4aa5dafbefea6ecb75ec48e128b0fa0dd Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Fri, 17 Jul 2026 12:21:50 -0300
Subject: [PATCH 16/18] fix: preserve source collapse state when changing
orientation
Changing the orientation forced the source visible via
setSourceCollapsed(false) without updating sourceCollapsed or notifying
listeners, leaving the layout and its state out of sync (e.g. a
collapsed source was re-expanded on mobile). Reapply the current
collapse state instead.
---
.../java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
index 8bf1652..9ca8f6a 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
@@ -436,7 +436,7 @@ public void setOrientation(Orientation orientation) {
private void setOrientation(Orientation orientation, boolean fromClient) {
if (currentLayout != null && orientation != getOrientation()) {
currentLayout.setOrientation(orientation);
- currentLayout.setSourceCollapsed(false);
+ updateSplitterPosition();
fireOrientationChangedEvent(orientation, fromClient);
}
}
From da1d264a1397d3c582654a5fd0dad1a948109f23 Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Fri, 17 Jul 2026 16:14:45 -0300
Subject: [PATCH 17/18] docs: add license headers
---
.../vaadin/addons/demo/DynamicTheme.java | 19 +++++++++++++++++
.../addons/demo/DynamicThemeInitializer.java | 19 +++++++++++++++++
.../META-INF/VAADIN/package.properties | 19 +++++++++++++++++
.../commons-demo/vaadin-select-overlay.css | 21 ++++++++++++++++++-
.../addons/demo/AppShellConfiguratorImpl.java | 19 +++++++++++++++++
5 files changed, 96 insertions(+), 1 deletion(-)
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java
index 2252e9b..6319247 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java
@@ -1,3 +1,22 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
package com.flowingcode.vaadin.addons.demo;
import com.vaadin.flow.component.Component;
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java
index 2a4c7fa..fad5616 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java
@@ -1,3 +1,22 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
package com.flowingcode.vaadin.addons.demo;
import com.vaadin.flow.server.ServiceInitEvent;
diff --git a/base/src/main/resources/META-INF/VAADIN/package.properties b/base/src/main/resources/META-INF/VAADIN/package.properties
index c66616f..2391356 100644
--- a/base/src/main/resources/META-INF/VAADIN/package.properties
+++ b/base/src/main/resources/META-INF/VAADIN/package.properties
@@ -1 +1,20 @@
+###
+# #%L
+# Commons Demo
+# %%
+# Copyright (C) 2020 - 2026 Flowing Code
+# %%
+# 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.
+# #L%
+###
vaadin.allowed-packages=com.flowingcode
diff --git a/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/vaadin-select-overlay.css b/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/vaadin-select-overlay.css
index dd6c297..6c8cd13 100644
--- a/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/vaadin-select-overlay.css
+++ b/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/vaadin-select-overlay.css
@@ -1,4 +1,23 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
/** Needed in order to prevent vaadin-select from remaining 'closing' when switching from Lumo to Aura/Base*/
:host([theme~='demo-footer-theme-select']) vaadin-select-overlay {
animation-name: none;
-}
\ No newline at end of file
+}
diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/AppShellConfiguratorImpl.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/AppShellConfiguratorImpl.java
index e2195ae..a4a9994 100644
--- a/base/src/test/java/com/flowingcode/vaadin/addons/demo/AppShellConfiguratorImpl.java
+++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/AppShellConfiguratorImpl.java
@@ -1,3 +1,22 @@
+/*-
+ * #%L
+ * Commons Demo
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
package com.flowingcode.vaadin.addons.demo;
import com.vaadin.flow.component.page.AppShellConfigurator;
From 2bad090c3fb0433b235154e5a72b8239842bd0d9 Mon Sep 17 00:00:00 2001
From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com>
Date: Fri, 17 Jul 2026 16:45:46 -0300
Subject: [PATCH 18/18] fix: preserve splitOrientation in demo without source
---
.../vaadin/addons/demo/TabbedDemo.java | 29 ++++++++++++-------
.../addons/demo/it/TabbedDemoUIUnitTest.java | 17 +++++++++++
2 files changed, 35 insertions(+), 11 deletions(-)
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
index 9ca8f6a..0cab2b8 100644
--- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
@@ -79,6 +79,10 @@ public class TabbedDemo extends VerticalLayout implements RouterLayout {
private HorizontalLayout footer;
private SplitLayoutDemo currentLayout;
private Checkbox themeCB;
+ // The desired split orientation. Mirrors the current layout's orientation when one exists, and
+ // outlives it while no layout is present, so a device-driven adjustment (e.g. portrait -> VERTICAL
+ // made on a source-less demo) is carried over to the next source-bearing demo.
+ private Orientation splitOrientation = Orientation.HORIZONTAL;
private Button helperButton;
private DemoHelperViewer demoHelperViewer;
@@ -223,10 +227,8 @@ public void showRouterLayoutContent(HasElement content) {
createSourceCodeTab(demo.getClass(), demoSource).ifPresent(sourceTabs::add);
}
- Orientation splitOrientation = null;
SourcePosition previousPosition = null;
if (currentLayout != null) {
- splitOrientation = currentLayout.getOrientation();
previousPosition = currentLayout.getSourcePosition();
}
@@ -247,10 +249,11 @@ public void showRouterLayoutContent(HasElement content) {
currentLayout.setSourcePosition(resolveDefaultPosition(previousPosition));
}
- if (splitOrientation != null) {
- setOrientation(splitOrientation);
- updateSplitterPosition();
- }
+ // A freshly created layout defaults to HORIZONTAL; adopt the orientation remembered across
+ // demos (which may have changed while no layout was present). This is initialization, not a
+ // state change, so it does not fire an OrientationChangedEvent.
+ currentLayout.setOrientation(splitOrientation);
+ updateSplitterPosition();
setupDemoHelperButton(currentLayout.getContent().getPrimaryComponent().getClass());
} else {
@@ -421,7 +424,7 @@ private void toggleSplitterOrientation(boolean fromClient) {
* @return the current orientation
*/
public Orientation getOrientation() {
- return currentLayout.getOrientation();
+ return splitOrientation;
}
/**
@@ -434,11 +437,15 @@ public void setOrientation(Orientation orientation) {
}
private void setOrientation(Orientation orientation, boolean fromClient) {
- if (currentLayout != null && orientation != getOrientation()) {
- currentLayout.setOrientation(orientation);
- updateSplitterPosition();
- fireOrientationChangedEvent(orientation, fromClient);
+ if (orientation == splitOrientation) {
+ return;
+ }
+ splitOrientation = orientation;
+ if (currentLayout != null) {
+ currentLayout.setOrientation(orientation);
+ updateSplitterPosition();
}
+ fireOrientationChangedEvent(orientation, fromClient);
}
/**
diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java
index 721a613..eab91c9 100644
--- a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java
+++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java
@@ -183,6 +183,23 @@ public void orientationPersistsAcrossContentChange() {
assertEquals(Orientation.VERTICAL, demo.getOrientation());
}
+ @Test
+ public void orientationSetWithoutLayoutCarriesOverToNextDemo() {
+ // A source-less demo has no layout; setting the orientation there (as a device-driven portrait
+ // adjustment does on attach) must be remembered and applied to the next source-bearing demo.
+ TabbedDemoView demo = new TabbedDemoView();
+ Component noSource = new TabbedDemoViewNoSource();
+ demo.showRouterLayoutContent(noSource);
+
+ demo.setOrientation(Orientation.VERTICAL);
+ assertEquals(Orientation.VERTICAL, demo.getOrientation());
+
+ demo.removeRouterLayoutContent(noSource);
+ demo.showRouterLayoutContent(new TabbedDemoViewSingleSource());
+
+ assertEquals(Orientation.VERTICAL, demo.getOrientation());
+ }
+
/** Counts elements with the given tag in the subtree rooted at {@code root} (inclusive). */
private static long count(Element root, String tag) {
long self = tag.equals(root.getTag()) ? 1 : 0;