diff --git a/__tests__/sidebar.test.tsx b/__tests__/sidebar.test.tsx
deleted file mode 100644
index f610e4da..00000000
--- a/__tests__/sidebar.test.tsx
+++ /dev/null
@@ -1,225 +0,0 @@
-import { fireEvent, render, screen } from '@testing-library/react';
-import React from 'react';
-import { ReqoreUIProvider } from '../src';
-import ReqoreSidebar from '../src/components/Sidebar';
-import { qorusSidebarItems } from '../src/mock/menu';
-
-test('Renders sidebar', () => {
- render(
-
-
-
- );
-
- expect(document.querySelectorAll('.sidebarItem').length).toBe(7);
- expect(document.querySelectorAll('.sidebarSection').length).toBe(3);
-});
-
-test('Sidebar can be collapsed', () => {
- const handleClick = jest.fn();
-
- render(
-
-
-
- );
-
- expect(document.querySelectorAll('.expanded').length).toBe(1);
-
- fireEvent.click(screen.getByText('Collapse'));
-
- expect(handleClick).toHaveBeenCalledTimes(1);
-
- expect(document.querySelectorAll('.expanded').length).toBe(0);
-});
-
-test('Can open submenu manually', () => {
- render(
-
-
-
- );
-
- fireEvent.click(screen.getByText('Menu item 3'));
-
- expect(document.querySelectorAll('.sidebarItem').length).toBe(10);
- expect(document.querySelectorAll('.sidebarSection').length).toBe(3);
-});
-
-test('Submenu opens automatically if path matches', () => {
- render(
-
-
-
- );
-
- expect(document.querySelectorAll('.sidebarItem').length).toBe(10);
- expect(document.querySelectorAll('.sidebarSection').length).toBe(3);
- expect(document.querySelectorAll('.active').length).toBe(2);
-});
-
-test('Bookmarks can be added and removed', () => {
- const handleBookmarksChange = jest.fn();
-
- render(
-
-
-
- );
-
- const addBookmarkButton = document.querySelector('.favorite');
-
- fireEvent.click(addBookmarkButton);
-
- expect(handleBookmarksChange).toHaveBeenCalledWith(['menu-item-1']);
- expect(document.querySelectorAll('.sidebarItem').length).toBe(7);
- expect(document.querySelectorAll('.sidebarSection').length).toBe(4);
-
- const removeBookmarkButton = document.querySelector('.favorite');
-
- fireEvent.click(removeBookmarkButton);
-
- expect(handleBookmarksChange).toHaveBeenCalledWith([]);
-
- expect(document.querySelectorAll('.sidebarItem').length).toBe(7);
- expect(document.querySelectorAll('.sidebarSection').length).toBe(3);
-});
-
-test('Bookmarks clicks are not propagated through', () => {
- const handleBookmarksChange = jest.fn();
- const handleClick = jest.fn();
-
- render(
-
-
-
- );
-
- const addBookmarkButton = document.querySelector('.favorite');
-
- fireEvent.click(addBookmarkButton);
-
- expect(handleBookmarksChange).toHaveBeenCalledWith(['menu-item-1']);
- expect(handleClick).toHaveBeenCalledTimes(0);
-});
-
-test('Renders item as
element with onClick', () => {
- const handleItemClick = jest.fn();
-
- render(
-
-
-
- );
-
- const menuItem = document.querySelector('p.sidebarItem');
-
- expect(menuItem).toBeTruthy();
-
- fireEvent.click(menuItem);
-
- expect(handleItemClick).toHaveBeenCalled();
-
- expect(document.querySelectorAll('.sidebarItem').length).toBe(8);
- expect(document.querySelectorAll('.sidebarSection').length).toBe(4);
-});
-
-test('Renders floating sidebar with item as
element with onClick, closes on item click', () => {
- const handleItemClick = jest.fn();
- const handleClose = jest.fn();
-
- render(
-
-
-
- );
-
- const menuItem = document.querySelector('p.sidebarItem');
-
- expect(menuItem).toBeTruthy();
-
- fireEvent.click(menuItem);
-
- expect(handleItemClick).toHaveBeenCalled();
- expect(handleClose).toHaveBeenCalled();
-
- expect(document.querySelectorAll('.sidebarItem').length).toBe(9);
- expect(document.querySelectorAll('.sidebarSection').length).toBe(5);
-});
-
-test('Renders custom item at the top', () => {
- render(
-
- Hello, I am a custom item! }]}
- />
-
- );
-
- expect(document.querySelectorAll('.sidebarItem').length).toBe(7);
- expect(document.querySelectorAll('.sidebarSection').length).toBe(4);
- expect(screen.getByText('Hello, I am a custom item!')).toBeTruthy();
-});
diff --git a/package.json b/package.json
index 155b4be0..4012d3a4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@qoretechnologies/reqore",
- "version": "0.39.0",
+ "version": "0.40.0",
"description": "ReQore is a highly theme-able and modular UI library for React",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/src/components/ControlGroup/index.tsx b/src/components/ControlGroup/index.tsx
index 91d05e50..c0247e4f 100644
--- a/src/components/ControlGroup/index.tsx
+++ b/src/components/ControlGroup/index.tsx
@@ -17,16 +17,14 @@ import { StyledEffect } from '../Effect';
import { StyledHeader } from '../Header';
import { StyledParagraph } from '../Paragraph';
-export interface IReqoreControlGroupProps
- extends React.HTMLAttributes,
- IWithReqoreFlat,
+export interface IReqoreControlGroupComponentProps
+ extends IWithReqoreFlat,
IWithReqoreSize,
IWithReqoreMinimal,
IReqoreIntent,
IWithReqoreFluid,
IWithReqoreCustomTheme {
stack?: boolean;
- children: any;
fixed?: boolean;
rounded?: boolean;
responsive?: boolean;
@@ -54,6 +52,12 @@ export interface IReqoreControlGroupProps
fill?: boolean;
}
+export interface IReqoreControlGroupProps
+ extends React.HTMLAttributes,
+ IReqoreControlGroupComponentProps {
+ children: any;
+}
+
export interface IReqoreControlGroupStyle extends IReqoreControlGroupProps {
theme: IReqoreTheme;
}
diff --git a/src/components/Menu/section.tsx b/src/components/Menu/section.tsx
new file mode 100644
index 00000000..36add301
--- /dev/null
+++ b/src/components/Menu/section.tsx
@@ -0,0 +1,82 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { TReqoreIntent } from '../../constants/theme';
+import { IReqoreComponent } from '../../types/global';
+import ReqoreButton, { IReqoreButtonProps } from '../Button';
+import ReqoreControlGroup from '../ControlGroup';
+
+export interface IReqoreMenuSectionProps extends IReqoreComponent, IReqoreButtonProps {
+ children: any;
+ collapsible?: boolean;
+ isCollapsed?: boolean;
+ onCollapseChange?: (collapsed: boolean) => void;
+ activeIntent?: TReqoreIntent;
+}
+
+export const ReqoreMenuSection = ({
+ children,
+ _insidePopover,
+ _popoverId,
+ customTheme,
+ wrap,
+ flat,
+ minimal,
+ collapsible = true,
+ isCollapsed,
+ activeIntent,
+ onCollapseChange,
+ ...rest
+}: IReqoreMenuSectionProps) => {
+ const [_isCollapsed, setIsCollapsed] = useState(isCollapsed);
+
+ useEffect(() => {
+ setIsCollapsed(isCollapsed);
+ }, [isCollapsed]);
+
+ const handleCollapseChange = useCallback(
+ (event: React.MouseEvent) => {
+ if (collapsible) {
+ setIsCollapsed(!_isCollapsed);
+ onCollapseChange?.(!_isCollapsed);
+ }
+
+ rest.onClick?.(event);
+ },
+ [_isCollapsed, onCollapseChange, collapsible, rest.onClick]
+ );
+
+ return (
+
+
+ {!_isCollapsed || !collapsible ? (
+
+ {React.Children.map(children, (child) => {
+ return child
+ ? React.cloneElement(child, {
+ _insidePopover: child.props?._insidePopover ?? _insidePopover,
+ _popoverId: child.props?._popoverId ?? _popoverId,
+ customTheme: child.props?.customTheme || customTheme,
+ wrap: 'wrap' in (child.props || {}) ? child.props.wrap : wrap,
+ flat: 'flat' in (child.props || {}) ? child.props.flat : flat,
+ minimal: 'minimal' in (child.props || {}) ? child.props.minimal : minimal,
+ })
+ : null;
+ })}
+
+ ) : null}
+
+ );
+};
diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx
deleted file mode 100644
index d92d059b..00000000
--- a/src/components/Sidebar/index.tsx
+++ /dev/null
@@ -1,536 +0,0 @@
-import classnames from 'classnames';
-import { size } from 'lodash';
-import map from 'lodash/map';
-import { darken, rgba } from 'polished';
-import React, { useState } from 'react';
-import Scroll from 'react-scrollbar';
-import { useUpdateEffect } from 'react-use';
-import styled, { css } from 'styled-components';
-import { IReqoreSidebarTheme, IReqoreTheme } from '../../constants/theme';
-import { changeLightness, getMainColor, getReadableColor } from '../../helpers/colors';
-import { transformMenu } from '../../helpers/sidebar';
-import useLatestZIndex from '../../hooks/useLatestZIndex';
-import { useReqoreTheme } from '../../hooks/useTheme';
-import { ActiveIconScale, InactiveIconScale, ScaleIconOnHover } from '../../styles';
-import { IReqoreIconName } from '../../types/icons';
-import { ReqoreBackdrop } from '../Drawer/backdrop';
-import ReqoreIcon from '../Icon';
-import SidebarItem from './item';
-
-export interface IQorusSidebarCustomItem {
- element: React.FC;
-}
-
-export interface IQorusSidebarItems {
- [sectionId: string]: {
- title?: string | undefined;
- items: IQorusSidebarItem[];
- };
-}
-
-export interface IQorusSidebarItem {
- name: string;
- props?: any;
- activePaths?: string[];
- submenu?: IQorusSidebarItem[];
- id: string;
- as?: React.ElementType | string;
- icon?: IReqoreIconName;
- exact?: boolean;
- element?: React.ElementType;
-}
-
-export interface IQorusSidebarProps {
- isCollapsed?: boolean;
- onCollapseChange?: (isCollapsed?: boolean) => void;
- items: IQorusSidebarItems;
- bookmarks?: string[];
- customItems?: IQorusSidebarCustomItem[];
- collapseLabel?: string;
- closeLabel?: string;
- path: string;
- wrapperStyle?: React.CSSProperties;
- onBookmarksChange?: (bookmarks: string[]) => void;
- useNativeTitle?: boolean;
- position?: 'left' | 'right';
- collapsible?: boolean;
- bordered?: boolean;
- customTheme?: IReqoreSidebarTheme;
- flat?: boolean;
- floating?: boolean;
- hasFloatingBackdrop?: boolean;
- onCloseClick?: () => void;
- isOpen?: boolean;
- closeOnItemClick?: boolean;
-}
-
-export interface IReqoreSidebarStyle {
- expanded?: boolean;
- theme: IReqoreTheme;
- bordered?: boolean;
- position?: 'left' | 'right';
- customThemeId?: string;
- flat?: boolean;
- floating?: boolean;
- isOpen?: boolean;
- zIndex?: number;
-}
-
-const StyledSidebar = styled.div`
- // 80px is header + footer
- font-size: 14px;
- font-weight: 500;
- display: flex;
- overflow: hidden;
- flex-flow: column;
- color: ${({ theme }: IReqoreSidebarStyle) =>
- theme.sidebar?.color ||
- getReadableColor(theme, undefined, undefined, true, getMainColor(theme, 'sidebar'))};
- background-color: ${({ theme }) => theme.sidebar?.main || theme.main};
- box-shadow: ${({ floating }) =>
- floating ? `0px 0px 30px 0px ${rgba('#000000', 0.4)}` : undefined};
-
- ${({ theme, bordered, position, flat, floating }) =>
- !floating &&
- css`
- ${(position === 'left' || bordered) &&
- !flat &&
- css`
- border-right: 1px solid
- ${theme.sidebar?.border || darken(0.05, getMainColor(theme, 'sidebar'))};
- `}
- ${(position === 'right' || bordered) &&
- !flat &&
- css`
- border-left: 1px solid
- ${theme.sidebar?.border || darken(0.05, getMainColor(theme, 'sidebar'))};
- `}
- `}
-
- ${({ floating, flat, theme, position, isOpen, zIndex }) =>
- floating
- ? css`
- position: fixed;
- top: 10px;
- ${position}: ${isOpen ? 10 : -200}px;
- bottom: 10px;
- border-radius: 10px;
- z-index: ${zIndex};
- border: ${!flat
- ? `1px solid ${theme.sidebar?.border || darken(0.05, getMainColor(theme, 'sidebar'))}`
- : undefined};
- `
- : css`
- height: 100%;
- `}
-
- // Custom scrollbar
- .sidebarScroll {
- flex: 1;
- }
-
- transition: all 0.2s ease-in-out;
-
- &.expanded {
- min-width: 180px !important;
- max-width: 180px !important;
-
- .sidebarItem {
- text-align: left;
-
- span.bp3-icon {
- margin-right: 10px;
- vertical-align: text-bottom;
- }
-
- .submenuExpand {
- margin-left: 10px;
- }
-
- &:hover {
- .favorite {
- display: block;
- }
- }
- }
-
- .sidebarSubItem {
- padding-left: 24px;
- }
-
- .favorite {
- display: none;
- opacity: 0.6;
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- right: 10px;
-
- &:hover {
- opacity: 1;
- }
- }
- }
-
- &:not(.expanded) {
- min-width: 50px !important;
- max-width: 50px !important;
- .sidebarItem,
- .sidebarSubItem {
- text-align: center;
- justify-content: center;
- }
- }
-
- // Section
- .sidebarSection {
- .sidebarLink {
- display: inline-block;
- color: inherit;
- width: 100%;
-
- .bp3-popover-target {
- width: 100%;
- }
-
- &:hover {
- text-decoration: none;
- }
- }
-
- .sidebarItem.active {
- ${ActiveIconScale}
-
- color: ${({ theme }) =>
- theme.sidebar?.item?.activeColor ||
- theme.sidebar?.item?.color ||
- getReadableColor(theme, undefined, undefined, false, getMainColor(theme, 'sidebar'))};
-
- background-color: ${({ theme }) =>
- theme.sidebar?.item?.activeBackground ||
- theme.sidebar?.item?.background ||
- darken(0.06, getMainColor(theme, 'sidebar'))};
-
- span.bp3-icon:not(.favorite) {
- color: ${({ theme }) =>
- theme.sidebar?.icon?.activeColor ||
- theme.sidebar?.item?.activeColor ||
- theme.sidebar?.icon?.color ||
- theme.sidebar?.item?.color ||
- 'inherit'};
- }
- }
-
- .sidebarSubItem.active {
- ${ActiveIconScale}
-
- color: ${({ theme }) =>
- theme.sidebar?.subItem?.activeColor ||
- theme.sidebar?.subItem?.color ||
- getReadableColor(theme, undefined, undefined, false, getMainColor(theme, 'sidebar'))};
- background-color: ${({ theme }) =>
- theme.sidebar?.subItem?.activeBackground ||
- theme.sidebar?.subItem?.background ||
- darken(0.08, getMainColor(theme, 'sidebar'))};
- span.bp3-icon:not(.favorite) {
- color: ${({ theme }) =>
- theme.sidebar?.icon?.activeColor ||
- theme.sidebar?.subItem?.activeColor ||
- theme.sidebar?.item?.activeColor ||
- theme.sidebar?.icon?.color ||
- theme.sidebar?.subItem?.color ||
- theme.sidebar?.item?.color ||
- 'inherit'};
- }
- }
-
- .sidebarItem {
- ${InactiveIconScale}
-
- position: relative;
-
- span.bp3-icon:not(.favorite) {
- display: inline-block;
- font-size: 16px;
- color: ${({ theme }) => theme.sidebar?.icon?.color || 'inherit'};
- }
-
- padding: 12px;
- }
-
- .sidebarItem,
- .bp3-popover-wrapper.reqore-sidebar-item {
- color: ${({ theme }) => theme.sidebar?.item?.color || 'inherit'};
-
- background-color: ${({ theme }) =>
- theme.sidebar?.item?.background || getMainColor(theme, 'sidebar')};
-
- &:hover {
- ${ScaleIconOnHover}
-
- color: ${({ theme }) =>
- theme.sidebar?.item?.hoverColor || theme.sidebar?.item?.color || 'inherit'};
- background-color: ${({ theme }) =>
- theme.sidebar?.item?.hoverBackground ||
- theme.sidebar?.item?.background ||
- darken(0.03, getMainColor(theme, 'sidebar'))};
-
- span.bp3-icon:not(.favorite) {
- color: ${({ theme }) =>
- theme.sidebar?.icon?.hoverColor ||
- theme.sidebar?.item?.hoverColor ||
- theme.sidebar?.icon?.color ||
- theme.sidebar?.item?.color ||
- 'inherit'};
- }
- }
- }
-
- .sidebarSubItem {
- border-left: 5px solid
- ${({ theme }) => {
- if (theme.sidebar?.subItem?.border) {
- return theme.sidebar?.subItem?.border;
- }
-
- const color = getMainColor(theme, 'sidebar');
-
- return changeLightness(color, 0.17);
- }};
- }
-
- .sidebarSubItem,
- .bp3-popover-wrapper.reqore-sidebar-subitem {
- color: ${({ theme }) => theme.sidebar?.subItem?.color || 'inherit'};
-
- background-color: ${({ theme }) =>
- theme.sidebar?.subItem?.background || darken(0.04, getMainColor(theme, 'sidebar'))};
-
- &:hover {
- ${ScaleIconOnHover}
-
- color: ${({ theme }) =>
- theme.sidebar?.subItem?.hoverColor || theme.sidebar?.subItem?.color || 'inherit'};
- background-color: ${({ theme }) =>
- theme.sidebar?.subItem?.hoverBackground ||
- theme.sidebar?.subItem?.background ||
- darken(0.06, getMainColor(theme, 'sidebar'))};
-
- span.bp3-icon:not(.favorite) {
- color: ${({ theme }) =>
- theme.sidebar?.icon?.hoverColor ||
- theme.sidebar?.subItem?.hoverColor ||
- theme.sidebar?.icon?.color ||
- theme.sidebar?.subItem?.color ||
- 'inherit'};
- }
- }
- }
-
- .sidebarItem,
- .sidebarSubItem {
- min-height: 50px;
- display: flex;
- align-items: center;
- cursor: pointer;
- transition: all 0.2s ease-in-out;
-
- &.active {
- font-weight: 500;
- }
- }
- }
-`;
-
-const StyledDivider = styled.div<{ theme?: any; hasTitle?: boolean }>`
- width: 100%;
- text-transform: uppercase;
- font-size: 11px;
- font-weight: 600;
- display: flex;
- justify-content: center;
- align-items: center;
- letter-spacing: 1.8px;
- margin-top: 0;
- padding: 8px;
-
- background-color: ${({ theme, hasTitle }) =>
- theme.sidebar?.section?.background ||
- (hasTitle ? darken(0.02, getMainColor(theme, 'sidebar')) : getMainColor(theme, 'sidebar'))};
- color: inherit;
-`;
-
-const ReqoreSidebar: React.FC = ({
- isCollapsed,
- onCollapseChange,
- onCloseClick,
- path,
- items,
- bookmarks,
- customItems,
- collapseLabel,
- closeLabel,
- wrapperStyle,
- onBookmarksChange,
- useNativeTitle,
- position = 'left',
- collapsible = true,
- bordered,
- customTheme,
- flat,
- floating,
- hasFloatingBackdrop,
- isOpen,
- closeOnItemClick,
-}) => {
- const [_isCollapsed, setIsCollapsed] = useState(isCollapsed || false);
- useState;
- const [expandedSection, setExpandedSection] = useState(null);
- const [_bookmarks, setBookmarks] = useState(bookmarks || []);
- const theme: IReqoreTheme = useReqoreTheme('sidebar', customTheme);
- const zIndex = useLatestZIndex();
-
- useUpdateEffect(() => {
- if (onBookmarksChange) {
- onBookmarksChange(_bookmarks);
- }
- }, [_bookmarks]);
-
- useUpdateEffect(() => {
- setIsCollapsed(isCollapsed);
- }, [isCollapsed]);
-
- const handleSectionToggle: (sectionId: string) => void = (sectionId) => {
- setExpandedSection((currentExpandedSection) =>
- sectionId === currentExpandedSection ? null : sectionId
- );
- };
-
- const handleFavoriteClick = (id: string) => {
- setBookmarks((current) => {
- return [...current, id];
- });
- };
-
- const handleUnfavoriteClick = (id: string) => {
- setBookmarks((current) => {
- return [...current].filter((item) => item !== id);
- });
- };
-
- const menu: IQorusSidebarItems = transformMenu(items, _bookmarks, customItems);
-
- return (
- <>
- {floating && hasFloatingBackdrop && isOpen ? (
- onCloseClick?.()}
- zIndex={zIndex}
- />
- ) : null}
-
-
- {map(menu, ({ title, items }, sectionId: string) =>
- size(items) ? (
- <>
- {sectionId !== '_qorusCustomElements' && (
-
- {!_isCollapsed ? title || '' : ''}
-
- )}
-
- {map(items, (itemData, key) => (
- void onCloseClick() : undefined
- }
- />
- ))}
-
- >
- ) : null
- )}
-
-
- {collapsible && (
-
- )}
- {floating && onCloseClick ? (
-
- ) : null}
-
- >
- );
-};
-
-export default ReqoreSidebar;
diff --git a/src/components/Sidebar/item.tsx b/src/components/Sidebar/item.tsx
deleted file mode 100644
index fec7a3c2..00000000
--- a/src/components/Sidebar/item.tsx
+++ /dev/null
@@ -1,264 +0,0 @@
-import classnames from 'classnames';
-import map from 'lodash/map';
-import { useMount } from 'react-use';
-import { IQorusSidebarItem } from '.';
-import { ReqorePopover, useReqoreTheme } from '../..';
-import { IReqoreTheme } from '../../constants/theme';
-import { getMainColor, getReadableColor } from '../../helpers/colors';
-import { isActiveMulti } from '../../helpers/sidebar';
-import ReqoreIcon from '../Icon';
-
-export interface SidebarItemProps {
- itemData: IQorusSidebarItem;
- isCollapsed?: boolean;
- subItem?: boolean;
- onSectionToggle?: (sectionId: string) => any;
- isExpanded?: boolean;
- isActive?: boolean;
- tooltip?: string;
- children?: any;
- expandedSection?: string;
- onFavoriteClick?: (id: string) => void;
- onUnfavoriteClick?: (id: string) => void;
- favoriteItems?: any;
- formatItemName?: (itemName: string) => string;
- currentPath?: string;
- sectionName?: string;
- hasFavorites?: boolean;
- useNativeTitle?: boolean;
- bookmarks?: string[];
- onClick(): void;
-}
-
-export interface ISidebarTooltipProps {
- isCollapsed?: boolean;
- children: any;
- itemData: IQorusSidebarItem;
- isActive?: boolean;
- isSubcategory?: boolean;
- isSubitem?: boolean;
- onClick?: any;
- useNativeTitle?: boolean;
-}
-
-const SidebarItemTooltip = ({
- isCollapsed,
- children,
- itemData,
- isActive,
- isSubitem,
- isSubcategory,
- onClick,
- useNativeTitle,
-}: ISidebarTooltipProps) => {
- const Element = itemData.as || 'div';
-
- if (useNativeTitle) {
- return (
- {
- itemData.props?.onClick?.(e);
- onClick?.(e);
- }}
- className={classnames('sidebarItem', 'sidebarLink', {
- sidebarSubItem: isSubitem,
- active: isActive,
- submenuCategory: isSubcategory,
- })}
- title={itemData.name}
- >
- {children}
-
- );
- }
-
- return (
- {
- itemData.props?.onClick?.(e);
- onClick?.(e);
- },
- className: classnames('sidebarItem', 'sidebarLink', {
- sidebarSubItem: isSubitem,
- active: isActive,
- submenuCategory: isSubcategory,
- }),
- }}
- content={itemData.name}
- placement='right'
- isReqoreComponent
- show={isCollapsed}
- >
- {children}
-
- );
-};
-
-const SidebarItem = ({
- itemData,
- isCollapsed,
- subItem,
- onSectionToggle,
- isExpanded,
- onFavoriteClick,
- onUnfavoriteClick,
- formatItemName,
- currentPath,
- sectionName,
- hasFavorites,
- useNativeTitle,
- onClick,
-}: SidebarItemProps) => {
- const handleFavoriteClick = (event) => {
- event.persist();
- event.stopPropagation();
- event.preventDefault();
-
- if (onFavoriteClick) {
- onFavoriteClick(itemData.id);
- }
- };
-
- const handleUnfavoriteClick = (event) => {
- event.persist();
- event.stopPropagation();
- event.preventDefault();
-
- if (onUnfavoriteClick) {
- onUnfavoriteClick(itemData.id);
- }
- };
-
- const isActive = isActiveMulti(
- itemData.activePaths || [itemData.props?.href],
- currentPath,
- itemData.exact
- );
-
- const getItemName: (itemName: string) => string = (itemName) =>
- formatItemName ? formatItemName(itemName) : itemName;
-
- return (
- <>
- {
- onSectionToggle(itemData.name);
- }
- : () => void onClick?.()
- }
- >
- {' '}
- {!isCollapsed && getItemName(itemData.name)}
- {itemData.submenu && (
-
- )}
- {!itemData.submenu && !isCollapsed && hasFavorites ? (
- <>
- {sectionName === '_qorusBookmarks' ? (
-
- ) : (
-
- )}
- >
- ) : null}
-
- >
- );
-};
-
-const SidebarItemWrapper = ({
- itemData,
- isCollapsed,
- onSectionToggle,
- expandedSection,
- favoriteItems,
- currentPath,
- onFavoriteClick,
- onUnfavoriteClick,
- hasFavorites,
- sectionName,
- useNativeTitle,
- onClick,
-}: SidebarItemProps) => {
- const theme: IReqoreTheme = useReqoreTheme();
-
- useMount(() => {
- if (
- !itemData.element &&
- isActiveMulti(itemData.activePaths || [itemData.props?.href], currentPath, itemData.exact)
- ) {
- onSectionToggle(itemData.name);
- }
- });
-
- if (itemData.element) {
- const { element: Element } = itemData;
-
- return (
-
- );
- }
-
- return (
- <>
-
- {expandedSection === itemData.name &&
- map(itemData.submenu, (subItemData: any, key: number) => (
-
- ))}
- >
- );
-};
-
-export default SidebarItemWrapper;
diff --git a/src/helpers/sidebar.ts b/src/helpers/sidebar.ts
deleted file mode 100644
index 336ba009..00000000
--- a/src/helpers/sidebar.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { reduce, size } from 'lodash';
-import { IQorusSidebarCustomItem, IQorusSidebarItems } from '../components/Sidebar';
-
-export const transformMenu = (
- menu: IQorusSidebarItems,
- bookmarks: string[],
- customItems?: IQorusSidebarCustomItem[]
-) => {
- let newMenu: IQorusSidebarItems = { ...menu };
- let _qorusCustomElements;
- let _qorusBookmarks;
-
- if (size(bookmarks)) {
- _qorusBookmarks = {
- title: 'Bookmarks',
- items: [],
- };
-
- newMenu = reduce(
- newMenu,
- (cur, menuSection, name: string) => {
- const newSection = { ...menuSection };
-
- newSection.items = [...newSection.items]
- .map((newSectionItem) => {
- const copySectionItem = { ...newSectionItem };
-
- if (copySectionItem.submenu) {
- copySectionItem.submenu = copySectionItem.submenu.filter((submenuItem) => {
- const isItemInSubmenu = bookmarks.find(
- (favoriteItem) => favoriteItem === submenuItem.id
- );
-
- if (isItemInSubmenu) {
- _qorusBookmarks.items.push(submenuItem);
- }
-
- return !isItemInSubmenu;
- });
-
- if (size(copySectionItem.submenu)) {
- return copySectionItem;
- } else {
- return undefined;
- }
- } else {
- if (!bookmarks.find((favoriteItem) => favoriteItem === copySectionItem.id)) {
- return copySectionItem;
- } else {
- _qorusBookmarks.items.push(copySectionItem);
- return undefined;
- }
- }
- })
- .filter((newSectionItem) => newSectionItem);
-
- return { ...cur, [name]: newSection };
- },
- {}
- );
-
- newMenu = { _qorusBookmarks, ...newMenu };
- }
-
- if (size(customItems)) {
- _qorusCustomElements = {
- items: customItems,
- };
-
- newMenu = { _qorusCustomElements, ...newMenu };
- }
-
- return newMenu;
-};
-
-export const isActive = (to: string, currentPath: string, exact?: boolean): boolean =>
- exact ? currentPath === to : currentPath.startsWith(to);
-
-export const isActiveMulti = (to: string[], currentPath: string, exact?: boolean): boolean => {
- let active: boolean = false;
-
- to.forEach((path: string) => {
- if (isActive(path, currentPath, exact)) {
- active = true;
- }
- });
-
- return active;
-};
diff --git a/src/index.tsx b/src/index.tsx
index 145e6013..2f9058d3 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -30,6 +30,7 @@ export { default as ReqoreLayoutContent } from './components/Layout/content';
export { default as ReqoreMenu } from './components/Menu';
export { default as ReqoreMenuDivider } from './components/Menu/divider';
export { default as ReqoreMenuItem } from './components/Menu/item';
+export { ReqoreMenuSection } from './components/Menu/section';
export { default as ReqoreMessage } from './components/Message';
export { ReqoreModal } from './components/Modal';
export { ReqoreMultiSelect } from './components/MultiSelect';
@@ -44,7 +45,6 @@ export { ReqorePanel } from './components/Panel';
export { ReqoreP, ReqoreP as ReqoreParagraph } from './components/Paragraph';
export { default as ReqorePopover } from './components/Popover';
export { default as ReqoreRadioGroup } from './components/RadioGroup';
-export { default as ReqoreSidebar } from './components/Sidebar';
export { ReqoreHorizontalSpacer, ReqoreSpacer, ReqoreVerticalSpacer } from './components/Spacer';
export { ReqoreSpinner } from './components/Spinner';
export { default as ReqoreTable } from './components/Table';
diff --git a/src/mock/menu.ts b/src/mock/menu.ts
index 2a14cae7..2c701fca 100644
--- a/src/mock/menu.ts
+++ b/src/mock/menu.ts
@@ -1,59 +1,57 @@
-import { IQorusSidebarItems } from "../components/Sidebar";
-
-export const qorusSidebarItems: IQorusSidebarItems = {
+export const qorusSidebarItems = {
Menu: {
- title: "Menu",
+ title: 'Menu',
items: [
{
- name: "Menu item 1",
- as: "a",
+ name: 'Menu item 1',
+ as: 'a',
props: {
- href: "/item-1",
+ href: '/item-1',
},
- icon: "Home3Line",
- id: "menu-item-1",
+ icon: 'Home3Line',
+ id: 'menu-item-1',
},
{
- name: "Menu item 2",
- as: "a",
+ name: 'Menu item 2',
+ as: 'a',
props: {
- href: "/item-2",
+ href: '/item-2',
},
- icon: "Settings4Fill",
- id: "menu-item-2",
+ icon: 'Settings4Fill',
+ id: 'menu-item-2',
},
{
- name: "Menu item 3",
- icon: "File3Line",
- id: "menu-item-3",
- activePaths: ["/item-3"],
+ name: 'Menu item 3',
+ icon: 'File3Line',
+ id: 'menu-item-3',
+ activePaths: ['/item-3'],
submenu: [
{
- name: "Submenu item 1",
- as: "a",
+ name: 'Submenu item 1',
+ as: 'a',
props: {
- href: "/item-3/item-1",
+ href: '/item-3/item-1',
},
- icon: "NodeTree",
- id: "submenu-item-1",
+ icon: 'NodeTree',
+ id: 'submenu-item-1',
},
{
- name: "Submenu item 2",
- as: "a",
+ name: 'Submenu item 2',
+ as: 'a',
props: {
- href: "/item-3/item-2",
+ href: '/item-3/item-2',
},
- icon: "Chat3Fill",
- id: "submenu-item-2",
+ icon: 'Chat3Fill',
+ id: 'submenu-item-2',
},
{
- name: "Super really long Submenu item 3",
- as: "a",
+ name: 'Super really long Submenu item 3',
+ as: 'a',
props: {
- href: "/item-3/item-3",
+ href: '/item-3/item-3',
},
- icon: "DatabaseFill",
- id: "submenu-item-3",
+ icon: 'DatabaseFill',
+ id: 'submenu-item-3',
},
],
},
@@ -62,33 +60,33 @@ export const qorusSidebarItems: IQorusSidebarItems = {
Menu2: {
items: [
{
- name: "Super Long Really Another item 1",
- as: "a",
+ name: 'Super Long Really Another item 1',
+ as: 'a',
props: {
- href: "/another-item-1",
+ href: '/another-item-1',
},
- icon: "Home6Line",
- id: "another-item-1",
+ icon: 'Home6Line',
+ id: 'another-item-1',
},
{
- name: "Another item 2",
- as: "a",
+ name: 'Another item 2',
+ as: 'a',
props: {
- href: "/another-item-2",
+ href: '/another-item-2',
},
- icon: "TableFill",
- id: "another-item-2",
+ icon: 'TableFill',
+ id: 'another-item-2',
},
{
- name: "Another item 3",
- as: "a",
+ name: 'Another item 3',
+ as: 'a',
props: {
onClick: () => {
- alert("Click");
+ alert('Click');
},
},
- icon: "BellLine",
- id: "another-item-3",
+ icon: 'BellLine',
+ id: 'another-item-3',
},
],
},
diff --git a/src/stories/Menu/Menu.stories.tsx b/src/stories/Menu/Menu.stories.tsx
index 6dc81000..e0919ac4 100644
--- a/src/stories/Menu/Menu.stories.tsx
+++ b/src/stories/Menu/Menu.stories.tsx
@@ -1,7 +1,11 @@
import { StoryFn, StoryObj } from '@storybook/react';
+import { fireEvent, within } from '@storybook/testing-library';
+import { noop } from 'lodash';
import { IReqoreMenuProps } from '../../components/Menu';
import { IReqoreMenuItemProps } from '../../components/Menu/item';
+import { ReqoreMenuSection } from '../../components/Menu/section';
import {
+ ReqoreControlGroup,
ReqoreInput,
ReqoreMenu,
ReqoreMenuDivider,
@@ -91,121 +95,160 @@ const meta = {
export default meta;
type Story = StoryObj;
-const Template: StoryFn = (args) => {
- return (
-
-
-
- Selected success
-
-
- Save
+const MenuWithSubmenus = (args: IReqoreMenuProps) => (
+
+
+
+ Submenu Item 1
-
alert('Item clicked')}
- rightIcon='FahrenheitFill'
- onRightIconClick={() => alert('Icon clicked')}
- tooltip={{
- content: 'You sure?',
- }}
- intent='danger'
+ icon='DualSim1Line'
+ rightIcon='MoneyEuroBoxLine'
+ disabled
+ onRightIconClick={noop}
>
- Delete
+ Submenu Item 2
+
+
+ Submenu Item 3
+
+
+ Submenu Item 4
+
+
+
+
+ Submenu Item 5
+
+
+ Submenu Item 6
+
+
+
+
+);
- alert('Icon clicked')}
- description='Button with right icon and description'
- customTheme={{
- main: 'info:darken:1:0.3',
- }}
- >
- Some button
-
+const Template: StoryFn = (args) => {
+ return (
+
+
+
+
+ Selected success
+
+
+ Save
+
+
+ alert('Item clicked')}
+ rightIcon='FahrenheitFill'
+ onRightIconClick={() => alert('Icon clicked')}
+ tooltip={{
+ content: 'You sure?',
+ }}
+ intent='danger'
+ >
+ Delete
+
-
- This is a really long item that should wrap
-
-
- Disabled
-
-
- Disabled intent
-
-
-
- Item 1
-
- Item 2
-
-
- Item 3
-
-
- Item 4
-
-
- }
- isReqoreComponent
- noWrapper
- handler='click'
- placement='right'
- >
- I have a submenu on click
-
-
-
- I am selected!
-
- alert('Icon clicked')}
+ description='Button with right icon and description'
+ customTheme={{
+ main: 'info:darken:1:0.3',
+ }}
+ >
+ Some button
+
+
+
+ This is a really long item that should wrap
+
+
+ Disabled
+
+
+ Disabled intent
+
+
+
+ Item 1
+
+ Item 2
+
+
+ Item 3
+
+
+ Item 4
+
+
+ }
+ isReqoreComponent
+ noWrapper
+ handler='click'
+ placement='right'
+ >
+ I have a submenu on click
+
+
+
+ I am selected!
+
+
- Fancy
-
-
+ }}
+ description='I also have a description'
+ badge={10}
+ >
+ Fancy
+
+
+
+
);
};
@@ -252,3 +295,13 @@ export const Transparent: Story = {
transparent: true,
},
};
+
+export const SubmenuCanBeToggled: Story = {
+ render: (args) => ,
+ play: async ({ canvasElement, ...rest }) => {
+ const canvas = within(canvasElement);
+
+ await fireEvent.click(canvas.queryAllByText('Submenu 2 active')[0]);
+ await fireEvent.click(canvas.queryAllByText('Collapsed submenu')[0]);
+ },
+};
diff --git a/src/stories/Sidebar/Sidebar.stories.tsx b/src/stories/Sidebar/Sidebar.stories.tsx
deleted file mode 100644
index 6704aeb7..00000000
--- a/src/stories/Sidebar/Sidebar.stories.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { StoryFn, StoryObj } from '@storybook/react';
-import { noop } from 'lodash';
-import { IQorusSidebarProps } from '../../components/Sidebar';
-import { ReqorePanel, ReqoreSidebar } from '../../index';
-import { qorusSidebarItems } from '../../mock/menu';
-import { StoryMeta } from '../utils';
-import { FlatArg, NoContentArg, argManager } from '../utils/args';
-
-const { disableArg, createArg } = argManager();
-
-const meta = {
- title: 'Navigation/Sidebar/Stories',
- component: ReqoreSidebar,
- args: {
- path: '/',
- floating: false,
- bordered: false,
- isOpen: false,
- position: 'left',
- withoutContent: true,
- },
- argTypes: {
- ...disableArg('items'),
- ...FlatArg,
- ...createArg('path', {
- defaultValue: '/',
- name: 'Path',
- description: 'Mock location path',
- type: 'string',
- }),
- ...createArg('floating', {
- control: 'boolean',
- defaultValue: false,
- name: 'Floating',
- description: 'If the sidebar should be floating',
- }),
- ...createArg('bordered', {
- control: 'boolean',
- defaultValue: false,
- name: 'Bordered',
- description: 'If the sidebar should be bordered',
- }),
- ...createArg('isOpen', {
- control: 'boolean',
- defaultValue: false,
- name: 'Is Open',
- description: 'If the sidebar should be shown when floating',
- }),
- ...createArg('position', {
- control: 'select',
- options: ['left', 'right'],
- defaultValue: 'left',
- name: 'Sidebar position',
- description: 'Sidebar positions - this decides which border is drawn',
- }),
- ...NoContentArg,
- },
-} as StoryMeta;
-
-export default meta;
-type Story = StoryObj;
-
-const Template: StoryFn = (args: IQorusSidebarProps) => {
- return (
- (
-
-
- Hello I am a custom element!
-
-
- ),
- },
- ]}
- />
- );
-};
-
-export const Basic: Story = {
- render: Template,
-};
-
-export const Floating: Story = {
- render: Template,
-
- args: {
- floating: true,
- isOpen: true,
- hasFloatingBackdrop: true,
- },
-};