Add new Interactive card
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -565,6 +565,12 @@ export const Card: ForwardRefExoticComponent<
|
||||
CardProps & RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type CardBaseProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const CardBody: ForwardRefExoticComponent<
|
||||
CardBodyProps & RefAttributes<HTMLDivElement>
|
||||
@@ -595,6 +601,13 @@ export interface CardBodyProps
|
||||
extends CardBodyOwnProps,
|
||||
React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
// @public (undocumented)
|
||||
export type CardButtonVariant = {
|
||||
onPress: () => void;
|
||||
href?: never;
|
||||
label: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const CardDefinition: {
|
||||
readonly styles: {
|
||||
@@ -602,10 +615,14 @@ export const CardDefinition: {
|
||||
};
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-Card';
|
||||
readonly overlay: 'bui-CardOverlay';
|
||||
};
|
||||
readonly propDefs: {
|
||||
readonly children: {};
|
||||
readonly className: {};
|
||||
readonly onPress: {};
|
||||
readonly href: {};
|
||||
readonly label: {};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -670,15 +687,32 @@ export interface CardHeaderProps
|
||||
React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
// @public (undocumented)
|
||||
export type CardOwnProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
export type CardLinkVariant = {
|
||||
href: string;
|
||||
onPress?: never;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface CardProps
|
||||
extends CardOwnProps,
|
||||
React.HTMLAttributes<HTMLDivElement> {}
|
||||
export type CardOwnProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
onPress?: () => void;
|
||||
href?: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type CardProps = CardBaseProps &
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'onPress'> &
|
||||
(CardButtonVariant | CardLinkVariant | CardStaticVariant);
|
||||
|
||||
// @public (undocumented)
|
||||
export type CardStaticVariant = {
|
||||
onPress?: never;
|
||||
href?: never;
|
||||
label?: never;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const Cell: {
|
||||
|
||||
@@ -27,6 +27,48 @@
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bui-CardOverlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
appearance: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
&:hover::after,
|
||||
&[data-focused]::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: color-mix(in srgb, currentColor 5%, transparent);
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Interactive elements inside the card must sit above the overlay so they
|
||||
* remain independently clickable. The overlay handles the rest of the surface.
|
||||
*/
|
||||
.bui-Card[data-interactive]
|
||||
:is(button, a[href], [role='button'], [role='link'], input, select, textarea):not(
|
||||
.bui-CardOverlay
|
||||
) {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.bui-CardBody {
|
||||
|
||||
@@ -231,6 +231,94 @@ export const BgOnProviders = meta.story({
|
||||
),
|
||||
});
|
||||
|
||||
export const Interactive = meta.story({
|
||||
render: () => (
|
||||
<Card
|
||||
style={{ width: '300px' }}
|
||||
onPress={() => alert('Card pressed')}
|
||||
label="View component details"
|
||||
>
|
||||
<CardHeader>
|
||||
<Text weight="bold">Interactive Card</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Text>
|
||||
Click anywhere on this card to trigger the press handler. The entire
|
||||
card surface is interactive.
|
||||
</Text>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Text variant="body-small" color="secondary">
|
||||
Click to interact
|
||||
</Text>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
),
|
||||
});
|
||||
|
||||
export const InteractiveAsLink = meta.story({
|
||||
render: () => (
|
||||
<Card
|
||||
style={{ width: '300px' }}
|
||||
href="https://backstage.io"
|
||||
label="Open Backstage documentation"
|
||||
>
|
||||
<CardHeader>
|
||||
<Text weight="bold">Link Card</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Text>
|
||||
This card navigates to a URL when clicked. The entire card surface
|
||||
acts as a link.
|
||||
</Text>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Text variant="body-small" color="secondary">
|
||||
Opens backstage.io
|
||||
</Text>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
),
|
||||
});
|
||||
|
||||
export const InteractiveWithNestedButtons = meta.story({
|
||||
render: () => (
|
||||
<Card
|
||||
style={{ width: '300px' }}
|
||||
onPress={() => alert('Card pressed')}
|
||||
label="View plugin details"
|
||||
>
|
||||
<CardHeader>
|
||||
<Text weight="bold">Card with Actions</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Text>
|
||||
Clicking the card background triggers the card press handler. The
|
||||
buttons below remain independently interactive.
|
||||
</Text>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Flex gap="2">
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onPress={() => alert('Primary action')}
|
||||
>
|
||||
Primary
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onPress={() => alert('Secondary action')}
|
||||
>
|
||||
Secondary
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
),
|
||||
});
|
||||
|
||||
export const CustomCardWithBox = meta.story({
|
||||
render: () => (
|
||||
<Flex direction="column" gap="4">
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
import { Button as RAButton, Link as RALink } from 'react-aria-components';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import {
|
||||
CardDefinition,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
CardFooterDefinition,
|
||||
} from './definition';
|
||||
import type {
|
||||
CardOwnProps,
|
||||
CardProps,
|
||||
CardHeaderProps,
|
||||
CardBodyProps,
|
||||
@@ -38,18 +40,31 @@ import { Box } from '../Box/Box';
|
||||
export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
|
||||
const { ownProps, restProps, dataAttributes } = useDefinition(
|
||||
CardDefinition,
|
||||
props,
|
||||
props as CardOwnProps &
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'onPress'>,
|
||||
);
|
||||
const { classes, children } = ownProps;
|
||||
const { classes, children, onPress, href, label } = ownProps;
|
||||
const isInteractive = !!(onPress || href);
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg="neutral"
|
||||
ref={ref}
|
||||
className={classes.root}
|
||||
data-interactive={isInteractive || undefined}
|
||||
{...dataAttributes}
|
||||
{...restProps}
|
||||
>
|
||||
{href && (
|
||||
<RALink className={classes.overlay} href={href} aria-label={label} />
|
||||
)}
|
||||
{onPress && !href && (
|
||||
<RAButton
|
||||
className={classes.overlay}
|
||||
onPress={onPress}
|
||||
aria-label={label}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -31,10 +31,14 @@ export const CardDefinition = defineComponent<CardOwnProps>()({
|
||||
styles,
|
||||
classNames: {
|
||||
root: 'bui-Card',
|
||||
overlay: 'bui-CardOverlay',
|
||||
},
|
||||
propDefs: {
|
||||
children: {},
|
||||
className: {},
|
||||
onPress: {},
|
||||
href: {},
|
||||
label: {},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -16,10 +16,44 @@
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* Flat own-props shape used by the component definition system.
|
||||
* @public
|
||||
*/
|
||||
export type CardOwnProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
onPress?: () => void;
|
||||
href?: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type CardBaseProps = { children?: ReactNode; className?: string };
|
||||
|
||||
/** @public */
|
||||
export type CardButtonVariant = {
|
||||
/** Handler called when the card is pressed. Makes the card interactive as a button. */
|
||||
onPress: () => void;
|
||||
href?: never;
|
||||
/** Accessible label announced by screen readers for the interactive card. */
|
||||
label: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type CardLinkVariant = {
|
||||
/** URL to navigate to. Makes the card interactive as a link. */
|
||||
href: string;
|
||||
onPress?: never;
|
||||
/** Accessible label announced by screen readers for the interactive card. */
|
||||
label?: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type CardStaticVariant = {
|
||||
onPress?: never;
|
||||
href?: never;
|
||||
label?: never;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -27,9 +61,9 @@ export type CardOwnProps = {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface CardProps
|
||||
extends CardOwnProps,
|
||||
React.HTMLAttributes<HTMLDivElement> {}
|
||||
export type CardProps = CardBaseProps &
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'onPress'> &
|
||||
(CardButtonVariant | CardLinkVariant | CardStaticVariant);
|
||||
|
||||
/** @public */
|
||||
export type CardHeaderOwnProps = {
|
||||
|
||||
Reference in New Issue
Block a user