diff --git a/base/pom.xml b/base/pom.xml
index 05e6fd0..0508fdd 100644
--- a/base/pom.xml
+++ b/base/pom.xml
@@ -5,7 +5,7 @@
com.flowingcode.vaadin.addons.democommons-demo
- 5.3.2-SNAPSHOT
+ 5.4.0-SNAPSHOTCommons DemoCommon classes for add-ons demo
@@ -75,6 +75,12 @@
vaadin-coretrue
+
+
+ com.vaadin
+ vaadin
+ test
+ org.projectlombok
@@ -99,13 +105,13 @@
com.flowingcode.vaadinjson-migration-helper
- 0.9.2
+ 0.9.5junitjunit
- 4.11
+ 4.13.2test
@@ -126,7 +132,14 @@
-
+
+
+ com.flowingcode.vaadin.test
+ testbench-rpc
+ 1.5.0
+ test
+
+
io.github.bonigarciawebdrivermanager
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/CommonsDemoIcons.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/CommonsDemoIcons.java
new file mode 100644
index 0000000..e9bf8ac
--- /dev/null
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/CommonsDemoIcons.java
@@ -0,0 +1,78 @@
+/*-
+ * #%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.dependency.JsModule;
+import com.vaadin.flow.component.icon.IconFactory;
+import java.util.Locale;
+
+/**
+ * CommonsDemo icons.
+ *
+ * @author Javier Godoy / Flowing Code
+ */
+public enum CommonsDemoIcons implements IconFactory {
+ ROTATE, FLIP, HIDE_SOURCE, SHOW_SOURCE, GITHUB, HOME;
+
+ /**
+ * The Iconset name, i.e. {@code "commons-demo"}.
+ */
+ public static final String ICONSET = "commons-demo";
+
+ /**
+ * Return the full icon name.
+ *
+ * @return the full icon name, i.e. {@code "commons-demo:name"}.
+ */
+ public String getIconName() {
+ return ICONSET + ':' + getIconPart();
+ }
+
+ /**
+ * Return the icon name within the iconset.
+ *
+ * @return the icon name, i.e. {@code "name"}.
+ */
+ public String getIconPart() {
+ return name().toLowerCase(Locale.ENGLISH).replace('_', '-').replaceFirst("^-", "");
+ }
+
+ /**
+ * Create a new {@link Icon} instance with the icon determined by the name.
+ *
+ * @return a new instance of {@link Icon} component
+ */
+ @Override
+ public Icon create() {
+ return new Icon(getIconPart());
+ }
+
+ /**
+ * Server side component for CommonsDemo icons.
+ */
+ @JsModule("./commons-demo-iconset.ts")
+ @SuppressWarnings("serial")
+ public static final class Icon extends com.vaadin.flow.component.icon.Icon {
+ private Icon(String icon) {
+ super(ICONSET, icon);
+ }
+ }
+
+}
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..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
@@ -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__";
@@ -74,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/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();
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/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java
index 1fccdc2..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
@@ -76,6 +76,20 @@ public MultiSourceCodeViewer(List sourceCodeTabs, Map properties) {
}
public SourceCodeViewer(String url, String language, Map 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:
+ *
+ * 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/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 5d83475..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
@@ -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());
@@ -59,7 +59,11 @@ public boolean isEmpty() {
return code.isEmpty();
}
- private void setSourcePosition(SourcePosition position) {
+ public SourcePosition getSourcePosition() {
+ return sourcePosition;
+ }
+
+ public void setSourcePosition(SourcePosition position) {
if (!position.equals(sourcePosition)) {
getContent().removeAll();
switch (position) {
@@ -76,10 +80,6 @@ private void setSourcePosition(SourcePosition position) {
}
}
- public void toggleSourcePosition() {
- setSourcePosition(sourcePosition.toggle());
- }
-
public void setOrientation(Orientation o) {
getContent().setOrientation(o);
getContent()
@@ -93,26 +93,22 @@ public Orientation getOrientation() {
return getContent().getOrientation();
}
- public void setSplitterPosition(int pos) {
- getContent().setSplitterPosition(pos);
- }
-
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:
+ default:
+ getContent().setSplitterPosition(100);
+ }
}
}
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 cc70e12..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
@@ -20,6 +20,9 @@
package com.flowingcode.vaadin.addons.demo;
import com.flowingcode.vaadin.addons.GithubBranch;
+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.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEventListener;
@@ -42,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;
@@ -70,14 +74,15 @@ public class TabbedDemo extends VerticalLayout implements RouterLayout {
private static final int MOBILE_DEVICE_BREAKPOINT_WIDTH = 768;
private boolean autoVisibility;
+ private boolean sourceCollapsed;
private EnhancedRouteTabs tabs;
private HorizontalLayout footer;
private SplitLayoutDemo currentLayout;
- private Checkbox orientationCB;
- private Checkbox codeCB;
- private Checkbox codePositionCB;
private Checkbox themeCB;
- private Orientation splitOrientation;
+ // 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;
@@ -89,23 +94,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();
- }
+ // 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();
});
- codeCB = new Checkbox("Show Source Code");
- codeCB.setValue(true);
- codeCB.addClassName("smallcheckbox");
- codeCB.addValueChangeListener(ev -> updateSplitterPosition());
- codePositionCB = new Checkbox("Toggle Code Position");
- codePositionCB.setValue(true);
- codePositionCB.addClassName("smallcheckbox");
- codePositionCB.addValueChangeListener(ev -> toggleSourcePosition());
+ 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");
@@ -117,7 +113,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();
@@ -231,6 +227,11 @@ public void showRouterLayoutContent(HasElement content) {
createSourceCodeTab(demo.getClass(), demoSource).ifPresent(sourceTabs::add);
}
+ SourcePosition previousPosition = null;
+ if (currentLayout != null) {
+ previousPosition = currentLayout.getSourcePosition();
+ }
+
if (!sourceTabs.isEmpty()) {
currentLayout = new SplitLayoutDemo(demo, sourceTabs);
if (currentLayout.isEmpty()) {
@@ -241,11 +242,19 @@ public void showRouterLayoutContent(HasElement content) {
if (currentLayout != null) {
content = currentLayout;
- if (splitOrientation != null) {
- setOrientation(splitOrientation);
- updateSplitterPosition();
+
+ // 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));
}
+ // 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 {
currentLayout = null;
@@ -334,13 +343,7 @@ public void removeRouterLayoutContent(HasElement oldContent) {
private void updateSplitterPosition() {
if (currentLayout != null) {
- if (codeCB.getValue()) {
- currentLayout.showSourceCode();
- } else {
- currentLayout.hideSourceCode();
- }
- orientationCB.setEnabled(codeCB.getValue());
- codePositionCB.setEnabled(codeCB.getValue());
+ currentLayout.setSourceCollapsed(sourceCollapsed);
}
}
@@ -350,29 +353,69 @@ 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);
}
/**
* Toggles the position of the source code relative to the demo content.
*/
public void toggleSourcePosition() {
+ toggleSourcePosition(false);
+ }
+
+ private void toggleSourcePosition(boolean fromClient) {
if (currentLayout != null) {
- currentLayout.toggleSourcePosition();
+ setSourcePosition(currentLayout.getSourcePosition().toggle(), fromClient);
}
}
- private void toggleSplitterOrientation() {
+ /**
+ * Sets the position of the source code relative to the demo content.
+ *
+ * @param sourcePosition the new source position
+ */
+ public void setSourcePosition(SourcePosition sourcePosition) {
+ setSourcePosition(sourcePosition, false);
+ }
+
+ 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;
+ 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;
}
+
+ Orientation splitOrientation = getOrientation();
if (Orientation.HORIZONTAL.equals(splitOrientation)) {
splitOrientation = Orientation.VERTICAL;
} else {
splitOrientation = Orientation.HORIZONTAL;
}
- setOrientation(splitOrientation);
+ setOrientation(splitOrientation, fromClient);
}
/**
@@ -381,7 +424,7 @@ private void toggleSplitterOrientation() {
* @return the current orientation
*/
public Orientation getOrientation() {
- return currentLayout.getOrientation();
+ return splitOrientation;
}
/**
@@ -390,12 +433,19 @@ public Orientation getOrientation() {
* @param orientation the new orientation
*/
public void setOrientation(Orientation orientation) {
+ setOrientation(orientation, false);
+ }
+
+ private void setOrientation(Orientation orientation, boolean fromClient) {
+ if (orientation == splitOrientation) {
+ return;
+ }
splitOrientation = orientation;
if (currentLayout != null) {
currentLayout.setOrientation(orientation);
- currentLayout.setSplitterPosition(50);
+ updateSplitterPosition();
}
- orientationCB.setValue(Orientation.HORIZONTAL.equals(orientation));
+ fireOrientationChangedEvent(orientation, fromClient);
}
/**
@@ -466,7 +516,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);
@@ -491,9 +541,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);
}
/**
@@ -512,8 +559,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(codeCB.getValue() && !mobile);
- codePositionCB.setValue(codeCB.getValue() && !mobile);
+ boolean shouldCollapse = sourceCollapsed || mobile;
+ if (shouldCollapse != sourceCollapsed) {
+ fireSourceCollapseChangedEvent(shouldCollapse, false);
+ }
boolean portraitOrientation = receiver.getBodyClientHeight() > receiver.getBodyClientWidth();
adjustSplitOrientation(portraitOrientation);
@@ -522,11 +571,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);
}
/**
@@ -557,4 +605,46 @@ private void setupDemoHelperButton(Class> helperClass) {
}
}
+ /**
+ * Adds a listener for {@link SourceCollapseChangedEvent}.
+ *
+ * @param listener the listener to add
+ */
+ public void addSourceCollapseListener(
+ ComponentEventListener listener) {
+ ComponentUtil.addListener(this, SourceCollapseChangedEvent.class, listener);
+ }
+
+ /**
+ * Adds a listener for {@link SourcePositionChangedEvent}.
+ *
+ * @param listener the listener to add
+ */
+ public void addSourcePositionChangedListener(
+ ComponentEventListener listener) {
+ ComponentUtil.addListener(this, SourcePositionChangedEvent.class, listener);
+ }
+
+ /**
+ * Adds a listener for {@link OrientationChangedEvent}.
+ *
+ * @param listener the listener to add
+ */
+ public void addOrientationChangedListener(
+ ComponentEventListener listener) {
+ ComponentUtil.addListener(this, OrientationChangedEvent.class, listener);
+ }
+
+ private void fireSourceCollapseChangedEvent(boolean collapsed, boolean fromClient) {
+ fireEvent(new SourceCollapseChangedEvent(this, fromClient, collapsed));
+ }
+
+ private void fireSourcePositionChangedEvent(SourcePosition sourcePosition, boolean fromClient) {
+ fireEvent(new SourcePositionChangedEvent(this, fromClient, sourcePosition));
+ }
+
+ private void fireOrientationChangedEvent(Orientation orientation, boolean fromClient) {
+ fireEvent(new OrientationChangedEvent(this, fromClient, orientation));
+ }
+
}
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/OrientationChangedEvent.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/OrientationChangedEvent.java
new file mode 100644
index 0000000..ba79252
--- /dev/null
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/OrientationChangedEvent.java
@@ -0,0 +1,38 @@
+/*-
+ * #%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.events;
+
+import com.flowingcode.vaadin.addons.demo.TabbedDemo;
+import com.vaadin.flow.component.ComponentEvent;
+import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation;
+import lombok.Getter;
+
+@SuppressWarnings("serial")
+public class OrientationChangedEvent extends ComponentEvent {
+
+ @Getter
+ private Orientation orientation;
+
+ public OrientationChangedEvent(TabbedDemo source, boolean fromClient, Orientation orientation) {
+ super(source, fromClient);
+ this.orientation = orientation;
+ }
+
+}
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourceCollapseChangedEvent.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourceCollapseChangedEvent.java
new file mode 100644
index 0000000..0877a6a
--- /dev/null
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourceCollapseChangedEvent.java
@@ -0,0 +1,41 @@
+/*-
+ * #%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.events;
+
+import com.flowingcode.vaadin.addons.demo.TabbedDemo;
+import com.vaadin.flow.component.ComponentEvent;
+import com.vaadin.flow.component.DomEvent;
+import com.vaadin.flow.component.EventData;
+import lombok.Getter;
+
+@SuppressWarnings("serial")
+@DomEvent("source-collapse-changed")
+public class SourceCollapseChangedEvent extends ComponentEvent {
+
+ @Getter
+ private boolean collapsed;
+
+ public SourceCollapseChangedEvent(TabbedDemo source, boolean fromClient,
+ @EventData("event.detail.collapsed") boolean collapsed) {
+ super(source, fromClient);
+ this.collapsed = collapsed;
+ }
+
+}
diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourcePositionChangedEvent.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourcePositionChangedEvent.java
new file mode 100644
index 0000000..b4b7d7a
--- /dev/null
+++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourcePositionChangedEvent.java
@@ -0,0 +1,39 @@
+/*-
+ * #%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.events;
+
+import com.flowingcode.vaadin.addons.demo.SourcePosition;
+import com.flowingcode.vaadin.addons.demo.TabbedDemo;
+import com.vaadin.flow.component.ComponentEvent;
+import lombok.Getter;
+
+@SuppressWarnings("serial")
+public class SourcePositionChangedEvent extends ComponentEvent {
+
+ @Getter
+ private SourcePosition sourcePosition;
+
+ public SourcePositionChangedEvent(TabbedDemo source, boolean fromClient,
+ SourcePosition sourcePosition) {
+ super(source, fromClient);
+ this.sourcePosition = sourcePosition;
+ }
+
+}
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/commons-demo-iconset.ts b/base/src/main/resources/META-INF/resources/frontend/commons-demo-iconset.ts
new file mode 100644
index 0000000..079ab08
--- /dev/null
+++ b/base/src/main/resources/META-INF/resources/frontend/commons-demo-iconset.ts
@@ -0,0 +1,124 @@
+/*-
+ * #%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 '@vaadin/icon/vaadin-icon.js';
+import { Iconset } from '@vaadin/icon/vaadin-iconset.js';
+import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+/**
+ISC License
+
+Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2023 as part of Feather (MIT).
+All other copyright (c) for Lucide are held by Lucide Contributors 2025.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+---
+
+The MIT License (MIT) (for portions derived from Feather)
+
+Copyright (c) 2013-2023 Cole Bemis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+---
+
+Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
+License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
+Copyright 2024 Fonticons, Inc.
+
+---
+
+All brand icons are trademarks of their respective owners. The use of these
+trademarks does not indicate endorsement of the trademark holder by Font
+Awesome, nor vice versa. **Please do not use brand logos for any purpose except
+to represent the company, product, or service to which they refer.**
+
+*/
+
+registerStyles(
+ 'vaadin-button',
+ css`
+ [part] ::slotted(vaadin-icon[icon^='commons-demo:']), [part] ::slotted(iron-icon[icon^='commons-demo:'])
+ {
+ padding: 0.25em;
+ box-sizing: border-box !important;
+ }`,
+);
+
+const template = document.createElement('template');
+template.innerHTML = `
+`;
+
+customElements.whenDefined('vaadin-iconset').then(Iconset=>{
+ Iconset.register('commons-demo', 24, template);
+});
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; }
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;
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..eab91c9
--- /dev/null
+++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java
@@ -0,0 +1,211 @@
+/*-
+ * #%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.TabbedDemoViewPrimarySource;
+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());
+ }
+
+ // --- 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
+ 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());
+ }
+
+ @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;
+ 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/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)
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
diff --git a/pom.xml b/pom.xml
index 9e9e1af..362867d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
com.flowingcode.vaadin.addons.democommons-demo-aggregator
- 5.3.2-SNAPSHOT
+ 5.4.0-SNAPSHOTpomCommons Demo Aggregator
diff --git a/processor/pom.xml b/processor/pom.xml
index bd82cb0..48f9113 100644
--- a/processor/pom.xml
+++ b/processor/pom.xml
@@ -5,7 +5,7 @@
com.flowingcode.vaadin.addons.democommons-demo-processor
- 5.3.2-SNAPSHOT
+ 5.4.0-SNAPSHOTCommons Demo ProcessorAnnotation processor for Commons Demo: copies @DemoSource-referenced files into the class output