TabbedCard component (#464)

* Initial typing for infocard component

* Changes as suggested in PR and rewrite of BottomLink to TypeScript

* Removed classes property from card because material-ui did not like classes.header being set

* Prettier

* Early TabbedCard component. WIP

* Refactored TabbedCard to be easier to use

* Fixed some typing issues and tests

* Fixed lint issues and changed lint rule. Do we want to keep it?

* Added controlled mode when value and onChange is set on TabbedCard

* Fixed typo

* Added a test and fixed the design to look like the mockups more

* Removed export of BottomLink from TabbedCard

* Added test for controlled state
This commit is contained in:
Sebastian Qvarfordt
2020-04-15 10:31:40 +02:00
committed by GitHub
parent fadffc6589
commit 2c251e94e0
10 changed files with 353 additions and 7 deletions
+4
View File
@@ -51,6 +51,10 @@ module.exports = {
bundledDependencies: true,
},
],
'@typescript-eslint/no-unused-vars': [
'warn',
{ vars: 'all', args: 'after-used', ignoreRestSiblings: true },
],
},
overrides: [
{
@@ -16,9 +16,8 @@
import React, { FC } from 'react';
import { makeStyles } from '@material-ui/core';
import InfoCard from 'layout/InfoCard';
import { Props as BottomLinkProps } from 'layout/InfoCard/BottomLink';
import { Props as BottomLinkProps } from '../layout/BottomLink';
import CircleProgress from './CircleProgress';
type Props = {
@@ -14,18 +14,18 @@
* limitations under the License.
*/
import React, { FC } from 'react';
import {
Divider,
Link,
ListItem,
ListItemIcon,
Divider,
ListItemText,
makeStyles,
} from '@material-ui/core';
import Box from '@material-ui/core/Box';
import grey from '@material-ui/core/colors/grey';
import ArrowIcon from '@material-ui/icons/ArrowForward';
import React, { FC } from 'react';
import grey from '@material-ui/core/colors/grey';
import Box from '@material-ui/core/Box';
const useStyles = makeStyles(theme => ({
root: {
@@ -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, Props } from './BottomLink';
@@ -25,7 +25,7 @@ import {
makeStyles,
} from '@material-ui/core';
import ErrorBoundary from 'layout/ErrorBoundary/ErrorBoundary';
import BottomLink, { Props as BottomLinkProps } from './BottomLink';
import BottomLink, { Props as BottomLinkProps } from '../BottomLink';
const useStyles = makeStyles(theme => ({
header: {
@@ -0,0 +1,77 @@
/*
* 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, { useState } from 'react';
import { TabbedCard, CardTab } from '.';
export default {
title: 'Tabbed Card',
component: TabbedCard,
};
export const Default = () => {
return (
<TabbedCard title="Default Example Header">
<CardTab label="Option 1">some content 1</CardTab>
<CardTab label="Option 2">some content 2</CardTab>
<CardTab label="Option 3">some content 3</CardTab>
<CardTab label="Option 4">some content 4</CardTab>
</TabbedCard>
);
};
const linkInfo = { title: 'Go to XYZ Location', link: '#' };
export const WithFooterLink = () => {
return (
<TabbedCard title="Footer Link Example Header" deepLink={linkInfo}>
<CardTab label="Option 1">some content 1</CardTab>
<CardTab label="Option 2">some content 2</CardTab>
<CardTab label="Option 3">some content 3</CardTab>
<CardTab label="Option 4">some content 4</CardTab>
</TabbedCard>
);
};
export const WithControlledTabValue = () => {
const [selectedTab, setSelectedTab] = useState('one');
const handleChange = (_ev, newSelectedTab) => setSelectedTab(newSelectedTab);
return (
<>
<span>Selected tab is {selectedTab}</span>
<TabbedCard
value={selectedTab}
onChange={handleChange}
title="Controlled Value Example"
>
<CardTab value="one" label="Option 1">
some content 1
</CardTab>
<CardTab value="two" label="Option 2">
some content 2
</CardTab>
<CardTab value="three" label="Option 3">
some content 3
</CardTab>
<CardTab value="four" label="Option 4">
some content 4
</CardTab>
</TabbedCard>
</>
);
};
@@ -0,0 +1,105 @@
/*
* 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, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { TabbedCard, CardTab } from '.';
const minProps = {
title: 'Some title',
deepLink: {
title: 'A deepLink title',
link: '/mocked',
},
};
describe('<TabbedCard />', () => {
it('renders without exploding', () => {
const rendered = render(
wrapInTestApp(
<TabbedCard title={minProps.title}>
<CardTab label="Test 1">Test Content</CardTab>
<CardTab label="Test 2">Test Content</CardTab>
</TabbedCard>,
),
);
expect(rendered.getByText('Some title')).toBeInTheDocument();
});
it('renders a deepLink when prop is set', () => {
const rendered = render(
wrapInTestApp(
<TabbedCard deepLink={minProps.deepLink}>
<CardTab label="Test 1">Test Content</CardTab>
<CardTab label="Test 2">Test Content</CardTab>
</TabbedCard>,
),
);
expect(rendered.getByText('A deepLink title')).toBeInTheDocument();
});
it('switches tabs when clicking', () => {
const rendered = render(
wrapInTestApp(
<TabbedCard>
<CardTab label="Test 1">Test Content 1</CardTab>
<CardTab label="Test 2">Test Content 2</CardTab>
</TabbedCard>,
),
);
expect(rendered.getByText('Test Content 1')).toBeInTheDocument();
fireEvent.click(rendered.getByText('Test 2'));
expect(rendered.getByText('Test Content 2')).toBeInTheDocument();
});
it('switches tabs when clicking in controlled mode', () => {
let selectedTab = 'one';
const handleTabChange = jest.fn(
(_ev, newSelectedTab) => (selectedTab = newSelectedTab),
);
const rendered = render(
wrapInTestApp(
<TabbedCard value={selectedTab} onChange={handleTabChange}>
<CardTab value="one" label="Test 1">
Test Content 1
</CardTab>
<CardTab value="two" label="Test 2">
Test Content 2
</CardTab>
</TabbedCard>,
),
);
expect(rendered.getByText('Test Content 1')).toBeInTheDocument();
fireEvent.click(rendered.getByText('Test 2'));
expect(handleTabChange.mock.calls.length).toBe(1);
rendered.rerender(
<TabbedCard value={selectedTab} onChange={handleTabChange}>
<CardTab value="one" label="Test 1">
Test Content 1
</CardTab>
<CardTab value="two" label="Test 2">
Test Content 2
</CardTab>
</TabbedCard>,
);
expect(rendered.getByText('Test Content 2')).toBeInTheDocument();
});
});
@@ -0,0 +1,127 @@
/*
* 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, useState, ReactElement, ReactNode } from 'react';
import {
Card,
CardContent,
CardHeader,
Divider,
withStyles,
makeStyles,
Tabs,
Tab,
TabProps,
} from '@material-ui/core';
import BottomLink, { Props as BottomLinkProps } from '../BottomLink';
import ErrorBoundary from '../ErrorBoundary/ErrorBoundary';
const useTabsStyles = makeStyles(theme => ({
root: {
padding: theme.spacing(0, 2, 0, 2.5),
minHeight: theme.spacing(3),
},
indicator: {
backgroundColor: theme.palette.info.main,
height: theme.spacing(0.3),
},
}));
const BoldHeader = withStyles(theme => ({
root: { padding: theme.spacing(2, 2, 2, 2.5), display: 'inline-block' },
title: { fontWeight: 700 },
subheader: { paddingTop: theme.spacing(1) },
}))(CardHeader);
type Props = {
slackChannel?: string;
children?: ReactElement<TabProps>[];
onChange?: (event: React.ChangeEvent<{}>, value: number | string) => void;
title?: string;
value?: number | string;
deepLink?: BottomLinkProps;
};
const TabbedCard: FC<Props> = ({
slackChannel = '#backstage',
children,
title,
deepLink,
value,
onChange,
}) => {
const tabsClasses = useTabsStyles();
const [selectedIndex, selectIndex] = useState(0);
const handleChange = onChange
? onChange
: (_ev, newSelectedIndex: number) => selectIndex(newSelectedIndex);
let selectedTabContent: ReactNode;
if (!value) {
React.Children.map(children, (child, index) => {
if (index === selectedIndex) selectedTabContent = child?.props.children;
});
} else {
React.Children.map(children, child => {
if (child?.props.value === value)
selectedTabContent = child?.props.children;
});
}
return (
<Card>
<ErrorBoundary slackChannel={slackChannel}>
{title && <BoldHeader title={title} />}
<Tabs
classes={tabsClasses}
value={value || selectedIndex}
onChange={handleChange}
>
{children}
</Tabs>
<Divider />
<CardContent>{selectedTabContent}</CardContent>
{deepLink && <BottomLink {...deepLink} />}
</ErrorBoundary>
</Card>
);
};
const useCardTabStyles = makeStyles(theme => ({
root: {
minWidth: theme.spacing(6),
minHeight: theme.spacing(3),
margin: theme.spacing(0, 2, 0, 0),
padding: theme.spacing(0.5, 0, 0.5, 0),
textTransform: 'none',
},
selected: {
fontWeight: 'bold',
},
}));
type CardTabProps = TabProps & {
children: ReactNode;
};
const CardTab: FC<CardTabProps> = ({ children, ...props }) => {
const classes = useCardTabStyles();
return <Tab disableRipple classes={classes} {...props} />;
};
export { TabbedCard, CardTab };
@@ -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 * from './TabbedCard';