Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create side nav from menu entries #6957

Merged
merged 3 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions vaadin-side-nav-flow-parent/vaadin-side-nav-flow/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
<artifactId>vaadin-flow-components-base</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-icons-flow</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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<MenuEntry> menuEntries) {
Objects.requireNonNull(menuEntries, "menuEntries cannot be null");

for (MenuEntry menuEntry : menuEntries) {
addItem(new SideNavItem(menuEntry));
}
}

/**
* Gets the label of this side navigation menu.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,18 @@
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;
import com.vaadin.flow.component.Synchronize;
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;
Expand All @@ -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;

Expand Down Expand Up @@ -197,6 +204,66 @@ public SideNavItem(String label, Class<? extends Component> view,
setPrefixComponent(prefixComponent);
}

/**
* Creates a new menu item from the given {@link MenuEntry}.
* <p>
* 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:
* <ul>
* <li>Icon set: the icon string contains ":" and is in the format
* "icon-set:icon-name", for example "vaadin:file"</li>
* <li>SVG icon: the icon string ends with ".svg"</li>
* </ul>
*
* @param entry
* the menu entry to create the item from
* @see MenuEntry
* @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
// 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<? extends 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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -218,6 +221,75 @@ 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);
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 {

Expand Down Expand Up @@ -399,6 +401,29 @@ 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, null, null);
List<MenuEntry> 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<MenuEntry>) null);
}

enum SetLabelOption {
SET_NO_LABEL, SET_LABEL_BEFORE_ITEMS_CREATION, SET_LABEL_DURING_ITEMS_CREATION, SET_LABEL_AFTER_ITEMS_CREATION
}
Expand Down