Add new Interactive card
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -7,6 +7,8 @@ import {
|
||||
CardFooter,
|
||||
} from '../../../../../packages/ui/src/components/Card/Card';
|
||||
import { Text } from '../../../../../packages/ui/src/components/Text/Text';
|
||||
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
|
||||
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
@@ -32,6 +34,70 @@ export const HeaderAndBody = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const InteractiveButton = () => {
|
||||
return (
|
||||
<Card
|
||||
style={{ width: '300px' }}
|
||||
onPress={() => {}}
|
||||
label="View component details"
|
||||
>
|
||||
<CardHeader>Interactive Card</CardHeader>
|
||||
<CardBody>
|
||||
Click anywhere on this card to trigger the press handler.
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Text variant="body-small" color="secondary">
|
||||
Click to interact
|
||||
</Text>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const InteractiveLink = () => {
|
||||
return (
|
||||
<Card
|
||||
style={{ width: '300px' }}
|
||||
href="https://backstage.io"
|
||||
label="Open Backstage documentation"
|
||||
>
|
||||
<CardHeader>Link Card</CardHeader>
|
||||
<CardBody>This card navigates to a URL when clicked.</CardBody>
|
||||
<CardFooter>
|
||||
<Text variant="body-small" color="secondary">
|
||||
Opens backstage.io
|
||||
</Text>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const InteractiveWithNestedButtons = () => {
|
||||
return (
|
||||
<Card
|
||||
style={{ width: '300px' }}
|
||||
onPress={() => {}}
|
||||
label="View plugin details"
|
||||
>
|
||||
<CardHeader>Card with Actions</CardHeader>
|
||||
<CardBody>
|
||||
Clicking the card background triggers the card press handler. The
|
||||
buttons below remain independently interactive.
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Flex gap="2">
|
||||
<Button size="small" variant="secondary" onPress={() => {}}>
|
||||
Primary
|
||||
</Button>
|
||||
<Button size="small" variant="tertiary" onPress={() => {}}>
|
||||
Secondary
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithLongBody = () => {
|
||||
return (
|
||||
<Card style={{ width: '300px', height: '200px' }}>
|
||||
|
||||
@@ -12,8 +12,18 @@ import {
|
||||
defaultSnippet,
|
||||
headerAndBodySnippet,
|
||||
withLongBodySnippet,
|
||||
interactiveButtonSnippet,
|
||||
interactiveLinkSnippet,
|
||||
interactiveWithNestedButtonsSnippet,
|
||||
} from './snippets';
|
||||
import { Default, HeaderAndBody, WithLongBody } from './components';
|
||||
import {
|
||||
Default,
|
||||
HeaderAndBody,
|
||||
WithLongBody,
|
||||
InteractiveButton,
|
||||
InteractiveLink,
|
||||
InteractiveWithNestedButtons,
|
||||
} from './components';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { Theming } from '@/components/Theming';
|
||||
import { CardDefinition } from '../../../utils/definitions';
|
||||
@@ -81,6 +91,49 @@ When body content exceeds the available height, CardBody scrolls while header an
|
||||
code={withLongBodySnippet}
|
||||
/>
|
||||
|
||||
## Interactive cards
|
||||
|
||||
Cards can be made interactive without wrapping the entire card in a button or link — which would conflict with any interactive elements inside. Instead, a transparent overlay covers the card surface, and nested buttons and links remain independently clickable above it.
|
||||
|
||||
### Button
|
||||
|
||||
Pass `onPress` and a `label` (used as the accessible name for screen readers) to make the whole card surface pressable.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
layout="side-by-side"
|
||||
preview={<InteractiveButton />}
|
||||
code={interactiveButtonSnippet}
|
||||
/>
|
||||
|
||||
### Link
|
||||
|
||||
Pass `href` to make the card surface navigate to a URL. The `label` prop is optional but recommended for accessibility when the card content alone may not sufficiently describe the destination.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
layout="side-by-side"
|
||||
preview={<InteractiveLink />}
|
||||
code={interactiveLinkSnippet}
|
||||
/>
|
||||
|
||||
### With nested buttons
|
||||
|
||||
Buttons and links inside the card remain independently interactive. Clicking them does not trigger the card's `onPress` handler.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
layout="side-by-side"
|
||||
preview={<InteractiveWithNestedButtons />}
|
||||
code={interactiveWithNestedButtonsSnippet}
|
||||
/>
|
||||
|
||||
<Theming definition={CardDefinition} />
|
||||
|
||||
<ChangelogComponent component="card" />
|
||||
|
||||
@@ -15,6 +15,25 @@ const optionalChildrenPropDef: Record<string, PropDef> = {
|
||||
|
||||
export const cardPropDefs: Record<string, PropDef> = {
|
||||
...optionalChildrenPropDef,
|
||||
onPress: {
|
||||
type: 'enum',
|
||||
values: ['() => void'],
|
||||
responsive: false,
|
||||
description:
|
||||
'Handler called when the card is pressed. Makes the card interactive as a button. Requires label.',
|
||||
},
|
||||
href: {
|
||||
type: 'string',
|
||||
responsive: false,
|
||||
description:
|
||||
'URL to navigate to. Makes the card interactive as a link. Mutually exclusive with onPress.',
|
||||
},
|
||||
label: {
|
||||
type: 'string',
|
||||
responsive: false,
|
||||
description:
|
||||
'Accessible label announced by screen readers for the interactive overlay. Required when onPress is provided.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,50 @@ export const headerAndBodySnippet = `<Card style={{ width: '300px', height: '200
|
||||
<CardBody>Body content without a footer</CardBody>
|
||||
</Card>`;
|
||||
|
||||
export const interactiveButtonSnippet = `<Card
|
||||
style={{ width: '300px' }}
|
||||
onPress={() => console.log('Card pressed')}
|
||||
label="View component details"
|
||||
>
|
||||
<CardHeader>Interactive Card</CardHeader>
|
||||
<CardBody>Click anywhere on this card to trigger the press handler.</CardBody>
|
||||
<CardFooter>Click to interact</CardFooter>
|
||||
</Card>`;
|
||||
|
||||
export const interactiveLinkSnippet = `<Card
|
||||
style={{ width: '300px' }}
|
||||
href="https://backstage.io"
|
||||
label="Open Backstage documentation"
|
||||
>
|
||||
<CardHeader>Link Card</CardHeader>
|
||||
<CardBody>This card navigates to a URL when clicked.</CardBody>
|
||||
<CardFooter>Opens backstage.io</CardFooter>
|
||||
</Card>`;
|
||||
|
||||
export const interactiveWithNestedButtonsSnippet = `import { Button, Flex } from '@backstage/ui';
|
||||
|
||||
<Card
|
||||
style={{ width: '300px' }}
|
||||
onPress={() => console.log('Card pressed')}
|
||||
label="View plugin details"
|
||||
>
|
||||
<CardHeader>Card with Actions</CardHeader>
|
||||
<CardBody>
|
||||
Clicking the card background triggers the card press handler.
|
||||
The buttons below remain independently interactive.
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Flex gap="2">
|
||||
<Button size="small" variant="secondary" onPress={() => console.log('Primary')}>
|
||||
Primary
|
||||
</Button>
|
||||
<Button size="small" variant="tertiary" onPress={() => console.log('Secondary')}>
|
||||
Secondary
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardFooter>
|
||||
</Card>`;
|
||||
|
||||
export const withLongBodySnippet = `import { Text } from '@backstage/ui';
|
||||
|
||||
<Card style={{ width: '300px', height: '200px' }}>
|
||||
|
||||
Reference in New Issue
Block a user