From 34c03e94bec9253ef2495062a2f1fb3cec8ef6aa Mon Sep 17 00:00:00 2001 From: Foxhoundn Date: Mon, 18 Mar 2024 11:29:05 +0100 Subject: [PATCH 1/3] Removed sidebar to be replaced by ReqoreMenu everywhere. --- package.json | 2 +- src/components/ControlGroup/index.tsx | 12 +- src/components/Menu/section.tsx | 82 ++++ src/components/Sidebar/index.tsx | 536 ------------------------ src/components/Sidebar/item.tsx | 264 ------------ src/mock/menu.ts | 94 ++--- src/stories/Menu/Menu.stories.tsx | 269 +++++++----- src/stories/Sidebar/Sidebar.stories.tsx | 97 ----- 8 files changed, 298 insertions(+), 1058 deletions(-) create mode 100644 src/components/Menu/section.tsx delete mode 100644 src/components/Sidebar/index.tsx delete mode 100644 src/components/Sidebar/item.tsx delete mode 100644 src/stories/Sidebar/Sidebar.stories.tsx 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/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, - }, -}; From 7e58d3d3ea176fecbe4512dd20ca49db962ad172 Mon Sep 17 00:00:00 2001 From: Foxhoundn Date: Mon, 18 Mar 2024 11:31:24 +0100 Subject: [PATCH 2/3] Remove sidebar.ts and update index.tsx --- src/helpers/sidebar.ts | 89 ------------------------------------------ src/index.tsx | 2 +- 2 files changed, 1 insertion(+), 90 deletions(-) delete mode 100644 src/helpers/sidebar.ts 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'; From 5d8a8c3278d303d07d416609589f1e9ea381a705 Mon Sep 17 00:00:00 2001 From: Foxhoundn Date: Mon, 18 Mar 2024 11:33:00 +0100 Subject: [PATCH 3/3] Remove sidebar test file and update sidebar component tests --- __tests__/sidebar.test.tsx | 225 ------------------------------------- 1 file changed, 225 deletions(-) delete mode 100644 __tests__/sidebar.test.tsx 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(); -});