Skip to content
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

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

## [4.2.0] - 2024-10-08

Expand Down
47 changes: 40 additions & 7 deletions cypress/cypress/component/PanelSidebar/PanelSidebar.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import React, { PropsWithChildren, ReactNode } from "react";
import { PanelSideBarProvider, PanelSideBarLayout, PanelItem, PanelLinkRendererProps, usePanelSideBarContext } from "react-pattern-ui";
import { faBars, faCogs, faInfo } from "@fortawesome/free-solid-svg-icons";
import React, { PropsWithChildren } from "react";
import {
PanelSideBarProvider,
PanelSideBarLayout,
PanelItem,
PanelLinkRendererProps,
usePanelSideBarContext,
PanelItemOnSideBarCollapseOptions,
} from "react-pattern-ui";
import { faBars, faCogs, faInfo, faHome, faPerson } from "@fortawesome/free-solid-svg-icons";

type AppRoutes = "home" | "settings" | "dropdownTest" | "dropdown-test1" | "dropdown-test2" | "info";
type TSideBarMenuItem = PanelItem<AppRoutes>;
Expand All @@ -27,7 +34,7 @@ const getPanelSidebarInternal = (items: TSideBarMenuItem[], config?: PanelSideBa
}
}}
>
<>{elem.item.title}</>
<>{elem.children}</>
</div>
)}
>
Expand All @@ -38,17 +45,31 @@ const getPanelSidebarInternal = (items: TSideBarMenuItem[], config?: PanelSideBa
);
};

const getSidebarItems = (active?: boolean, disabled?: boolean, expanded?: boolean): TSideBarMenuItem[] => [
const getSidebarItems = (
active?: boolean,
disabled?: boolean,
expanded?: boolean,
onSidebarCollapseOptions?: PanelItemOnSideBarCollapseOptions,
): TSideBarMenuItem[] => [
{
id: "home",
title: "Home",
icon: faBars,
disabled,
onSidebarCollapseOptions: onSidebarCollapseOptions ? { ...onSidebarCollapseOptions } : undefined,
children: [
{
title: "Home",
id: "home",
active,
icon: faHome,
},
{
title: "Profile",
id: "profile",
onSidebarCollapseOptions: {
fallbackIcon: faPerson,
},
},
],
},
Expand Down Expand Up @@ -99,11 +120,12 @@ interface PanelSideBarProps extends PropsWithChildren {
active?: boolean;
disabled?: boolean;
expanded?: boolean;
onSidebarCollapseOptions?: PanelItemOnSideBarCollapseOptions;
}

const PanelSideBarWithTiles = (props: PanelSideBarProps) => {
const { active, disabled, expanded, children } = props;
return getPanelSidebarInternal(getSidebarItems(active, disabled, expanded), { children });
const { active, disabled, expanded, onSidebarCollapseOptions, children } = props;
return getPanelSidebarInternal(getSidebarItems(active, disabled, expanded, onSidebarCollapseOptions), { children });
};

const PanelSideBarNoTiles = (props: PanelSideBarProps) => {
Expand Down Expand Up @@ -267,4 +289,15 @@ describe("PanelSidebar.cy.tsx", () => {
cy.get("button[title=Home]").should("be.visible");
cy.get("button[title=Info]").should("not.exist");
});

it("toggle sidebar with visible icons", () => {
cy.mount(<PanelSideBarWithTiles onSidebarCollapseOptions={{ showIcon: true }} />);
cy.get('[data-icon="angle-left"]').should("be.visible");
cy.get("#side-nav-toggle").click();
cy.get('[data-icon="angle-right"]').should("be.visible");
cy.get(".toggled").should("exist");
cy.get(".side-nav__items").should("be.visible");
cy.get("#home").should("be.visible");
cy.get("#profile > .nav-link > svg").should("be.visible");
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { ReactNode } from "react";

export type PanelItemOnSideBarCollapseOptions = {
/**
* Whether the sidebar maintains the panel item icon visible on collapsing.
* It works whether {@link PanelSideBarContextProps#renderFirstItemsLevelAsTiles} is enabled and the panel item has {@link PanelItem#children}
*/
showIcon?: boolean;

/**
* 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.
* @see {@link PanelItemOnSideBarCollapseOptions#showIcon} {@link PanelItem#icon}
*/
fallbackIcon?: IconProp;
};

export type PanelItem<TPanelItemId extends string, TPanelItem = Record<string, unknown>> = TPanelItem & {
/**
* The panel icon.
Expand Down Expand Up @@ -44,4 +58,9 @@ export type PanelItem<TPanelItemId extends string, TPanelItem = Record<string, u
* Whether collapse only with icon.
*/
collapseIconOnly?: boolean;

/**
* The panel item options once the sidebar gets collapsed.
*/
onSidebarCollapseOptions?: PanelItemOnSideBarCollapseOptions;
};
69 changes: 48 additions & 21 deletions src/lib/Layout/PanelSideBarLayout/PanelSideBar/PanelSideBarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,43 @@ export interface PanelSideBarItemProps<TPanelItemId extends string, TPanelItem>
depth?: number;
active?: boolean;
isParentHidden?: boolean;
isIconShownOnSidebarCollapse: boolean;
}

const PanelSidebarItemNavLink = <TPanelItemId extends string, TPanelItem>({
item,
collapsedWithIcon,
className,
}: {
item: PanelItem<TPanelItemId, TPanelItem>;
collapsedWithIcon?: boolean;
className?: string;
}) => {
const { icon, title, onSidebarCollapseOptions } = item;
const panelIconClassName = collapsedWithIcon ? "ms-1 me-3 p-1" : "me-2";
const displayIcon = icon || (collapsedWithIcon && onSidebarCollapseOptions?.fallbackIcon);

return (
<span className={className}>
{displayIcon && <FontAwesomeIcon icon={displayIcon} className={panelIconClassName} />}
{!collapsedWithIcon || !displayIcon ? title : ""}
</span>
);
};

// eslint-disable-next-line complexity
const PanelSideBarItem = <TPanelItemId extends string, TPanelItem>(props: PanelSideBarItemProps<TPanelItemId, TPanelItem>) => {
const { depth = 0, children: item, isParentHidden = false } = props;
const { LinkRenderer, toggledMenuItemIds, toggleMenuItem, hiddenMenuItemIds } = usePanelSideBarContext<TPanelItemId, TPanelItem>();
const hasitem = !!item.children?.length;
const isActive = (hasitem && item.children && hasActiveChildren(item.children)) || item.active;
const { depth = 0, children: item, isParentHidden = false, isIconShownOnSidebarCollapse } = props;
const { LinkRenderer, toggledMenuItemIds, toggleMenuItem, hiddenMenuItemIds, isSidebarOpen } = usePanelSideBarContext<
TPanelItemId,
TPanelItem
>();

const hasItems = !!item.children?.length;
const isActive = (hasItems && item.children && hasActiveChildren(item.children)) || item.active;
const isOpen = toggledMenuItemIds?.includes(item.id);
const scrollToActiveItemRef = useRef<HTMLDivElement>(null);
const collapsedWithIcon = isIconShownOnSidebarCollapse && !isSidebarOpen;

useEffect(() => {
if (scrollToActiveItemRef.current && isActive) {
Expand All @@ -33,55 +60,54 @@ const PanelSideBarItem = <TPanelItemId extends string, TPanelItem>(props: PanelS
<NavItem
hidden={isParentHidden || hiddenMenuItemIds.includes(item.id)}
onClick={() => {
if (hasitem && !item.collapseIconOnly) {
if (hasItems && !item.collapseIconOnly) {
toggleMenuItem(item.id);
}
}}
className={classNames({ "menu-open": isOpen, active: isActive })}
style={{ paddingLeft: depth ? `${depth + 1}rem` : undefined }}
style={{ paddingLeft: !collapsedWithIcon && depth ? `${depth + 1}rem` : undefined }}
>
<div ref={scrollToActiveItemRef}>
{hasitem ? (
{hasItems ? (
<div className={classNames("d-flex flex-row", { "justify-content-between": item.collapseIconOnly })}>
{item.collapseIconOnly && (
<LinkRenderer item={item}>
<span className="nav-link">
{item.icon && <FontAwesomeIcon icon={item.icon} className="me-2" />}
{item.title}
</span>
<PanelSidebarItemNavLink<TPanelItemId, TPanelItem>
className="nav-link"
item={item}
collapsedWithIcon={collapsedWithIcon}
/>
</LinkRenderer>
)}

<a
role="button"
className={classNames("nav-link", { "w-100": !item.collapseIconOnly }, { "dropdown-toggle": hasitem })}
className={classNames(
"nav-link",
{ "w-100": !item.collapseIconOnly },
{ "dropdown-toggle": hasItems && !collapsedWithIcon },
)}
onClick={() => {
if (item.collapseIconOnly) {
toggleMenuItem(item.id);
}
}}
>
{!item.collapseIconOnly && (
<span>
{item.icon && <FontAwesomeIcon className="me-2" icon={item.icon} />}
{item.title}
</span>
<PanelSidebarItemNavLink<TPanelItemId, TPanelItem> item={item} collapsedWithIcon={collapsedWithIcon} />
)}
</a>
</div>
) : (
<>
<LinkRenderer item={item}>
<span className="nav-link">
{item.icon && <FontAwesomeIcon icon={item.icon} className="me-2" />}
{item.title}
</span>
<PanelSidebarItemNavLink<TPanelItemId, TPanelItem> className="nav-link" item={item} collapsedWithIcon={collapsedWithIcon} />
</LinkRenderer>
</>
)}
</div>
</NavItem>
{hasitem && (
{hasItems && (
<Collapse isOpen={isOpen} navbar className={classNames("item-menu", { "mb-1": isOpen })}>
{item.children?.map((childItem) => (
<PanelSideBarItem
Expand All @@ -90,6 +116,7 @@ const PanelSideBarItem = <TPanelItemId extends string, TPanelItem>(props: PanelS
depth={depth + 1}
active={item.active}
isParentHidden={hiddenMenuItemIds.includes(item.id)}
isIconShownOnSidebarCollapse={isIconShownOnSidebarCollapse}
/>
))}
</Collapse>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import classNames from "classnames";

interface PanelSideBarToggleProps extends ButtonProps {
toggled: boolean;
isIconShownOnSidebarCollapse: boolean;
}

export const PanelSideBarToggle = (props: PanelSideBarToggleProps) => {
const { toggled, ...buttonProps } = props;
const { toggled, isIconShownOnSidebarCollapse, ...buttonProps } = props;
const { theme } = usePanelSideBarContext();

return (
Expand All @@ -19,6 +20,7 @@ export const PanelSideBarToggle = (props: PanelSideBarToggleProps) => {
{ "side-nav-toggle-dark": theme == "dark" },
{ "side-nav-toggle-light": theme == "light" },
{ "side-nav-toggle-blue": theme == "blue" },
{ "show-icons": isIconShownOnSidebarCollapse },
)}
id="side-nav-toggle"
color="primary"
Expand Down
20 changes: 17 additions & 3 deletions src/lib/Layout/PanelSideBarLayout/PanelSideBar/PanelSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { usePanelSideBarContext } from "./Context/PanelSideBarContext";
import { PanelItem } from "./Definitions/PanelItem";
import { PanelSideBarItem } from "./PanelSideBarItem";

export const PanelSideBar = <TPanelItemId extends string, TPanelItem>() => {
interface PanelSideBarProps {
isIconShownOnSidebarCollapse: boolean;
}

export const PanelSideBar = <TPanelItemId extends string, TPanelItem>(props: PanelSideBarProps) => {
const { isIconShownOnSidebarCollapse } = props;
const {
activePanelId,
menuItems,
Expand All @@ -22,6 +27,7 @@ export const PanelSideBar = <TPanelItemId extends string, TPanelItem>() => {
{ "sidenav-dark": theme == "dark" },
{ "sidenav-light": theme == "light" },
{ "sidenav-blue": theme == "blue" },
{ "show-icons": isIconShownOnSidebarCollapse },
);

const activePanel: PanelItem<TPanelItemId, TPanelItem> | undefined = menuItems.find((x) => x.id === activePanelId);
Expand Down Expand Up @@ -78,7 +84,11 @@ export const PanelSideBar = <TPanelItemId extends string, TPanelItem>() => {
<div className="side-nav__tiles">{<PanelItemsRenderer items={menuItems} />}</div>
<div className="side-nav__items">
{activePanel?.children?.map((item) => (
<PanelSideBarItem<TPanelItemId, TPanelItem> key={item.id} children={item} />
<PanelSideBarItem<TPanelItemId, TPanelItem>
key={item.id}
children={item}
isIconShownOnSidebarCollapse={isIconShownOnSidebarCollapse}
/>
))}
</div>
</nav>
Expand All @@ -88,7 +98,11 @@ export const PanelSideBar = <TPanelItemId extends string, TPanelItem>() => {
<nav id="side-nav" className={className}>
<div className="side-nav__items">
{menuItems?.map((item) => (
<PanelSideBarItem<TPanelItemId, TPanelItem> key={item.id} children={item} />
<PanelSideBarItem<TPanelItemId, TPanelItem>
key={item.id}
children={item}
isIconShownOnSidebarCollapse={isIconShownOnSidebarCollapse}
/>
))}
</div>
</nav>
Expand Down
29 changes: 24 additions & 5 deletions src/lib/Layout/PanelSideBarLayout/PanelSideBarLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import classNames from "classnames";
import { MutableRefObject, PropsWithChildren, ReactNode } from "react";
import { MutableRefObject, PropsWithChildren, ReactNode, useMemo } from "react";
import "../../../../styles/Layout/index.scss";
import { PanelSideBar } from "./PanelSideBar/PanelSidebar";
import { PanelSideBarLayoutContent } from "./PanelSideBarLayoutContent";
Expand Down Expand Up @@ -58,11 +58,20 @@ export const PanelSideBarLayout = <TPanelItemId extends string, TPanelItem>(prop
mainContentBodyRef,
} = props;

const { isSidebarOpen, toggleSidebar, renderFirstItemsLevelAsTiles } = usePanelSideBarContext<TPanelItemId, TPanelItem>();
const { isSidebarOpen, toggleSidebar, renderFirstItemsLevelAsTiles, menuItems, activePanelId } = usePanelSideBarContext<
TPanelItemId,
TPanelItem
>();

if (useResponsiveLayout && !useToggleButton) {
throw new Error("Responsive layout can be used only with toggle button in the navbar!");
}

const isIconShownOnSidebarCollapse = useMemo(
() => menuItems.find((x) => x.id === activePanelId)?.onSidebarCollapseOptions?.showIcon ?? false,
[menuItems, activePanelId],
);

return (
<>
<PanelSidebarNavbar
Expand All @@ -80,9 +89,19 @@ export const PanelSideBarLayout = <TPanelItemId extends string, TPanelItem>(prop
{ "section-tiles": renderFirstItemsLevelAsTiles },
)}
>
<PanelSideBar<TPanelItemId, TPanelItem> />
{collapsible && !useToggleButton && <PanelSideBarToggle onClick={toggleSidebar} toggled={!isSidebarOpen} />}
<PanelSideBarLayoutContent footer={footer} mainContentBodyRef={mainContentBodyRef}>
<PanelSideBar<TPanelItemId, TPanelItem> isIconShownOnSidebarCollapse={isIconShownOnSidebarCollapse} />
{collapsible && !useToggleButton && (
<PanelSideBarToggle
onClick={toggleSidebar}
toggled={!isSidebarOpen}
isIconShownOnSidebarCollapse={isIconShownOnSidebarCollapse}
/>
)}
<PanelSideBarLayoutContent
footer={footer}
mainContentBodyRef={mainContentBodyRef}
isIconShownOnSidebarCollapse={isIconShownOnSidebarCollapse}
>
{children}
</PanelSideBarLayoutContent>
</section>
Expand Down
Loading