Merge pull request #4803 from backstage/freben/no-item-card

ItemCard: deprecate and replace with composable pieces
This commit is contained in:
Fredrik Adelöw
2021-03-05 09:35:31 +01:00
committed by GitHub
20 changed files with 593 additions and 344 deletions
@@ -87,8 +87,8 @@ export const Breadcrumbs = ({ children, ...props }: Props) => {
}}
>
<List>
{expandablePages.map(pageLink => (
<ListItem button>
{expandablePages.map((pageLink, index) => (
<ListItem key={index} button>
<StyledBox clone>{pageLink}</StyledBox>
</ListItem>
))}
@@ -13,86 +13,101 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Grid } from '@material-ui/core';
import {
Card,
CardActions,
CardContent,
CardMedia,
makeStyles,
Typography,
} from '@material-ui/core';
import React from 'react';
import { MemoryRouter } from 'react-router';
import { ItemCard } from '.';
import { Button } from '../../components';
import { ItemCardGrid } from './ItemCardGrid';
import { ItemCardHeader } from './ItemCardHeader';
export default {
title: 'Layout/Item Card',
component: ItemCard,
title: 'Layout/Item Cards',
};
const text =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
const useStyles = makeStyles({
grid: {
gridTemplateColumns: 'repeat(auto-fill, 12em)',
},
header: {
color: 'black',
backgroundImage: 'linear-gradient(to bottom right, red, yellow)',
},
});
export const Default = () => (
<Grid container spacing={4}>
<Grid item xs={12} md={3}>
<ItemCard
title="Item Card"
description="This is the description of an Item Card"
label="Button"
subtitle="Pretitle"
onClick={() => {}}
/>
</Grid>
<Grid item xs={12} md={3}>
<ItemCard
title="Item Card"
description="This is the description of an Item Card"
label="Button"
subtitle="Pretitle"
onClick={() => {}}
/>
</Grid>
</Grid>
);
export const Tags = () => (
<Grid container spacing={4}>
<Grid item xs={12} md={3}>
<ItemCard
title="Item Card"
description="This is the description of an Item Card with Tags"
tags={['one tag', 'one tag']}
label="Button"
/>
</Grid>
<Grid item xs={12} md={3}>
<ItemCard
title="Item Card"
description="This is the description of an Item Card with Tags"
tags={['one tag', 'two tag']}
label="Button"
/>
</Grid>
<Grid item xs={12} md={3}>
<ItemCard
title="Item Card"
description="This is the description of an Item Card without Tags"
label="Button"
/>
</Grid>
</Grid>
);
export const Link = () => (
<MemoryRouter>
<Grid container spacing={4}>
<Grid item xs={12} md={3}>
<ItemCard
title="Backstage"
description="This is the description of an Item Card with link"
label="Read More"
href="https://backstage.io"
/>
</Grid>
<Grid item xs={12} md={3}>
<ItemCard
title="Backstage @ GitHub"
description="This is the description of an Item Card with link"
label="Read More"
href="https://github.com/backstage/backstage"
/>
</Grid>
</Grid>
<Typography paragraph>
The most basic setup is to place a bunch of cards into a large grid,
leaving styling to the defaults. Try to resize the window to see how they
rearrange themselves to fit the viewport.
</Typography>
<ItemCardGrid>
{[...Array(10).keys()].map(index => (
<Card key={index}>
<CardMedia>
<ItemCardHeader title={`Card #${index}`} subtitle="Subtitle" />
</CardMedia>
<CardContent>
{text
.split(' ')
.slice(0, 5 + Math.floor(Math.random() * 30))
.join(' ')}
</CardContent>
<CardActions>
<Button color="primary" to="/catalog">
Go There!
</Button>
</CardActions>
</Card>
))}
</ItemCardGrid>
</MemoryRouter>
);
export const Styling = () => {
const classes = useStyles();
return (
<MemoryRouter>
<Typography paragraph>
Both the grid and the header can be styled, using the{' '}
<Typography variant="caption">classes</Typography> property. This lets
you for example tweak the column sizes and the background of the header.
</Typography>
<ItemCardGrid classes={{ root: classes.grid }}>
{[...Array(10).keys()].map(index => (
<Card key={index}>
<CardMedia>
<ItemCardHeader
title={`Card #${index}`}
subtitle="Subtitle"
classes={{ root: classes.header }}
/>
</CardMedia>
<CardContent>
{text
.split(' ')
.slice(0, 5 + Math.floor(Math.random() * 30))
.join(' ')}
</CardContent>
<CardActions>
<Button color="primary" to="/catalog">
Go There!
</Button>
</CardActions>
</Card>
))}
</ItemCardGrid>
</MemoryRouter>
);
};
+59 -60
View File
@@ -13,33 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Card, Chip, makeStyles, Typography } from '@material-ui/core';
import clsx from 'clsx';
import {
Box,
Card,
CardActions,
CardContent,
CardMedia,
Chip,
} from '@material-ui/core';
import React, { ReactNode } from 'react';
import { Link } from '../../components';
const useStyles = makeStyles(theme => ({
header: {
color: theme.palette.common.white,
padding: theme.spacing(2, 2, 6),
backgroundImage: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)',
},
content: {
padding: theme.spacing(2),
},
description: {
height: 175,
overflow: 'hidden',
textOverflow: 'ellipsis',
},
withTags: {
height: 'calc(175px - 32px - 8px)',
},
footer: {
display: 'flex',
flexDirection: 'row-reverse',
},
}));
import { Button } from '../../components';
import { ItemCardHeader } from './ItemCardHeader';
type ItemCardProps = {
description?: string;
@@ -53,6 +37,31 @@ type ItemCardProps = {
href?: string;
};
/**
* This card type has been deprecated. Instead use plain MUI Card and helpers
* where appropriate.
*
* <code>
* <!--
* <Card>
* <CardMedia>
* <ItemCardHeader title="My Card" subtitle="neat!" />
* </CardMedia>
* <CardContent>
* Some text
* </CardContent>
* <CardActions>
* <Button color="primary" to="https://backstage.io">
* Get Started
* </Button>
* </CardActions>
* </Card>
* -->
* </code>
*
* @deprecated Use plain MUI <Card> and composable helpers instead.
* @see https://material-ui.com/components/cards/
*/
export const ItemCard = ({
description,
tags,
@@ -63,43 +72,33 @@ export const ItemCard = ({
onClick,
href,
}: ItemCardProps) => {
const classes = useStyles();
return (
<Card>
<div className={classes.header}>
{(subtitle || type) && (
<Typography variant="subtitle2">{subtitle ?? type}</Typography>
<CardMedia>
<ItemCardHeader title={title} subtitle={subtitle || type} />
</CardMedia>
<CardContent>
{tags?.length ? (
<Box>
{tags.map((tag, i) => (
<Chip size="small" label={tag} key={i} />
))}
</Box>
) : null}
{description}
</CardContent>
<CardActions>
{!href && (
<Button to="#" onClick={onClick} color="primary">
{label}
</Button>
)}
<Typography variant="h6">{title}</Typography>
</div>
<div className={classes.content}>
{tags?.map((tag, i) => (
<Chip label={tag} key={`tag-${i}`} />
))}
<Typography
variant="body2"
paragraph
className={clsx(
classes.description,
tags && tags.length > 0 && classes.withTags,
)}
>
{description}
</Typography>
<div className={classes.footer}>
{!href && (
<Button onClick={onClick} color="primary">
{label}
</Button>
)}
{href && (
<Button component={Link} to={href} color="primary">
{label}
</Button>
)}
</div>
</div>
{href && (
<Button to={href} color="primary">
{label}
</Button>
)}
</CardActions>
</Card>
);
};
@@ -0,0 +1,50 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import { Card } from '@material-ui/core';
import { screen } from '@testing-library/react';
import React from 'react';
import { ItemCardGrid } from './ItemCardGrid';
describe('<ItemCardGrid />', () => {
it('renders default without exploding', async () => {
await renderInTestApp(
<ItemCardGrid>
<Card>Hello!</Card>
</ItemCardGrid>,
);
expect(screen.getByRole('grid')).toBeInTheDocument();
expect(screen.getByText('Hello!')).toBeInTheDocument();
});
it('renders custom styles', async () => {
await renderInTestApp(
<>
<ItemCardGrid>
<Card>Hello!</Card>
</ItemCardGrid>
<ItemCardGrid classes={{ root: 'my-css-class' }}>
<Card>Goodbye!</Card>
</ItemCardGrid>
</>,
);
expect(screen.getAllByRole('grid')[0]).toHaveStyle({
gridTemplateColumns: 'repeat(auto-fill, minmax(22em, 1fr))',
});
expect(screen.getAllByRole('grid')[1]).toHaveClass('my-css-class');
});
});
@@ -0,0 +1,62 @@
/*
* Copyright 2021 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 { createStyles, makeStyles, Theme, WithStyles } from '@material-ui/core';
import React from 'react';
const styles = (theme: Theme) =>
createStyles({
root: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(22em, 1fr))',
gridAutoRows: '1fr',
gridGap: theme.spacing(2),
},
});
const useStyles = makeStyles(styles);
export type ItemCardGridProps = Partial<WithStyles<typeof styles>> & {
/**
* The Card items of the grid.
*/
children?: React.ReactNode;
};
/**
* A default grid to use when arranging "item cards" - cards that let users
* select among several options.
*
* The immediate children are expected to be MUI Card components.
*
* Styles for the grid can be overridden using the `classes` prop, e.g.:
*
* <code>
* <ItemCardGrid title="Hello" classes={{ root: myClassName }} />
* </code>
*
* This can be useful for e.g. overriding gridTemplateColumns to adapt the
* minimum size of the cells to fit the content better.
*/
export const ItemCardGrid = (props: ItemCardGridProps) => {
const { children, ...otherProps } = props;
const classes = useStyles(otherProps);
return (
<div role="grid" className={classes.root} {...otherProps}>
{children}
</div>
);
};
@@ -0,0 +1,60 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import { Card, CardMedia } from '@material-ui/core';
import { screen } from '@testing-library/react';
import React from 'react';
import { ItemCardHeader } from './ItemCardHeader';
describe('<ItemCardHeader />', () => {
it('renders default without exploding', async () => {
await renderInTestApp(
<Card>
<CardMedia>
<ItemCardHeader title="My Title" subtitle="My Subtitle" />
</CardMedia>
</Card>,
);
expect(screen.getByText('My Title')).toBeInTheDocument();
expect(screen.getByText('My Subtitle')).toBeInTheDocument();
});
it('renders custom children', async () => {
await renderInTestApp(
<Card>
<CardMedia>
<ItemCardHeader title="My Title">My Custom Text</ItemCardHeader>
</CardMedia>
</Card>,
);
expect(screen.getByText('My Title')).toBeInTheDocument();
expect(screen.getByText('My Custom Text')).toBeInTheDocument();
});
it('renders custom styles', async () => {
await renderInTestApp(
<Card>
<CardMedia>
<ItemCardHeader classes={{ root: 'my-css-class' }}>
My Custom Text
</ItemCardHeader>
</CardMedia>
</Card>,
);
expect(screen.getByText('My Custom Text')).toHaveClass('my-css-class');
});
});
@@ -0,0 +1,85 @@
/*
* Copyright 2021 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 {
createStyles,
makeStyles,
Theme,
Typography,
WithStyles,
} from '@material-ui/core';
import React from 'react';
const styles = (theme: Theme) =>
createStyles({
root: {
color: theme.palette.common.white,
padding: theme.spacing(2, 2, 3),
backgroundImage: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)',
backgroundPosition: 0,
backgroundSize: 'inherit',
},
});
const useStyles = makeStyles(styles);
export type ItemCardHeaderProps = Partial<WithStyles<typeof styles>> & {
/**
* A large title to show in the header, providing the main heading.
*
* Use this if you want to have the default styling and placement of a title.
*/
title?: React.ReactNode;
/**
* A slightly smaller title to show in the header, providing additional
* details.
*
* Use this if you want to have the default styling and placement of a
* subtitle.
*/
subtitle?: React.ReactNode;
/**
* Custom children to draw in the header.
*
* If the title and/or subtitle were specified, the children are drawn below
* those.
*/
children?: React.ReactNode;
};
/**
* A simple card header, rendering a default look for "item cards" - cards that
* are arranged in a grid for users to select among several options.
*
* This component expects to be placed within a MUI <CardMedia>.
*
* Styles for the header can be overridden using the `classes` prop, e.g.:
*
* <code>
* <ItemCardHeader title="Hello" classes={{ root: myClassName }} />
* </code>
*/
export const ItemCardHeader = (props: ItemCardHeaderProps) => {
const { title, subtitle, children } = props;
const classes = useStyles(props);
return (
<div className={classes.root}>
{subtitle && <Typography variant="subtitle2">{subtitle}</Typography>}
{title && <Typography variant="h6">{title}</Typography>}
{children}
</div>
);
};
@@ -15,3 +15,7 @@
*/
export { ItemCard } from './ItemCard';
export { ItemCardGrid } from './ItemCardGrid';
export type { ItemCardGridProps } from './ItemCardGrid';
export { ItemCardHeader } from './ItemCardHeader';
export type { ItemCardHeaderProps } from './ItemCardHeader';
+19
View File
@@ -234,12 +234,31 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
margin: `0 ${theme.spacing(0.5)}px 0 -${theme.spacing(0.5)}px`,
},
},
MuiCard: {
root: {
// When cards have a forced size, such as when they are arranged in a
// CSS grid, the content needs to flex such that the actions (buttons
// etc) end up at the bottom of the card instead of just below the body
// contents.
display: 'flex',
flexDirection: 'column',
},
},
MuiCardHeader: {
root: {
// Reduce padding between header and content
paddingBottom: 0,
},
},
MuiCardContent: {
root: {
// When cards have a forced size, such as when they are arranged in a
// CSS grid, the content needs to flex such that the actions (buttons
// etc) end up at the bottom of the card instead of just below the body
// contents.
flexGrow: 1,
},
},
MuiCardActions: {
root: {
// We default to putting the card actions at the end