diff --git a/README.md b/README.md
index 90ab49b..aca8a4a 100644
--- a/README.md
+++ b/README.md
@@ -179,6 +179,16 @@ For instance, the following source would be rendered as `foo();` in Vaadin 23+,
Strictly, the constructor of `SourceCodeViewer` receives a map with arbitrary variables defined by the caller, and `TabbedDemo` defines "vaadin" and "flow" variables. Implementation details in PR https://github.com/FlowingCode/CommonsDemo/pull/44.
+The `@DemoSource` annotation also supports a `condition` attribute to control the visibility of the source tab based on the environment. This feature uses the same syntax and operators as the conditional directives.
+
+
+```java
+@DemoSource(value = "/src/test/resources/META-INF/resources/frontend/example.css", condition = "vaadin ge 24")
+public class MyDemo extends Div {
+ ...
+}
+```
+
### Fragment highlighting
This feature supports highlighting a source code fragment in order to emphasize a section of the code snippet.
diff --git a/pom.xml b/pom.xml
index 4054cff..1ebeb38 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
com.flowingcode.vaadin.addons.demo
commons-demo
- 5.0.1-SNAPSHOT
+ 5.1.0-SNAPSHOT
Commons Demo
Common classes for add-ons demo
@@ -130,7 +130,7 @@
io.github.bonigarcia
webdrivermanager
- 5.3.0
+ 6.3.2
test
diff --git a/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java b/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java
index 8b6885c..1ea0b84 100644
--- a/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java
+++ b/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSource.java
@@ -2,7 +2,7 @@
* #%L
* Commons Demo
* %%
- * Copyright (C) 2020 - 2025 Flowing Code
+ * 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.
@@ -77,4 +77,6 @@
/** Source code position in the layout */
SourcePosition sourcePosition() default SourcePosition.SECONDARY;
+ String condition() default "";
+
}
diff --git a/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSourceConditionHelper.java b/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSourceConditionHelper.java
new file mode 100644
index 0000000..977fad3
--- /dev/null
+++ b/src/main/java/com/flowingcode/vaadin/addons/demo/DemoSourceConditionHelper.java
@@ -0,0 +1,93 @@
+/*-
+ * #%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 java.util.Map;
+import lombok.NonNull;
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+class DemoSourceConditionHelper {
+
+ public boolean eval(String condition, Map env) {
+ if (condition == null) {
+ return true;
+ }
+
+ String[] expr = condition.split(" ");
+
+ if (expr.length == 3) {
+ String lhs = env.get(expr[0]);
+
+ if (lhs == null) {
+ return false;
+ }
+
+ String operator = expr[1];
+ String rhs = expr[2];
+
+ switch (operator) {
+ case "lt":
+ return compare(lhs, rhs) < 0;
+ case "le":
+ return compare(lhs, rhs) <= 0;
+ case "eq":
+ return compare(lhs, rhs) == 0;
+ case "ge":
+ return compare(lhs, rhs) >= 0;
+ case "gt":
+ return compare(lhs, rhs) > 0;
+ case "ne":
+ return compare(lhs, rhs) != 0;
+ default:
+ throw new IllegalArgumentException("Unknown operator: " + operator);
+ }
+ } else {
+ throw new IllegalArgumentException("Invalid condition: '" + condition
+ + "'. Must be exactly 3 components: [VARIABLE] [OPERATOR] [VERSION]");
+ }
+ }
+
+ private int compare(String a, String b) {
+ String[] aa = split(a);
+ String[] bb = split(b);
+
+ int minLength = Math.min(aa.length, bb.length);
+
+ for (int i = 0; i < minLength; i++) {
+ int ai = Integer.parseInt(aa[i]);
+ int bi = Integer.parseInt(bb[i]);
+ int c = Integer.compare(ai, bi);
+ if (c != 0) {
+ return c;
+ }
+ }
+
+ return 0;
+ }
+
+ private String[] split(@NonNull String version) {
+ if (!version.matches("^\\d+(\\.\\d+){0,2}$")) {
+ throw new IllegalArgumentException("Invalid version: '" + version
+ + "'. Must be 'major', 'major.minor', or major.minor.patch'");
+ }
+ return version.split("\\.");
+ }
+}
diff --git a/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java b/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java
index cbdf2d5..1fccdc2 100644
--- a/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java
+++ b/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java
@@ -2,7 +2,7 @@
* #%L
* Commons Demo
* %%
- * Copyright (C) 2020 - 2025 Flowing Code
+ * 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.
@@ -25,6 +25,7 @@
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.tabs.Tab;
import elemental.json.JsonValue;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -42,6 +43,12 @@ public class MultiSourceCodeViewer extends Div {
private EnhancedTabs tabs;
public MultiSourceCodeViewer(List sourceCodeTabs, Map properties) {
+ sourceCodeTabs = new ArrayList<>(sourceCodeTabs);
+ sourceCodeTabs.removeIf(tab -> !DemoSourceConditionHelper.eval(tab.getCondition(), properties));
+ if (sourceCodeTabs.isEmpty()) {
+ return;
+ }
+
if (sourceCodeTabs.size() > 1) {
tabs = new EnhancedTabs(createTabs(sourceCodeTabs));
tabs.addSelectedChangeListener(ev -> onTabSelected(ev.getSelectedTab()));
@@ -69,6 +76,10 @@ public MultiSourceCodeViewer(List sourceCodeTabs, Map sourceCodeTabs) {
return sourceCodeTabs.stream().map(this::createTab).toArray(Tab[]::new);
}
@@ -134,7 +145,12 @@ private void fetchContents(String url, String language) {
}
public SourcePosition getSourcePosition() {
- return (SourcePosition) ComponentUtil.getData(selectedTab, DATA_POSITION);
+ if (selectedTab != null) {
+ return (SourcePosition) ComponentUtil.getData(selectedTab, DATA_POSITION);
+ } else {
+ return SourcePosition.SECONDARY;
+ }
+
}
private Optional findTabWithFilename(String filename) {
diff --git a/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeTab.java b/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeTab.java
index 31f7632..aa2df50 100644
--- a/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeTab.java
+++ b/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeTab.java
@@ -2,7 +2,7 @@
* #%L
* Commons Demo
* %%
- * Copyright (C) 2020 - 2024 Flowing Code
+ * 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.
@@ -36,6 +36,7 @@ public class SourceCodeTab {
private final String url;
private String caption;
private String language;
+ private String condition;
@NonNull
private final SourcePosition sourcePosition;
diff --git a/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java b/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
index 03ba67d..5762111 100644
--- a/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
+++ b/src/main/java/com/flowingcode/vaadin/addons/demo/SplitLayoutDemo.java
@@ -2,7 +2,7 @@
* #%L
* Commons Demo
* %%
- * Copyright (C) 2020 - 2023 Flowing Code
+ * 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.
@@ -54,6 +54,10 @@ public SplitLayoutDemo(Component demo, List tabs) {
getContent().setSizeFull();
}
+ public boolean isEmpty() {
+ return code.isEmpty();
+ }
+
private void setSourcePosition(SourcePosition position) {
if (!position.equals(sourcePosition)) {
getContent().removeAll();
diff --git a/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
index 7f7fada..d8a5c3d 100644
--- a/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
+++ b/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
@@ -2,7 +2,7 @@
* #%L
* Commons Demo
* %%
- * Copyright (C) 2020 - 2025 Flowing Code
+ * 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.
@@ -181,16 +181,21 @@ public void showRouterLayoutContent(HasElement content) {
}
if (!sourceTabs.isEmpty()) {
- content = new SplitLayoutDemo(demo, sourceTabs);
- currentLayout = (SplitLayoutDemo) content;
+ currentLayout = new SplitLayoutDemo(demo, sourceTabs);
+ if (currentLayout.isEmpty()) {
+ demo.getElement().removeAttribute("slot");
+ currentLayout = null;
+ }
+ }
+
+ if (currentLayout != null) {
+ content = currentLayout;
if (splitOrientation != null) {
setOrientation(splitOrientation);
updateSplitterPosition();
}
- if (currentLayout != null) {
- setupDemoHelperButton(currentLayout.getContent().getPrimaryComponent().getClass());
- }
+ setupDemoHelperButton(currentLayout.getContent().getPrimaryComponent().getClass());
} else {
currentLayout = null;
demo.getElement().getStyle().set("height", "100%");
@@ -245,6 +250,10 @@ private Optional createSourceCodeTab(Class> annotatedClass, Dem
builder.language(annotation.caption());
}
+ if (!annotation.condition().isEmpty()) {
+ builder.condition(annotation.condition());
+ }
+
builder.sourcePosition(annotation.sourcePosition());
return Optional.of(builder.build());
diff --git a/src/test/java/com/flowingcode/vaadin/addons/demo/MultiSourceDemo.java b/src/test/java/com/flowingcode/vaadin/addons/demo/MultiSourceDemo.java
index fcfa8ee..e7e7751 100644
--- a/src/test/java/com/flowingcode/vaadin/addons/demo/MultiSourceDemo.java
+++ b/src/test/java/com/flowingcode/vaadin/addons/demo/MultiSourceDemo.java
@@ -1,7 +1,7 @@
/*-
* #%L
* %%
- * Copyright (C) 2020 - 2024 Flowing Code
+ * 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.
@@ -29,9 +29,13 @@
// show-source @DemoSource
// show-source @DemoSource(clazz = AdditionalSources.class)
// show-source @DemoSource("/src/test/resources/META-INF/resources/frontend/multi-source-demo.css")
+// show-source @DemoSource(value="/src/test/resources/META-INF/resources/frontend/condition-true.css", condition = "vaadin ge 14")
+// show-source @DemoSource(value="/src/test/resources/META-INF/resources/frontend/condition-false.css", condition = "vaadin eq 0")
@DemoSource
@DemoSource(clazz = AdditionalSources.class)
@DemoSource("/src/test/resources/META-INF/resources/frontend/multi-source-demo.css")
+@DemoSource(value="/src/test/resources/META-INF/resources/frontend/condition-true.css", condition = "vaadin ge 14")
+@DemoSource(value="/src/test/resources/META-INF/resources/frontend/condition-false.css", condition = "vaadin eq 0")
@StyleSheet("./multi-source-demo.css")
public class MultiSourceDemo extends Div {
public MultiSourceDemo() {
diff --git a/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoView.java b/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoView.java
new file mode 100644
index 0000000..0aebc70
--- /dev/null
+++ b/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoView.java
@@ -0,0 +1,67 @@
+/*-
+ * #%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.AdHocDemo;
+import com.flowingcode.vaadin.addons.demo.DemoSource;
+import com.flowingcode.vaadin.addons.demo.TabbedDemo;
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.Span;
+import com.vaadin.flow.router.Route;
+
+@SuppressWarnings("serial")
+@GithubLink("https://github.com/FlowingCode/CommonsDemo")
+public class TabbedDemoView extends TabbedDemo {
+
+ public TabbedDemoView() {
+ addDemo(TabbedDemoViewNoSource.class);
+ addDemo(TabbedDemoViewSingleSource.class);
+ addDemo(TabbedDemoViewMultiSource.class);
+ addDemo(TabbedDemoViewConditionalTrue.class);
+ addDemo(TabbedDemoViewConditionalFalse.class);
+ }
+
+ protected abstract static class AbstractDemoView extends Div {
+ public AbstractDemoView() {
+ add(new Span(this.getClass().getSimpleName()));
+ }
+ }
+
+ @Route(value = "it/tabbed-demo-no-source", layout = TabbedDemoView.class)
+ public static class TabbedDemoViewNoSource extends AbstractDemoView { }
+
+ @DemoSource(clazz = TabbedDemoView.class)
+ @Route(value = "it/tabbed-demo-single-source", layout = TabbedDemoView.class)
+ public static class TabbedDemoViewSingleSource extends AbstractDemoView { }
+
+ @DemoSource(clazz = TabbedDemoView.class)
+ @DemoSource(clazz = AdHocDemo.class)
+ @Route(value = "it/tabbed-demo-multi-source", layout = TabbedDemoView.class)
+ public static class TabbedDemoViewMultiSource extends AbstractDemoView { }
+
+ @DemoSource(clazz = TabbedDemoView.class, condition = "vaadin ge 14")
+ @Route(value = "it/tabbed-demo-conditional-true", layout = TabbedDemoView.class)
+ public static class TabbedDemoViewConditionalTrue extends AbstractDemoView { }
+
+ @DemoSource(clazz = TabbedDemoView.class, condition = "vaadin eq 0")
+ @Route(value = "it/tabbed-demo-conditional-false", layout = TabbedDemoView.class)
+ public static class TabbedDemoViewConditionalFalse extends AbstractDemoView { }
+}
diff --git a/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoViewIT.java b/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoViewIT.java
new file mode 100644
index 0000000..411c816
--- /dev/null
+++ b/src/test/java/com/flowingcode/vaadin/addons/demo/it/TabbedDemoViewIT.java
@@ -0,0 +1,137 @@
+/*-
+ * #%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.hamcrest.CoreMatchers.not;
+import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewConditionalFalse;
+import com.flowingcode.vaadin.addons.demo.it.TabbedDemoView.TabbedDemoViewConditionalTrue;
+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.router.Route;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.SearchContext;
+
+public class TabbedDemoViewIT extends AbstractViewTest {
+
+ private SourceCodeViewerElement viewer;
+
+ protected void open(Class extends Component> clazz) {
+ if (viewer != null) {
+ throw new IllegalStateException();
+ }
+ getDriver().get(getURL(clazz.getAnnotation(Route.class).value()));
+ getCommandExecutor().waitForVaadin();
+ getDriver().findElement(By.id("content"));
+ }
+
+ public static Matcher hasElement(String cssSelector) {
+ return new TypeSafeMatcher<>() {
+
+ @Override
+ protected boolean matchesSafely(SearchContext container) {
+ return !container.findElements(By.cssSelector(cssSelector)).isEmpty();
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("a page containing element: ").appendValue(cssSelector);
+ }
+
+ @Override
+ protected void describeMismatchSafely(SearchContext container,
+ Description mismatchDescription) {
+ mismatchDescription.appendText("no elements matched selector: ").appendValue(cssSelector);
+ }
+ };
+ }
+
+ private Matcher hasPrimaryCodeTabs() {
+ return hasElement("[slot='primary'] vaadin-menu-bar");
+ }
+
+ private Matcher hasPrimaryCodeViewer() {
+ return hasElement("[slot='primary'] code-viewer");
+ }
+
+ private Matcher hasSecondaryCodeTabs() {
+ return hasElement("[slot='secondary'] vaadin-menu-bar");
+ }
+
+ private Matcher hasSecondaryCodeViewer() {
+ return hasElement("[slot='secondary'] code-viewer");
+ }
+
+ private void assertThat(Matcher matcher) {
+ Assert.assertThat(getDriver(), matcher);
+ }
+
+ @Test
+ public void testSimpleNoSource() {
+ open(TabbedDemoViewNoSource.class);
+ assertThat(not(hasPrimaryCodeTabs()));
+ assertThat(not(hasPrimaryCodeViewer()));
+ assertThat(not(hasSecondaryCodeTabs()));
+ assertThat(not(hasSecondaryCodeViewer()));
+ }
+
+ @Test
+ public void testSimpleSingleSource() {
+ open(TabbedDemoViewSingleSource.class);
+ assertThat(not(hasPrimaryCodeTabs()));
+ assertThat(not(hasPrimaryCodeViewer()));
+ assertThat(not(hasSecondaryCodeTabs()));
+ assertThat(hasSecondaryCodeViewer());
+ }
+
+ @Test
+ public void testSimpleMultiSource() {
+ open(TabbedDemoViewMultiSource.class);
+ assertThat(not(hasPrimaryCodeTabs()));
+ assertThat(not(hasPrimaryCodeViewer()));
+ assertThat(hasSecondaryCodeTabs());
+ assertThat(hasSecondaryCodeViewer());
+ }
+
+ @Test
+ public void testSimpleConditionalTrue() {
+ open(TabbedDemoViewConditionalTrue.class);
+ assertThat(not(hasPrimaryCodeTabs()));
+ assertThat(not(hasPrimaryCodeViewer()));
+ assertThat(not(hasSecondaryCodeTabs()));
+ assertThat(hasSecondaryCodeViewer());
+ }
+
+ @Test
+ public void testSimpleConditionalFalse() {
+ open(TabbedDemoViewConditionalFalse.class);
+ assertThat(not(hasPrimaryCodeTabs()));
+ assertThat(not(hasPrimaryCodeViewer()));
+ assertThat(not(hasSecondaryCodeTabs()));
+ assertThat(not(hasSecondaryCodeViewer()));
+ }
+
+}
diff --git a/src/test/resources/META-INF/resources/frontend/condition-false.css b/src/test/resources/META-INF/resources/frontend/condition-false.css
new file mode 100644
index 0000000..6a37837
--- /dev/null
+++ b/src/test/resources/META-INF/resources/frontend/condition-false.css
@@ -0,0 +1 @@
+/** This source file is conditionally hidden. */
\ No newline at end of file
diff --git a/src/test/resources/META-INF/resources/frontend/condition-true.css b/src/test/resources/META-INF/resources/frontend/condition-true.css
new file mode 100644
index 0000000..c63b5c4
--- /dev/null
+++ b/src/test/resources/META-INF/resources/frontend/condition-true.css
@@ -0,0 +1 @@
+/** This source is shown when Vaadin version is 14+ */
\ No newline at end of file