feat(core): add Tabs component
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -44,6 +44,10 @@ type PaletteAdditions = {
|
||||
link: string;
|
||||
gold: string;
|
||||
sidebar: string;
|
||||
tabbar: {
|
||||
indicator: string;
|
||||
background: string;
|
||||
};
|
||||
bursts: {
|
||||
fontColor: string;
|
||||
slackChannelText: string;
|
||||
|
||||
Reference in New Issue
Block a user