From d44757d1b81652718aaad6e7b4a051c0c63e12a1 Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:01:06 -0300 Subject: [PATCH 01/18] build: set version to 5.4.0-SNAPSHOT --- base/pom.xml | 2 +- pom.xml | 2 +- processor/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/base/pom.xml b/base/pom.xml index 05e6fd0..d2e3426 100644 --- a/base/pom.xml +++ b/base/pom.xml @@ -5,7 +5,7 @@ com.flowingcode.vaadin.addons.demo commons-demo - 5.3.2-SNAPSHOT + 5.4.0-SNAPSHOT Commons Demo Common classes for add-ons demo 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.demo commons-demo-aggregator - 5.3.2-SNAPSHOT + 5.4.0-SNAPSHOT pom Commons 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.demo commons-demo-processor - 5.3.2-SNAPSHOT + 5.4.0-SNAPSHOT Commons Demo Processor Annotation processor for Commons Demo: copies @DemoSource-referenced files into the class output From 5f82bccfc9aeda515425a6d44f05724f684ebc8e Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:15:07 -0300 Subject: [PATCH 02/18] feat: add commons-demo iconset --- .../vaadin/addons/demo/CommonsDemoIcons.java | 78 +++++++++++++ .../frontend/commons-demo-iconset.ts | 103 ++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 base/src/main/java/com/flowingcode/vaadin/addons/demo/CommonsDemoIcons.java create mode 100644 base/src/main/resources/META-INF/resources/frontend/commons-demo-iconset.ts 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..0689382 --- /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; + + /** + * 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/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..a677f82 --- /dev/null +++ b/base/src/main/resources/META-INF/resources/frontend/commons-demo-iconset.ts @@ -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% + */ +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. + +*/ + +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); +}); From 3d3059313edba8c70b2c75b2139f58f0e7617370 Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:15:07 -0300 Subject: [PATCH 03/18] feat: add github and home icons --- .../vaadin/addons/demo/CommonsDemoIcons.java | 2 +- .../frontend/commons-demo-iconset.ts | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) 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 index 0689382..e9bf8ac 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/CommonsDemoIcons.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/CommonsDemoIcons.java @@ -29,7 +29,7 @@ * @author Javier Godoy / Flowing Code */ public enum CommonsDemoIcons implements IconFactory { - ROTATE, FLIP, HIDE_SOURCE, SHOW_SOURCE; + ROTATE, FLIP, HIDE_SOURCE, SHOW_SOURCE, GITHUB, HOME; /** * The Iconset name, i.e. {@code "commons-demo"}. 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 index a677f82..079ab08 100644 --- 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 @@ -63,6 +63,19 @@ 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( @@ -95,6 +108,14 @@ template.innerHTML = ` + + + + + + + + `; From 994c3fd984f2c4b53e6d07cc58e023fb6b47c168 Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:38:30 -0300 Subject: [PATCH 04/18] feat: add collapse event for source code visibility --- .../vaadin/addons/demo/TabbedDemo.java | 37 +++++++++++++---- .../events/SourceCollapseChangedEvent.java | 41 +++++++++++++++++++ 2 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourceCollapseChangedEvent.java 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..4329a06 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,7 @@ package com.flowingcode.vaadin.addons.demo; import com.flowingcode.vaadin.addons.GithubBranch; +import com.flowingcode.vaadin.addons.demo.events.SourceCollapseChangedEvent; import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.ComponentEventListener; @@ -70,6 +71,7 @@ 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; @@ -101,7 +103,12 @@ public TabbedDemo() { codeCB = new Checkbox("Show Source Code"); codeCB.setValue(true); codeCB.addClassName("smallcheckbox"); - codeCB.addValueChangeListener(ev -> updateSplitterPosition()); + codeCB.addValueChangeListener( + ev -> fireSourceCollapseChangedEvent(!ev.getValue(), ev.isFromClient())); + addSourceCollapseListener(ev -> { + sourceCollapsed = ev.isCollapsed(); + updateSplitterPosition(); + }); codePositionCB = new Checkbox("Toggle Code Position"); codePositionCB.setValue(true); codePositionCB.addClassName("smallcheckbox"); @@ -334,13 +341,13 @@ public void removeRouterLayoutContent(HasElement oldContent) { private void updateSplitterPosition() { if (currentLayout != null) { - if (codeCB.getValue()) { - currentLayout.showSourceCode(); - } else { + if (sourceCollapsed) { currentLayout.hideSourceCode(); + } else { + currentLayout.showSourceCode(); } - orientationCB.setEnabled(codeCB.getValue()); - codePositionCB.setEnabled(codeCB.getValue()); + orientationCB.setEnabled(!sourceCollapsed); + codePositionCB.setEnabled(!sourceCollapsed); } } @@ -512,8 +519,8 @@ 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); + codeCB.setValue(!sourceCollapsed && !mobile); + codePositionCB.setValue(!sourceCollapsed && !mobile); boolean portraitOrientation = receiver.getBodyClientHeight() > receiver.getBodyClientWidth(); adjustSplitOrientation(portraitOrientation); @@ -557,4 +564,18 @@ 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); + } + + private void fireSourceCollapseChangedEvent(boolean collapsed, boolean fromClient) { + fireEvent(new SourceCollapseChangedEvent(this, fromClient, collapsed)); + } + } 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; + } + +} From 4af27204b0e54e668c1d0dd2cf65ac5b47581452 Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:42:37 -0300 Subject: [PATCH 05/18] feat: add source position change event --- .../vaadin/addons/demo/SplitLayoutDemo.java | 10 ++--- .../vaadin/addons/demo/TabbedDemo.java | 39 ++++++++++++++++++- .../events/SourcePositionChangedEvent.java | 39 +++++++++++++++++++ 3 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourcePositionChangedEvent.java 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..1f7a148 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 @@ -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() 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 4329a06..d9a3ee1 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 @@ -21,6 +21,7 @@ import com.flowingcode.vaadin.addons.GithubBranch; 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; @@ -112,7 +113,7 @@ public TabbedDemo() { codePositionCB = new Checkbox("Toggle Code Position"); codePositionCB.setValue(true); codePositionCB.addClassName("smallcheckbox"); - codePositionCB.addValueChangeListener(ev -> toggleSourcePosition()); + codePositionCB.addValueChangeListener(ev -> toggleSourcePosition(ev.isFromClient())); themeCB = new Checkbox("Dark Theme"); themeCB.setValue(false); themeCB.addClassName("smallcheckbox"); @@ -365,8 +366,28 @@ public void setSourceVisible(boolean visible) { * 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); + } + } + + /** + * 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); + fireSourcePositionChangedEvent(sourcePosition, fromClient); } } @@ -574,8 +595,22 @@ public void addSourceCollapseListener( 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); + } + 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)); + } + } 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; + } + +} From f95a0f90b3cafb44898c3e0d9e784d3a3fae4b1d Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:44:03 -0300 Subject: [PATCH 06/18] feat: add orientation change event --- .../vaadin/addons/demo/TabbedDemo.java | 26 +++++++++++-- .../demo/events/OrientationChangedEvent.java | 38 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 base/src/main/java/com/flowingcode/vaadin/addons/demo/events/OrientationChangedEvent.java 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 d9a3ee1..f0d433f 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,7 @@ 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; @@ -98,7 +99,7 @@ public TabbedDemo() { orientationCB.addClassName("smallcheckbox"); orientationCB.addValueChangeListener(ev -> { if (ev.isFromClient()) { - toggleSplitterOrientation(); + toggleSplitterOrientation(ev.isFromClient()); } }); codeCB = new Checkbox("Show Source Code"); @@ -391,7 +392,7 @@ private void setSourcePosition(SourcePosition sourcePosition, boolean fromClient } } - private void toggleSplitterOrientation() { + private void toggleSplitterOrientation(boolean fromClient) { if (currentLayout == null) { return; } @@ -400,7 +401,7 @@ private void toggleSplitterOrientation() { } else { splitOrientation = Orientation.HORIZONTAL; } - setOrientation(splitOrientation); + setOrientation(splitOrientation, fromClient); } /** @@ -418,12 +419,17 @@ public Orientation getOrientation() { * @param orientation the new orientation */ public void setOrientation(Orientation orientation) { + setOrientation(orientation, false); + } + + private void setOrientation(Orientation orientation, boolean fromClient) { splitOrientation = orientation; if (currentLayout != null) { currentLayout.setOrientation(orientation); currentLayout.setSplitterPosition(50); } orientationCB.setValue(Orientation.HORIZONTAL.equals(orientation)); + fireOrientationChangedEvent(orientation, fromClient); } /** @@ -605,6 +611,16 @@ public void addSourcePositionChangedListener( 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)); } @@ -613,4 +629,8 @@ private void fireSourcePositionChangedEvent(SourcePosition sourcePosition, boole 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; + } + +} From d849cd9f10ee4dda9eb62651e03e60109b25d47c Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:15:23 -0300 Subject: [PATCH 07/18] feat: implement collapse/flip/rotate as buttons --- .../addons/demo/MultiSourceCodeViewer.java | 14 +++ .../vaadin/addons/demo/SourceCodeViewer.java | 108 +++++++++++++++- .../vaadin/addons/demo/SplitLayoutDemo.java | 2 +- .../vaadin/addons/demo/TabbedDemo.java | 42 ++----- .../frontend/source-code-viewer-buttons.ts | 66 ++++++++++ .../styles/commons-demo/shared-styles.css | 119 +++++++++++++++++- 6 files changed, 312 insertions(+), 39 deletions(-) create mode 100644 base/src/main/resources/META-INF/resources/frontend/source-code-viewer-buttons.ts 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..11baff5 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: + *

    + *
  • 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;