Skip to content

Commit 0e4974a

Browse files
Michele-MasciaveGianmarco Manni
andauthored
Added Hide Feature on MenuItems (#56)
Co-authored-by: Gianmarco Manni <gianmarco.manni@neolution.ch>
1 parent 3a505b9 commit 0e4974a

9 files changed

Lines changed: 255 additions & 59 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- various utility functions `getPreExpandedMenuItems`, `getChildrenPanelItemsIds`, `getHiddenPanelIds`
13+
- possibility to dynamic display the menu items
14+
- improved Context status management so that the Layout is using only the states inside it
15+
16+
### Fixed
17+
18+
- issue for which hiding a menu entry was breaking the rules of hook
19+
1020
## [4.0.0] - 2024-05-14
1121

1222
### Added

cypress/cypress/component/PanelSidebar/PanelSidebar.cy.tsx

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import React from "react";
2-
import { PanelSideBarProvider, PanelSideBarLayout, PanelItem, PanelLinkRendererProps } from "react-pattern-ui";
1+
import React, { PropsWithChildren, ReactNode } from "react";
2+
import { PanelSideBarProvider, PanelSideBarLayout, PanelItem, PanelLinkRendererProps, usePanelSideBarContext } from "react-pattern-ui";
33
import { faBars, faCogs } from "@fortawesome/free-solid-svg-icons";
44

55
type AppRoutes = "home" | "settings" | "dropdownTest" | "dropdown-test1" | "dropdown-test2";
66
type TSideBarMenuItem = PanelItem<AppRoutes>;
77

88
// Configuration object for avoiding duplicated code
9-
interface PanelSideBarConfiguration {
9+
interface PanelSideBarConfiguration extends PropsWithChildren {
1010
renderFirstItemsLevelAsTiles?: boolean;
1111
useToggleButton?: boolean;
1212
}
1313

1414
const getPanelSidebarInternal = (items: TSideBarMenuItem[], config?: PanelSideBarConfiguration) => {
15-
const { renderFirstItemsLevelAsTiles = true, useToggleButton = false } = config ?? {};
15+
const { renderFirstItemsLevelAsTiles = true, useToggleButton = false, children } = config ?? {};
1616
return (
1717
<PanelSideBarProvider
1818
menuItems={items}
@@ -32,13 +32,13 @@ const getPanelSidebarInternal = (items: TSideBarMenuItem[], config?: PanelSideBa
3232
)}
3333
>
3434
<PanelSideBarLayout useToggleButton={useToggleButton}>
35-
<div id="pageContent">Cypress</div>
35+
<div id="pageContent">{children ?? "Cypress"}</div>
3636
</PanelSideBarLayout>
3737
</PanelSideBarProvider>
3838
);
3939
};
4040

41-
const getSidebarItems = (active?: boolean, disabled?: boolean): TSideBarMenuItem[] => [
41+
const getSidebarItems = (active?: boolean, disabled?: boolean, expanded?: boolean): TSideBarMenuItem[] => [
4242
{
4343
id: "home",
4444
title: "Home",
@@ -65,6 +65,7 @@ const getSidebarItems = (active?: boolean, disabled?: boolean): TSideBarMenuItem
6565
{
6666
title: "Dropdown",
6767
id: "dropdownTest",
68+
expanded,
6869
children: [
6970
{
7071
title: "Dropdown test 1",
@@ -81,19 +82,24 @@ const getSidebarItems = (active?: boolean, disabled?: boolean): TSideBarMenuItem
8182
},
8283
];
8384

84-
const PanelSideBarWithTiles = (props: { active?: boolean; disabled?: boolean }) => {
85-
const { active, disabled } = props;
86-
return getPanelSidebarInternal(getSidebarItems(active, disabled));
85+
interface PanelSideBarProps extends PropsWithChildren {
86+
active?: boolean;
87+
disabled?: boolean;
88+
expanded?: boolean;
89+
}
90+
91+
const PanelSideBarWithTiles = (props: PanelSideBarProps) => {
92+
const { active, disabled, expanded, children } = props;
93+
return getPanelSidebarInternal(getSidebarItems(active, disabled, expanded), { children });
8794
};
8895

89-
const PanelSideBarNoTiles = (props: { active?: boolean; disabled?: boolean }) => {
96+
const PanelSideBarNoTiles = (props: PanelSideBarProps) => {
9097
const { active, disabled } = props;
9198
return getPanelSidebarInternal(getSidebarItems(active, disabled), { renderFirstItemsLevelAsTiles: false, useToggleButton: true });
9299
};
93100

94101
describe("PanelSidebar.cy.tsx", () => {
95102
it("icon and titles rendered correctly", () => {
96-
const sidebarItems = getSidebarItems();
97103
cy.mount(<PanelSideBarWithTiles />);
98104

99105
// Check if icon are rendered
@@ -143,7 +149,6 @@ describe("PanelSidebar.cy.tsx", () => {
143149
});
144150

145151
it("toggle sidebar", () => {
146-
const sidebarItems = getSidebarItems();
147152
cy.mount(<PanelSideBarWithTiles />);
148153

149154
// Check toggle sidebar
@@ -172,4 +177,73 @@ describe("PanelSidebar.cy.tsx", () => {
172177
cy.get("#sidebar-toggle").click();
173178
cy.get("#side-nav").should("have.css", "width", "0px");
174179
});
180+
181+
it("check dropdown correctly pre-expanded", () => {
182+
cy.mount(<PanelSideBarWithTiles expanded />);
183+
cy.get("button[title=Settings]").click();
184+
cy.get("#dropdown-test1").should("be.visible");
185+
cy.get("#dropdown-test2").should("be.visible");
186+
});
187+
188+
it("dynamically toggle menu item", () => {
189+
const Button = () => {
190+
const { toggleMenuItem } = usePanelSideBarContext();
191+
return (
192+
<button id="test-toggle" onClick={() => toggleMenuItem("dropdownTest")}>
193+
Toggle
194+
</button>
195+
);
196+
};
197+
198+
cy.mount(<PanelSideBarWithTiles expanded children={<Button />} />);
199+
cy.get("button[title=Settings]").click();
200+
cy.get("li:has(.dropdown-toggle)").should("be.visible").should("have.class", "menu-open");
201+
cy.get("#test-toggle").click();
202+
cy.get("li:has(.dropdown-toggle)").should("be.visible").should("not.have.class", "menu-open");
203+
cy.get("#test-toggle").click();
204+
cy.get("li:has(.dropdown-toggle)").should("be.visible").should("have.class", "menu-open");
205+
});
206+
207+
it("dynamically hide menu items", () => {
208+
const Button = () => {
209+
const { setHiddenMenuItemsIds } = usePanelSideBarContext();
210+
return (
211+
<button id="test-hide" onClick={() => setHiddenMenuItemsIds(["dropdown-test1", "dropdown-test2"])}>
212+
Hide
213+
</button>
214+
);
215+
};
216+
217+
cy.mount(<PanelSideBarWithTiles expanded children={<Button />} />);
218+
cy.get("button[title=Settings]").click();
219+
cy.get("#dropdown-test1").should("be.visible");
220+
cy.get("#dropdown-test2").should("be.visible");
221+
cy.get("#test-hide").click();
222+
cy.get("#dropdown-test1").should("not.be.visible");
223+
cy.get("#dropdown-test2").should("not.be.visible");
224+
});
225+
226+
it("dynamically open and close menu item", () => {
227+
const Button = () => {
228+
const { openMenuItems, closeMenuItems } = usePanelSideBarContext();
229+
return (
230+
<>
231+
<button id="test-open-item" onClick={() => openMenuItems(["dropdownTest"])}>
232+
Open
233+
</button>
234+
<button id="test-close-item" onClick={() => closeMenuItems(["dropdownTest"])}>
235+
Close
236+
</button>
237+
</>
238+
);
239+
};
240+
241+
cy.mount(<PanelSideBarWithTiles children={<Button />} />);
242+
cy.get("button[title=Settings]").click();
243+
cy.get("li:has(.dropdown-toggle)").should("be.visible").should("not.have.class", "menu-open");
244+
cy.get("#test-open-item").click();
245+
cy.get("li:has(.dropdown-toggle)").should("be.visible").should("have.class", "menu-open");
246+
cy.get("#test-close-item").click();
247+
cy.get("li:has(.dropdown-toggle)").should("be.visible").should("not.have.class", "menu-open");
248+
});
175249
});

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from "./lib/Layout/PanelSideBarLayout/PanelSideBarLayout";
66
export * from "./lib/Layout/PanelSideBarLayout/PanelSideBar/Context/PanelSideBarContext";
77
export * from "./lib/Layout/PanelSideBarLayout/PanelSideBar/Definitions/PanelItem";
88
export * from "./lib/Layout/PanelSideBarLayout/PanelSideBar/Definitions/PanelLinkRenderer";
9+
export * from "./lib/Layout/PanelSideBarLayout/PanelSideBar/Utils/panelUtils";

src/lib/Layout/PanelSideBarLayout/PanelSideBar/Context/PanelSideBarContext.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { Context, createContext, useContext, useEffect, useMemo, useState } from "react";
2-
import { getActivePanel } from "../Utils/getActivePanel";
2+
import { getActivePanel, getActivePanelParentsIds } from "../Utils/getActivePanel";
33
import { PanelSideBarContextProps } from "./PanelSideBarContextProps";
4+
import { getHiddenPanelIds, getPreExpandedMenuItems } from "../Utils/panelUtils";
45

56
export type MenuItemToggleFn<TPanelItemId extends string> = (menuItemId: TPanelItemId) => void;
67

@@ -36,14 +37,15 @@ export const PanelSideBarProvider = <TPanelItemId extends string, TPanelItem>(
3637
theme = "blue",
3738
} = props;
3839
const menuItems = useMemo(() => defaultMenuItems, [defaultMenuItems]);
39-
4040
const [isSidebarOpen, setIsSidebarOpen] = useState(sidebarOpenByDefault);
4141
const toggleSidebar = () => setIsSidebarOpen((prev) => !prev);
4242

4343
const [activePanelId, setActivePanelId] = useState(getActivePanel(menuItems, defaultActivePanelId)?.id);
4444
const setActivePanel = (panelId: TPanelItemId) => setActivePanelId(panelId);
4545

46-
const preExpandedMenuItemIds = menuItems.filter((x) => x.expanded).map((x) => x.id);
46+
const [hiddenMenuItemIds, setHiddenMenuItemsIds] = useState<TPanelItemId[]>(getHiddenPanelIds(menuItems));
47+
48+
const preExpandedMenuItemIds = getPreExpandedMenuItems(menuItems);
4749
const [toggledMenuItemIds, setToggledMenuItemIds] = useState<TPanelItemId[]>(
4850
activePanelId ? preExpandedMenuItemIds.concat(activePanelId) : preExpandedMenuItemIds,
4951
);
@@ -63,12 +65,28 @@ export const PanelSideBarProvider = <TPanelItemId extends string, TPanelItem>(
6365
const activePanelId = getActivePanel(menuItems, defaultActivePanelId)?.id;
6466
setActivePanelId(activePanelId);
6567
if (activePanelId) {
66-
setToggledMenuItemIds((prev) => (prev.includes(activePanelId) ? prev : [...prev, activePanelId]));
68+
setToggledMenuItemIds((prev) => {
69+
const toggledMenuItemIds = [...getActivePanelParentsIds(menuItems, activePanelId), activePanelId].filter((x) => !prev.includes(x));
70+
return [...prev, ...toggledMenuItemIds];
71+
});
6772
}
6873
}, [menuItems]);
6974

7075
const untoggleMenuItems = () => setToggledMenuItemIds([]);
7176

77+
const openMenuItems = (panelItemIds: TPanelItemId[]) => {
78+
setToggledMenuItemIds((prev) => [...prev, ...panelItemIds.filter((x) => !prev.includes(x))]);
79+
};
80+
81+
const closeMenuItems = (panelItemIds: TPanelItemId[], includeActivePanel?: boolean) => {
82+
const activePanels = activePanelId ? [...getActivePanelParentsIds(menuItems, activePanelId), activePanelId] : [];
83+
setToggledMenuItemIds((prev) =>
84+
includeActivePanel
85+
? prev.filter((x) => !panelItemIds.includes(x))
86+
: prev.filter((x) => !panelItemIds.filter((y) => !activePanels.includes(y)).includes(x)),
87+
);
88+
};
89+
7290
return (
7391
<PanelSideBarContext.Provider
7492
value={{
@@ -84,6 +102,10 @@ export const PanelSideBarProvider = <TPanelItemId extends string, TPanelItem>(
84102
theme,
85103
renderFirstItemsLevelAsTiles,
86104
renderTilesAsLinks,
105+
openMenuItems,
106+
closeMenuItems,
107+
hiddenMenuItemIds,
108+
setHiddenMenuItemsIds,
87109
}}
88110
>
89111
{children}

src/lib/Layout/PanelSideBarLayout/PanelSideBar/Context/PanelSideBarContextProps.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Dispatch, SetStateAction } from "react";
12
import { PanelItem } from "../Definitions/PanelItem";
23
import { PanelLinkRenderer } from "../Definitions/PanelLinkRenderer";
34
import { MenuItemToggleFn } from "./PanelSideBarContext";
@@ -27,7 +28,7 @@ export interface PanelSideBarContextProps<TPanelItemId extends string, TPanelIte
2728
toggleMenuItem: MenuItemToggleFn<TPanelItemId>;
2829

2930
/**
30-
* The default active panel id that will be taken if no active panel is dinamically found
31+
* The default active panel id that will be taken if no active panel is dynamically found
3132
*/
3233
defaultActivePanelId?: TPanelItemId;
3334

@@ -63,4 +64,27 @@ export interface PanelSideBarContextProps<TPanelItemId extends string, TPanelIte
6364
* The component used to render the menu item links.
6465
*/
6566
LinkRenderer: PanelLinkRenderer<TPanelItemId, TPanelItem>;
67+
68+
/**
69+
* The list of toggled menu item identifier
70+
*/
71+
hiddenMenuItemIds: TPanelItemId[];
72+
73+
/**
74+
* Function to get the hidden menu items
75+
*/
76+
setHiddenMenuItemsIds: Dispatch<SetStateAction<TPanelItemId[]>>;
77+
78+
/**
79+
* Function to open menu items
80+
* @param panelItemIds the panel item identifiers to open
81+
*/
82+
openMenuItems: (panelItemIds: TPanelItemId[]) => void;
83+
84+
/**
85+
* Function to close menu items
86+
* @param panelItemIds the panel item identifiers to close
87+
* @param includeActivePanel whether needs to include the active panel
88+
*/
89+
closeMenuItems: (panelItemIds: TPanelItemId[], includeActivePanel?: boolean) => void;
6690
}

src/lib/Layout/PanelSideBarLayout/PanelSideBar/PanelSideBarItem.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
22
import classNames from "classnames";
3-
import { useState, useRef, useEffect } from "react";
3+
import { useRef, useEffect } from "react";
44
import { Collapse, NavItem } from "reactstrap";
55
import { PanelItem } from "./../PanelSideBar/Definitions/PanelItem";
66
import { usePanelSideBarContext } from "./Context/PanelSideBarContext";
77
import { hasActiveChildren } from "./Utils/getActivePanel";
88

99
export interface PanelSideBarItemProps<TPanelItemId extends string, TPanelItem> {
1010
children: PanelItem<TPanelItemId, TPanelItem>;
11-
onClick?: (menuItemId: TPanelItemId) => void;
1211
depth?: number;
1312
active?: boolean;
14-
toggledItemIds: string[];
13+
isParentHidden?: boolean;
1514
}
1615

1716
// eslint-disable-next-line complexity
1817
const PanelSideBarItem = <TPanelItemId extends string, TPanelItem>(props: PanelSideBarItemProps<TPanelItemId, TPanelItem>) => {
19-
const { depth = 0, children: item, onClick, toggledItemIds = [] } = props;
20-
const { LinkRenderer } = usePanelSideBarContext<TPanelItemId, TPanelItem>();
18+
const { depth = 0, children: item, isParentHidden = false } = props;
19+
const { LinkRenderer, toggledMenuItemIds, toggleMenuItem, hiddenMenuItemIds } = usePanelSideBarContext<TPanelItemId, TPanelItem>();
2120
const hasitem = !!item.children?.length;
2221
const isActive = (hasitem && item.children && hasActiveChildren(item.children)) || item.active;
23-
const [isOpen, setIsOpen] = useState(isActive || toggledItemIds?.includes(item.id) || item.expanded);
24-
if (item.display === false) {
25-
return null;
26-
}
22+
const isOpen = toggledMenuItemIds?.includes(item.id);
2723
const scrollToActiveItemRef = useRef<HTMLDivElement>(null);
2824

2925
useEffect(() => {
@@ -35,7 +31,12 @@ const PanelSideBarItem = <TPanelItemId extends string, TPanelItem>(props: PanelS
3531
return (
3632
<>
3733
<NavItem
38-
onClick={() => onClick && onClick(item.id)}
34+
hidden={isParentHidden || hiddenMenuItemIds.includes(item.id)}
35+
onClick={() => {
36+
if (hasitem && !item.collapseIconOnly) {
37+
toggleMenuItem(item.id);
38+
}
39+
}}
3940
className={classNames({ "menu-open": isOpen, active: isActive })}
4041
style={{ paddingLeft: depth ? `${depth + 1}rem` : undefined }}
4142
>
@@ -54,7 +55,11 @@ const PanelSideBarItem = <TPanelItemId extends string, TPanelItem>(props: PanelS
5455
<a
5556
role="button"
5657
className={classNames("nav-link", { "w-100": !item.collapseIconOnly }, { "dropdown-toggle": hasitem })}
57-
onClick={() => setIsOpen(!isOpen)}
58+
onClick={() => {
59+
if (item.collapseIconOnly) {
60+
toggleMenuItem(item.id);
61+
}
62+
}}
5863
>
5964
{!item.collapseIconOnly && (
6065
<span>
@@ -76,17 +81,15 @@ const PanelSideBarItem = <TPanelItemId extends string, TPanelItem>(props: PanelS
7681
)}
7782
</div>
7883
</NavItem>
79-
8084
{hasitem && (
8185
<Collapse isOpen={isOpen} navbar className={classNames("item-menu", { "mb-1": isOpen })}>
8286
{item.children?.map((childItem) => (
8387
<PanelSideBarItem
8488
key={childItem.id}
8589
children={childItem}
86-
onClick={() => onClick && onClick(childItem.id)}
8790
depth={depth + 1}
8891
active={item.active}
89-
toggledItemIds={toggledItemIds}
92+
isParentHidden={hiddenMenuItemIds.includes(item.id)}
9093
/>
9194
))}
9295
</Collapse>

0 commit comments

Comments
 (0)