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:
committed by
GitHub
parent
fadffc6589
commit
2c251e94e0
@@ -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 = {
|
||||
|
||||
+4
-4
@@ -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';
|
||||
Reference in New Issue
Block a user