Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions docs/data/material/components/cards/ActionableCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as React from 'react';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import Typography from '@mui/material/Typography';
import CardHeader from '@mui/material/CardHeader';
import CardActions from '@mui/material/CardActions';
import IconButton from '@mui/material/IconButton';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share';

export default function ActionableCard() {
return (
<React.Fragment>
<Card href="#primary-action" sx={{ maxWidth: 345 }}>
<CardMedia
component="img"
height="140"
image="/static/images/cards/contemplative-reptile.jpg"
alt="green iguana"
/>
<CardHeader title="Lizard" />
<CardContent>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Lizards are a widespread group of squamate reptiles, with over 6,000
species, ranging across all continents except Antarctica
</Typography>
</CardContent>
<CardActions>
<IconButton
aria-label="add to favorites"
onClick={() => alert('Favorite clicked')}
>
<FavoriteIcon />
</IconButton>
<IconButton aria-label="share" onClick={() => alert('Share clicked')}>
<ShareIcon />
</IconButton>
</CardActions>
</Card>
<Card onClick={() => alert('Card clicked')} sx={{ maxWidth: 345 }}>
<CardMedia
component="img"
height="140"
image="/static/images/cards/contemplative-reptile.jpg"
alt="green iguana"
/>
<CardHeader title="Lizard" />
<CardContent>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Lizards are a widespread group of squamate reptiles, with over 6,000
species, ranging across all continents except Antarctica
</Typography>
</CardContent>
<CardActions>
<IconButton
aria-label="add to favorites"
onClick={() => alert('Favorite clicked')}
>
<FavoriteIcon />
</IconButton>
<IconButton aria-label="share" onClick={() => alert('Share clicked')}>
<ShareIcon />
</IconButton>
</CardActions>
</Card>
</React.Fragment>
);
}
69 changes: 69 additions & 0 deletions docs/data/material/components/cards/ActionableCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as React from 'react';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import Typography from '@mui/material/Typography';
import CardHeader from '@mui/material/CardHeader';
import CardActions from '@mui/material/CardActions';
import IconButton from '@mui/material/IconButton';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share';

export default function ActionableCard() {
return (
<React.Fragment>
<Card href="#primary-action" sx={{ maxWidth: 345 }}>
<CardMedia
component="img"
height="140"
image="/static/images/cards/contemplative-reptile.jpg"
alt="green iguana"
/>
<CardHeader title="Lizard" />
<CardContent>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Lizards are a widespread group of squamate reptiles, with over 6,000
species, ranging across all continents except Antarctica
</Typography>
</CardContent>
<CardActions>
<IconButton
aria-label="add to favorites"
onClick={() => alert('Favorite clicked')}
>
<FavoriteIcon />
</IconButton>
<IconButton aria-label="share" onClick={() => alert('Share clicked')}>
<ShareIcon />
</IconButton>
</CardActions>
</Card>
<Card onClick={() => alert('Card clicked')} sx={{ maxWidth: 345 }}>
<CardMedia
component="img"
height="140"
image="/static/images/cards/contemplative-reptile.jpg"
alt="green iguana"
/>
<CardHeader title="Lizard" />
<CardContent>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Lizards are a widespread group of squamate reptiles, with over 6,000
species, ranging across all continents except Antarctica
</Typography>
</CardContent>
<CardActions>
<IconButton
aria-label="add to favorites"
onClick={() => alert('Favorite clicked')}
>
<FavoriteIcon />
</IconButton>
<IconButton aria-label="share" onClick={() => alert('Share clicked')}>
<ShareIcon />
</IconButton>
</CardActions>
</Card>
</React.Fragment>
);
}
12 changes: 12 additions & 0 deletions docs/data/material/components/cards/cards.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ A card can also offer supplemental actions which should stand detached from the

{{"demo": "MultiActionAreaCard.js", "bg": true}}

## Actionable card with link

A card's primary action is related to its main subject: the heading. The primary action is to expand the subject in the heading, either by navigating through a link, or executing an action. For this reason, Card accepts `href` and `onClick`, which render the CardHead heading inside a link / button element. This element is focusable, and its focus style is reflected on the whole card. The card can also have other focusable elements in CardHeader, CardContent and CardActions. By default, CardActions, in this case, will render a visual indicating "Read more" element, but that could be customized or removed via slots.

Accessibility:

- all focusable elements are reachable, main action is executed when clicking on the whole card.
- all focusable elements are in the tab order.
- aria-description on the button/link to "read more".

{{"demo": "ActionableCard.js", "bg": true}}

## UI Controls

Supplemental actions within the card are explicitly called out using icons, text, and UI controls, typically placed at the bottom of the card.
Expand Down
8 changes: 8 additions & 0 deletions packages/mui-material/src/Card/Card.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export interface CardOwnProps extends DistributiveOmit<PaperOwnProps, 'classes'>
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme> | undefined;
/**
* If provided, the card will render a clickable link.
*/
href?: string | undefined;
/**
* If provided, the card will call this function when clicked.
*/
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined;
}

export interface CardTypeMap<
Expand Down
50 changes: 39 additions & 11 deletions packages/mui-material/src/Card/Card.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { styled } from '../zero-styled';
import { useDefaultProps } from '../DefaultPropsProvider';
import Paper from '../Paper';
import { getCardUtilityClass } from './cardClasses';
import { CardContextProvider } from './CardContext';

const useUtilityClasses = (ownerState) => {
const { classes } = ownerState;
Expand All @@ -22,30 +23,49 @@ const useUtilityClasses = (ownerState) => {
const CardRoot = styled(Paper, {
name: 'MuiCard',
slot: 'Root',
})({
})(({ ownerState, theme }) => ({
overflow: 'hidden',
});
position: 'relative',
...(ownerState.clickable && {
'&::after': {
content: '""',
position: 'absolute',
inset: 0,
backgroundColor: 'currentcolor',
opacity: 0,
transition: theme.transitions.create('opacity', {
duration: theme.transitions.duration.short,
}),
pointerEvents: 'none',
},
'&:hover::after': {
opacity: (theme.vars || theme).palette.action.hoverOpacity,
},
}),
}));

const Card = React.forwardRef(function Card(inProps, ref) {
const props = useDefaultProps({
props: inProps,
name: 'MuiCard',
});

const { className, raised = false, ...other } = props;
const { className, raised = false, href, onClick, ...other } = props;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supporting both link (via href) and "button-like" (via implicit "onClick without href") creates annoying TS issues similar to what ButtonBase suffers, and makes it easy to create accessibility failures

Chip, like Card is also a thing that can be static, a link, or a clickable button(-like), I think we should align their APIs

What do you think about #48015 (comment)? @silviuaavram

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree about the V2 part, this way we avoid any confusion.


const ownerState = { ...props, raised };
const ownerState = { ...props, raised, clickable: !!(href || onClick) };

const classes = useUtilityClasses(ownerState);

return (
<CardRoot
className={clsx(classes.root, className)}
elevation={raised ? 8 : undefined}
ref={ref}
ownerState={ownerState}
{...other}
/>
<CardContextProvider value={{ href, onClick }}>
<CardRoot
className={clsx(classes.root, className)}
elevation={raised ? 8 : undefined}
ref={ref}
ownerState={ownerState}
{...other}
/>
</CardContextProvider>
);
});

Expand All @@ -66,6 +86,14 @@ Card.propTypes /* remove-proptypes */ = {
* @ignore
*/
className: PropTypes.string,
/**
* If provided, the card will render a clickable link.
*/
href: PropTypes.string,
/**
* If provided, the card will call this function when clicked.
*/
onClick: PropTypes.func,
/**
* If `true`, the card will use raised styling.
* @default false
Expand Down
22 changes: 22 additions & 0 deletions packages/mui-material/src/Card/CardContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';
import * as React from 'react';

type CardContextValue = {
href?: string | undefined;
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined;
};

const CardContext = React.createContext<CardContextValue | null>(null);

const useCardContext = () => {
const context = React.useContext(CardContext);

if (context === null) {
throw new Error('useCardContext must be used within a Card');
}
return context;
};

const CardContextProvider = CardContext.Provider;

export { CardContextProvider, useCardContext };
18 changes: 17 additions & 1 deletion packages/mui-material/src/CardActions/CardActions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,24 @@ import { SxProps } from '@mui/system';
import { Theme } from '../styles';
import { InternalStandardProps as StandardProps } from '../internal';
import { CardActionsClasses } from './cardActionsClasses';
import { CreateSlotsAndSlotProps, SlotProps } from '../utils';

export interface CardActionsProps extends StandardProps<React.HTMLAttributes<HTMLDivElement>> {
export interface CardActionSlots {
/**
* The component that renders the root slot.
* @default 'div'
*/
readMore: React.ElementType;
}

export type CardActionsSlotsAndSlotProps = CreateSlotsAndSlotProps<
CardActionSlots,
{
readMore: SlotProps<'div', CardActionsClasses, CardActionsProps>;
}
>;
export interface CardActionsProps
extends StandardProps<React.HTMLAttributes<HTMLDivElement>>, CardActionsSlotsAndSlotProps {
/**
* The content of the component.
*/
Expand Down
Loading
Loading