Skip to content

Commit c823925

Browse files
committed
Implement active item customisation
1 parent 8868cd6 commit c823925

6 files changed

Lines changed: 97 additions & 97 deletions

File tree

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

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ export interface PanelLinkRendererProps<T> {
66
children: ReactNode;
77
}
88

9-
type MenuItemToggleFn<TPanelItem, TMenuItem> = (panelItem: PanelItem<TPanelItem>, menuItem: PanelMenuItem<TMenuItem>) => void;
9+
export type MenuItemToggleFn<TMenuItem> = (menuItem: PanelMenuItem<TMenuItem>) => void;
1010

1111
export interface PanelSideBarContextProps<TPanelItem, TMenuItem> {
12-
activeMenuId: string;
1312
activePanelId: string;
1413
/**
1514
* The global panel items.
@@ -20,8 +19,8 @@ export interface PanelSideBarContextProps<TPanelItem, TMenuItem> {
2019
*/
2120
LinkRenderer: ComponentType<PanelLinkRendererProps<TMenuItem>>;
2221
setActivePanel: (panelId: string) => void;
23-
toggleMenuItem: MenuItemToggleFn<TPanelItem, TMenuItem>;
24-
setLocalPanelItems: (panelItems: PanelItem<TPanelItem, TMenuItem>) => void;
22+
toggledMenuItemIds: string[];
23+
toggleMenuItem: MenuItemToggleFn<TMenuItem>;
2524
}
2625

2726
export const PanelSideBarContext = createContext<PanelSideBarContextProps<any, any> | null>(null);
@@ -38,55 +37,31 @@ export const PanelSideBarProvider = <TPanelItem, TMenuItem>(props: PanelSideBarM
3837
const { children, globalItems, LinkRenderer } = props;
3938

4039
const getFirstPanelId = () => globalItems.find((x) => x.id)?.id ?? "";
41-
const getFirstMenuItemId = () => {
42-
const firstPanel = globalItems.find((x) => x.id);
43-
return firstPanel?.items?.find((x) => x.id)?.id ?? "";
44-
};
45-
4640
const [activePanelId, setActivePanelId] = useState(getFirstPanelId());
47-
const [activeMenuId, setActiveMenuId] = useState(getFirstMenuItemId());
48-
const [globalPanelState, setGlobalPanelState] = useState<PanelItem<TPanelItem, TMenuItem>[]>(globalItems);
49-
const [localPanelItems, setLocalPanelItems] = useState<PanelItem<TPanelItem, TMenuItem>[]>([]);
50-
51-
const setActivePanel = (panelId: string) => setActivePanelId(panelId);
41+
const [toggledMenuItemIds, setToggledMenuItemIds] = useState<string[]>([]);
5242

53-
const toggleMenuItem: MenuItemToggleFn<TPanelItem, TMenuItem> = (panelItem, menuItem) => {
54-
setGlobalPanelState((prev) =>
55-
prev.map((x) => {
56-
const panel: PanelItem<TPanelItem, TMenuItem> = JSON.parse(JSON.stringify(x));
43+
const toggleMenuItem: MenuItemToggleFn<TMenuItem> = (menuItem) => {
44+
setToggledMenuItemIds((prev) => {
45+
const idExists = !!prev.find((id) => id == menuItem.id);
5746

58-
if (panel.id === panelItem.id) {
59-
let panelMenuItem = panel.items?.find((item) => item.id === menuItem.id);
47+
if (idExists) {
48+
return prev.filter((id) => id !== menuItem.id);
49+
}
6050

61-
if (panelMenuItem?.id === menuItem.id) {
62-
panelMenuItem.expanded = !panelMenuItem.expanded;
63-
}
64-
}
65-
66-
return panel;
67-
}),
68-
);
69-
70-
if (!menuItem.children && globalPanelState.map((x) => x.id).includes(panelItem.id)) setActiveMenuId(menuItem.id);
51+
return [...prev, menuItem.id];
52+
});
7153
};
7254

73-
const localPanelItemHandler = (items: PanelItem<TPanelItem, TMenuItem>[]) => {
74-
if (items?.length > 0) {
75-
const [firstLocalItem] = items;
76-
setActivePanel(firstLocalItem.id);
77-
}
78-
setLocalPanelItems(items);
79-
};
55+
const setActivePanel = (panelId: string) => setActivePanelId(panelId);
8056

8157
return (
8258
<PanelSideBarContext.Provider
8359
value={{
84-
activeMenuId,
8560
activePanelId,
86-
globalItems: [...globalPanelState, ...(localPanelItems ?? [])],
61+
globalItems,
8762
LinkRenderer,
8863
setActivePanel,
89-
setLocalPanelItems: localPanelItemHandler,
64+
toggledMenuItemIds,
9065
toggleMenuItem,
9166
}}
9267
>

src/Layout/PanelSideBarLayout/PanelSideBar/Definitions/PanelSideBarMenuItem.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { IconProp } from "@fortawesome/fontawesome-svg-core";
22

33
export type PanelItem<TPanelItem = Record<string, unknown>, TMenuItem = Record<string, unknown>> = TPanelItem & {
4-
/**
5-
* The panel item identifier.
6-
*/
7-
id: string;
84
/**
95
* The panel icon.
106
*/
117
icon: IconProp;
8+
/**
9+
* The panel item identifier.
10+
*/
11+
id: string;
1212
/**
1313
* The panel menu items.
1414
*/
@@ -32,6 +32,10 @@ export type PanelMenuItem<T = Record<string, unknown>> = T & {
3232
* The menu item title.
3333
*/
3434
title: string;
35+
/**
36+
* Whether the item is active.
37+
*/
38+
active?: boolean;
3539
/**
3640
* The menu item children items.
3741
*/

src/Layout/PanelSideBarLayout/PanelSideBar/PanelSideBarItem.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@ import { PanelMenuItem } from "./Definitions/PanelSideBarMenuItem";
99
export interface PanelSideBarItemProps {
1010
item: PanelMenuItem<unknown>;
1111
LinkRenderer: ComponentType<LinkRendererProps>;
12-
onClick: (menuItem: PanelMenuItem<unknown>) => void;
12+
onClick?: (menuItem: PanelMenuItem<unknown>) => void;
1313
depth?: number;
14-
activeId?: string;
14+
active?: boolean;
15+
toggledItemIds: string[];
1516
}
1617

17-
export const PanelSideBarItem = (props: PanelSideBarItemProps) => {
18-
const { activeId, depth = 0, item, LinkRenderer, onClick } = props;
18+
const PanelSideBarItem = (props: PanelSideBarItemProps) => {
19+
const { depth = 0, item, LinkRenderer, onClick, toggledItemIds = [] } = props;
1920

2021
const hasChildren = !!item.children?.length;
21-
const isOpen = item.expanded === true;
22+
const isOpen = toggledItemIds?.includes(item.id) || item.expanded;
2223

2324
if (item.display === false) {
2425
return null;
@@ -27,8 +28,8 @@ export const PanelSideBarItem = (props: PanelSideBarItemProps) => {
2728
return (
2829
<>
2930
<NavItem
30-
onClick={() => onClick(item)}
31-
className={classNames({ "menu-open": isOpen, active: item.id === activeId })}
31+
onClick={() => onClick && onClick(item)}
32+
className={classNames({ "menu-open": isOpen, active: item.active })}
3233
style={{ paddingLeft: depth ? `${depth + 1}rem` : undefined }}
3334
>
3435
{hasChildren ? (
@@ -57,13 +58,16 @@ export const PanelSideBarItem = (props: PanelSideBarItemProps) => {
5758
key={childItem.id}
5859
item={childItem}
5960
LinkRenderer={LinkRenderer}
60-
onClick={() => onClick(childItem)}
61+
onClick={() => onClick && onClick(childItem)}
6162
depth={depth + 1}
62-
activeId={activeId}
63+
active={item.active}
64+
toggledItemIds={toggledItemIds}
6365
/>
6466
))}
6567
</Collapse>
6668
)}
6769
</>
6870
);
6971
};
72+
73+
export { PanelSideBarItem };

src/Layout/PanelSideBarLayout/PanelSideBar/PanelSideBarToggle.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const PanelSideBarToggle = (props: PanelSideBarToggleProps) => {
1111

1212
return (
1313
<Button {...buttonProps} id="side-nav-toggle" color="primary">
14-
<FontAwesomeIcon icon={toggled ? faAngleLeft : faAngleRight} />
14+
<FontAwesomeIcon icon={toggled ? faAngleRight : faAngleLeft} />
1515
</Button>
1616
);
1717
};

src/Layout/PanelSideBarLayout/PanelSideBar/PanelSidebar.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
22
import classNames from "classnames";
3-
import { useEffect } from "react";
43
import { Button } from "reactstrap";
54
import { usePanelSideBarContext } from "./Context/PanelSideBarContext";
65
import { PanelItem } from "./Definitions/PanelSideBarMenuItem";
@@ -13,18 +12,15 @@ interface PanelSideBarProps {
1312
export const PanelSideBar = (props: PanelSideBarProps) => {
1413
const { localItems = [] } = props;
1514

16-
const { activeMenuId, activePanelId, globalItems, LinkRenderer, setActivePanel, setLocalPanelItems, toggleMenuItem } =
17-
usePanelSideBarContext();
15+
const { activePanelId, globalItems, LinkRenderer, setActivePanel, toggledMenuItemIds, toggleMenuItem } = usePanelSideBarContext();
1816

1917
const activePanel: PanelItem = globalItems.find((x) => x.id === activePanelId);
20-
21-
useEffect(() => {
22-
setLocalPanelItems(localItems);
23-
}, [localItems]);
18+
const localActivePanel: PanelItem | undefined = localItems?.find((x) => x.id === activePanelId);
2419

2520
const panelItemsRenderer = (items: PanelItem[]) =>
2621
items?.map(({ disabled, icon, id, title }) => (
2722
<Button
23+
key={id}
2824
color="primary"
2925
outline
3026
className={classNames("tile", { active: activePanelId === id })}
@@ -38,18 +34,25 @@ export const PanelSideBar = (props: PanelSideBarProps) => {
3834

3935
return (
4036
<nav id="side-nav" className="panel-layout">
41-
<div className="side-nav__tiles">{panelItemsRenderer(globalItems)}</div>
37+
<div className="side-nav__tiles">
38+
{panelItemsRenderer(globalItems)}
39+
{panelItemsRenderer(localItems)}
40+
</div>
4241

4342
<div className="side-nav__items">
44-
{activePanel?.items?.map((item, i) => (
43+
{activePanel?.items?.map((item) => (
4544
<PanelSideBarItem
46-
key={i}
45+
key={item.id}
4746
item={item}
4847
LinkRenderer={LinkRenderer}
49-
onClick={(menuItem) => toggleMenuItem(activePanel, menuItem)}
50-
activeId={activeMenuId}
48+
onClick={(menuItem) => toggleMenuItem(menuItem)}
49+
toggledItemIds={toggledMenuItemIds}
5150
/>
5251
))}
52+
53+
{localActivePanel?.items?.map((item) => (
54+
<PanelSideBarItem key={item.id} item={item} LinkRenderer={LinkRenderer} toggledItemIds={toggledMenuItemIds} />
55+
))}
5356
</div>
5457
</nav>
5558
);

styles/Layout/PanelSideBarLayout.scss

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,24 @@ $sidenav-light-link-active-color: $light;
2828
$topnav-dark-toggler-color: fade-out($light, 0.5);
2929
$topnav-light-toggler-color: $gray-900;
3030

31+
@mixin chevron-common {
32+
border: none;
33+
display: block;
34+
position: absolute;
35+
right: 1rem;
36+
transition: ease-in-out 0.2s;
37+
width: 0.4rem;
38+
$svg-attributes: "fill='white' strike='white'";
39+
content: url("data:image/svg+xml; utf8, <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'><path d='M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z' #{$svg-attributes}/></svg>");
40+
}
41+
3142
@mixin chevron-left {
32-
margin-left: 0;
33-
border-top: 0.3em solid transparent;
34-
border-right: 0.3em solid;
35-
border-bottom: 0.3em solid transparent;
36-
border-left: 0;
43+
@include chevron-common();
3744
}
3845

3946
@mixin chevron-down {
40-
margin-left: 0.3em;
41-
border-top: 0.3em solid;
42-
border-right: 0.3em solid transparent;
43-
border-bottom: 0;
44-
border-left: 0.3em solid transparent;
47+
@include chevron-common();
48+
transform: rotate(-90deg);
4549
}
4650

4751
@mixin slim-scrollbar {
@@ -179,36 +183,34 @@ section.content:first-of-type {
179183
background-color: $primary-hover;
180184
}
181185

182-
&.menu-open .nav-link::after {
183-
right: 0.3rem;
186+
&.menu-open .nav-link.dropdown-toggle::after {
184187
@include chevron-down();
185188
}
186189

187-
&.active {
190+
&.active,
191+
.active {
188192
background-color: $sidenav-light-active-bg-color;
189193
color: $sidenav-light-active-color;
190194
}
191-
}
192195

193-
li::marker {
194-
content: none;
195-
}
196+
.nav-link {
197+
display: flex;
198+
align-items: center;
199+
color: inherit;
200+
padding: 0.5rem;
196201

197-
.nav-link {
198-
display: flex;
199-
align-items: center;
200-
color: inherit;
201-
padding: 0.5rem;
202+
> svg {
203+
margin-right: 0.5rem;
204+
}
202205

203-
> svg {
204-
margin-right: 0.5rem;
206+
&.dropdown-toggle::after {
207+
@include chevron-left();
208+
}
205209
}
210+
}
206211

207-
&::after {
208-
position: absolute;
209-
right: 0;
210-
@include chevron-left();
211-
}
212+
li::marker {
213+
content: none;
212214
}
213215
}
214216
}
@@ -221,11 +223,19 @@ section.content:first-of-type {
221223
justify-content: center;
222224
width: 0;
223225
bottom: 0;
224-
box-shadow: 0 0 0.125rem 0.0625rem rgba($color: $black, $alpha: 0.75);
226+
border: none;
227+
border-left: 3px solid $primary-hover;
228+
border-radius: 0;
229+
background-color: $primary;
225230
top: $topnav-base-height;
226231
left: $sidenav-base-width;
227232
z-index: $zindex-fixed;
228233
transition: $sidebar-transition;
234+
235+
&:focus,
236+
&:hover {
237+
background-color: $primary-hover;
238+
}
229239
}
230240

231241
@include media-breakpoint-up(md) {
@@ -238,12 +248,16 @@ section.content:first-of-type {
238248

239249
> #side-nav-toggle {
240250
transition: $sidebar-transition;
241-
left: #{$tile-size + $slim-scrollbar-base-width};
251+
left: #{$tile-size};
242252
}
243253

244254
> section.content:first-of-type {
245255
transition: #{margin $sidebar-transition};
246256
margin-left: #{$tile-size + $toggle-base-width};
257+
258+
.dropdown-toggle::after {
259+
display: none;
260+
}
247261
}
248262
}
249263
}

0 commit comments

Comments
 (0)