Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/lucky-terms-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Card: Add `data-component` attributes to `Card` and its subcomponents (`Icon`, `Image`, `Heading`, `Description`, `Metadata`, `Menu`). Add an `as` prop (`'div' | 'section'`) so standalone Cards can render as a labelled region landmark; `as="section"` requires `aria-label` or `aria-labelledby`. `Card` now requires `children`. Also improves docs and stories.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 49 additions & 4 deletions packages/react/src/Card/Card.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,27 @@
},
{
"id": "experimental-components-card-features--with-metadata"
},
{
"id": "experimental-components-card-features--with-menu"
},
{
"id": "experimental-components-card-features--standalone-section"
},
{
"id": "experimental-components-card-features--in-list"
},
{
"id": "experimental-components-card-features--interactive-content"
}
],
"props": [
{
"name": "children",
"type": "React.ReactNode",
"required": true,
"description": "The contents of the card. Provide either `Card.*` subcomponents (for example `Card.Heading`, `Card.Description`, `Card.Metadata`) or any custom content. A card with no children will not render."
},
{
"name": "className",
"type": "string",
Expand All @@ -32,6 +50,12 @@
"type": "'medium' | 'large'",
"defaultValue": "'large'",
"description": "Controls the border radius of the Card."
},
{
"name": "as",
"type": "'div' | 'section'",
"defaultValue": "'div'",
"description": "The HTML element to render. Use `'section'` for **standalone** Cards (not inside a list of cards) so screen readers announce the Card as a labelled region; in that case, either `aria-label` or `aria-labelledby` is required. Use the default `'div'` (and omit `Card.Heading`) when the Card is inside an `<li>` of a list of cards — the surrounding list already provides grouping."
}
],
"subcomponents": [
Expand Down Expand Up @@ -73,21 +97,42 @@
"name": "as",
"type": "'h2' | 'h3' | 'h4' | 'h5' | 'h6'",
"defaultValue": "'h3'",
"description": "The heading level to render."
"description": "The heading level to render. Use on standalone Cards (with `as=\"section\"` on the parent `Card`) only; do not use `Card.Heading` when the Card is inside an `<li>` of a list of cards."
}
]
},
{
"name": "Card.Description",
"props": []
"props": [
{
"name": "children",
"type": "React.ReactNode",
"required": true,
"description": "The descriptive text for the card. Rendered inside a `<p>` element so should be flowing text content."
}
]
},
{
"name": "Card.Menu",
"props": []
"props": [
{
"name": "children",
"type": "React.ReactNode",
"required": true,
"description": "The interactive control(s) to render in the top-right corner of the card, typically a single `IconButton` or `ActionMenu` trigger. When a card contains a menu, make sure the control's accessible name includes enough context to distinguish it from other cards (for example, 'Options for Project Alpha' rather than just 'Options')."
}
]
},
{
"name": "Card.Metadata",
"props": []
"props": [
{
"name": "children",
"type": "React.ReactNode",
"required": true,
"description": "The metadata content to render at the bottom of the card. Accepts any content, including plain text, icons, and other Primer components (for example a `Label`, `Octicon`, or any combination). Avoid using `RelativeTime` until its outstanding accessibility issues are resolved."
}
]
}
]
}
168 changes: 138 additions & 30 deletions packages/react/src/Card/Card.features.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,164 @@
import type {Meta} from '@storybook/react-vite'
import {RepoIcon, StarIcon} from '@primer/octicons-react'
import {KebabHorizontalIcon, RepoIcon, RepoForkedIcon, StarIcon} from '@primer/octicons-react'
import {ActionList, ActionMenu, Button, IconButton, VisuallyHidden} from '..'
import {Card} from './index'
import classes from './Card.stories.module.css'

const meta = {
title: 'Experimental/Components/Card/Features',
component: Card,
decorators: [
Story => (
<div className={classes.WidthConstraintContainer}>
<Story />
</div>
),
],
} satisfies Meta<typeof Card>

export default meta

export const WithImage = () => {
return (
<div style={{maxWidth: '400px'}}>
<Card>
<Card.Image src="https://github.com/octocat.png" alt="Octocat" />
<Card.Heading>Card with Image</Card.Heading>
<Card.Description>This card uses an edge-to-edge image instead of an icon.</Card.Description>
</Card>
</div>
<Card>
<Card.Image src="https://github.com/octocat.png" alt="Octocat" />
<Card.Heading>Card with Image</Card.Heading>
<Card.Description>This card uses an edge-to-edge image instead of an icon.</Card.Description>
</Card>
)
}

export const WithMetadata = () => {
return (
<div style={{maxWidth: '400px'}}>
<Card>
<Card.Icon icon={RepoIcon} />
<Card.Heading>primer/react</Card.Heading>
<Card.Description>
{"GitHub's design system implemented as React components for building consistent user interfaces."}
</Card.Description>
<Card.Metadata>
<StarIcon size={16} />
1.2k stars
</Card.Metadata>
</Card>
)
}

export const WithMenu = () => {
return (
<Card>
<Card.Icon icon={RepoIcon} />
<Card.Heading>primer/react</Card.Heading>
<Card.Description>
{"GitHub's design system implemented as React components for building consistent user interfaces."}
</Card.Description>
<Card.Menu>
<ActionMenu>
<ActionMenu.Anchor>
<IconButton icon={KebabHorizontalIcon} aria-label="More options for primer/react" variant="invisible" />
</ActionMenu.Anchor>
<ActionMenu.Overlay>
<ActionList>
<ActionList.Item>Star</ActionList.Item>
<ActionList.Item>Watch</ActionList.Item>
<ActionList.Item>Fork</ActionList.Item>
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
</Card.Menu>
</Card>
)
}

export const CustomContent = () => (
<Card>
<div className={classes.CustomContentLayout}>
<h3>Custom Content Card</h3>
<p>This card uses arbitrary custom content instead of the built-in subcomponents.</p>
<ul>
<li>Item one</li>
<li>Item two</li>
<li>Item three</li>
</ul>
</div>
</Card>
)

export const StandaloneSection = () => (
<Card as="section" aria-labelledby="standalone-card-heading">
<Card.Icon icon={RepoIcon} />
<Card.Heading id="standalone-card-heading">primer/react</Card.Heading>
<Card.Description>
{
"Standalone cards render as a labelled <section> landmark. The Card.Heading's id is referenced via aria-labelledby so screen readers announce the heading as the section's accessible name."
}
</Card.Description>
</Card>
)

export const InList = () => (
<ul className={classes.CardList} aria-label="Repositories">
<li>
<Card>
<Card.Icon icon={RepoIcon} />
<Card.Heading>primer/react</Card.Heading>
<Card.Description>
{"GitHub's design system implemented as React components for building consistent user interfaces."}
</Card.Description>
<Card.Description>primer/react</Card.Description>
<Card.Metadata>
<StarIcon size={16} />
1.2k stars
</Card.Metadata>
</Card>
</div>
</li>
<li>
<Card>
<Card.Icon icon={RepoIcon} />
<Card.Description>primer/css</Card.Description>
<Card.Metadata>
<StarIcon size={16} />
850 stars
</Card.Metadata>
</Card>
</li>
<li>
<Card>
<Card.Icon icon={RepoIcon} />
<Card.Description>primer/octicons</Card.Description>
<Card.Metadata>
<StarIcon size={16} />
2.1k stars
</Card.Metadata>
</Card>
</li>
</ul>
)

/**
* When several Cards share the same interactive controls (for example "Star"
* or "Fork" buttons in a list of repositories), the controls' accessible
* names must include enough context to distinguish one card's action from
* another's. This story uses `VisuallyHidden` to append the repo name to
* each button's accessible name — a common pattern across GitHub.
*/
export const InteractiveContent = () => {
const repos = [{name: 'primer/react'}, {name: 'primer/css'}, {name: 'primer/octicons'}]

return (
<ul className={classes.CardList} aria-label="Repositories">
{repos.map(repo => (
<li key={repo.name}>
<Card>
<Card.Icon icon={RepoIcon} />
<Card.Description>{repo.name}</Card.Description>
<Card.Metadata>
<Button leadingVisual={StarIcon} size="small">
Star <VisuallyHidden>{repo.name}</VisuallyHidden>
</Button>
<Button leadingVisual={RepoForkedIcon} size="small">
Fork <VisuallyHidden>{repo.name}</VisuallyHidden>
</Button>
</Card.Metadata>
</Card>
</li>
))}
</ul>
)
}

export const CustomContent = () => (
<div style={{maxWidth: '400px'}}>
<Card>
<div style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
<strong>Custom Content Card</strong>
<p style={{margin: 0}}>This card uses arbitrary custom content instead of the built-in subcomponents.</p>
<ul style={{margin: 0, paddingLeft: '16px'}}>
<li>Item one</li>
<li>Item two</li>
<li>Item three</li>
</ul>
</div>
</Card>
</div>
)
28 changes: 28 additions & 0 deletions packages/react/src/Card/Card.stories.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.WidthConstraintContainer {
max-width: 400px;
}

.CustomContentLayout {
display: flex;
flex-direction: column;
gap: var(--stack-gap-condensed);
}

.CustomContentLayout > h3,
.CustomContentLayout > p,
.CustomContentLayout > ul {
margin: 0;
}

.CustomContentLayout > ul {
padding-inline-start: var(--base-size-16);
}

.CardList {
display: flex;
flex-direction: column;
gap: var(--stack-gap-condensed);
margin: 0;
padding: 0;
list-style: none;
}
40 changes: 23 additions & 17 deletions packages/react/src/Card/Card.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import type {Meta, StoryFn} from '@storybook/react-vite'
import {RocketIcon} from '@primer/octicons-react'
import {PeopleIcon, RocketIcon} from '@primer/octicons-react'
import {Card} from './index'
import classes from './Card.stories.module.css'

const meta = {
title: 'Experimental/Components/Card',
component: Card,
decorators: [
Story => (
<div className={classes.WidthConstraintContainer}>
<Story />
</div>
),
],
} satisfies Meta<typeof Card>

export default meta

export const Default = () => {
return (
<div style={{maxWidth: '400px'}}>
<Card>
<Card.Icon icon={RocketIcon} />
<Card.Heading>Card Heading</Card.Heading>
<Card.Description>This is a description of the card providing supplemental information.</Card.Description>
<Card.Metadata>Updated 2 hours ago</Card.Metadata>
</Card>
</div>
<Card>
<Card.Icon icon={RocketIcon} />
<Card.Heading>Card Heading</Card.Heading>
<Card.Description>This is a description of the card providing supplemental information.</Card.Description>
<Card.Metadata>
<PeopleIcon size={16} />3 contributors
</Card.Metadata>
</Card>
)
}

Expand All @@ -30,14 +38,12 @@ type PlaygroundArgs = {
}

export const Playground: StoryFn<PlaygroundArgs> = ({showIcon, showMetadata, padding, borderRadius}) => (
<div style={{maxWidth: '400px'}}>
<Card padding={padding} borderRadius={borderRadius}>
{showIcon && <Card.Icon icon={RocketIcon} />}
<Card.Heading>Playground Card</Card.Heading>
<Card.Description>Experiment with the Card component and its subcomponents.</Card.Description>
{showMetadata && <Card.Metadata>Just now</Card.Metadata>}
</Card>
</div>
<Card padding={padding} borderRadius={borderRadius}>
{showIcon && <Card.Icon icon={RocketIcon} />}
<Card.Heading>Playground Card</Card.Heading>
<Card.Description>Experiment with the Card component and its subcomponents.</Card.Description>
{showMetadata && <Card.Metadata>Just now</Card.Metadata>}
</Card>
)

Playground.args = {
Expand Down
Loading
Loading