From e6e281fc6e0f8c8917df0fb6afc51e11be8ea87a Mon Sep 17 00:00:00 2001 From: Filip Witosz Date: Tue, 16 Feb 2021 13:17:41 +0100 Subject: [PATCH 1/3] Tabs, tabs in breadcrumbs and popover now closes on click when menu is inside --- __tests__/breadcrumbs.test.tsx | 27 +- __tests__/tabs.test.tsx | 176 ++++++++++++ package.json | 2 +- src/components/Breadcrumbs/index.tsx | 156 ++++++---- src/components/InternalPopover/index.tsx | 16 +- src/components/Menu/index.tsx | 14 +- src/components/Menu/item.tsx | 72 +++-- src/components/Popover/index.tsx | 19 +- src/components/Tabs/content.tsx | 32 +++ src/components/Tabs/index.tsx | 78 +++++ src/components/Tabs/item.tsx | 134 +++++++++ src/components/Tabs/list.tsx | 271 ++++++++++++++++++ src/hooks/usePopover.tsx | 2 + src/index.tsx | 4 + .../Breadcrumbs/Breadcrumbs.stories.tsx | 170 +++++++++++ src/stories/Tabs/Tabs.stories.tsx | 242 ++++++++++++++++ src/types/global.ts | 6 +- 17 files changed, 1312 insertions(+), 109 deletions(-) create mode 100644 __tests__/tabs.test.tsx create mode 100644 src/components/Tabs/content.tsx create mode 100644 src/components/Tabs/index.tsx create mode 100644 src/components/Tabs/item.tsx create mode 100644 src/components/Tabs/list.tsx create mode 100644 src/stories/Tabs/Tabs.stories.tsx diff --git a/__tests__/breadcrumbs.test.tsx b/__tests__/breadcrumbs.test.tsx index 612901eb..e7146b9e 100644 --- a/__tests__/breadcrumbs.test.tsx +++ b/__tests__/breadcrumbs.test.tsx @@ -1,4 +1,4 @@ -import { act, fireEvent, render } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; import React from 'react'; import { ReqoreBreadcrumbs, @@ -56,28 +56,3 @@ test('Renders shortened properly', () => { ); expect(document.querySelectorAll('.reqore-breadcrumbs-item').length).toBe(3); }); - -test('Shows hidden items on click', () => { - act(() => { - render( - - - - - - ); - - fireEvent.click(document.querySelectorAll('.reqore-breadcrumbs-item')[1]); - }); - - expect(document.querySelectorAll('.reqore-breadcrumbs-item').length).toBe(3); -}); diff --git a/__tests__/tabs.test.tsx b/__tests__/tabs.test.tsx new file mode 100644 index 00000000..d5db1195 --- /dev/null +++ b/__tests__/tabs.test.tsx @@ -0,0 +1,176 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; +import { + ReqoreLayoutContent, + ReqoreTabs, + ReqoreTabsContent, + ReqoreUIProvider, +} from '../src'; + +test('Renders full properly', () => { + render( +
+ + + + Tab 1 content + Tab 2 content + Tab 3 content + Tab 4 content + Tab 5 content + + + +
+ ); + + expect(document.querySelectorAll('.reqore-tabs-list').length).toBe(1); + expect(document.querySelectorAll('.reqore-tabs').length).toBe(1); + expect(document.querySelectorAll('.reqore-tabs-list-item').length).toBe(5); + expect(document.querySelectorAll('.reqore-tabs-content').length).toBe(1); + expect(screen.getByText('Tab 1 content')).toBeTruthy(); +}); + +test('Renders shortened properly', () => { + act(() => { + render( + + + + Tab 1 content + Tab 2 content + Tab 3 content + Tab 4 content + Tab 5 content + + + + ); + }); + + expect(document.querySelectorAll('.reqore-tabs-list-item').length).toBe(2); + expect( + document.querySelectorAll('.reqore-tabs-list .reqore-popover-wrapper') + .length + ).toBe(1); +}); + +test('Default active tab can be specified', () => { + act(() => { + render( + + + + Tab 1 content + Tab 2 content + Tab 3 content + Tab 4 content + Tab 5 content + + + + ); + }); + + expect(screen.getByText('Tab 4 content')).toBeTruthy(); +}); + +test('Changes tab and runs callback', () => { + const cb = jest.fn(); + + act(() => { + render( + + + + Tab 1 content + Tab 2 content + Tab 3 content + Tab 4 content + Tab 5 content + + + + ); + + fireEvent.click(document.querySelectorAll('.reqore-tabs-list-item')[2]); + }); + + expect(screen.getByText('Tab 3 content')).toBeTruthy(); + expect(cb).toHaveBeenCalledWith('tab3'); + expect( + document.querySelectorAll('.reqore-tabs-list-item__active').length + ).toBe(1); +}); + +test('Does not change tab and run callback when disabled', () => { + const cb = jest.fn(); + + act(() => { + render( + + + + Tab 1 content + Tab 2 content + Tab 3 content + Tab 4 content + Tab 5 content + + + + ); + + fireEvent.click(document.querySelectorAll('.reqore-tabs-list-item')[2]); + }); + + expect(screen.getByText('Tab 1 content')).toBeTruthy(); + expect(cb).not.toHaveBeenCalled(); + expect( + document.querySelectorAll('.reqore-tabs-list-item__active').length + ).toBe(1); +}); diff --git a/package.json b/package.json index b889bc23..d38813d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@qoretechnologies/reqore", - "version": "0.2.8", + "version": "0.3.0", "description": "ReQore is a UI library of components for Qorus connected apps", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/components/Breadcrumbs/index.tsx b/src/components/Breadcrumbs/index.tsx index 95e2b2a1..6e24c4eb 100644 --- a/src/components/Breadcrumbs/index.tsx +++ b/src/components/Breadcrumbs/index.tsx @@ -5,9 +5,15 @@ import { useMeasure } from 'react-use'; import styled, { css } from 'styled-components'; import { ReqorePopover } from '../..'; import { IReqoreTheme } from '../../constants/theme'; -import { getReadableColor } from '../../helpers/colors'; +import { changeLightness, getReadableColor } from '../../helpers/colors'; import ReqoreMenu from '../Menu'; import ReqoreMenuItem from '../Menu/item'; +import { IReqoreTabsListItem } from '../Tabs'; +import { StyledTabListItem } from '../Tabs/item'; +import ReqoreTabsList, { + getTabsLength, + StyledReqoreTabsList, +} from '../Tabs/list'; import ReqoreBreadcrumbsItem, { IReqoreBreadcrumbItemProps } from './item'; export interface IReqoreBreadcrumbItem { @@ -16,7 +22,12 @@ export interface IReqoreBreadcrumbItem { icon?: IconName; active?: boolean; as?: any; - props?: React.HTMLAttributes; + props?: React.HTMLAttributes; + withTabs?: { + tabs: IReqoreTabsListItem[]; + onTabChange: (tabId: string) => any; + activeTab: string; + }; } export interface IReqoreBreadcrumbsProps @@ -40,12 +51,30 @@ const StyledReqoreBreadcrumbs = styled.div<{ theme: IReqoreTheme }>` display: flex; align-items: center; - > * { + > *, + ${StyledReqoreTabsList} > *, + ${StyledTabListItem} * { color: ${theme.breadcrumbs?.item?.color || theme.breadcrumbs?.main || getReadableColor(theme.main, undefined, undefined, true)}; } + ${StyledReqoreTabsList} { + flex: 1; + } + + ${StyledTabListItem} { + &:not(:last-child) { + border-right: 1px solid + ${changeLightness( + theme.breadcrumbs?.item?.color || + theme.breadcrumbs?.main || + getReadableColor(theme.main, undefined, undefined, true), + 0.65 + )}; + } + } + &:first-child { overflow: hidden; flex: 1; @@ -62,6 +91,10 @@ const getBreadcrumbsLength = ( return len + 50; } + if (item.withTabs) { + return len + getTabsLength(item.withTabs.tabs); + } + return len + 27 + item.label.length * 10 + 35; }, 0); @@ -99,6 +132,73 @@ const ReqoreBreadcrumbs = ({ const transformedItems = getTransformedItems(items, _testWidth || width); + const renderItem = ( + item: IReqoreBreadcrumbItem | IReqoreBreadcrumbItem[], + index: number + ) => { + if (isArray(item)) { + return ( + + + + {item.map(({ icon, label, as, tooltip, props }) => ( + + {label} + + ))} + + } + /> + + ); + } + + if (item.withTabs) { + return ( + + + + + ); + } + + return ( + + {index !== 0 && ( + + )} + + + ); + }; + return ( - isArray(item) ? ( - - - - {item.map(({ icon, label, as, tooltip, props }) => ( - - {label} - - ))} - - } - /> - - ) : ( - - {index !== 0 && ( - - )} - - - ) + ) => renderItem(item, index) )} {rightElement &&
{rightElement}
} diff --git a/src/components/InternalPopover/index.tsx b/src/components/InternalPopover/index.tsx index 3979f751..5a45bab4 100644 --- a/src/components/InternalPopover/index.tsx +++ b/src/components/InternalPopover/index.tsx @@ -1,4 +1,5 @@ import { Placement, VirtualElement } from '@popperjs/core'; +import { isString } from 'lodash'; import { darken } from 'polished'; import React, { MutableRefObject, useContext, useRef, useState } from 'react'; import { usePopper } from 'react-popper'; @@ -73,7 +74,7 @@ const StyledPopoverContent = styled.div` export interface IReqoreInternalPopoverProps { element: Element | VirtualElement; - content: JSX.Element | string | number; + content: JSX.Element | string | number | any; id: string; placement?: Placement; } @@ -127,7 +128,18 @@ const InternalPopover: React.FC = ({ style={styles.arrow} data-popper-arrow /> - {content} + + {React.Children.map(content, (child) => { + if (isString(child)) { + return child; + } + + return React.cloneElement(child, { + _insidePopover: true, + _popoverId: id, + }); + })} + ); diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index 617bdcce..07aa9d5d 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -3,8 +3,11 @@ import styled, { css } from 'styled-components'; import { IReqoreTheme } from '../../constants/theme'; import ReqoreThemeProvider from '../../containers/ThemeProvider'; import { changeLightness } from '../../helpers/colors'; +import { IReqoreComponent } from '../../types/global'; -export interface IReqoreMenuProps extends React.HTMLAttributes { +export interface IReqoreMenuProps + extends IReqoreComponent, + React.HTMLAttributes { children: any; position?: 'left' | 'right'; } @@ -29,10 +32,15 @@ const StyledReqoreMenu = styled.div` `; const ReqoreMenu: React.FC = forwardRef( - ({ children, position, ...rest }, ref: any) => ( + ({ children, position, _insidePopover, _popoverId, ...rest }, ref: any) => ( - {children} + {React.Children.map(children, (child) => + React.cloneElement(child, { + _insidePopover, + _popoverId, + }) + )} ) diff --git a/src/components/Menu/item.tsx b/src/components/Menu/item.tsx index f4962776..3af67743 100644 --- a/src/components/Menu/item.tsx +++ b/src/components/Menu/item.tsx @@ -1,8 +1,9 @@ import { Icon, IconName } from '@blueprintjs/core'; -import React, { forwardRef } from 'react'; +import React, { forwardRef, useContext } from 'react'; import styled, { css } from 'styled-components'; import { IReqoreTheme } from '../../constants/theme'; import ReqoreThemeProvider from '../../containers/ThemeProvider'; +import PopoverContext from '../../context/PopoverContext'; import { changeLightness, getReadableColor } from '../../helpers/colors'; import { IReqoreComponent } from '../../types/global'; @@ -14,6 +15,7 @@ export interface IReqoreMenuItemProps rightIcon?: IconName; as?: JSX.Element | React.ElementType | never; selected?: boolean; + disabled?: boolean; } const StyledElementContent = styled.div<{ theme: IReqoreTheme }>` @@ -28,8 +30,14 @@ const StyledElementContent = styled.div<{ theme: IReqoreTheme }>` } `; -const StyledElement = styled.div<{ theme: IReqoreTheme; selected: boolean }>` - min-height: 25px; +export interface IReqoreMenuItemStyle { + theme: IReqoreTheme; + selected: boolean; + disabled: boolean; +} + +const StyledElement = styled.div` + min-height: 35px; color: ${({ theme, selected }) => getReadableColor(theme.main, undefined, undefined, !selected)}; display: flex; @@ -43,18 +51,28 @@ const StyledElement = styled.div<{ theme: IReqoreTheme; selected: boolean }>` background-color: ${({ theme, selected }) => selected ? changeLightness(theme.main, 0.09) : 'transparent'}; - ${({ theme, selected }) => - !selected && - css` - &:hover { - background-color: ${changeLightness(theme.main, 0.05)}; - } - `} + ${({ theme, selected, disabled }) => + !disabled + ? css` + ${!selected && + css` + &:hover { + background-color: ${changeLightness(theme.main, 0.05)}; + } + `} - &:hover { - color: ${({ theme }) => getReadableColor(theme.main, undefined, undefined)}; - text-decoration: none; - } + &:hover { + color: ${({ theme }) => + getReadableColor(theme.main, undefined, undefined)}; + text-decoration: none; + } + ` + : css` + cursor: not-allowed; + > * { + opacity: 0.5; + } + `} &:not(:first-child) { margin-top: 4px; @@ -62,7 +80,24 @@ const StyledElement = styled.div<{ theme: IReqoreTheme; selected: boolean }>` `; const ReqoreMenuItem: React.FC = forwardRef( - ({ children, icon, rightIcon, as, selected, onClick, ...rest }, ref: any) => { + ( + { + children, + icon, + rightIcon, + as, + selected, + onClick, + disabled, + id, + _insidePopover, + _popoverId, + ...rest + }, + ref: any + ) => { + const { removePopover } = useContext(PopoverContext); + return ( = forwardRef( event.persist(); event.stopPropagation(); - onClick && onClick(event); + onClick && onClick(id, event); + + if (_insidePopover) { + removePopover(_popoverId); + } }} selected={selected} ref={ref} + disabled={disabled} > {icon && } diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx index bd346ede..5ba52ae1 100644 --- a/src/components/Popover/index.tsx +++ b/src/components/Popover/index.tsx @@ -1,8 +1,10 @@ import { Placement } from '@popperjs/core'; import React, { useState } from 'react'; +import styled from 'styled-components'; import usePopover from '../../hooks/usePopover'; +import { IReqoreComponent } from '../../types/global'; -export interface IReqorePopoverProps { +export interface IReqorePopoverProps extends IReqoreComponent { component: any; componentProps?: any; children?: any; @@ -13,6 +15,10 @@ export interface IReqorePopoverProps { content: any; } +export const StyledPopover = styled.span` + overflow: hidden; +`; + const Popover = ({ component: Component, componentProps, @@ -22,6 +28,7 @@ const Popover = ({ placement, show, isReqoreComponent, + ...rest }: IReqorePopoverProps) => { const [ref, setRef] = useState(null); @@ -29,16 +36,18 @@ const Popover = ({ if (isReqoreComponent) { return ( - + {children} ); } return ( - - {children} - + + + {children} + + ); }; diff --git a/src/components/Tabs/content.tsx b/src/components/Tabs/content.tsx new file mode 100644 index 00000000..b09432b9 --- /dev/null +++ b/src/components/Tabs/content.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import styled from 'styled-components'; + +export interface IReqoreTabsContent + extends React.HTMLAttributes { + children?: any; + id: string; +} + +const StyledTabsContent = styled.div` + display: flex; + flex-flow: column; + overflow-y: auto; + overflow-x: hidden; + flex: 1; + padding: 10px; +`; + +const ReqoreTabsContent = ({ + children, + className, + ...rest +}: IReqoreTabsContent) => ( + + {children} + +); + +export default ReqoreTabsContent; diff --git a/src/components/Tabs/index.tsx b/src/components/Tabs/index.tsx new file mode 100644 index 00000000..9fbbd5c7 --- /dev/null +++ b/src/components/Tabs/index.tsx @@ -0,0 +1,78 @@ +import { IconName } from '@blueprintjs/core'; +import React, { ReactElement, useState } from 'react'; +import styled, { css } from 'styled-components'; +import ReqoreTabsList from './list'; + +export interface IReqoreTabsListItem { + label: string; + icon?: IconName; + as?: any; + disabled?: boolean; + id: string; + tooltip?: string; + props?: React.HTMLAttributes; + onClick?: (event: any) => any; +} + +export interface IReqoreTabsProps extends React.HTMLAttributes { + tabs: IReqoreTabsListItem[]; + activeTab?: string; + onTabChange?: (tabId: string) => any; + children?: ReactElement[] | ReactElement; + fill?: boolean; + vertical?: boolean; + + // Internal prop, ignore! + _testWidth?: number; +} + +const StyledTabs = styled.div<{ vertical?: boolean }>` + display: flex; + ${({ vertical }) => css` + min-height: 100px; + width: 100%; + flex-flow: ${vertical ? 'row' : 'column'}; + `} +`; + +const ReqoreTabs = ({ + tabs, + activeTab, + children, + className, + onTabChange, + fill, + _testWidth, + vertical, + ...rest +}: IReqoreTabsProps) => { + const [_activeTab, setActiveTab] = useState(activeTab || tabs[0].id); + + return ( + + { + setActiveTab(tabId); + + if (onTabChange) { + onTabChange(tabId); + } + }} + /> + {React.Children.map(children, (child) => + child.props.id === _activeTab ? child : null + )} + + ); +}; + +export default ReqoreTabs; diff --git a/src/components/Tabs/item.tsx b/src/components/Tabs/item.tsx new file mode 100644 index 00000000..eb1cb0f9 --- /dev/null +++ b/src/components/Tabs/item.tsx @@ -0,0 +1,134 @@ +import { Icon } from '@blueprintjs/core'; +import React, { useState } from 'react'; +import styled, { css } from 'styled-components'; +import { IReqoreTabsListItem } from '.'; +import { IReqoreTheme } from '../../constants/theme'; +import ReqoreThemeProvider from '../../containers/ThemeProvider'; +import { changeLightness, getReadableColor } from '../../helpers/colors'; +import usePopover from '../../hooks/usePopover'; + +export interface IReqoreTabListItemProps extends IReqoreTabsListItem { + active?: boolean; + vertical?: boolean; +} + +export interface IReqoreTabListItemStyle { + theme: IReqoreTheme; + active?: boolean; + disabled?: boolean; + vertical?: boolean; +} + +const StyledLabel = styled.span` + overflow: hidden; + text-overflow: ellipsis; +`; + +export const StyledTabListItem = styled.div` + ${({ theme, active, disabled, vertical }: IReqoreTabListItemStyle) => { + const textColor = getReadableColor(theme.main, undefined, undefined, true); + + return css` + display: flex; + align-items: center; + padding: ${vertical ? '10px' : 0} 15px; + transition: background-color 0.15s linear; + + text-transform: uppercase; + letter-spacing: 2px; + font-size: 12px; + + &:not(:last-child) { + border-${vertical ? 'bottom' : 'right'}: 1px solid ${changeLightness( + theme.main, + 0.05 + )}; + } + + * { + color: ${textColor}; + } + + ${ + active && + css` + background-color: ${changeLightness(theme.main, 0.05)}; + * { + font-weight: 700; + color: ${getReadableColor(theme.main, undefined, undefined)}; + } + ` + } + + ${ + !disabled + ? css` + cursor: pointer; + &:hover { + color: ${getReadableColor(theme.main, undefined, undefined)}; + background-color: ${changeLightness(theme.main, 0.025)}; + } + ` + : css` + cursor: not-allowed; + > * { + opacity: 0.5; + } + ` + } + `; + }} + + > *:first-child:not(:last-child) { + margin-right: 5px; + } + + a { + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + ${StyledLabel} { + } +`; + +const ReqoreTabsListItem = ({ + tooltip, + label, + props, + icon, + active, + as, + disabled, + vertical, + onClick, +}: IReqoreTabListItemProps) => { + const [ref, setRef] = useState(null); + + usePopover(ref, tooltip, undefined, undefined, !!tooltip); + + return ( + + + {icon && } + {label && {label}} + + + ); +}; + +export default ReqoreTabsListItem; diff --git a/src/components/Tabs/list.tsx b/src/components/Tabs/list.tsx new file mode 100644 index 00000000..d495bd4d --- /dev/null +++ b/src/components/Tabs/list.tsx @@ -0,0 +1,271 @@ +import { isArray } from 'lodash'; +import React from 'react'; +import { useMeasure } from 'react-use'; +import styled, { css } from 'styled-components'; +import { IReqoreTabsListItem } from '.'; +import { ReqorePopover } from '../..'; +import { IReqoreTheme } from '../../constants/theme'; +import { changeLightness, getReadableColor } from '../../helpers/colors'; +import ReqoreMenu from '../Menu'; +import ReqoreMenuItem from '../Menu/item'; +import { StyledPopover } from '../Popover'; +import ReqoreTabsListItem, { + IReqoreTabListItemProps, + StyledTabListItem, +} from './item'; + +export interface IReqoreTabsListProps + extends React.HTMLAttributes { + tabs: IReqoreTabsListItem[]; + activeTab?: string; + onTabChange?: (tabId: string) => any; + fill?: boolean; + vertical?: boolean; + + // Internal prop, ignore! + _testWidth?: number; +} + +export interface IReqoreTabsListStyle { + theme: IReqoreTheme; + fill?: boolean; + vertical?: boolean; +} + +export const StyledReqoreTabsList = styled.div` + ${({ theme, fill, vertical }) => css` + height: ${vertical ? '100%' : '40px'}; + width: ${vertical ? '140px' : '100%'}; + flex-flow: ${vertical ? 'column' : 'row'}; + display: flex; + align-items: center; + border-${vertical ? 'right' : 'bottom'}: 1px solid ${changeLightness( + theme.main, + 0.05 + )}; + + ${ + fill && + css` + justify-content: space-around; + ` + } + + > * { + &:first-child { + border-top-left-radius: 10px; + } + + &:last-child { + border-${vertical ? 'bottom-left' : 'top-right'}-radius: 10px; + } + } + + ${StyledPopover} { + min-height: 40px; + > ${StyledTabListItem} { + height: 100%; + } + } + + > *, + ${StyledPopover} > * { + color: ${getReadableColor(theme.main, undefined, undefined, true)}; + ${vertical ? 'width' : 'height'}: 100%; + ${ + vertical + ? css` + height: ${fill ? '100%' : 'auto'}; + ` + : css` + white-space: nowrap; + ` + }; + + justify-content: ${vertical ? 'flex-start' : 'space-evenly'}; + + + ${ + fill && + !vertical && + css` + width: 100%; + justify-content: center; + ` + } + } + `} +`; + +const isTabHidden = (items: IReqoreTabsListItem[], activeTab: string) => + items.find((item) => item.id === activeTab); + +const getMoreLabel = (items: IReqoreTabsListItem[], activeTab: string) => { + if (isTabHidden(items, activeTab)) { + return isTabHidden(items, activeTab).label; + } + + return 'More'; +}; + +const getLabel = ( + item: IReqoreTabsListItem | IReqoreTabsListItem[], + activeTab: string +) => { + if (!isArray(item)) { + return item.label?.length || 0; + } + + return getMoreLabel(item, activeTab).length; +}; + +export const getTabsLength = ( + items: (IReqoreTabsListItem | IReqoreTabsListItem[])[], + type: 'width' | 'height' = 'width', + activeTab: string +): number => + items.reduce((len, item) => { + if (type === 'height') { + const rows = getLabel(item, activeTab) / 4 || 1; + + return len + rows * 15 + 10; + } + + return len + 27 + getLabel(item, activeTab) * 10 + 15; + }, 0); + +const getTransformedItems = ( + items: (IReqoreTabsListItem | IReqoreTabsListItem[])[], + size: number, + type: 'width' | 'height' = 'width', + activeTab: string +): (IReqoreTabsListItem | IReqoreTabsListItem[])[] => { + if (!size) { + return items; + } + let newItems = [...items]; + + while ( + getTabsLength(newItems, type, activeTab) > size && + newItems.length > 1 + ) { + if (isArray(newItems[newItems.length - 1])) { + newItems[newItems.length - 1].unshift( + newItems[newItems.length - 2] as IReqoreTabsListItem + ); + newItems[newItems.length - 2] = undefined; + } else { + const lastItem = newItems[newItems.length - 1]; + newItems[newItems.length - 1] = [lastItem]; + } + + newItems = newItems.filter((i) => i); + } + + return newItems; +}; + +const ReqoreTabsList = ({ + tabs, + onTabChange, + activeTab, + _testWidth, + fill, + vertical, + ...rest +}: IReqoreTabsListProps) => { + const [ref, { width, height }] = useMeasure(); + + const transformedItems = getTransformedItems( + tabs, + vertical ? height : _testWidth || width, + vertical ? 'height' : 'width', + activeTab + ); + + return ( + + {transformedItems.map( + (item: IReqoreTabsListItem | IReqoreTabsListItem[], index: number) => + isArray(item) ? ( + + + {item.map( + ({ icon, label, as, tooltip, props, disabled, id }) => ( + ) => { + if (!disabled) { + onTabChange(id); + + if (props?.onClick) { + props.onClick(event); + } + } + }, + }} + placement='right' + isReqoreComponent + content={tooltip} + key={index + label} + > + {label} + + ) + )} + + } + /> + + ) : ( + + ) => { + if (!item.disabled) { + onTabChange(item.id); + + if (item.props?.onClick) { + item.props.onClick(event); + } + } + }} + /> + + ) + )} + + ); +}; + +export default ReqoreTabsList; diff --git a/src/hooks/usePopover.tsx b/src/hooks/usePopover.tsx index 5c2a3cb8..87bbbd0c 100644 --- a/src/hooks/usePopover.tsx +++ b/src/hooks/usePopover.tsx @@ -50,6 +50,8 @@ const usePopover = ( } }; }); + + return current; }; export default usePopover; diff --git a/src/index.tsx b/src/index.tsx index 192b523d..c54baab5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,6 +15,10 @@ export { default as ReqoreNotificationsWrapper } from './components/Notification export { default as ReqoreNotification } from './components/Notifications/notification'; export { default as ReqorePopover } from './components/Popover'; export { default as ReqoreSidebar } from './components/Sidebar'; +export { default as ReqoreTabs } from './components/Tabs'; +export { default as ReqoreTabsContent } from './components/Tabs/content'; +export { default as ReqoreTabsListItem } from './components/Tabs/item'; +export { default as ReqoreTabsList } from './components/Tabs/list'; export { default as ReqoreNotifications } from './containers/NotificationsProvider'; export { default as ReqoreUIProvider } from './containers/UIProvider'; export { default as ReqoreNotificationsContext } from './context/NotificationContext'; diff --git a/src/stories/Breadcrumbs/Breadcrumbs.stories.tsx b/src/stories/Breadcrumbs/Breadcrumbs.stories.tsx index f8f99ac2..dcc25d6d 100644 --- a/src/stories/Breadcrumbs/Breadcrumbs.stories.tsx +++ b/src/stories/Breadcrumbs/Breadcrumbs.stories.tsx @@ -129,3 +129,173 @@ CustomColors.args = { }, }, }; + +export const WithTabs = Template.bind({}); +WithTabs.args = { + theme: { + main: '#ffffff', + }, + breadcrumbs: { + items: [ + { + label: 'Page 1', + icon: 'home', + tooltip: 'Hooooooome!', + }, + { + label: 'Page 2', + icon: 'cog', + as: 'a', + props: { + href: 'https://google.com', + }, + }, + { + label: 'Page 3', + icon: 'notifications', + tooltip: 'Click to go to page 3!', + }, + { + label: 'Page 4', + icon: 'notifications', + }, + { + label: 'Page 5', + icon: 'notifications', + }, + { + withTabs: { + tabs: [ + { + label: 'Tab 1', + id: 'tab1', + icon: 'home', + tooltip: 'Hooooooome!', + }, + { + label: 'Tab 2', + id: 'tab2', + icon: 'cog', + as: 'a', + props: { + href: 'https://google.com', + }, + }, + { + label: 'Tab 3', + id: 'tab3', + icon: 'notifications', + tooltip: 'Click to go to page 3!', + }, + { + label: 'Tab 4', + id: 'tab4', + icon: 'notifications', + active: true, + }, + { + label: 'Tab 5', + id: 'tab5', + icon: 'notifications', + disabled: true, + }, + ], + activeTab: 'tab1', + onTabChange: (tabId) => { + alert(`Tab ${tabId} clicked`); + }, + }, + }, + ], + rightElement: ( + + Right Element + + ), + }, +}; + +export const WithTabsDark = Template.bind({}); +WithTabsDark.args = { + theme: { + main: '#222222', + }, + breadcrumbs: { + items: [ + { + label: 'Page 1', + icon: 'home', + tooltip: 'Hooooooome!', + }, + { + label: 'Page 2', + icon: 'cog', + as: 'a', + props: { + href: 'https://google.com', + }, + }, + { + label: 'Page 3', + icon: 'notifications', + tooltip: 'Click to go to page 3!', + }, + { + label: 'Page 4', + icon: 'notifications', + }, + { + label: 'Page 5', + icon: 'notifications', + }, + { + withTabs: { + tabs: [ + { + label: 'Tab 1', + id: 'tab1', + icon: 'home', + tooltip: 'Hooooooome!', + }, + { + label: 'Tab 2', + id: 'tab2', + icon: 'cog', + as: 'a', + props: { + href: 'https://google.com', + }, + }, + { + label: 'Tab 3', + id: 'tab3', + icon: 'notifications', + tooltip: 'Click to go to page 3!', + }, + { + label: 'Tab 4', + id: 'tab4', + icon: 'notifications', + active: true, + }, + { + label: 'Tab 5', + id: 'tab5', + icon: 'notifications', + disabled: true, + }, + ], + activeTab: 'tab1', + onTabChange: (tabId) => { + alert(`Tab ${tabId} clicked`); + }, + }, + }, + ], + rightElement: ( + + Right Element + + ), + }, +}; diff --git a/src/stories/Tabs/Tabs.stories.tsx b/src/stories/Tabs/Tabs.stories.tsx new file mode 100644 index 00000000..84992499 --- /dev/null +++ b/src/stories/Tabs/Tabs.stories.tsx @@ -0,0 +1,242 @@ +import { Meta, Story } from '@storybook/react/types-6-0'; +import React from 'react'; +import { ReqoreFooter } from '../../components/Navbar'; +import { IReqoreTabsListItem, IReqoreTabsProps } from '../../components/Tabs'; +import { IReqoreTheme } from '../../constants/theme'; +import { + ReqoreContent, + ReqoreHeader, + ReqoreLayoutContent, + ReqoreTabs, + ReqoreTabsContent, + ReqoreUIProvider, +} from '../../index'; + +const tabs = { + tabs: [ + { + label: 'Tab 1', + id: 'tab1', + icon: 'home', + tooltip: 'Hooooooome!', + }, + { + label: 'Tab 2', + id: 'tab2', + icon: 'cog', + as: 'a', + props: { + href: 'https://google.com', + }, + }, + { + label: 'Really long tab name with tooltip', + id: 'tab3', + icon: 'notifications', + tooltip: 'Click to go to page 3!', + }, + { + id: 'tab4', + icon: 'person', + }, + { + label: 'Tab 5', + id: 'tab5', + icon: 'notifications', + disabled: true, + }, + { + label: 'Lorem Ipsum', + id: 'tab6', + icon: 'notifications', + }, + { + label: 'Hey I am another long tab', + id: 'tab7', + icon: 'notifications', + }, + { + label: 'Tab 8', + id: 'tab8', + icon: 'notifications', + }, + { + label: 'Tab 9', + id: 'tab9', + icon: 'notifications', + }, + ] as IReqoreTabsListItem[], + children:

Test

, +} as IReqoreTabsProps; + +export default { + title: 'ReQore/Tabs', + args: { + theme: { + main: '#ffffff', + }, + tabs, + }, +} as Meta; + +const Template: Story<{ + theme: IReqoreTheme; + tabs: IReqoreTabsProps; +}> = ({ theme, tabs }: { theme: IReqoreTheme; tabs: IReqoreTabsProps }) => { + return ( + + + + + + +

Tab 1

+
+ +

Tab 2

+
+ +

Tab 3

+
+ +

Tab 4

+
+ +

Tab 5

+
+ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur + vitae nisi ligula. Proin laoreet vitae risus sed pretium. Sed eu + elit non tortor congue molestie vel ut elit. Duis id turpis + tincidunt libero accumsan cursus. Quisque urna nibh, tempor ac + lorem sed, dapibus ornare arcu. Vestibulum at nunc tempus, + ultricies justo eget, luctus turpis. Curabitur et sapien in est + rhoncus suscipit vitae id purus. Cras malesuada, metus vitae + vulputate ornare, ligula ipsum fringilla ligula, a cursus tortor + ligula semper sem. Praesent fringilla dui volutpat elementum + ultrices. Nam rutrum, augue finibus tempus auctor, eros metus + iaculis neque, at vehicula ante quam et metus. Etiam quis commodo + nunc. Aenean rhoncus, mi ac tincidunt efficitur, nisl nunc tempor + ipsum, accumsan eleifend felis nisl vitae diam. Nunc vitae dapibus + nisi. Donec cursus vitae justo ut interdum. Suspendisse mattis, + erat vel molestie faucibus, tortor neque mollis erat, vulputate + aliquet tortor nulla eu diam. Ut eget fringilla sapien. Vivamus ut + leo in tellus ultrices lacinia non vel lorem. Sed imperdiet sapien + ut odio sagittis gravida nec vel lacus. Morbi euismod libero sed + tristique condimentum. Nullam id erat justo. Ut in justo turpis. + Nam eleifend tincidunt lacinia. Duis non lacinia purus. Mauris + accumsan nunc nulla, at consectetur odio pharetra eu. Phasellus + non massa maximus, placerat velit eget, porta ante. Sed porttitor + arcu orci, vitae vestibulum odio blandit et. Fusce fringilla ipsum + eu ante venenatis dapibus. Morbi ornare ultricies risus ut + ullamcorper. Curabitur feugiat enim libero, non commodo nibh + facilisis nec. Integer suscipit quis ligula a porta. Mauris eget + odio tincidunt, fermentum tellus non, tincidunt tellus. Aliquam + erat volutpat. Ut metus libero, facilisis quis est nec, auctor + finibus justo. Quisque scelerisque placerat mi ac ullamcorper. + Aenean vestibulum lorem id elit blandit, id viverra tortor + fringilla. Vivamus consectetur sodales mauris, in semper tellus + dapibus vitae. Donec eget sapien in eros accumsan vestibulum. + Praesent nec purus libero. Aliquam maximus sem ac quam gravida, eu + fermentum quam varius. In quam augue, rutrum maximus posuere non, + molestie ut arcu. Sed pretium rhoncus ullamcorper. Praesent quis + purus ac dolor tincidunt porttitor eget vitae felis. Sed posuere + tellus eu ex dignissim porta. Nullam in molestie nisl. Class + aptent taciti sociosqu ad litora torquent per conubia nostra, per + inceptos himenaeos. Proin et varius justo, in faucibus diam. + Aenean arcu ex, gravida vitae nunc nec, tincidunt venenatis + sapien. Duis congue pulvinar ipsum. Integer maximus felis mattis + ipsum vestibulum lacinia. Maecenas tortor risus, malesuada a est + at, lobortis tempus tellus. Quisque commodo velit id nisi viverra, + non dignissim turpis tristique. Suspendisse felis est, fringilla + non pellentesque sit amet, varius quis sem. In non velit massa. + Aenean at porttitor est. Sed viverra eleifend cursus. Ut nec justo + porttitor, sollicitudin nisl ac, malesuada leo. Morbi consequat + ornare suscipit. Maecenas eleifend quam sagittis tortor volutpat + porttitor. Vivamus dictum ex id finibus tristique. Nulla rutrum, + ante sagittis molestie aliquet, lectus quam condimentum nisl, a + convallis mauris massa sed risus. Curabitur ac suscipit dolor, nec + dapibus justo. Pellentesque nec sollicitudin sapien. Ut metus + neque, volutpat eu est eu, iaculis suscipit augue. Fusce blandit + magna tincidunt turpis semper venenatis. Nullam sit amet accumsan + massa. Donec interdum ut dui nec varius. Pellentesque ut tellus + sit amet tellus mollis consectetur. Aenean aliquam leo elit. + Nullam convallis felis et rhoncus fermentum. Nunc rutrum nulla sed + nisl efficitur laoreet. Suspendisse potenti. Sed vitae consequat + tellus. Vestibulum in eros at turpis pretium rutrum. Phasellus + commodo commodo libero, eu volutpat nunc tempus nec. Donec tempus + dignissim lectus, quis molestie quam dignissim a. Cras eget mauris + id libero suscipit consequat. Nulla facilisi. Donec arcu arcu, + venenatis quis mattis quis, pretium nec justo. Praesent aliquet + tortor in nulla pellentesque, condimentum congue sapien laoreet. + Nullam semper consectetur feugiat. Nunc a orci non leo malesuada + aliquam eu eget tellus. Suspendisse potenti. Nam ornare ornare + ullamcorper. Sed semper porttitor sem, eu vehicula purus egestas + sit amet. Cras sit amet aliquet augue. Cras velit leo, + pellentesque quis eros eu, feugiat gravida quam. Mauris in turpis + nunc. Suspendisse varius urna quam, sit amet tempus sem tristique + ut. Quisque at velit nec nibh varius tempor venenatis vel felis. + Morbi velit sapien, facilisis ut odio quis, laoreet euismod magna. + Quisque efficitur euismod diam, in ullamcorper sapien tristique + vitae. Nullam ac volutpat quam. Morbi vulputate arcu nisl, et + tempus dui tincidunt vel. Morbi finibus pretium pretium. Aliquam + fermentum turpis sit amet est faucibus elementum ac quis sem. + Maecenas scelerisque commodo magna eget blandit. Praesent vitae + bibendum mauris, eu blandit dui. + + +

Tab 7

+
+ +

Tab 8

+
+ +

Tab 9

+
+
+
+ +
+
+ ); +}; + +export const Default = Template.bind({}); +export const Fill = Template.bind({}); +Fill.args = { + tabs: { + ...tabs, + fill: true, + }, +}; + +export const Vertical = Template.bind({}); +Vertical.args = { + tabs: { + ...tabs, + vertical: true, + }, +}; + +export const VerticalFill = Template.bind({}); +VerticalFill.args = { + tabs: { + ...tabs, + vertical: true, + fill: true, + }, +}; + +export const Dark = Template.bind({}); +Dark.args = { + theme: { + main: '#222222', + }, +}; + +export const CustomMainColor = Template.bind({}); +CustomMainColor.args = { + theme: { + main: '#194d5d', + }, +}; diff --git a/src/types/global.ts b/src/types/global.ts index dac51ca3..020abdbd 100644 --- a/src/types/global.ts +++ b/src/types/global.ts @@ -1,6 +1,4 @@ -import { Placement } from '@popperjs/core'; - export interface IReqoreComponent { - tooltip?: string; - tooltipPlacement?: Placement; + _insidePopover?: boolean; + _popoverId?: string; } From bb903d4b71c0ad66452c8229048687249ed12f34 Mon Sep 17 00:00:00 2001 From: Filip Witosz Date: Tue, 16 Feb 2021 13:25:28 +0100 Subject: [PATCH 2/3] TS Fixes. --- src/components/Menu/item.tsx | 4 +++- src/components/Tabs/list.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/Menu/item.tsx b/src/components/Menu/item.tsx index 3af67743..d580d51f 100644 --- a/src/components/Menu/item.tsx +++ b/src/components/Menu/item.tsx @@ -7,15 +7,17 @@ import PopoverContext from '../../context/PopoverContext'; import { changeLightness, getReadableColor } from '../../helpers/colors'; import { IReqoreComponent } from '../../types/global'; +// @ts-ignore export interface IReqoreMenuItemProps extends IReqoreComponent, - React.HTMLAttributes { + React.HTMLAttributes { children?: any; icon?: IconName; rightIcon?: IconName; as?: JSX.Element | React.ElementType | never; selected?: boolean; disabled?: boolean; + onClick?: (itemId: string, event: React.MouseEvent) => void; } const StyledElementContent = styled.div<{ theme: IReqoreTheme }>` diff --git a/src/components/Tabs/list.tsx b/src/components/Tabs/list.tsx index d495bd4d..0bc52f3a 100644 --- a/src/components/Tabs/list.tsx +++ b/src/components/Tabs/list.tsx @@ -150,13 +150,15 @@ const getTransformedItems = ( newItems.length > 1 ) { if (isArray(newItems[newItems.length - 1])) { - newItems[newItems.length - 1].unshift( + (newItems[newItems.length - 1] as IReqoreTabsListItem[]).unshift( newItems[newItems.length - 2] as IReqoreTabsListItem ); newItems[newItems.length - 2] = undefined; } else { const lastItem = newItems[newItems.length - 1]; - newItems[newItems.length - 1] = [lastItem]; + (newItems[newItems.length - 1] as IReqoreTabsListItem[]) = [ + lastItem, + ] as IReqoreTabsListItem[]; } newItems = newItems.filter((i) => i); From f887a1a54607797f9f84e671a41af4103706739c Mon Sep 17 00:00:00 2001 From: Filip Witosz Date: Tue, 16 Feb 2021 17:03:43 +0100 Subject: [PATCH 3/3] Fixed typescript errors and tabs sorting. --- __tests__/tabs.test.tsx | 2 +- src/components/Breadcrumbs/index.tsx | 13 +++++++++++-- src/stories/Breadcrumbs/Breadcrumbs.stories.tsx | 7 +++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/__tests__/tabs.test.tsx b/__tests__/tabs.test.tsx index d5db1195..8a5c179b 100644 --- a/__tests__/tabs.test.tsx +++ b/__tests__/tabs.test.tsx @@ -65,7 +65,7 @@ test('Renders shortened properly', () => { ); }); - expect(document.querySelectorAll('.reqore-tabs-list-item').length).toBe(2); + expect(document.querySelectorAll('.reqore-tabs-list-item').length).toBe(3); expect( document.querySelectorAll('.reqore-tabs-list .reqore-popover-wrapper') .length diff --git a/src/components/Breadcrumbs/index.tsx b/src/components/Breadcrumbs/index.tsx index 6e24c4eb..ef9af322 100644 --- a/src/components/Breadcrumbs/index.tsx +++ b/src/components/Breadcrumbs/index.tsx @@ -92,7 +92,10 @@ const getBreadcrumbsLength = ( } if (item.withTabs) { - return len + getTabsLength(item.withTabs.tabs); + return ( + len + + getTabsLength(item.withTabs.tabs, 'width', item.withTabs.activeTab) + ); } return len + 27 + item.label.length * 10 + 35; @@ -105,9 +108,11 @@ const getTransformedItems = ( if (!width) { return items; } + + let stop = false; let newItems = [...items]; - while (getBreadcrumbsLength(newItems) > width) { + while (getBreadcrumbsLength(newItems) > width && !stop) { if (isArray(newItems[1])) { newItems[1].push(newItems[2] as IReqoreBreadcrumbItem); newItems[2] = undefined; @@ -117,6 +122,10 @@ const getTransformedItems = ( } newItems = newItems.filter((i) => i); + + if ((newItems[2] as IReqoreBreadcrumbItem).withTabs) { + stop = true; + } } return newItems; diff --git a/src/stories/Breadcrumbs/Breadcrumbs.stories.tsx b/src/stories/Breadcrumbs/Breadcrumbs.stories.tsx index dcc25d6d..d4cd258b 100644 --- a/src/stories/Breadcrumbs/Breadcrumbs.stories.tsx +++ b/src/stories/Breadcrumbs/Breadcrumbs.stories.tsx @@ -276,16 +276,15 @@ WithTabsDark.args = { label: 'Tab 4', id: 'tab4', icon: 'notifications', - active: true, + disabled: true, }, { - label: 'Tab 5', + label: 'Really long tab name', id: 'tab5', icon: 'notifications', - disabled: true, }, ], - activeTab: 'tab1', + activeTab: 'tab5', onTabChange: (tabId) => { alert(`Tab ${tabId} clicked`); },