From f1dbf5a9fafaca408799ac5284434db0a403fc92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20DOREAU?= Date: Sat, 6 Jun 2020 17:56:39 +0200 Subject: [PATCH] refactor(core): update tabs --- .../core/src/components/Tabs/Tab.test.tsx | 4 +- packages/core/src/components/Tabs/Tab.tsx | 27 +++-- packages/core/src/components/Tabs/TabBar.tsx | 6 +- packages/core/src/components/Tabs/TabIcon.tsx | 34 ++---- .../core/src/components/Tabs/TabPanel.tsx | 9 +- .../core/src/components/Tabs/Tabs.stories.tsx | 26 ++++- packages/core/src/components/Tabs/Tabs.tsx | 101 +++++++++++------- packages/core/src/components/Tabs/index.ts | 2 +- packages/core/src/components/Tabs/utils.ts | 25 +---- packages/theme/src/themes.ts | 2 - packages/theme/src/types.ts | 1 - 11 files changed, 121 insertions(+), 116 deletions(-) diff --git a/packages/core/src/components/Tabs/Tab.test.tsx b/packages/core/src/components/Tabs/Tab.test.tsx index a841af776a..5b155ef536 100644 --- a/packages/core/src/components/Tabs/Tab.test.tsx +++ b/packages/core/src/components/Tabs/Tab.test.tsx @@ -17,11 +17,11 @@ import React from 'react'; import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; -import Tab from './Tab'; +import { StyledTab } from './Tab'; describe('', () => { it('renders without exploding', () => { - const rendered = render(wrapInTestApp()); + const rendered = render(wrapInTestApp()); expect(rendered.getByText('test')).toBeInTheDocument(); }); }); diff --git a/packages/core/src/components/Tabs/Tab.tsx b/packages/core/src/components/Tabs/Tab.tsx index 3726642d94..93243b2900 100644 --- a/packages/core/src/components/Tabs/Tab.tsx +++ b/packages/core/src/components/Tabs/Tab.tsx @@ -15,18 +15,15 @@ */ import React from 'react'; -import { Tab, withStyles, Theme } from '@material-ui/core'; +import { Tab, makeStyles } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; -const withStylesProps = (styles: any) => (Component: any) => (props: any) => { - const Comp = withStyles((theme: Theme) => styles(props, theme))(Component); - return ; -}; - interface StyledTabProps { - label: string; + label?: string; + icon?: any; // TODO: define type for material-ui icons isFirstNav?: boolean; isFirstIndex?: boolean; + value?: any; } const tabMarginLeft = (isFirstNav: boolean, isFirstIndex: boolean) => { @@ -39,14 +36,15 @@ const tabMarginLeft = (isFirstNav: boolean, isFirstIndex: boolean) => { return '40px'; }; -const tabStyles = (props: any, theme: BackstageTheme) => ({ +const useStyles = makeStyles(theme => ({ root: { textTransform: 'none', height: '64px', fontWeight: theme.typography.fontWeightBold, fontSize: theme.typography.pxToRem(13), color: theme.palette.textSubtle, - marginLeft: tabMarginLeft(props.isFirstNav, props.isFirstIndex), + marginLeft: props => + tabMarginLeft(props.isFirstNav as boolean, props.isFirstIndex as boolean), width: '130px', minWidth: '130px', '&:hover': { @@ -55,11 +53,10 @@ const tabStyles = (props: any, theme: BackstageTheme) => ({ color: theme.palette.textSubtle, }, }, -}); +})); -const StyledTab = withStylesProps(tabStyles)((props: StyledTabProps) => { +export const StyledTab = (props: StyledTabProps) => { + const classes = useStyles(props); const { isFirstNav, isFirstIndex, ...rest } = props; - return ; -}); - -export default StyledTab; + return ; +}; diff --git a/packages/core/src/components/Tabs/TabBar.tsx b/packages/core/src/components/Tabs/TabBar.tsx index 510111dc37..379d60668b 100644 --- a/packages/core/src/components/Tabs/TabBar.tsx +++ b/packages/core/src/components/Tabs/TabBar.tsx @@ -19,7 +19,7 @@ import { Tabs, makeStyles } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; interface StyledTabsProps { - value: number; + value: number | boolean; onChange: (event: React.ChangeEvent<{}>, newValue: number) => void; } @@ -40,7 +40,7 @@ const useStyles = makeStyles(theme => ({ }, })); -const StyledTabs: FC = props => { +export const StyledTabs: FC = props => { const classes = useStyles(props); return ( = props => { /> ); }; - -export default StyledTabs; diff --git a/packages/core/src/components/Tabs/TabIcon.tsx b/packages/core/src/components/Tabs/TabIcon.tsx index fd58735427..ffb2e12cbd 100644 --- a/packages/core/src/components/Tabs/TabIcon.tsx +++ b/packages/core/src/components/Tabs/TabIcon.tsx @@ -15,24 +15,17 @@ */ import React from 'react'; -import { IconButton, withStyles, Theme } from '@material-ui/core'; - -export const withStylesProps = (styles: any) => (Component: any) => ( - props: any, -) => { - const Comp = withStyles((theme: Theme) => styles(props, theme))(Component); - return ; -}; +import { IconButton, makeStyles } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; interface StyledIconProps { ariaLabel: string; children: any; - classes: any; isNext?: boolean; onClick: any; } -const iconStyles = (props: StyledIconProps) => ({ +const useStyles = makeStyles(() => ({ root: { color: '#6E6E6E', overflow: 'visible', @@ -40,25 +33,22 @@ const iconStyles = (props: StyledIconProps) => ({ textAlign: 'center', borderRadius: '50%', backgroundColor: '#E6E6E6', - marginLeft: props.isNext ? 'auto' : '0', - marginRight: props.isNext ? '0' : '10px', + marginLeft: props => (props.isNext ? 'auto' : '0'), + marginRight: props => (props.isNext ? '0' : '10px'), '&:hover': { backgroundColor: '#E6E6E6', opacity: '1', }, }, -}); +})); -const StyledIcon = withStylesProps(iconStyles)((props: StyledIconProps) => { - const { - classes: { root }, - ariaLabel, - onClick, - } = props; +export const StyledIcon = (props: StyledIconProps) => { + const classes = useStyles(props); + const { ariaLabel, onClick } = props; return ( { {props.children} ); -}); - -export default StyledIcon; +}; diff --git a/packages/core/src/components/Tabs/TabPanel.tsx b/packages/core/src/components/Tabs/TabPanel.tsx index 32536dc86d..ba8ca4bdee 100644 --- a/packages/core/src/components/Tabs/TabPanel.tsx +++ b/packages/core/src/components/Tabs/TabPanel.tsx @@ -19,18 +19,17 @@ import Box from '@material-ui/core/Box'; export interface TabPanelProps { children: any; - value: any; - index: number; + value?: any; + index?: number; } -const TabPanel: FC = props => { +export const TabPanel: FC = props => { const { children, value, index, ...other } = props; return ( ); }; - -export default TabPanel; diff --git a/packages/core/src/components/Tabs/Tabs.stories.tsx b/packages/core/src/components/Tabs/Tabs.stories.tsx index 5258e9a51a..930a825392 100644 --- a/packages/core/src/components/Tabs/Tabs.stories.tsx +++ b/packages/core/src/components/Tabs/Tabs.stories.tsx @@ -15,7 +15,8 @@ */ import React from 'react'; -import Tabs from './Tabs'; +import { Tabs } from './Tabs'; +import AccessAlarmIcon from '@material-ui/icons/AccessAlarm'; export default { title: 'Tabs', @@ -45,3 +46,26 @@ export const Expandable = () => ( /> ); + +export const Icons = () => ( +
+ ({ + icon: , + content:
Content {index}
, + }))} + /> +
+); + +export const IconsAndLabels = () => ( +
+ ({ + icon: , + label: `ANOTHER TAB`, + content:
Content {index}
, + }))} + /> +
+); diff --git a/packages/core/src/components/Tabs/Tabs.tsx b/packages/core/src/components/Tabs/Tabs.tsx index cb1f5d6138..e56cb93bfb 100644 --- a/packages/core/src/components/Tabs/Tabs.tsx +++ b/packages/core/src/components/Tabs/Tabs.tsx @@ -14,26 +14,34 @@ * limitations under the License. */ -import React, { FC, useRef, useEffect, MutableRefObject } from 'react'; +import React, { + FC, + useRef, + useEffect, + MutableRefObject, + useState, +} from 'react'; import { BackstageTheme } from '@backstage/theme'; import { AppBar } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; import NavigateNextIcon from '@material-ui/icons/NavigateNext'; -import { chunkArray, useWindowWidth } from './utils'; +import { chunkArray } from './utils'; +import { useWindowSize } from 'react-use'; /* Import Components */ -import TabPanel from './TabPanel'; -import TabIcon from './TabIcon'; -import Tab from './Tab'; -import TabBar from './TabBar'; +import { TabPanel } from './TabPanel'; +import { StyledIcon } from './TabIcon'; +import { StyledTab } from './Tab'; +import { StyledTabs } from './TabBar'; /* Props Types */ -interface TabProps { - label: string; +export interface TabProps { content: any; + label?: string; + icon?: any; // TODO: define type for material-ui icons } export interface TabsProps { @@ -46,36 +54,35 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({ width: '100%', }, styledTabs: { - backgroundColor: theme.palette.tabbar.background, + backgroundColor: theme.palette.background.paper, }, appbar: { boxShadow: 'none', - backgroundColor: theme.palette.tabbar.background, + backgroundColor: theme.palette.background.paper, paddingLeft: '10px', paddingRight: '10px', }, })); -const Tabs: FC = ({ tabs }) => { +export const Tabs: FC = ({ tabs }) => { const classes = useStyles(); - const [value, setValue] = React.useState(0); - const [navIndex, setNavIndex] = React.useState(0); - const [chunkedTabs, setChunkedTabs] = React.useState([[]] as TabProps[][]); + const [value, setValue] = useState([0, 0]); // [selectedChunckedNavIndex, selectedIndex] + const [navIndex, setNavIndex] = useState(0); + const [numberOfChunkedElement, setNumberOfChunkedElement] = useState(0); + const [chunkedTabs, setChunkedTabs] = useState([[]]); const wrapper = useRef() as MutableRefObject; - const size = useWindowWidth(); + const { width } = useWindowSize(); const handleChange = (_: React.ChangeEvent<{}>, newValue: number) => { - setValue(newValue); + setValue([navIndex, newValue]); }; const navigateToPrevChunk = () => { - setValue(navIndex - 1 === 0 ? 0 : 1); setNavIndex(navIndex - 1); }; const navigateToNextChunk = () => { - setValue(1); setNavIndex(navIndex + 1); }; @@ -88,58 +95,70 @@ const Tabs: FC = ({ tabs }) => { const numberOfTabIcons = navIndex === 0 ? 1 : 2; const wrapperWidth = wrapper.current.offsetWidth - padding - numberOfTabIcons * 30; + const flattenIndex = value[0] * numberOfChunkedElement + value[1]; + const newChunkedElementSize = Math.floor(wrapperWidth / 170); - const numberOfChunkedElement = Math.floor(wrapperWidth / 170); - setChunkedTabs( - chunkArray([...tabs], numberOfChunkedElement) as TabProps[][], - ); - }, [size]); + setNumberOfChunkedElement(newChunkedElementSize); + setChunkedTabs(chunkArray([...tabs], newChunkedElementSize)); + setValue([ + Math.floor(flattenIndex / newChunkedElementSize), + flattenIndex % newChunkedElementSize, + ]); + }, [width]); + + const currentIndex = navIndex === value[0] ? value[1] : false; return (
-
- +
+ {navIndex !== 0 && ( - - + )} {chunkedTabs[navIndex].map((tab, index) => ( - ))} {hasNextNavIndex() && ( - - + )} - +
- {chunkedTabs[navIndex].map((tab, index) => ( + {currentIndex !== false ? ( + chunkedTabs[navIndex].map((tab, index) => ( + + {tab.content} + + )) + ) : ( + // Render if the selected tab index is outside the current rendered chunked array - {tab.content} + {chunkedTabs[value[0]][value[1]].content} - ))} + )}
); }; - -export default Tabs; diff --git a/packages/core/src/components/Tabs/index.ts b/packages/core/src/components/Tabs/index.ts index 03995fe2ea..835ab5a8c3 100644 --- a/packages/core/src/components/Tabs/index.ts +++ b/packages/core/src/components/Tabs/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './Tabs'; +export { Tabs as default } from './Tabs'; diff --git a/packages/core/src/components/Tabs/utils.ts b/packages/core/src/components/Tabs/utils.ts index d3fa0dbd3e..3e0ab6f2c3 100644 --- a/packages/core/src/components/Tabs/utils.ts +++ b/packages/core/src/components/Tabs/utils.ts @@ -13,30 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useState, useEffect } from 'react'; +import { TabProps } from './Tabs'; -export const chunkArray = (myArray: any[], chunkSize: number) => { +export const chunkArray = ( + myArray: TabProps[], + chunkSize: number, +): TabProps[][] => { const results = []; while (myArray.length) { results.push(myArray.splice(0, chunkSize)); } return results; }; - -export const useWindowWidth = () => { - const isClient = typeof window === 'object'; - const getWidth = () => (isClient ? window.innerWidth : undefined); - const [windowWidth, setWindowWidth] = useState(getWidth); - - useEffect((): any => { - if (!isClient) { - return false; - } - - const handleResize = () => setWindowWidth(getWidth()); - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - return windowWidth; -}; diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 630e81c146..3071c0289e 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -62,7 +62,6 @@ export const lightTheme = createTheme({ }, tabbar: { indicator: '#9BF0E1', - background: '#FFFFFF', }, }, }); @@ -112,7 +111,6 @@ export const darkTheme = createTheme({ }, tabbar: { indicator: '#9BF0E1', - background: '#424242', }, }, }); diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 8d18caf4d1..fdb9e09869 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -46,7 +46,6 @@ type PaletteAdditions = { sidebar: string; tabbar: { indicator: string; - background: string; }; bursts: { fontColor: string;