From f09aae3e768f7692ce188e58635b0cda50c6d881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Tue, 17 Dec 2024 07:35:48 +0100 Subject: [PATCH 1/3] feat: create side nav items from menu entries --- .../vaadin-side-nav-flow/pom.xml | 5 ++ .../flow/component/sidenav/SideNavItem.java | 65 ++++++++++++++++++ .../sidenav/tests/SideNavItemTest.java | 67 +++++++++++++++++++ 3 files changed, 137 insertions(+) diff --git a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/pom.xml b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/pom.xml index e0a86b8d45c..1bf25d3cbe2 100644 --- a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/pom.xml +++ b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/pom.xml @@ -21,6 +21,11 @@ vaadin-flow-components-base ${project.version} + + com.vaadin + vaadin-icons-flow + ${project.version} + jakarta.platform jakarta.jakartaee-web-api diff --git a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/main/java/com/vaadin/flow/component/sidenav/SideNavItem.java b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/main/java/com/vaadin/flow/component/sidenav/SideNavItem.java index f87d5720656..7d8889a9576 100644 --- a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/main/java/com/vaadin/flow/component/sidenav/SideNavItem.java +++ b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/main/java/com/vaadin/flow/component/sidenav/SideNavItem.java @@ -24,6 +24,8 @@ import java.util.function.Function; import java.util.stream.Collectors; +import org.slf4j.LoggerFactory; + import com.vaadin.flow.component.Component; import com.vaadin.flow.component.ComponentUtil; import com.vaadin.flow.component.HasEnabled; @@ -31,6 +33,9 @@ import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.dependency.JsModule; import com.vaadin.flow.component.dependency.NpmPackage; +import com.vaadin.flow.component.icon.AbstractIcon; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.SvgIcon; import com.vaadin.flow.component.shared.HasPrefix; import com.vaadin.flow.component.shared.HasSuffix; import com.vaadin.flow.dom.Element; @@ -43,6 +48,8 @@ import com.vaadin.flow.router.RouteParameters; import com.vaadin.flow.router.internal.ConfigureRoutes; import com.vaadin.flow.router.internal.HasUrlParameterFormat; +import com.vaadin.flow.server.menu.MenuConfiguration; +import com.vaadin.flow.server.menu.MenuEntry; import elemental.json.JsonArray; @@ -197,6 +204,64 @@ public SideNavItem(String label, Class view, setPrefixComponent(prefixComponent); } + /** + * Creates a new menu item from the given {@link MenuEntry}. + *

+ * If the entry has an icon string, creates an instance of {@link Icon} or + * {@link SvgIcon} based on the icon string and sets it as prefix component. + * Note that only the following icon types are supported: + *

+ * + * @param entry + * the menu entry to create the item from + * @see MenuEntry + * @see MenuConfiguration + */ + public SideNavItem(MenuEntry entry) { + setLabel(entry.title()); + + // If there is a menu class, use it as the path to also add path aliases + // Client routes have no menu class, so use the path as fallback + if (entry.menuClass() != null) { + setPath(entry.menuClass()); + } else { + setPath(entry.path()); + } + + AbstractIcon icon = createIconFromMenuEntry(entry); + if (icon != null) { + setPrefixComponent(icon); + } + } + + private AbstractIcon> createIconFromMenuEntry( + MenuEntry entry) { + // No icon + if (entry.icon() == null) { + return null; + } + + // Icon set + if (entry.icon().contains(":") && entry.icon().split(":").length == 2) { + return new Icon(entry.icon()); + } + + // SVG icon + if (entry.icon().endsWith(".svg")) { + return new SvgIcon(entry.icon()); + } + + // Icon component doesn't support other types of icons, log a warning + LoggerFactory.getLogger(SideNavItem.class) + .warn("Icon type not supported: {}", entry.icon()); + + return null; + } + @Override protected void setupSideNavItem(SideNavItem item) { item.getElement().setAttribute("slot", "children"); diff --git a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavItemTest.java b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavItemTest.java index bd11d914d71..7761c78e687 100644 --- a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavItemTest.java +++ b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavItemTest.java @@ -31,6 +31,8 @@ import com.vaadin.flow.component.Component; import com.vaadin.flow.component.ComponentUtil; import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.SvgIcon; import com.vaadin.flow.component.sidenav.SideNavItem; import com.vaadin.flow.dom.Element; import com.vaadin.flow.router.BeforeEvent; @@ -43,6 +45,7 @@ import com.vaadin.flow.router.RouteParameters; import com.vaadin.flow.router.Router; import com.vaadin.flow.server.VaadinContext; +import com.vaadin.flow.server.menu.MenuEntry; import com.vaadin.flow.server.startup.ApplicationRouteRegistry; import elemental.json.JsonArray; @@ -218,6 +221,70 @@ public void createWithPathAndPrefix_pathAndPrefixIsSet() { Assert.assertEquals(prefixComponent, sideNavItem.getPrefixComponent()); } + @Test + public void createFromMenuEntry_setsLabelAndPath() { + MenuEntry entry = new MenuEntry("path", "Test label", 0.0, null, null); + SideNavItem sideNavItem = new SideNavItem(entry); + + Assert.assertEquals("Test label", sideNavItem.getLabel()); + Assert.assertEquals("path", sideNavItem.getPath()); + } + + @Test + public void createFromMenuEntry_withMenuClass_setsRouteAndAliases() { + runWithMockRouter(() -> { + MenuEntry entry = new MenuEntry("path", "Test label", 0.0, null, + TestRouteWithAliases.class); + SideNavItem sideNavItem = new SideNavItem(entry); + + Assert.assertEquals("Test label", sideNavItem.getLabel()); + Assert.assertEquals("foo/bar", sideNavItem.getPath()); + Assert.assertEquals(Set.of("foo/baz", "foo/qux"), + sideNavItem.getPathAliases()); + }, TestRouteWithAliases.class); + } + + @Test + public void createFromMenuEntry_withIconSetIcon_setsIcon() { + MenuEntry entry = new MenuEntry("path", "Test label", 0.0, + "vaadin:icon", null); + SideNavItem sideNavItem = new SideNavItem(entry); + + Assert.assertNotNull(sideNavItem.getPrefixComponent()); + Assert.assertTrue(sideNavItem.getPrefixComponent() instanceof Icon); + Assert.assertEquals("vaadin:icon", + ((Icon) sideNavItem.getPrefixComponent()).getIcon()); + } + + @Test + public void createFromMenuEntry_withSvgIcon_setsIcon() { + MenuEntry entry = new MenuEntry("path", "Test label", 0.0, + "assets/globe.svg", null); + SideNavItem sideNavItem = new SideNavItem(entry); + + Assert.assertNotNull(sideNavItem.getPrefixComponent()); + Assert.assertTrue(sideNavItem.getPrefixComponent() instanceof SvgIcon); + Assert.assertEquals("assets/globe.svg", + ((SvgIcon) sideNavItem.getPrefixComponent()).getSrc()); + } + + @Test + public void createFromMenuEntry_withoutIcon_noIconSet() { + MenuEntry entry = new MenuEntry("path", "Test label", 0.0, null, null); + SideNavItem sideNavItem = new SideNavItem(entry); + + Assert.assertNull(sideNavItem.getPrefixComponent()); + } + + @Test + public void createFromMenuEntry_unsupportedIcon_noIconSet() { + MenuEntry entry = new MenuEntry("path", "Test label", 0.0, + "assets/globe.png", null); + SideNavItem sideNavItem = new SideNavItem(entry); + + Assert.assertNull(sideNavItem.getPrefixComponent()); + } + // EXPAND AND COLLAPSE TESTS @Test From 067ce9cdfe78131f913fc7179b3a19a22e93df23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Tue, 17 Dec 2024 08:45:41 +0100 Subject: [PATCH 2/3] create side nav from multiple entries --- .../flow/component/sidenav/SideNav.java | 21 +++++++++++++++ .../component/sidenav/tests/SideNavTest.java | 26 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/main/java/com/vaadin/flow/component/sidenav/SideNav.java b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/main/java/com/vaadin/flow/component/sidenav/SideNav.java index a918f2669b9..9bd00fd5cbd 100644 --- a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/main/java/com/vaadin/flow/component/sidenav/SideNav.java +++ b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/main/java/com/vaadin/flow/component/sidenav/SideNav.java @@ -16,6 +16,7 @@ package com.vaadin.flow.component.sidenav; import java.io.Serializable; +import java.util.Collection; import java.util.Objects; import com.vaadin.flow.component.HasSize; @@ -26,6 +27,8 @@ import com.vaadin.flow.component.dependency.NpmPackage; import com.vaadin.flow.dom.Element; import com.vaadin.flow.internal.JsonSerializer; +import com.vaadin.flow.server.menu.MenuConfiguration; +import com.vaadin.flow.server.menu.MenuEntry; /** * A side navigation menu with support for hierarchical and flat menus. @@ -61,6 +64,24 @@ public SideNav(String label) { setLabel(label); } + /** + * Creates a new menu from the given menu entries, which can be retrieved + * from {@link MenuConfiguration}. + * + * @param menuEntries + * the menu entries to add + * @see MenuConfiguration + * @see MenuEntry + * @see SideNavItem#SideNavItem(MenuEntry) + */ + public SideNav(Collection menuEntries) { + Objects.requireNonNull(menuEntries, "menuEntries cannot be null"); + + for (MenuEntry menuEntry : menuEntries) { + addItem(new SideNavItem(menuEntry)); + } + } + /** * Gets the label of this side navigation menu. * diff --git a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavTest.java b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavTest.java index 3d50d210033..16f849e2dba 100644 --- a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavTest.java +++ b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavTest.java @@ -21,6 +21,7 @@ import static com.vaadin.flow.component.sidenav.tests.SideNavTest.SetLabelOption.SET_NO_LABEL; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Objects; @@ -31,6 +32,7 @@ import com.vaadin.flow.component.sidenav.SideNav; import com.vaadin.flow.component.sidenav.SideNavItem; import com.vaadin.flow.dom.Element; +import com.vaadin.flow.server.menu.MenuEntry; public class SideNavTest { @@ -399,6 +401,30 @@ public void removeUnknownItem_nothingHappens() { Assert.assertEquals(sideNav.getItems(), sideNavItems); } + @Test + public void createFromMenuEntries_menuEntriesAdded() { + MenuEntry entry1 = new MenuEntry("path1", "Item 1", 0.0, null, null); + MenuEntry entry2 = new MenuEntry("path2", "Item 2", 1.0, null, null); + MenuEntry entry3 = new MenuEntry("path3", "Item 3", 2.0, "vaadin:file", + null); + List menuEntries = List.of(entry1, entry2, entry3); + + SideNav nav = new SideNav(menuEntries); + + Assert.assertEquals(3, nav.getItems().size()); + Assert.assertEquals("Item 1", nav.getItems().get(0).getLabel()); + Assert.assertEquals("path1", nav.getItems().get(0).getPath()); + Assert.assertEquals("Item 2", nav.getItems().get(1).getLabel()); + Assert.assertEquals("path2", nav.getItems().get(1).getPath()); + Assert.assertEquals("Item 3", nav.getItems().get(2).getLabel()); + Assert.assertEquals("path3", nav.getItems().get(2).getPath()); + } + + @Test(expected = NullPointerException.class) + public void createFromNullMenuEntries_throws() { + new SideNav((Collection) null); + } + enum SetLabelOption { SET_NO_LABEL, SET_LABEL_BEFORE_ITEMS_CREATION, SET_LABEL_DURING_ITEMS_CREATION, SET_LABEL_AFTER_ITEMS_CREATION } From 7619b0ae25b592a7d9e6da57419dc115856f7f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Tue, 17 Dec 2024 10:45:27 +0100 Subject: [PATCH 3/3] cleanup --- .../java/com/vaadin/flow/component/sidenav/SideNavItem.java | 2 ++ .../vaadin/flow/component/sidenav/tests/SideNavItemTest.java | 5 +++++ .../com/vaadin/flow/component/sidenav/tests/SideNavTest.java | 3 +-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/main/java/com/vaadin/flow/component/sidenav/SideNavItem.java b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/main/java/com/vaadin/flow/component/sidenav/SideNavItem.java index 7d8889a9576..0d721909a41 100644 --- a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/main/java/com/vaadin/flow/component/sidenav/SideNavItem.java +++ b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/main/java/com/vaadin/flow/component/sidenav/SideNavItem.java @@ -222,6 +222,8 @@ public SideNavItem(String label, Class view, * @see MenuConfiguration */ public SideNavItem(MenuEntry entry) { + Objects.requireNonNull(entry, "Menu entry cannot be null"); + setLabel(entry.title()); // If there is a menu class, use it as the path to also add path aliases diff --git a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavItemTest.java b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavItemTest.java index 7761c78e687..54e3c07ab85 100644 --- a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavItemTest.java +++ b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavItemTest.java @@ -221,6 +221,11 @@ public void createWithPathAndPrefix_pathAndPrefixIsSet() { Assert.assertEquals(prefixComponent, sideNavItem.getPrefixComponent()); } + @Test(expected = NullPointerException.class) + public void createFromMenuEntry_entryIsNull_throws() { + new SideNavItem((MenuEntry) null); + } + @Test public void createFromMenuEntry_setsLabelAndPath() { MenuEntry entry = new MenuEntry("path", "Test label", 0.0, null, null); diff --git a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavTest.java b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavTest.java index 16f849e2dba..efd97b1d25f 100644 --- a/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavTest.java +++ b/vaadin-side-nav-flow-parent/vaadin-side-nav-flow/src/test/java/com/vaadin/flow/component/sidenav/tests/SideNavTest.java @@ -405,8 +405,7 @@ public void removeUnknownItem_nothingHappens() { public void createFromMenuEntries_menuEntriesAdded() { MenuEntry entry1 = new MenuEntry("path1", "Item 1", 0.0, null, null); MenuEntry entry2 = new MenuEntry("path2", "Item 2", 1.0, null, null); - MenuEntry entry3 = new MenuEntry("path3", "Item 3", 2.0, "vaadin:file", - null); + MenuEntry entry3 = new MenuEntry("path3", "Item 3", 2.0, null, null); List menuEntries = List.of(entry1, entry2, entry3); SideNav nav = new SideNav(menuEntries);