Skip to content

Commit 43446fd

Browse files
Icons visibility on Sidebar collapsing (#62)
This PR aims to introduce the possibility to show the sidebar items icons even if the Sidebar is collapsed.
1 parent 4cfea6f commit 43446fd

10 files changed

Lines changed: 173 additions & 42 deletions

File tree

CHANGELOG.md

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

1212
- Added the pkg.pr.new workflow.
13+
- `onSidebarCollapseOptions` to customize the panel item behavior on sidebar collapsing: keep showing icons enabling `showIcon`, use a fallback icon setting `fallbackIcon`. It works:
14+
- whether tiles are rendered via `renderFirstItemsLevelAsTiles`
15+
- the panel item has any `children`
1316

1417
## [4.2.0] - 2024-10-08
1518

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

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import React, { PropsWithChildren, ReactNode } from "react";
2-
import { PanelSideBarProvider, PanelSideBarLayout, PanelItem, PanelLinkRendererProps, usePanelSideBarContext } from "react-pattern-ui";
3-
import { faBars, faCogs, faInfo } from "@fortawesome/free-solid-svg-icons";
1+
import React, { PropsWithChildren } from "react";
2+
import {
3+
PanelSideBarProvider,
4+
PanelSideBarLayout,
5+
PanelItem,
6+
PanelLinkRendererProps,
7+
usePanelSideBarContext,
8+
PanelItemOnSideBarCollapseOptions,
9+
} from "react-pattern-ui";
10+
import { faBars, faCogs, faInfo, faHome, faPerson } from "@fortawesome/free-solid-svg-icons";
411

512
type AppRoutes = "home" | "settings" | "dropdownTest" | "dropdown-test1" | "dropdown-test2" | "info";
613
type TSideBarMenuItem = PanelItem<AppRoutes>;
@@ -27,7 +34,7 @@ const getPanelSidebarInternal = (items: TSideBarMenuItem[], config?: PanelSideBa
2734
}
2835
}}
2936
>
30-
<>{elem.item.title}</>
37+
<>{elem.children}</>
3138
</div>
3239
)}
3340
>
@@ -38,17 +45,31 @@ const getPanelSidebarInternal = (items: TSideBarMenuItem[], config?: PanelSideBa
3845
);
3946
};
4047

41-
const getSidebarItems = (active?: boolean, disabled?: boolean, expanded?: boolean): TSideBarMenuItem[] => [
48+
const getSidebarItems = (
49+
active?: boolean,
50+
disabled?: boolean,
51+
expanded?: boolean,
52+
onSidebarCollapseOptions?: PanelItemOnSideBarCollapseOptions,
53+
): TSideBarMenuItem[] => [
4254
{
4355
id: "home",
4456
title: "Home",
4557
icon: faBars,
4658
disabled,
59+
onSidebarCollapseOptions: onSidebarCollapseOptions ? { ...onSidebarCollapseOptions } : undefined,
4760
children: [
4861
{
4962
title: "Home",
5063
id: "home",
5164
active,
65+
icon: faHome,
66+
},
67+
{
68+
title: "Profile",
69+
id: "profile",
70+
onSidebarCollapseOptions: {
71+
fallbackIcon: faPerson,
72+
},
5273
},
5374
],
5475
},
@@ -99,11 +120,12 @@ interface PanelSideBarProps extends PropsWithChildren {
99120
active?: boolean;
100121
disabled?: boolean;
101122
expanded?: boolean;
123+
onSidebarCollapseOptions?: PanelItemOnSideBarCollapseOptions;
102124
}
103125

104126
const PanelSideBarWithTiles = (props: PanelSideBarProps) => {
105-
const { active, disabled, expanded, children } = props;
106-
return getPanelSidebarInternal(getSidebarItems(active, disabled, expanded), { children });
127+
const { active, disabled, expanded, onSidebarCollapseOptions, children } = props;
128+
return getPanelSidebarInternal(getSidebarItems(active, disabled, expanded, onSidebarCollapseOptions), { children });
107129
};
108130

109131
const PanelSideBarNoTiles = (props: PanelSideBarProps) => {
@@ -267,4 +289,15 @@ describe("PanelSidebar.cy.tsx", () => {
267289
cy.get("button[title=Home]").should("be.visible");
268290
cy.get("button[title=Info]").should("not.exist");
269291
});
292+
293+
it("toggle sidebar with visible icons", () => {
294+
cy.mount(<PanelSideBarWithTiles onSidebarCollapseOptions={{ showIcon: true }} />);
295+
cy.get('[data-icon="angle-left"]').should("be.visible");
296+
cy.get("#side-nav-toggle").click();
297+
cy.get('[data-icon="angle-right"]').should("be.visible");
298+
cy.get(".toggled").should("exist");
299+
cy.get(".side-nav__items").should("be.visible");
300+
cy.get("#home").should("be.visible");
301+
cy.get("#profile > .nav-link > svg").should("be.visible");
302+
});
270303
});

src/lib/Layout/PanelSideBarLayout/PanelSideBar/Definitions/PanelItem.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
import { IconProp } from "@fortawesome/fontawesome-svg-core";
22
import { ReactNode } from "react";
33

4+
export type PanelItemOnSideBarCollapseOptions = {
5+
/**
6+
* Whether the sidebar maintains the panel item icon visible on collapsing.
7+
* It works whether {@link PanelSideBarContextProps#renderFirstItemsLevelAsTiles} is enabled and the panel item has {@link PanelItem#children}
8+
*/
9+
showIcon?: boolean;
10+
11+
/**
12+
* The icon to be displayed when the active panel item has `showIcon` enabled, the sidebar is collapsed and the panel item does not have any icon.
13+
* @see {@link PanelItemOnSideBarCollapseOptions#showIcon} {@link PanelItem#icon}
14+
*/
15+
fallbackIcon?: IconProp;
16+
};
17+
418
export type PanelItem<TPanelItemId extends string, TPanelItem = Record<string, unknown>> = TPanelItem & {
519
/**
620
* The panel icon.
@@ -44,4 +58,9 @@ export type PanelItem<TPanelItemId extends string, TPanelItem = Record<string, u
4458
* Whether collapse only with icon.
4559
*/
4660
collapseIconOnly?: boolean;
61+
62+
/**
63+
* The panel item options once the sidebar gets collapsed.
64+
*/
65+
onSidebarCollapseOptions?: PanelItemOnSideBarCollapseOptions;
4766
};

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

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,43 @@ export interface PanelSideBarItemProps<TPanelItemId extends string, TPanelItem>
1111
depth?: number;
1212
active?: boolean;
1313
isParentHidden?: boolean;
14+
isIconShownOnSidebarCollapse: boolean;
1415
}
1516

17+
const PanelSidebarItemNavLink = <TPanelItemId extends string, TPanelItem>({
18+
item,
19+
collapsedWithIcon,
20+
className,
21+
}: {
22+
item: PanelItem<TPanelItemId, TPanelItem>;
23+
collapsedWithIcon?: boolean;
24+
className?: string;
25+
}) => {
26+
const { icon, title, onSidebarCollapseOptions } = item;
27+
const panelIconClassName = collapsedWithIcon ? "ms-1 me-3 p-1" : "me-2";
28+
const displayIcon = icon || (collapsedWithIcon && onSidebarCollapseOptions?.fallbackIcon);
29+
30+
return (
31+
<span className={className}>
32+
{displayIcon && <FontAwesomeIcon icon={displayIcon} className={panelIconClassName} />}
33+
{!collapsedWithIcon || !displayIcon ? title : ""}
34+
</span>
35+
);
36+
};
37+
1638
// eslint-disable-next-line complexity
1739
const PanelSideBarItem = <TPanelItemId extends string, TPanelItem>(props: PanelSideBarItemProps<TPanelItemId, TPanelItem>) => {
18-
const { depth = 0, children: item, isParentHidden = false } = props;
19-
const { LinkRenderer, toggledMenuItemIds, toggleMenuItem, hiddenMenuItemIds } = usePanelSideBarContext<TPanelItemId, TPanelItem>();
20-
const hasitem = !!item.children?.length;
21-
const isActive = (hasitem && item.children && hasActiveChildren(item.children)) || item.active;
40+
const { depth = 0, children: item, isParentHidden = false, isIconShownOnSidebarCollapse } = props;
41+
const { LinkRenderer, toggledMenuItemIds, toggleMenuItem, hiddenMenuItemIds, isSidebarOpen } = usePanelSideBarContext<
42+
TPanelItemId,
43+
TPanelItem
44+
>();
45+
46+
const hasItems = !!item.children?.length;
47+
const isActive = (hasItems && item.children && hasActiveChildren(item.children)) || item.active;
2248
const isOpen = toggledMenuItemIds?.includes(item.id);
2349
const scrollToActiveItemRef = useRef<HTMLDivElement>(null);
50+
const collapsedWithIcon = isIconShownOnSidebarCollapse && !isSidebarOpen;
2451

2552
useEffect(() => {
2653
if (scrollToActiveItemRef.current && isActive) {
@@ -33,55 +60,54 @@ const PanelSideBarItem = <TPanelItemId extends string, TPanelItem>(props: PanelS
3360
<NavItem
3461
hidden={isParentHidden || hiddenMenuItemIds.includes(item.id)}
3562
onClick={() => {
36-
if (hasitem && !item.collapseIconOnly) {
63+
if (hasItems && !item.collapseIconOnly) {
3764
toggleMenuItem(item.id);
3865
}
3966
}}
4067
className={classNames({ "menu-open": isOpen, active: isActive })}
41-
style={{ paddingLeft: depth ? `${depth + 1}rem` : undefined }}
68+
style={{ paddingLeft: !collapsedWithIcon && depth ? `${depth + 1}rem` : undefined }}
4269
>
4370
<div ref={scrollToActiveItemRef}>
44-
{hasitem ? (
71+
{hasItems ? (
4572
<div className={classNames("d-flex flex-row", { "justify-content-between": item.collapseIconOnly })}>
4673
{item.collapseIconOnly && (
4774
<LinkRenderer item={item}>
48-
<span className="nav-link">
49-
{item.icon && <FontAwesomeIcon icon={item.icon} className="me-2" />}
50-
{item.title}
51-
</span>
75+
<PanelSidebarItemNavLink<TPanelItemId, TPanelItem>
76+
className="nav-link"
77+
item={item}
78+
collapsedWithIcon={collapsedWithIcon}
79+
/>
5280
</LinkRenderer>
5381
)}
5482

5583
<a
5684
role="button"
57-
className={classNames("nav-link", { "w-100": !item.collapseIconOnly }, { "dropdown-toggle": hasitem })}
85+
className={classNames(
86+
"nav-link",
87+
{ "w-100": !item.collapseIconOnly },
88+
{ "dropdown-toggle": hasItems && !collapsedWithIcon },
89+
)}
5890
onClick={() => {
5991
if (item.collapseIconOnly) {
6092
toggleMenuItem(item.id);
6193
}
6294
}}
6395
>
6496
{!item.collapseIconOnly && (
65-
<span>
66-
{item.icon && <FontAwesomeIcon className="me-2" icon={item.icon} />}
67-
{item.title}
68-
</span>
97+
<PanelSidebarItemNavLink<TPanelItemId, TPanelItem> item={item} collapsedWithIcon={collapsedWithIcon} />
6998
)}
7099
</a>
71100
</div>
72101
) : (
73102
<>
74103
<LinkRenderer item={item}>
75-
<span className="nav-link">
76-
{item.icon && <FontAwesomeIcon icon={item.icon} className="me-2" />}
77-
{item.title}
78-
</span>
104+
<PanelSidebarItemNavLink<TPanelItemId, TPanelItem> className="nav-link" item={item} collapsedWithIcon={collapsedWithIcon} />
79105
</LinkRenderer>
80106
</>
81107
)}
82108
</div>
83109
</NavItem>
84-
{hasitem && (
110+
{hasItems && (
85111
<Collapse isOpen={isOpen} navbar className={classNames("item-menu", { "mb-1": isOpen })}>
86112
{item.children?.map((childItem) => (
87113
<PanelSideBarItem
@@ -90,6 +116,7 @@ const PanelSideBarItem = <TPanelItemId extends string, TPanelItem>(props: PanelS
90116
depth={depth + 1}
91117
active={item.active}
92118
isParentHidden={hiddenMenuItemIds.includes(item.id)}
119+
isIconShownOnSidebarCollapse={isIconShownOnSidebarCollapse}
93120
/>
94121
))}
95122
</Collapse>

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import classNames from "classnames";
66

77
interface PanelSideBarToggleProps extends ButtonProps {
88
toggled: boolean;
9+
isIconShownOnSidebarCollapse: boolean;
910
}
1011

1112
export const PanelSideBarToggle = (props: PanelSideBarToggleProps) => {
12-
const { toggled, ...buttonProps } = props;
13+
const { toggled, isIconShownOnSidebarCollapse, ...buttonProps } = props;
1314
const { theme } = usePanelSideBarContext();
1415

1516
return (
@@ -19,6 +20,7 @@ export const PanelSideBarToggle = (props: PanelSideBarToggleProps) => {
1920
{ "side-nav-toggle-dark": theme == "dark" },
2021
{ "side-nav-toggle-light": theme == "light" },
2122
{ "side-nav-toggle-blue": theme == "blue" },
23+
{ "show-icons": isIconShownOnSidebarCollapse },
2224
)}
2325
id="side-nav-toggle"
2426
color="primary"

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { usePanelSideBarContext } from "./Context/PanelSideBarContext";
55
import { PanelItem } from "./Definitions/PanelItem";
66
import { PanelSideBarItem } from "./PanelSideBarItem";
77

8-
export const PanelSideBar = <TPanelItemId extends string, TPanelItem>() => {
8+
interface PanelSideBarProps {
9+
isIconShownOnSidebarCollapse: boolean;
10+
}
11+
12+
export const PanelSideBar = <TPanelItemId extends string, TPanelItem>(props: PanelSideBarProps) => {
13+
const { isIconShownOnSidebarCollapse } = props;
914
const {
1015
activePanelId,
1116
menuItems,
@@ -22,6 +27,7 @@ export const PanelSideBar = <TPanelItemId extends string, TPanelItem>() => {
2227
{ "sidenav-dark": theme == "dark" },
2328
{ "sidenav-light": theme == "light" },
2429
{ "sidenav-blue": theme == "blue" },
30+
{ "show-icons": isIconShownOnSidebarCollapse },
2531
);
2632

2733
const activePanel: PanelItem<TPanelItemId, TPanelItem> | undefined = menuItems.find((x) => x.id === activePanelId);
@@ -78,7 +84,11 @@ export const PanelSideBar = <TPanelItemId extends string, TPanelItem>() => {
7884
<div className="side-nav__tiles">{<PanelItemsRenderer items={menuItems} />}</div>
7985
<div className="side-nav__items">
8086
{activePanel?.children?.map((item) => (
81-
<PanelSideBarItem<TPanelItemId, TPanelItem> key={item.id} children={item} />
87+
<PanelSideBarItem<TPanelItemId, TPanelItem>
88+
key={item.id}
89+
children={item}
90+
isIconShownOnSidebarCollapse={isIconShownOnSidebarCollapse}
91+
/>
8292
))}
8393
</div>
8494
</nav>
@@ -88,7 +98,11 @@ export const PanelSideBar = <TPanelItemId extends string, TPanelItem>() => {
8898
<nav id="side-nav" className={className}>
8999
<div className="side-nav__items">
90100
{menuItems?.map((item) => (
91-
<PanelSideBarItem<TPanelItemId, TPanelItem> key={item.id} children={item} />
101+
<PanelSideBarItem<TPanelItemId, TPanelItem>
102+
key={item.id}
103+
children={item}
104+
isIconShownOnSidebarCollapse={isIconShownOnSidebarCollapse}
105+
/>
92106
))}
93107
</div>
94108
</nav>

src/lib/Layout/PanelSideBarLayout/PanelSideBarLayout.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import classNames from "classnames";
2-
import { MutableRefObject, PropsWithChildren, ReactNode } from "react";
2+
import { MutableRefObject, PropsWithChildren, ReactNode, useMemo } from "react";
33
import "../../../../styles/Layout/index.scss";
44
import { PanelSideBar } from "./PanelSideBar/PanelSidebar";
55
import { PanelSideBarLayoutContent } from "./PanelSideBarLayoutContent";
@@ -58,11 +58,20 @@ export const PanelSideBarLayout = <TPanelItemId extends string, TPanelItem>(prop
5858
mainContentBodyRef,
5959
} = props;
6060

61-
const { isSidebarOpen, toggleSidebar, renderFirstItemsLevelAsTiles } = usePanelSideBarContext<TPanelItemId, TPanelItem>();
61+
const { isSidebarOpen, toggleSidebar, renderFirstItemsLevelAsTiles, menuItems, activePanelId } = usePanelSideBarContext<
62+
TPanelItemId,
63+
TPanelItem
64+
>();
6265

6366
if (useResponsiveLayout && !useToggleButton) {
6467
throw new Error("Responsive layout can be used only with toggle button in the navbar!");
6568
}
69+
70+
const isIconShownOnSidebarCollapse = useMemo(
71+
() => menuItems.find((x) => x.id === activePanelId)?.onSidebarCollapseOptions?.showIcon ?? false,
72+
[menuItems, activePanelId],
73+
);
74+
6675
return (
6776
<>
6877
<PanelSidebarNavbar
@@ -80,9 +89,19 @@ export const PanelSideBarLayout = <TPanelItemId extends string, TPanelItem>(prop
8089
{ "section-tiles": renderFirstItemsLevelAsTiles },
8190
)}
8291
>
83-
<PanelSideBar<TPanelItemId, TPanelItem> />
84-
{collapsible && !useToggleButton && <PanelSideBarToggle onClick={toggleSidebar} toggled={!isSidebarOpen} />}
85-
<PanelSideBarLayoutContent footer={footer} mainContentBodyRef={mainContentBodyRef}>
92+
<PanelSideBar<TPanelItemId, TPanelItem> isIconShownOnSidebarCollapse={isIconShownOnSidebarCollapse} />
93+
{collapsible && !useToggleButton && (
94+
<PanelSideBarToggle
95+
onClick={toggleSidebar}
96+
toggled={!isSidebarOpen}
97+
isIconShownOnSidebarCollapse={isIconShownOnSidebarCollapse}
98+
/>
99+
)}
100+
<PanelSideBarLayoutContent
101+
footer={footer}
102+
mainContentBodyRef={mainContentBodyRef}
103+
isIconShownOnSidebarCollapse={isIconShownOnSidebarCollapse}
104+
>
86105
{children}
87106
</PanelSideBarLayoutContent>
88107
</section>

0 commit comments

Comments
 (0)