diff --git a/base/pom.xml b/base/pom.xml index 05e6fd0..0508fdd 100644 --- a/base/pom.xml +++ b/base/pom.xml @@ -5,7 +5,7 @@ com.flowingcode.vaadin.addons.demo commons-demo - 5.3.2-SNAPSHOT + 5.4.0-SNAPSHOT Commons Demo Common classes for add-ons demo @@ -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/main/java/com/flowingcode/vaadin/addons/demo/CommonsDemoIcons.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/CommonsDemoIcons.java new file mode 100644 index 0000000..e9bf8ac --- /dev/null +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/CommonsDemoIcons.java @@ -0,0 +1,78 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.demo; + +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.icon.IconFactory; +import java.util.Locale; + +/** + * CommonsDemo icons. + * + * @author Javier Godoy / Flowing Code + */ +public enum CommonsDemoIcons implements IconFactory { + ROTATE, FLIP, HIDE_SOURCE, SHOW_SOURCE, GITHUB, HOME; + + /** + * The Iconset name, i.e. {@code "commons-demo"}. + */ + public static final String ICONSET = "commons-demo"; + + /** + * Return the full icon name. + * + * @return the full icon name, i.e. {@code "commons-demo:name"}. + */ + public String getIconName() { + return ICONSET + ':' + getIconPart(); + } + + /** + * Return the icon name within the iconset. + * + * @return the icon name, i.e. {@code "name"}. + */ + public String getIconPart() { + return name().toLowerCase(Locale.ENGLISH).replace('_', '-').replaceFirst("^-", ""); + } + + /** + * Create a new {@link Icon} instance with the icon determined by the name. + * + * @return a new instance of {@link Icon} component + */ + @Override + public Icon create() { + return new Icon(getIconPart()); + } + + /** + * Server side component for CommonsDemo icons. + */ + @JsModule("./commons-demo-iconset.ts") + @SuppressWarnings("serial") + public static final class Icon extends com.vaadin.flow.component.icon.Icon { + private Icon(String icon) { + super(ICONSET, icon); + } + } + +} diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java index 1ea0b84..1a999ae 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java @@ -22,6 +22,7 @@ import com.flowingcode.vaadin.addons.GithubBranch; import com.flowingcode.vaadin.addons.GithubLink; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -44,6 +45,7 @@ @Repeatable(DemoSources.class) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) +@Inherited public @interface DemoSource { static final String DEFAULT_VALUE = "__DEFAULT__"; @@ -74,8 +76,13 @@ */ String language() default DEFAULT_VALUE; - /** Source code position in the layout */ - SourcePosition sourcePosition() default SourcePosition.SECONDARY; + /** + * Source code position in the layout. Defaults to {@link SourcePosition#DEFAULT}, which adopts the + * position carried over from the previously shown demo, or {@link SourcePosition#SECONDARY} if + * there is none. An explicit {@link SourcePosition#PRIMARY} or {@link SourcePosition#SECONDARY} + * always takes precedence over the carried-over position. + */ + SourcePosition sourcePosition() default SourcePosition.DEFAULT; String condition() default ""; diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSources.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSources.java index 741aeb4..41a1ebe 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSources.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSources.java @@ -20,12 +20,14 @@ package com.flowingcode.vaadin.addons.demo; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) +@Inherited public @interface DemoSources { DemoSource[] value(); diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java index 2252e9b..6319247 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java @@ -1,3 +1,22 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package com.flowingcode.vaadin.addons.demo; import com.vaadin.flow.component.Component; diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java index 2a4c7fa..fad5616 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java @@ -1,3 +1,22 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package com.flowingcode.vaadin.addons.demo; import com.vaadin.flow.server.ServiceInitEvent; diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java index 1fccdc2..ba42e3c 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java @@ -76,6 +76,20 @@ public MultiSourceCodeViewer(List sourceCodeTabs, Map properties) { } public SourceCodeViewer(String url, String language, Map properties) { + addClassName("source-code-viewer"); + addClassName("has-code-viewer-gutter"); + codeViewer = new Element("code-viewer"); - getElement().appendChild(codeViewer); - getElement().getStyle().set("overflow", "auto"); - getElement().getStyle().set("display", "flex"); - codeViewer.getStyle().set("flex-grow", "1"); + + Div codeViewerWrapper = new Div(); + codeViewerWrapper.addClassName("source-code-viewer-codeviewer-wrapper"); + codeViewerWrapper.getElement().appendChild(codeViewer); + + // Non-scrolling overlay so the buttons stay pinned while the code scrolls + buttonsWrapper = new Div(); + buttonsWrapper.addClassName("source-code-viewer-buttons-wrapper"); + + add(codeViewerWrapper, buttonsWrapper); + setProperties(properties); - addAttachListener(ev -> fetchContents(url, language)); + addAttachListener(ev -> { + fetchContents(url, language); + observeScrollbar(); + }); + } + + /** + * Adds the overlay controls (show, hide, flip and rotate) pinned over the source code. + *

+ * The buttons do not change this viewer directly. Instead, each one dispatches a bubbling DOM + * event that is expected to be handled by an enclosing layout, which is what actually collapses, + * repositions or reorients the source panel: + *

    + *
  • 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/SourcePosition.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SourcePosition.java index fa96948..ce05c21 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SourcePosition.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SourcePosition.java @@ -21,15 +21,17 @@ public enum SourcePosition { - PRIMARY, SECONDARY; + PRIMARY, SECONDARY, DEFAULT; public SourcePosition toggle() { switch (this) { case SECONDARY: + case DEFAULT: return PRIMARY; case PRIMARY: - default: return SECONDARY; + default: + return this; } } diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java index 5d83475..0b4bae6 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java @@ -48,7 +48,7 @@ public SplitLayoutDemo(Component demo, List tabs) { properties.put("vaadin", VaadinVersion.getVaadinVersion()); properties.put("flow", Version.getFullVersion()); - code = new MultiSourceCodeViewer(tabs, properties); + code = new MultiSourceCodeViewer(tabs, properties).withButtons(); this.demo = demo; setSourcePosition(code.getSourcePosition()); @@ -59,7 +59,11 @@ public boolean isEmpty() { return code.isEmpty(); } - private void setSourcePosition(SourcePosition position) { + public SourcePosition getSourcePosition() { + return sourcePosition; + } + + public void setSourcePosition(SourcePosition position) { if (!position.equals(sourcePosition)) { getContent().removeAll(); switch (position) { @@ -76,10 +80,6 @@ private void setSourcePosition(SourcePosition position) { } } - public void toggleSourcePosition() { - setSourcePosition(sourcePosition.toggle()); - } - public void setOrientation(Orientation o) { getContent().setOrientation(o); getContent() @@ -93,26 +93,22 @@ public Orientation getOrientation() { return getContent().getOrientation(); } - public void setSplitterPosition(int pos) { - getContent().setSplitterPosition(pos); - } - public void setSizeFull() { getContent().setSizeFull(); } - public void showSourceCode() { - getContent().setSplitterPosition(50); - } - - public void hideSourceCode() { - switch (sourcePosition) { - case PRIMARY: - getContent().setSplitterPosition(0); - break; - case SECONDARY: - getContent().setSplitterPosition(100); - break; + public void setSourceCollapsed(boolean collapsed) { + if (!collapsed) { + getContent().setSplitterPosition(50); + } else { + switch (sourcePosition) { + case PRIMARY: + getContent().setSplitterPosition(0); + break; + case SECONDARY: + default: + getContent().setSplitterPosition(100); + } } } diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java index cc70e12..0cab2b8 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java @@ -20,6 +20,9 @@ package com.flowingcode.vaadin.addons.demo; import com.flowingcode.vaadin.addons.GithubBranch; +import com.flowingcode.vaadin.addons.demo.events.OrientationChangedEvent; +import com.flowingcode.vaadin.addons.demo.events.SourceCollapseChangedEvent; +import com.flowingcode.vaadin.addons.demo.events.SourcePositionChangedEvent; import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.ComponentEventListener; @@ -42,6 +45,7 @@ import com.vaadin.flow.router.RouterLayout; import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.server.Version; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -70,14 +74,15 @@ public class TabbedDemo extends VerticalLayout implements RouterLayout { private static final int MOBILE_DEVICE_BREAKPOINT_WIDTH = 768; private boolean autoVisibility; + private boolean sourceCollapsed; private EnhancedRouteTabs tabs; private HorizontalLayout footer; private SplitLayoutDemo currentLayout; - private Checkbox orientationCB; - private Checkbox codeCB; - private Checkbox codePositionCB; private Checkbox themeCB; - private Orientation splitOrientation; + // The desired split orientation. Mirrors the current layout's orientation when one exists, and + // outlives it while no layout is present, so a device-driven adjustment (e.g. portrait -> VERTICAL + // made on a source-less demo) is carried over to the next source-bearing demo. + private Orientation splitOrientation = Orientation.HORIZONTAL; private Button helperButton; private DemoHelperViewer demoHelperViewer; @@ -89,23 +94,14 @@ public TabbedDemo() { tabs = new EnhancedRouteTabs(); - // Footer - orientationCB = new Checkbox("Toggle Orientation"); - orientationCB.setValue(true); - orientationCB.addClassName("smallcheckbox"); - orientationCB.addValueChangeListener(ev -> { - if (ev.isFromClient()) { - toggleSplitterOrientation(); - } + // The source controls live inside SourceCodeViewer and signal across components + // via bubbling DOM events; collapse carries data, so it uses SourceCollapseChangedEvent. + addSourceCollapseListener(ev -> { + sourceCollapsed = ev.isCollapsed(); + updateSplitterPosition(); }); - codeCB = new Checkbox("Show Source Code"); - codeCB.setValue(true); - codeCB.addClassName("smallcheckbox"); - codeCB.addValueChangeListener(ev -> updateSplitterPosition()); - codePositionCB = new Checkbox("Toggle Code Position"); - codePositionCB.setValue(true); - codePositionCB.addClassName("smallcheckbox"); - codePositionCB.addValueChangeListener(ev -> toggleSourcePosition()); + getElement().addEventListener("source-flip", ev -> toggleSourcePosition(true)); + getElement().addEventListener("source-rotate", ev -> toggleSplitterOrientation(true)); themeCB = new Checkbox("Dark Theme"); themeCB.setValue(false); themeCB.addClassName("smallcheckbox"); @@ -117,7 +113,7 @@ public TabbedDemo() { footer = new HorizontalLayout(); footer.setWidthFull(); footer.setJustifyContentMode(JustifyContentMode.END); - footer.add(codeCB, codePositionCB, orientationCB, themeCB); + footer.add(themeCB); footer.setClassName("demo-footer"); Package pkg = this.getClass().getPackage(); @@ -231,6 +227,11 @@ public void showRouterLayoutContent(HasElement content) { createSourceCodeTab(demo.getClass(), demoSource).ifPresent(sourceTabs::add); } + SourcePosition previousPosition = null; + if (currentLayout != null) { + previousPosition = currentLayout.getSourcePosition(); + } + if (!sourceTabs.isEmpty()) { currentLayout = new SplitLayoutDemo(demo, sourceTabs); if (currentLayout.isEmpty()) { @@ -241,11 +242,19 @@ public void showRouterLayoutContent(HasElement content) { if (currentLayout != null) { content = currentLayout; - if (splitOrientation != null) { - setOrientation(splitOrientation); - updateSplitterPosition(); + + // A DEFAULT position is soft: it adopts the position carried over from the previously shown + // demo (SECONDARY if there is none). An explicit PRIMARY/SECONDARY always wins. + if (currentLayout.getSourcePosition() == SourcePosition.DEFAULT) { + currentLayout.setSourcePosition(resolveDefaultPosition(previousPosition)); } + // A freshly created layout defaults to HORIZONTAL; adopt the orientation remembered across + // demos (which may have changed while no layout was present). This is initialization, not a + // state change, so it does not fire an OrientationChangedEvent. + currentLayout.setOrientation(splitOrientation); + updateSplitterPosition(); + setupDemoHelperButton(currentLayout.getContent().getPrimaryComponent().getClass()); } else { currentLayout = null; @@ -334,13 +343,7 @@ public void removeRouterLayoutContent(HasElement oldContent) { private void updateSplitterPosition() { if (currentLayout != null) { - if (codeCB.getValue()) { - currentLayout.showSourceCode(); - } else { - currentLayout.hideSourceCode(); - } - orientationCB.setEnabled(codeCB.getValue()); - codePositionCB.setEnabled(codeCB.getValue()); + currentLayout.setSourceCollapsed(sourceCollapsed); } } @@ -350,29 +353,69 @@ private void updateSplitterPosition() { * @param visible {@code true} to make the source code visible, {@code false} otherwise */ public void setSourceVisible(boolean visible) { - codeCB.setValue(visible); - codePositionCB.setVisible(visible); + fireSourceCollapseChangedEvent(!visible, false); } /** * Toggles the position of the source code relative to the demo content. */ public void toggleSourcePosition() { + toggleSourcePosition(false); + } + + private void toggleSourcePosition(boolean fromClient) { if (currentLayout != null) { - currentLayout.toggleSourcePosition(); + setSourcePosition(currentLayout.getSourcePosition().toggle(), fromClient); } } - private void toggleSplitterOrientation() { + /** + * Sets the position of the source code relative to the demo content. + * + * @param sourcePosition the new source position + */ + public void setSourcePosition(SourcePosition sourcePosition) { + setSourcePosition(sourcePosition, false); + } + + private void setSourcePosition(SourcePosition sourcePosition, boolean fromClient) { + if (currentLayout != null) { + currentLayout.setSourcePosition(sourcePosition); + if (sourceCollapsed) { + updateSplitterPosition(); + } + SourcePosition resolvedPosition = sourcePosition == SourcePosition.DEFAULT + ? resolveDefaultPosition(currentLayout.getSourcePosition()) + : sourcePosition; + fireSourcePositionChangedEvent(resolvedPosition, fromClient); + } + } + + /** + * Resolves a {@link SourcePosition#DEFAULT} position against the position carried over from the + * previously shown demo. + * + * @param previous the carried-over position, or {@code null} if there is none + * @return {@code previous} when it is a concrete position, otherwise {@link SourcePosition#SECONDARY} + */ + private static SourcePosition resolveDefaultPosition(SourcePosition previous) { + return (previous == null || previous == SourcePosition.DEFAULT) + ? SourcePosition.SECONDARY + : previous; + } + + private void toggleSplitterOrientation(boolean fromClient) { if (currentLayout == null) { return; } + + Orientation splitOrientation = getOrientation(); if (Orientation.HORIZONTAL.equals(splitOrientation)) { splitOrientation = Orientation.VERTICAL; } else { splitOrientation = Orientation.HORIZONTAL; } - setOrientation(splitOrientation); + setOrientation(splitOrientation, fromClient); } /** @@ -381,7 +424,7 @@ private void toggleSplitterOrientation() { * @return the current orientation */ public Orientation getOrientation() { - return currentLayout.getOrientation(); + return splitOrientation; } /** @@ -390,12 +433,19 @@ public Orientation getOrientation() { * @param orientation the new orientation */ public void setOrientation(Orientation orientation) { + setOrientation(orientation, false); + } + + private void setOrientation(Orientation orientation, boolean fromClient) { + if (orientation == splitOrientation) { + return; + } splitOrientation = orientation; if (currentLayout != null) { currentLayout.setOrientation(orientation); - currentLayout.setSplitterPosition(50); + updateSplitterPosition(); } - orientationCB.setValue(Orientation.HORIZONTAL.equals(orientation)); + fireOrientationChangedEvent(orientation, fromClient); } /** @@ -466,7 +516,7 @@ public static void setColorScheme(Component component, @NonNull ColorScheme colo e.style.colorScheme = $0; """; - element.executeJs(script, theme); + element.executeJs(script, new Serializable[] {theme}); collectThemeChangeObservers(component).forEach(observer -> observer.onThemeChange(theme)); observeThemeChange(component); @@ -491,9 +541,6 @@ private static Stream collectThemeChangeObservers(Component private void updateFooterButtonsVisibility() { boolean hasSourceCode = currentLayout != null; ComponentUtil.fireEvent(this, new TabbedDemoSourceEvent(this, hasSourceCode)); - orientationCB.setVisible(hasSourceCode); - codeCB.setVisible(hasSourceCode); - codePositionCB.setVisible(hasSourceCode); } /** @@ -512,8 +559,10 @@ protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); getUI().ifPresent(ui -> ui.getPage().retrieveExtendedClientDetails(receiver -> { boolean mobile = receiver.getBodyClientWidth() <= MOBILE_DEVICE_BREAKPOINT_WIDTH; - codeCB.setValue(codeCB.getValue() && !mobile); - codePositionCB.setValue(codeCB.getValue() && !mobile); + boolean shouldCollapse = sourceCollapsed || mobile; + if (shouldCollapse != sourceCollapsed) { + fireSourceCollapseChangedEvent(shouldCollapse, false); + } boolean portraitOrientation = receiver.getBodyClientHeight() > receiver.getBodyClientWidth(); adjustSplitOrientation(portraitOrientation); @@ -522,11 +571,10 @@ protected void onAttach(AttachEvent attachEvent) { private void adjustSplitOrientation(boolean portraitOrientation) { if (portraitOrientation) { - splitOrientation = Orientation.VERTICAL; + setOrientation(Orientation.VERTICAL); } else { - splitOrientation = Orientation.HORIZONTAL; + setOrientation(Orientation.HORIZONTAL); } - setOrientation(splitOrientation); } /** @@ -557,4 +605,46 @@ private void setupDemoHelperButton(Class helperClass) { } } + /** + * Adds a listener for {@link SourceCollapseChangedEvent}. + * + * @param listener the listener to add + */ + public void addSourceCollapseListener( + ComponentEventListener listener) { + ComponentUtil.addListener(this, SourceCollapseChangedEvent.class, listener); + } + + /** + * Adds a listener for {@link SourcePositionChangedEvent}. + * + * @param listener the listener to add + */ + public void addSourcePositionChangedListener( + ComponentEventListener listener) { + ComponentUtil.addListener(this, SourcePositionChangedEvent.class, listener); + } + + /** + * Adds a listener for {@link OrientationChangedEvent}. + * + * @param listener the listener to add + */ + public void addOrientationChangedListener( + ComponentEventListener listener) { + ComponentUtil.addListener(this, OrientationChangedEvent.class, listener); + } + + private void fireSourceCollapseChangedEvent(boolean collapsed, boolean fromClient) { + fireEvent(new SourceCollapseChangedEvent(this, fromClient, collapsed)); + } + + private void fireSourcePositionChangedEvent(SourcePosition sourcePosition, boolean fromClient) { + fireEvent(new SourcePositionChangedEvent(this, fromClient, sourcePosition)); + } + + private void fireOrientationChangedEvent(Orientation orientation, boolean fromClient) { + fireEvent(new OrientationChangedEvent(this, fromClient, orientation)); + } + } diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/OrientationChangedEvent.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/OrientationChangedEvent.java new file mode 100644 index 0000000..ba79252 --- /dev/null +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/OrientationChangedEvent.java @@ -0,0 +1,38 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.demo.events; + +import com.flowingcode.vaadin.addons.demo.TabbedDemo; +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation; +import lombok.Getter; + +@SuppressWarnings("serial") +public class OrientationChangedEvent extends ComponentEvent { + + @Getter + private Orientation orientation; + + public OrientationChangedEvent(TabbedDemo source, boolean fromClient, Orientation orientation) { + super(source, fromClient); + this.orientation = orientation; + } + +} diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourceCollapseChangedEvent.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourceCollapseChangedEvent.java new file mode 100644 index 0000000..0877a6a --- /dev/null +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourceCollapseChangedEvent.java @@ -0,0 +1,41 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.demo.events; + +import com.flowingcode.vaadin.addons.demo.TabbedDemo; +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.DomEvent; +import com.vaadin.flow.component.EventData; +import lombok.Getter; + +@SuppressWarnings("serial") +@DomEvent("source-collapse-changed") +public class SourceCollapseChangedEvent extends ComponentEvent { + + @Getter + private boolean collapsed; + + public SourceCollapseChangedEvent(TabbedDemo source, boolean fromClient, + @EventData("event.detail.collapsed") boolean collapsed) { + super(source, fromClient); + this.collapsed = collapsed; + } + +} diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourcePositionChangedEvent.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourcePositionChangedEvent.java new file mode 100644 index 0000000..b4b7d7a --- /dev/null +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/events/SourcePositionChangedEvent.java @@ -0,0 +1,39 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.demo.events; + +import com.flowingcode.vaadin.addons.demo.SourcePosition; +import com.flowingcode.vaadin.addons.demo.TabbedDemo; +import com.vaadin.flow.component.ComponentEvent; +import lombok.Getter; + +@SuppressWarnings("serial") +public class SourcePositionChangedEvent extends ComponentEvent { + + @Getter + private SourcePosition sourcePosition; + + public SourcePositionChangedEvent(TabbedDemo source, boolean fromClient, + SourcePosition sourcePosition) { + super(source, fromClient); + this.sourcePosition = sourcePosition; + } + +} diff --git a/base/src/main/resources/META-INF/VAADIN/package.properties b/base/src/main/resources/META-INF/VAADIN/package.properties index c66616f..2391356 100644 --- a/base/src/main/resources/META-INF/VAADIN/package.properties +++ b/base/src/main/resources/META-INF/VAADIN/package.properties @@ -1 +1,20 @@ +### +# #%L +# Commons Demo +# %% +# Copyright (C) 2020 - 2026 Flowing Code +# %% +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# #L% +### vaadin.allowed-packages=com.flowingcode diff --git a/base/src/main/resources/META-INF/resources/frontend/commons-demo-iconset.ts b/base/src/main/resources/META-INF/resources/frontend/commons-demo-iconset.ts new file mode 100644 index 0000000..079ab08 --- /dev/null +++ b/base/src/main/resources/META-INF/resources/frontend/commons-demo-iconset.ts @@ -0,0 +1,124 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +import '@vaadin/icon/vaadin-icon.js'; +import { Iconset } from '@vaadin/icon/vaadin-iconset.js'; +import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; + +/** +ISC License + +Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2023 as part of Feather (MIT). +All other copyright (c) for Lucide are held by Lucide Contributors 2025. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--- + +The MIT License (MIT) (for portions derived from Feather) + +Copyright (c) 2013-2023 Cole Bemis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com +License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) +Copyright 2024 Fonticons, Inc. + +--- + +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.** + +*/ + +registerStyles( + 'vaadin-button', + css` + [part] ::slotted(vaadin-icon[icon^='commons-demo:']), [part] ::slotted(iron-icon[icon^='commons-demo:']) + { + padding: 0.25em; + box-sizing: border-box !important; + }`, +); + +const template = document.createElement('template'); +template.innerHTML = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +customElements.whenDefined('vaadin-iconset').then(Iconset=>{ + Iconset.register('commons-demo', 24, template); +}); diff --git a/base/src/main/resources/META-INF/resources/frontend/source-code-viewer-buttons.ts b/base/src/main/resources/META-INF/resources/frontend/source-code-viewer-buttons.ts new file mode 100644 index 0000000..030b82a --- /dev/null +++ b/base/src/main/resources/META-INF/resources/frontend/source-code-viewer-buttons.ts @@ -0,0 +1,66 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +import {html, LitElement} from 'lit'; +import {customElement} from 'lit/decorators.js'; +import '@vaadin/button/vaadin-button.js'; +import './commons-demo-iconset.js'; + +/** + * Overlay controls pinned over the source code. Each button dispatches a bubbling DOM event that is + * handled by an enclosing layout (see TabbedDemo), which is what actually collapses, repositions or + * reorients the source panel. Firing the events on the client avoids a server roundtrip on click. + */ +@customElement("source-code-viewer-buttons") +export class SourceCodeViewerButtons extends LitElement { + + // Render in light DOM so the shared stylesheet (shared-styles.css) styles the buttons. + createRenderRoot() { + return this; + } + + private fire(type: string, detail?: any) { + this.dispatchEvent(new CustomEvent(type, {bubbles: true, detail})); + } + + render() { + return html` + this.fire('source-collapse-changed', {collapsed: false})}> + + + this.fire('source-collapse-changed', {collapsed: true})}> + + + this.fire('source-flip')}> + + + this.fire('source-rotate')}> + + + `; + } +} diff --git a/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css b/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css index 6fef4f7..b0e87cd 100644 --- a/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css +++ b/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css @@ -51,5 +51,122 @@ code-highlighter code { cursor: pointer; } +.source-code-viewer { + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; +} + +.source-code-viewer-codeviewer-wrapper { + display: flex; + flex-grow: 1; + overflow: auto; +} + +.source-code-viewer-codeviewer-wrapper > code-viewer { + flex-grow: 1; +} + +.source-code-viewer-buttons-wrapper { + position: absolute; + inset: 0; + pointer-events: none; +} + +source-code-viewer-buttons { + position: absolute; + z-index: 1; + right: calc(0.25rem + var(--code-viewer-gutter, 0px)); + top: 0.25rem; + display: flex; + gap: var(--lumo-space-xs, 0.25rem); + container-type: inline-size; + width: 100%; + justify-content: end; + pointer-events: none; +} + +vaadin-button.source-code-viewer-button { + color: color-mix(in srgb, #f8f8f2 70%, transparent); + pointer-events: auto; + background: transparent; + display: none; + padding: 0; +} + +vaadin-button.source-code-viewer-button vaadin-icon { + --vaadin-icon-size: 20px; + padding: 0.125em; +} + +@container style(--lumo-size-m) { + vaadin-button.source-code-viewer-button vaadin-icon { + padding: 0.25em; + } +} + + +vaadin-button.source-code-viewer-show-button { + display: var(--source-code-viewer-show-button-display, none); + color: var(--lumo-body-text-color, var(--vaadin-text-color)); + position: fixed; +} + +[orientation="horizontal"] [slot="primary"] vaadin-button.source-code-viewer-show-button { + right: 0; +} + +[orientation="horizontal"] [slot="secondary"] vaadin-button.source-code-viewer-show-button { + right: 0.5rem; +} + +[orientation="vertical"] [slot="primary"] vaadin-button.source-code-viewer-show-button { + top: 0.5rem; +} + +[orientation="vertical"] [slot="secondary"] vaadin-button.source-code-viewer-show-button { + top: 0; +} + +/* Rotate the icons to match the layout disposition. */ +[orientation="horizontal"] [slot="secondary"] vaadin-button.source-code-viewer-button{ + transform: rotate(0deg); + &.source-code-viewer-rotate-button { transform: scaleX(-1) rotate(90deg); } +} + +[orientation="horizontal"] [slot="primary"] vaadin-button.source-code-viewer-button { + transform: rotate(180deg); + &.source-code-viewer-rotate-button { transform: rotate(0deg); } +} + +[orientation="vertical"] [slot="secondary"] vaadin-button.source-code-viewer-button { + transform: rotate(90deg); + &.source-code-viewer-rotate-button { transform: rotate(0deg); } +} + +[orientation="vertical"] [slot="primary"] vaadin-button.source-code-viewer-button { + transform: rotate(270deg); + &.source-code-viewer-rotate-button { transform: scaleX(-1) rotate(90deg); } +} + +@container (min-width: 82px) { + vaadin-button.source-code-viewer-flip-button { + display: block; + } +} + +@container (min-width: 53px) { + vaadin-button.source-code-viewer-rotate-button { + display: block; + } +} + +@container (min-width: 24px) { + vaadin-button.source-code-viewer-hide-button { + display: block; + } +} + .commons-demo-split-layout { overflow: hidden } -.commons-demo-split-layout > [slot] { overflow: auto; } \ No newline at end of file +.commons-demo-split-layout > [slot] { overflow: auto; } diff --git a/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/vaadin-select-overlay.css b/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/vaadin-select-overlay.css index dd6c297..6c8cd13 100644 --- a/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/vaadin-select-overlay.css +++ b/base/src/main/resources/META-INF/resources/frontend/styles/commons-demo/vaadin-select-overlay.css @@ -1,4 +1,23 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ /** Needed in order to prevent vaadin-select from remaining 'closing' when switching from Lumo to Aura/Base*/ :host([theme~='demo-footer-theme-select']) vaadin-select-overlay { animation-name: none; -} \ No newline at end of file +} diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/AppShellConfiguratorImpl.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/AppShellConfiguratorImpl.java index e2195ae..a4a9994 100644 --- a/base/src/test/java/com/flowingcode/vaadin/addons/demo/AppShellConfiguratorImpl.java +++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/AppShellConfiguratorImpl.java @@ -1,3 +1,22 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package com.flowingcode.vaadin.addons.demo; import com.vaadin.flow.component.page.AppShellConfigurator; diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayCallables.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayCallables.java new file mode 100644 index 0000000..aa0b15b --- /dev/null +++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayCallables.java @@ -0,0 +1,51 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.demo.it; + +import com.flowingcode.vaadin.addons.demo.SourcePosition; +import com.flowingcode.vaadin.testbench.rpc.RmiCallable; +import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation; + +/** + * testbench-rpc callable contract used by the level-E integration tests to read the server-side + * state coordinated by {@code TabbedDemo} directly from the browser test, instead of scraping DOM + * status elements. Implemented (with {@code @ClientCallable}) by {@link OverlayView}. + */ +public interface OverlayCallables extends RmiCallable { + + /** Current split orientation, {@code HORIZONTAL} or {@code VERTICAL}. */ + Orientation orientation(); + + /** Whether the source panel is currently collapsed (latest collapse event). */ + boolean sourceCollapsed(); + + /** Latest source position, {@code PRIMARY} or {@code SECONDARY} (null before any change). */ + SourcePosition sourcePosition(); + + /** {@code isFromClient()} of the most recent {@code SourceCollapseChangedEvent}. */ + boolean lastCollapseFromClient(); + + /** {@code isFromClient()} of the most recent {@code SourcePositionChangedEvent}. */ + boolean lastPositionFromClient(); + + /** {@code isFromClient()} of the most recent {@code OrientationChangedEvent}. */ + boolean lastOrientationFromClient(); + +} diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayControlsIT.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayControlsIT.java new file mode 100644 index 0000000..d783911 --- /dev/null +++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayControlsIT.java @@ -0,0 +1,189 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.demo.it; + +import static com.vaadin.flow.component.splitlayout.SplitLayout.Orientation.HORIZONTAL; +import static com.vaadin.flow.component.splitlayout.SplitLayout.Orientation.VERTICAL; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import com.flowingcode.vaadin.testbench.rpc.HasRpcSupport; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.WebElement; + +/** + * Integration tests for the source-code viewer overlay controls. Each test clicks a rendered + * overlay button and then asserts the effect on both the DOM (CSS-driven visibility, slot + * placement, orientation attribute) and the server-side state, the latter read over testbench-rpc + * through {@link OverlayCallables} — see {@link OverlayView} for the {@code @ClientCallable} + * implementation. + * + *

+ * The unit ({@code SplitLayoutDemoTest}) and UI-unit ({@code TabbedDemoUIUnitTest}) layers cover + * the mechanics and the programmatic ({@code fromClient=false}) paths; these tests add what only a + * real browser can show: CSS visibility, real geometry, and the genuine {@code fromClient=true} + * origin of a rendered button click. + */ +public class OverlayControlsIT extends AbstractViewTest implements HasRpcSupport { + + private final OverlayCallables $server = createCallableProxy(OverlayCallables.class); + + public OverlayControlsIT() { + super(null); + } + + /** Landscape viewport (deterministic HORIZONTAL, non-collapsed baseline) + navigate. */ + private void open() { + getDriver().manage().window().setSize(new Dimension(1200, 800)); + getDriver().get(getURL("it/rpc-overlay")); + getCommandExecutor().waitForVaadin(); + getDriver().findElement(By.id("content")); + waitUntil(d -> hasElement("vaadin-button.source-code-viewer-hide-button")); + } + + private WebElement button(String name) { + return getDriver() + .findElement(By.cssSelector("vaadin-button.source-code-viewer-" + name + "-button")); + } + + private void click(String name) { + button(name).click(); + getCommandExecutor().waitForVaadin(); + } + + private boolean hasElement(String cssSelector) { + return !getDriver().findElements(By.cssSelector(cssSelector)).isEmpty(); + } + + private String orientationAttribute() { + return getDriver().findElement(By.cssSelector("vaadin-split-layout")).getAttribute("orientation"); + } + + /** Rendered pixel width of the source panel (the slotted {@code code-viewer}). */ + private int sourcePanelWidth() { + // the slot that contains the source-code-viewer + String xpath = "//vaadin-split-layout/*[div[contains(@class, 'source-code-viewer')]]"; + return getDriver().findElement(By.xpath(xpath)).getSize().getWidth(); + } + + /** Rendered pixel width of the whole split layout. */ + private int splitLayoutWidth() { + return getDriver().findElement(By.cssSelector("vaadin-split-layout")).getSize().getWidth(); + } + + /** IT-BTN-01 — the action buttons are visible, the show button hidden, at a normal split. */ + @Test + public void overlayButtonsVisibility() { + open(); + assertTrue(button("hide").isDisplayed()); + assertTrue(button("flip").isDisplayed()); + assertTrue(button("rotate").isDisplayed()); + assertFalse(button("show").isDisplayed()); + } + + /** IT-COL-01 — clicking Hide collapses the panel, reveals Show, and is client-originated. */ + @Test + public void clickingHideCollapsesAndRevealsShow() { + open(); + assertFalse($server.sourceCollapsed()); + + click("hide"); + + assertTrue($server.sourceCollapsed()); + assertTrue($server.lastCollapseFromClient()); + waitUntil(d -> button("show").isDisplayed()); + // real geometry: the collapsed source panel measures ~0px wide + waitUntil(d -> sourcePanelWidth() < 5); + assertTrue("collapsed source panel should measure ~0px but was " + sourcePanelWidth(), + sourcePanelWidth() < 5); + } + + /** IT-COL-02 — clicking Show restores the panel. */ + @Test + public void clickingShowRestoresPanel() { + open(); + click("hide"); + waitUntil(d -> button("show").isDisplayed()); + + click("show"); + + assertFalse($server.sourceCollapsed()); + waitUntil(d -> button("hide").isDisplayed()); + assertFalse(button("show").isDisplayed()); + // real geometry: the splitter returns to ~50, so the source panel spans ~half the layout + waitUntil(d -> { + int total = splitLayoutWidth(); + return total > 0 && Math.abs(sourcePanelWidth() - total / 2.0) < total * 0.1; + }); + double ratio = (double) sourcePanelWidth() / splitLayoutWidth(); + assertTrue("restored source panel should span ~50% of the layout but was " + ratio, + ratio > 0.4 && ratio < 0.6); + } + + /** IT-FLIP-01 — clicking Flip swaps the source viewer between slots (client-originated). */ + @Test + public void clickingFlipSwapsSlots() { + open(); + assertTrue(hasElement("[slot='secondary'] code-viewer")); + assertFalse(hasElement("[slot='primary'] code-viewer")); + + click("flip"); + + waitUntil(d -> hasElement("[slot='primary'] code-viewer")); + assertTrue($server.lastPositionFromClient()); + + click("flip"); + waitUntil(d -> hasElement("[slot='secondary'] code-viewer")); + } + + /** IT-ROT-01 — clicking Rotate toggles the split orientation (client-originated). */ + @Test + public void clickingRotateTogglesOrientation() { + open(); + assertEquals(HORIZONTAL, $server.orientation()); + assertEquals("horizontal", orientationAttribute()); + + click("rotate"); + + assertEquals(VERTICAL, $server.orientation()); + waitUntil(d -> "vertical".equals(orientationAttribute())); + assertTrue($server.lastOrientationFromClient()); + + click("rotate"); + assertEquals(HORIZONTAL, $server.orientation()); + } + + /** + * IT-VIS-01 (optional) — at the collapsed extreme the overlay container falls below every + * {@code @container} threshold, so only the show button remains visible. + */ + @Test + public void collapsedPanelHidesActionButtons() { + open(); + click("hide"); + waitUntil(d -> button("show").isDisplayed()); + + assertFalse(button("hide").isDisplayed()); + assertFalse(button("flip").isDisplayed()); + assertFalse(button("rotate").isDisplayed()); + } +} diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayView.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayView.java new file mode 100644 index 0000000..4515dcb --- /dev/null +++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/OverlayView.java @@ -0,0 +1,103 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.demo.it; + +import com.flowingcode.vaadin.addons.GithubLink; +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.flowingcode.vaadin.addons.demo.SourcePosition; +import com.flowingcode.vaadin.addons.demo.TabbedDemo; +import com.flowingcode.vaadin.jsonmigration.InstrumentedRoute; +import com.flowingcode.vaadin.jsonmigration.JsonMigration; +import com.flowingcode.vaadin.jsonmigration.LegacyClientCallable; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation; +import elemental.json.JsonObject; +import elemental.json.JsonValue; + +@SuppressWarnings("serial") +@GithubLink("https://github.com/FlowingCode/CommonsDemo") +public class OverlayView extends TabbedDemo implements OverlayCallables { + + private boolean collapsed; + private boolean collapseFromClient; + private SourcePosition position; + private boolean positionFromClient; + private boolean orientationFromClient; + + public OverlayView() { + addDemo(JsonMigration.instrumentClass(RpcOverlayDemo.class)); + addSourceCollapseListener(ev -> { + collapsed = ev.isCollapsed(); + collapseFromClient = ev.isFromClient(); + }); + addSourcePositionChangedListener(ev -> { + position = ev.getSourcePosition(); + positionFromClient = ev.isFromClient(); + }); + addOrientationChangedListener(ev -> orientationFromClient = ev.isFromClient()); + } + + @Override + @LegacyClientCallable + public JsonValue $call(JsonObject invocation) { + return OverlayCallables.super.$call(invocation); + } + + @Override + public Orientation orientation() { + return getOrientation(); + } + + @Override + public boolean sourceCollapsed() { + return collapsed; + } + + @Override + public SourcePosition sourcePosition() { + return position; + } + + @Override + public boolean lastCollapseFromClient() { + return collapseFromClient; + } + + @Override + public boolean lastPositionFromClient() { + return positionFromClient; + } + + @Override + public boolean lastOrientationFromClient() { + return orientationFromClient; + } + + /** Single source-bearing demo, so the overlay controls are rendered. */ + @DemoSource(clazz = OverlayView.class) + @InstrumentedRoute(value = "it/rpc-overlay", layout = OverlayView.class) + public static class RpcOverlayDemo extends Div { + + public RpcOverlayDemo() { + add(new Span("RpcOverlayDemo")); + } + } +} diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java new file mode 100644 index 0000000..eab91c9 --- /dev/null +++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoUIUnitTest.java @@ -0,0 +1,211 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.demo.it; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import com.flowingcode.vaadin.addons.demo.SourcePosition; +import com.flowingcode.vaadin.addons.demo.events.OrientationChangedEvent; +import com.flowingcode.vaadin.addons.demo.events.SourceCollapseChangedEvent; +import com.flowingcode.vaadin.addons.demo.events.SourcePositionChangedEvent; +import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewMultiSource; +import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewNoSource; +import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewPrimarySource; +import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewSingleSource; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation; +import com.vaadin.flow.dom.Element; +import com.vaadin.testbench.unit.UIUnit4Test; +import com.vaadin.testbench.unit.ViewPackages; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +@ViewPackages(classes = TabbedDemoView.class) +public class TabbedDemoUIUnitTest extends UIUnit4Test { + + private static final String TAG_CODE_VIEWER = "code-viewer"; + private static final String TAG_SOURCE_CODE_VIEWER_BUTTONS = "source-code-viewer-buttons"; + + private static TabbedDemoView openWithSource() { + TabbedDemoView demo = new TabbedDemoView(); + demo.showRouterLayoutContent(new TabbedDemoViewSingleSource()); + return demo; + } + + // --- the overlay is present exactly when the demo has source ---------------------- + + @Test + public void overlayPresentForSingleSource() { + TabbedDemoView demo = openWithSource(); + assertEquals(1, count(demo.getElement(), TAG_SOURCE_CODE_VIEWER_BUTTONS)); + assertEquals(1, count(demo.getElement(), TAG_CODE_VIEWER)); + } + + @Test + public void overlayPresentOnceForMultiSource() { + TabbedDemoView demo = new TabbedDemoView(); + demo.showRouterLayoutContent(new TabbedDemoViewMultiSource()); + assertEquals(1, count(demo.getElement(), TAG_SOURCE_CODE_VIEWER_BUTTONS)); + assertEquals(1, count(demo.getElement(), TAG_CODE_VIEWER)); + } + + @Test + public void noOverlayWhenDemoHasNoSource() { + TabbedDemoView demo = new TabbedDemoView(); + demo.showRouterLayoutContent(new TabbedDemoViewNoSource()); + assertEquals(0, count(demo.getElement(), TAG_SOURCE_CODE_VIEWER_BUTTONS)); + assertEquals(0, count(demo.getElement(), TAG_CODE_VIEWER)); + } + + // --- setSourceVisible collapses/expands and fires the event (fromClient=false) ---- + + @Test + public void setSourceVisibleFiresCollapseEvent() { + TabbedDemoView demo = openWithSource(); + List events = new ArrayList<>(); + demo.addSourceCollapseListener(events::add); + + demo.setSourceVisible(false); + assertEquals(1, events.size()); + assertTrue(events.get(0).isCollapsed()); + assertFalse(events.get(0).isFromClient()); + + demo.setSourceVisible(true); + assertEquals(2, events.size()); + assertFalse(events.get(1).isCollapsed()); + assertFalse(events.get(1).isFromClient()); + } + + // --- toggleSourcePosition toggles the position and fires the event --------------- + + @Test + public void toggleSourcePositionFiresEventAndToggles() { + TabbedDemoView demo = openWithSource(); + List events = new ArrayList<>(); + demo.addSourcePositionChangedListener(events::add); + + demo.toggleSourcePosition(); + assertEquals(1, events.size()); + // Single source defaults to SECONDARY, so the first toggle yields PRIMARY. + assertEquals(SourcePosition.PRIMARY, events.get(0).getSourcePosition()); + assertFalse(events.get(0).isFromClient()); + + demo.toggleSourcePosition(); + assertEquals(2, events.size()); + assertEquals(SourcePosition.SECONDARY, events.get(1).getSourcePosition()); + assertNotEquals(events.get(0).getSourcePosition(), events.get(1).getSourcePosition()); + } + + // --- a DEFAULT position inherits the position carried over from the previous demo - + + @Test + public void defaultPositionInheritsFromPreviousDemo() { + TabbedDemoView demo = new TabbedDemoView(); + Component first = new TabbedDemoViewPrimarySource(); + demo.showRouterLayoutContent(first); + + // TabbedDemoViewSingleSource has no explicit position (DEFAULT), so it should adopt + // the PRIMARY position carried over from the previous demo. + demo.removeRouterLayoutContent(first); + demo.showRouterLayoutContent(new TabbedDemoViewSingleSource()); + + List events = new ArrayList<>(); + demo.addSourcePositionChangedListener(events::add); + + demo.toggleSourcePosition(); + assertEquals(1, events.size()); + // Inherited PRIMARY, so the first toggle yields SECONDARY. + assertEquals(SourcePosition.SECONDARY, events.get(0).getSourcePosition()); + } + + // --- setOrientation changes state and fires the event; same value is a no-op ------ + + @Test + public void setOrientationChangesStateAndFiresEvent() { + TabbedDemoView demo = openWithSource(); + assertEquals(Orientation.HORIZONTAL, demo.getOrientation()); + List events = new ArrayList<>(); + demo.addOrientationChangedListener(events::add); + + demo.setOrientation(Orientation.VERTICAL); + + assertEquals(Orientation.VERTICAL, demo.getOrientation()); + assertEquals(1, events.size()); + assertEquals(Orientation.VERTICAL, events.get(0).getOrientation()); + assertFalse(events.get(0).isFromClient()); + } + + @Test + public void settingCurrentOrientationIsANoOp() { + TabbedDemoView demo = openWithSource(); + List events = new ArrayList<>(); + demo.addOrientationChangedListener(events::add); + + demo.setOrientation(Orientation.HORIZONTAL); // already horizontal + + assertEquals(Orientation.HORIZONTAL, demo.getOrientation()); + assertTrue(events.isEmpty()); + } + + // --- orientation carries over to the next demo ------------------------------------ + + @Test + public void orientationPersistsAcrossContentChange() { + TabbedDemoView demo = new TabbedDemoView(); + Component first = new TabbedDemoViewSingleSource(); + demo.showRouterLayoutContent(first); + demo.setOrientation(Orientation.VERTICAL); + assertEquals(Orientation.VERTICAL, demo.getOrientation()); + + demo.removeRouterLayoutContent(first); + demo.showRouterLayoutContent(new TabbedDemoViewMultiSource()); + + assertEquals(Orientation.VERTICAL, demo.getOrientation()); + } + + @Test + public void orientationSetWithoutLayoutCarriesOverToNextDemo() { + // A source-less demo has no layout; setting the orientation there (as a device-driven portrait + // adjustment does on attach) must be remembered and applied to the next source-bearing demo. + TabbedDemoView demo = new TabbedDemoView(); + Component noSource = new TabbedDemoViewNoSource(); + demo.showRouterLayoutContent(noSource); + + demo.setOrientation(Orientation.VERTICAL); + assertEquals(Orientation.VERTICAL, demo.getOrientation()); + + demo.removeRouterLayoutContent(noSource); + demo.showRouterLayoutContent(new TabbedDemoViewSingleSource()); + + assertEquals(Orientation.VERTICAL, demo.getOrientation()); + } + + /** Counts elements with the given tag in the subtree rooted at {@code root} (inclusive). */ + private static long count(Element root, String tag) { + long self = tag.equals(root.getTag()) ? 1 : 0; + return self + root.getChildren() + .filter(e -> !e.isTextNode()) + .mapToLong(child -> count(child, tag)).sum(); + } + +} diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoView.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoView.java index 0aebc70..8d919f4 100644 --- a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoView.java +++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoView.java @@ -22,6 +22,7 @@ import com.flowingcode.vaadin.addons.GithubLink; import com.flowingcode.vaadin.addons.demo.AdHocDemo; import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.flowingcode.vaadin.addons.demo.SourcePosition; import com.flowingcode.vaadin.addons.demo.TabbedDemo; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; @@ -34,6 +35,7 @@ public class TabbedDemoView extends TabbedDemo { public TabbedDemoView() { addDemo(TabbedDemoViewNoSource.class); addDemo(TabbedDemoViewSingleSource.class); + addDemo(TabbedDemoViewPrimarySource.class); addDemo(TabbedDemoViewMultiSource.class); addDemo(TabbedDemoViewConditionalTrue.class); addDemo(TabbedDemoViewConditionalFalse.class); @@ -52,6 +54,10 @@ public static class TabbedDemoViewNoSource extends AbstractDemoView { } @Route(value = "it/tabbed-demo-single-source", layout = TabbedDemoView.class) public static class TabbedDemoViewSingleSource extends AbstractDemoView { } + @DemoSource(clazz = TabbedDemoView.class, sourcePosition = SourcePosition.PRIMARY) + @Route(value = "it/tabbed-demo-primary-source", layout = TabbedDemoView.class) + public static class TabbedDemoViewPrimarySource extends AbstractDemoView { } + @DemoSource(clazz = TabbedDemoView.class) @DemoSource(clazz = AdHocDemo.class) @Route(value = "it/tabbed-demo-multi-source", layout = TabbedDemoView.class) diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/ViewInitializerImpl.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/ViewInitializerImpl.java new file mode 100644 index 0000000..5de5e57 --- /dev/null +++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/it/ViewInitializerImpl.java @@ -0,0 +1,37 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.demo.it; +import com.flowingcode.vaadin.jsonmigration.InstrumentationViewInitializer; +import com.vaadin.flow.server.ServiceInitEvent; + +/** + * Service initializer for instrumented test views. + * + * @author Javier Godoy / Flowing Code + */ +@SuppressWarnings("serial") +public class ViewInitializerImpl extends InstrumentationViewInitializer { + + @Override + public void serviceInit(ServiceInitEvent event) { + registerInstrumentedRoute(OverlayView.RpcOverlayDemo.class); + } + +} diff --git a/base/src/test/java/com/flowingcode/vaadin/addons/demo/test/SplitLayoutDemoTest.java b/base/src/test/java/com/flowingcode/vaadin/addons/demo/test/SplitLayoutDemoTest.java new file mode 100644 index 0000000..8499bb5 --- /dev/null +++ b/base/src/test/java/com/flowingcode/vaadin/addons/demo/test/SplitLayoutDemoTest.java @@ -0,0 +1,179 @@ +/*- + * #%L + * Commons Demo + * %% + * Copyright (C) 2020 - 2026 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.demo.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import com.flowingcode.vaadin.addons.demo.SourceCodeTab; +import com.flowingcode.vaadin.addons.demo.SourcePosition; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.splitlayout.SplitLayout; +import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; +import lombok.SneakyThrows; +import lombok.experimental.ExtensionMethod; +import org.junit.Test; + +@ExtensionMethod(value = Reflect.class, suppressBaseMethods = true) +public class SplitLayoutDemoTest { + + private Div demo; + + private Component build(SourcePosition position) { + demo = new Div(); + demo.setId("content"); + return Reflect.newSplitLayoutDemo(demo, + List.of(new SourceCodeTab("src/test/java/Demo.java", position))); + } + + // --- collapse resolves to the correct splitter position ----------------------- + + @Test + public void collapseWithSecondarySourceMovesSplitterToFullPrimary() { + Component layout = build(SourcePosition.SECONDARY); + + layout.setSourceCollapsed(false); + assertEquals(50.0, splitterPosition(layout), 0.0); + + layout.setSourceCollapsed(true); + assertEquals(100.0, splitterPosition(layout), 0.0); + + layout.setSourceCollapsed(false); + assertEquals(50.0, splitterPosition(layout), 0.0); + } + + @Test + public void collapseWithPrimarySourceMovesSplitterToFullSecondary() { + Component layout = build(SourcePosition.PRIMARY); + + layout.setSourceCollapsed(true); + assertEquals(0.0, splitterPosition(layout), 0.0); + + layout.setSourceCollapsed(false); + assertEquals(50.0, splitterPosition(layout), 0.0); + } + + // --- source position swaps slot occupants and toggles cleanly ----------------- + + @Test + public void sourcePositionSwapsSlotsAndToggles() { + Component layout = build(SourcePosition.SECONDARY); + + // Default SECONDARY: demo in primary, source viewer in secondary. + assertSame(demo, layout.getContent().getPrimaryComponent()); + assertNotSame(demo, layout.getContent().getSecondaryComponent()); + Component code = layout.getContent().getSecondaryComponent(); + + layout.setSourcePosition(SourcePosition.PRIMARY); + assertSame(code, layout.getContent().getPrimaryComponent()); + assertSame(demo, layout.getContent().getSecondaryComponent()); + assertEquals(SourcePosition.PRIMARY, layout.getSourcePosition()); + + layout.setSourcePosition(SourcePosition.SECONDARY); + assertSame(demo, layout.getContent().getPrimaryComponent()); + assertSame(code, layout.getContent().getSecondaryComponent()); + assertEquals(SourcePosition.SECONDARY, layout.getSourcePosition()); + } + + // --- orientation set/get round-trips ------------------------------------------ + + @Test + public void orientationRoundTrips() { + Component layout = build(SourcePosition.SECONDARY); + assertEquals(Orientation.HORIZONTAL, layout.getOrientation()); + + layout.setOrientation(Orientation.VERTICAL); + assertEquals(Orientation.VERTICAL, layout.getOrientation()); + + layout.setOrientation(Orientation.HORIZONTAL); + assertEquals(Orientation.HORIZONTAL, layout.getOrientation()); + } + + private static double splitterPosition(Component layout) { + Double position = layout.getContent().getSplitterPosition(); + assertNotNull("splitter position was never set", position); + return position; + } + +} + + +/** + * Reflection accessors for the package-private {@code SplitLayoutDemo} and the protected + * {@code Composite.getContent()}, one static method per real method. Surfaced as Lombok + * {@code @ExtensionMethod}s so the test can drive the layout with real-API-looking call sites while + * the actual dispatch goes through reflection. + */ +final class Reflect { + + private Reflect() {} + + @SneakyThrows + static Component newSplitLayoutDemo(Component demo, List tabs) { + Class type = Class.forName("com.flowingcode.vaadin.addons.demo.SplitLayoutDemo"); + Constructor constructor = type.getDeclaredConstructor(Component.class, List.class); + constructor.setAccessible(true); + return (Component) constructor.newInstance(demo, tabs); + } + + public static void setSourceCollapsed(Component layout, boolean collapsed) { + call(layout, "setSourceCollapsed", new Class[] {boolean.class}, collapsed); + } + + public static void setSourcePosition(Component layout, SourcePosition position) { + call(layout, "setSourcePosition", new Class[] {SourcePosition.class}, position); + } + + public static void setOrientation(Component layout, Orientation orientation) { + call(layout, "setOrientation", new Class[] {Orientation.class}, orientation); + } + + public static Orientation getOrientation(Component layout) { + return (Orientation) call(layout, "getOrientation", new Class[0]); + } + + public static SourcePosition getSourcePosition(Component layout) { + return (SourcePosition) call(layout, "getSourcePosition", new Class[0]); + } + + public static SplitLayout getContent(Component layout) { + return (SplitLayout) call(layout, "getContent", new Class[0]); + } + + /** Locates {@code name(paramTypes)} on {@code target} or a supertype and invokes it. */ + @SneakyThrows + private static Object call(Object target, String name, Class[] paramTypes, Object... args) { + for (Class c = target.getClass(); c != null; c = c.getSuperclass()) { + try { + Method method = c.getDeclaredMethod(name, paramTypes); + method.setAccessible(true); + return method.invoke(target, args); + } catch (NoSuchMethodException declaredInSupertype) { + // keep walking up the hierarchy + } + } + throw new NoSuchMethodException(target.getClass().getName() + "#" + name); + } +} diff --git a/base/src/test/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener b/base/src/test/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener new file mode 100644 index 0000000..2d597cf --- /dev/null +++ b/base/src/test/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener @@ -0,0 +1 @@ +com.flowingcode.vaadin.addons.demo.it.ViewInitializerImpl \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9e9e1af..362867d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.flowingcode.vaadin.addons.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