feat(core): add Tabs component

This commit is contained in:
Rémi DOREAU
2020-05-30 03:43:40 +02:00
parent 827a6289e6
commit 3293fe96a4
12 changed files with 524 additions and 0 deletions
@@ -0,0 +1,27 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import Tab from './Tab';
describe('<Tab />', () => {
it('renders without exploding', () => {
const rendered = render(wrapInTestApp(<Tab label="test" />));
expect(rendered.getByText('test')).toBeInTheDocument();
});
});
+65
View File
@@ -0,0 +1,65 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Tab, withStyles, Theme } 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 <Comp {...props} />;
};
interface StyledTabProps {
label: string;
isFirstNav?: boolean;
isFirstIndex?: boolean;
}
const tabMarginLeft = (isFirstNav: boolean, isFirstIndex: boolean) => {
if (isFirstIndex) {
if (isFirstNav) {
return '20px';
}
return '0';
}
return '40px';
};
const tabStyles = (props: any, theme: BackstageTheme) => ({
root: {
textTransform: 'none',
height: '64px',
fontWeight: theme.typography.fontWeightBold,
fontSize: theme.typography.pxToRem(13),
color: theme.palette.textSubtle,
marginLeft: tabMarginLeft(props.isFirstNav, props.isFirstIndex),
width: '130px',
minWidth: '130px',
'&:hover': {
outline: 'none',
backgroundColor: 'transparent',
color: theme.palette.textSubtle,
},
},
});
const StyledTab = withStylesProps(tabStyles)((props: StyledTabProps) => {
const { isFirstNav, isFirstIndex, ...rest } = props;
return <Tab disableRipple {...rest} />;
});
export default StyledTab;
@@ -0,0 +1,54 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { Tabs, makeStyles } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
interface StyledTabsProps {
value: number;
onChange: (event: React.ChangeEvent<{}>, newValue: number) => void;
}
const useStyles = makeStyles<BackstageTheme>(theme => ({
indicator: {
display: 'flex',
justifyContent: 'center',
backgroundColor: theme.palette.tabbar.indicator,
height: '4px',
},
flexContainer: {
alignItems: 'center',
},
root: {
'&:last-child': {
marginLeft: 'auto',
},
},
}));
const StyledTabs: FC<StyledTabsProps> = props => {
const classes = useStyles(props);
return (
<Tabs
classes={classes}
{...props}
TabIndicatorProps={{ children: <span /> }}
/>
);
};
export default StyledTabs;
@@ -0,0 +1,72 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 <Comp {...props} />;
};
interface StyledIconProps {
ariaLabel: string;
children: any;
classes: any;
isNext?: boolean;
onClick: any;
}
const iconStyles = (props: StyledIconProps) => ({
root: {
color: '#6E6E6E',
overflow: 'visible',
fontSize: '1.5rem',
textAlign: 'center',
borderRadius: '50%',
backgroundColor: '#E6E6E6',
marginLeft: props.isNext ? 'auto' : '0',
marginRight: props.isNext ? '0' : '10px',
'&:hover': {
backgroundColor: '#E6E6E6',
opacity: '1',
},
},
});
const StyledIcon = withStylesProps(iconStyles)((props: StyledIconProps) => {
const {
classes: { root },
ariaLabel,
onClick,
} = props;
return (
<IconButton
onClick={onClick}
className={root}
size="small"
disableRipple
disableFocusRipple
aria-label={ariaLabel}
>
{props.children}
</IconButton>
);
});
export default StyledIcon;
@@ -0,0 +1,42 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import Box from '@material-ui/core/Box';
export interface TabPanelProps {
children: any;
value: any;
index: number;
}
const TabPanel: FC<TabPanelProps> = props => {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`scrollable-auto-tabpanel-${index}`}
aria-labelledby={`scrollable-auto-tab-${index}`}
{...other}
>
{value === index && <Box p={3}>{children}</Box>}
</div>
);
};
export default TabPanel;
@@ -0,0 +1,47 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import Tabs from './Tabs';
export default {
title: 'Tabs',
component: Tabs,
};
const containerStyle = {};
export const Default = () => (
<div style={containerStyle}>
<Tabs
tabs={[...Array(4)].map((_, index) => ({
label: `ANOTHER TAB`,
content: <div>Content {index}</div>,
}))}
/>
</div>
);
export const Expandable = () => (
<div style={containerStyle}>
<Tabs
tabs={[...Array(31)].map((_, index) => ({
label: `ANOTHER TAB`,
content: <div>Content {index}</div>,
}))}
/>
</div>
);
+145
View File
@@ -0,0 +1,145 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC, useRef, useEffect, MutableRefObject } 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 Components */
import TabPanel from './TabPanel';
import TabIcon from './TabIcon';
import Tab from './Tab';
import TabBar from './TabBar';
/* Props Types */
interface TabProps {
label: string;
content: any;
}
export interface TabsProps {
tabs: TabProps[];
}
const useStyles = makeStyles<BackstageTheme>((theme: BackstageTheme) => ({
root: {
flexGrow: 1,
width: '100%',
},
styledTabs: {
backgroundColor: theme.palette.tabbar.background,
},
appbar: {
boxShadow: 'none',
backgroundColor: theme.palette.tabbar.background,
paddingLeft: '10px',
paddingRight: '10px',
},
}));
const Tabs: FC<TabsProps> = ({ tabs }) => {
const classes = useStyles();
const [value, setValue] = React.useState(0);
const [navIndex, setNavIndex] = React.useState(0);
const [chunkedTabs, setChunkedTabs] = React.useState([[]] as TabProps[][]);
const wrapper = useRef() as MutableRefObject<HTMLDivElement>;
const size = useWindowWidth();
const handleChange = (_: React.ChangeEvent<{}>, newValue: number) => {
setValue(newValue);
};
const navigateToPrevChunk = () => {
setValue(navIndex - 1 === 0 ? 0 : 1);
setNavIndex(navIndex - 1);
};
const navigateToNextChunk = () => {
setValue(1);
setNavIndex(navIndex + 1);
};
const hasNextNavIndex = () => navIndex + 1 < chunkedTabs.length;
useEffect(() => {
// Each time the window is resized we calculate how many tabs wwe can render given the window width
const padding = 20; // The AppBar padding
const numberOfTabIcons = navIndex === 0 ? 1 : 2;
const wrapperWidth =
wrapper.current.offsetWidth - padding - numberOfTabIcons * 30;
const numberOfChunkedElement = Math.floor(wrapperWidth / 170);
setChunkedTabs(
chunkArray([...tabs], numberOfChunkedElement) as TabProps[][],
);
}, [size]);
return (
<div className={classes.root}>
<AppBar ref={wrapper} className={classes.appbar} position="static">
<div className={classes.root2}>
<TabBar value={value} onChange={handleChange}>
{navIndex !== 0 && (
<TabIcon
onClick={navigateToPrevChunk}
ariaLabel="navigate-before"
>
<NavigateBeforeIcon />
</TabIcon>
)}
{chunkedTabs[navIndex].map((tab, index) => (
<Tab
isFirstIndex={index === 0}
isFirstNav={navIndex === 0}
key={index}
label={tab.label}
/>
))}
{hasNextNavIndex() && (
<TabIcon
isNext
onClick={navigateToNextChunk}
ariaLabel="navigate-next"
>
<NavigateNextIcon />
</TabIcon>
)}
</TabBar>
</div>
</AppBar>
{chunkedTabs[navIndex].map((tab, index) => (
<TabPanel
key={index}
// Used to prevent issues with TabIcon inside the TabBar
value={navIndex === 0 ? value : value - 1}
index={index}
>
{tab.content}
</TabPanel>
))}
</div>
);
};
export default Tabs;
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default } from './Tabs';
@@ -0,0 +1,42 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useEffect } from 'react';
export const chunkArray = (myArray: any[], chunkSize: number) => {
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;
};
+1
View File
@@ -39,3 +39,4 @@ export { default as TrendLine } from './components/TrendLine';
export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular';
export * from './components/Status';
export { default as WarningPanel } from './components/WarningPanel';
export { default as Tabs } from './components/Tabs';
+8
View File
@@ -60,6 +60,10 @@ export const lightTheme = createTheme({
icon: '#BDBDBD',
background: '#404040',
},
tabbar: {
indicator: '#9BF0E1',
background: '#FFFFFF',
},
},
});
@@ -106,5 +110,9 @@ export const darkTheme = createTheme({
icon: '#181818',
background: '#BDBDBD',
},
tabbar: {
indicator: '#9BF0E1',
background: '#424242',
},
},
});
+4
View File
@@ -44,6 +44,10 @@ type PaletteAdditions = {
link: string;
gold: string;
sidebar: string;
tabbar: {
indicator: string;
background: string;
};
bursts: {
fontColor: string;
slackChannelText: string;