Skip to content

Commit 5e8fc6c

Browse files
authored
Adds CardDescription component (#12105)
* feat: Added card description component to card. * Added tests for card description. * Updated with core changes. * Fixed missing export for CardSubtitle. * Updated yarn.lock file. * Fixed card demo. * Updated with review comments. * Added snapshot. * Updated to have a react node instead of a card title. * Updated to have a react node instead of a card title. * Updated to have a react node instead of a card title. * Removed Card class test. * Added isBeta flag.
1 parent 070b384 commit 5e8fc6c

File tree

13 files changed

+200
-15
lines changed

13 files changed

+200
-15
lines changed

packages/react-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"tslib": "^2.8.1"
5555
},
5656
"devDependencies": {
57-
"@patternfly/patternfly": "6.5.0-prerelease.12",
57+
"@patternfly/patternfly": "6.5.0-prerelease.14",
5858
"case-anything": "^3.1.2",
5959
"css": "^3.0.0",
6060
"fs-extra": "^11.3.0"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { css } from '@patternfly/react-styles';
2+
import styles from '@patternfly/react-styles/css/components/Card/card';
3+
4+
export interface CardSubtitleProps {
5+
/** Content rendered inside the description. */
6+
children?: React.ReactNode;
7+
/** Id of the description. */
8+
id?: string;
9+
}
10+
11+
export const CardSubtitle: React.FunctionComponent<CardSubtitleProps> = ({
12+
children = null,
13+
id = '',
14+
...props
15+
}: CardSubtitleProps) => (
16+
<div {...props} id={id} className={css(styles.cardSubtitle)}>
17+
{children}
18+
</div>
19+
);
20+
CardSubtitle.displayName = 'CardSubtitle';

packages/react-core/src/components/Card/CardTitle.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useContext } from 'react';
22
import { css } from '@patternfly/react-styles';
33
import styles from '@patternfly/react-styles/css/components/Card/card';
44
import { CardContext } from './Card';
5+
import { CardSubtitle } from './CardSubtitle';
56

67
export interface CardTitleProps extends React.HTMLProps<HTMLDivElement> {
78
/** Content rendered inside the CardTitle */
@@ -10,23 +11,28 @@ export interface CardTitleProps extends React.HTMLProps<HTMLDivElement> {
1011
className?: string;
1112
/** Sets the base component to render. defaults to div */
1213
component?: keyof React.JSX.IntrinsicElements;
14+
/** @beta Subtitle of the card title */
15+
subtitle?: React.ReactNode;
1316
}
1417

1518
export const CardTitle: React.FunctionComponent<CardTitleProps> = ({
1619
children,
1720
className,
1821
component = 'div',
22+
subtitle,
1923
...props
2024
}: CardTitleProps) => {
2125
const { cardId } = useContext(CardContext);
2226
const Component = component as any;
2327
const titleId = cardId ? `${cardId}-title` : '';
28+
const subtitleId = cardId ? `${cardId}-subtitle` : '';
2429

2530
return (
2631
<div className={css(styles.cardTitle)}>
2732
<Component className={css(styles.cardTitleText, className)} id={titleId || undefined} {...props}>
2833
{children}
2934
</Component>
35+
{subtitle && <CardSubtitle id={subtitleId}>{subtitle}</CardSubtitle>}
3036
</div>
3137
);
3238
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { CardSubtitle } from '../CardSubtitle';
3+
4+
describe('CardSubtitle', () => {
5+
test('renders with PatternFly Core styles', () => {
6+
const { asFragment } = render(<CardSubtitle>text</CardSubtitle>);
7+
expect(asFragment()).toMatchSnapshot();
8+
});
9+
10+
test('extra props are spread to the root element', () => {
11+
const testId = 'card-subtitle';
12+
13+
render(<CardSubtitle data-testid={testId} />);
14+
expect(screen.getByTestId(testId)).toBeInTheDocument();
15+
});
16+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`CardSubtitle renders with PatternFly Core styles 1`] = `
4+
<DocumentFragment>
5+
<div
6+
class="pf-v6-c-card__subtitle"
7+
id=""
8+
>
9+
text
10+
</div>
11+
</DocumentFragment>
12+
`;

packages/react-core/src/components/Card/examples/Card.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,20 @@ import PlusIcon from '@patternfly/react-icons/dist/esm/icons/plus-icon';
2828

2929
Basic cards typically have a `<CardTitle>`, `<CardBody>` and `<CardFooter>`. You may omit these components as needed, but it is recommended to at least include a `<CardBody>` to provide details about the card item.
3030

31-
```ts file='./CardBasic.tsx'
31+
```ts file='./CardBasic.tsx'
32+
33+
```
34+
### Card with subtitle
35+
36+
A basic card that also has a subtitle
37+
38+
```ts file='./CardSubtitle.tsx' isBeta
39+
40+
```
41+
### Card with subtitle and Actions
42+
This card demonstrates having an image, action, and subtitle in a single card.
43+
44+
```ts file='./CardSubtitleActions.tsx' isBeta
3245

3346
```
3447

@@ -71,6 +84,8 @@ Select the "actions hasNoOffset" checkbox in the example below to illustrate thi
7184

7285
```
7386

87+
88+
7489
### Title inline with images and actions
7590

7691
Moving `<CardTitle>` within the `<CardHeader>` will style it inline with any images or actions.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Card, CardTitle, CardBody, CardFooter } from '@patternfly/react-core';
2+
3+
export const CardSubtitle: React.FunctionComponent = () => (
4+
<Card ouiaId="CardSubtitle">
5+
<CardTitle subtitle="Subtitle">Title</CardTitle>
6+
<CardBody>Body</CardBody>
7+
<CardFooter>Footer</CardFooter>
8+
</Card>
9+
);
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { useState } from 'react';
2+
import {
3+
Brand,
4+
Card,
5+
CardHeader,
6+
CardTitle,
7+
CardBody,
8+
CardFooter,
9+
Checkbox,
10+
Dropdown,
11+
DropdownList,
12+
DropdownItem,
13+
MenuToggle,
14+
MenuToggleElement,
15+
Divider
16+
} from '@patternfly/react-core';
17+
import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
18+
import pfLogo from '../../assets/PF-HorizontalLogo-Color.svg';
19+
20+
export const CardWithImageAndActions: React.FunctionComponent = () => {
21+
const [isOpen, setIsOpen] = useState<boolean>(false);
22+
const [isChecked, setIsChecked] = useState<boolean>(false);
23+
const [hasNoOffset, setHasNoOffset] = useState<boolean>(false);
24+
25+
const onSelect = () => {
26+
setIsOpen(!isOpen);
27+
};
28+
const onClick = (checked: boolean) => {
29+
setIsChecked(checked);
30+
};
31+
const toggleOffset = (checked: boolean) => {
32+
setHasNoOffset(checked);
33+
};
34+
35+
const dropdownItems = (
36+
<>
37+
<DropdownItem key="action">Action</DropdownItem>
38+
{/* Prevent default onClick functionality for example purposes */}
39+
<DropdownItem key="link" to="#" onClick={(event: any) => event.preventDefault()}>
40+
Link
41+
</DropdownItem>
42+
<DropdownItem key="disabled action" isDisabled>
43+
Disabled Action
44+
</DropdownItem>
45+
<DropdownItem key="disabled link" isDisabled to="#" onClick={(event: any) => event.preventDefault()}>
46+
Disabled Link
47+
</DropdownItem>
48+
<Divider component="li" key="separator" />
49+
<DropdownItem key="separated action">Separated Action</DropdownItem>
50+
<DropdownItem key="separated link" to="#" onClick={(event: any) => event.preventDefault()}>
51+
Separated Link
52+
</DropdownItem>
53+
</>
54+
);
55+
56+
const headerActions = (
57+
<>
58+
<Dropdown
59+
onSelect={onSelect}
60+
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
61+
<MenuToggle
62+
ref={toggleRef}
63+
isExpanded={isOpen}
64+
onClick={() => setIsOpen(!isOpen)}
65+
variant="plain"
66+
aria-label="Card header images and actions example kebab toggle"
67+
icon={<EllipsisVIcon />}
68+
/>
69+
)}
70+
isOpen={isOpen}
71+
onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
72+
>
73+
<DropdownList>{dropdownItems}</DropdownList>
74+
</Dropdown>
75+
<Checkbox
76+
isChecked={isChecked}
77+
onChange={(_event, checked) => onClick(checked)}
78+
aria-label="card checkbox example"
79+
id="check-1"
80+
name="check1"
81+
/>
82+
</>
83+
);
84+
85+
return (
86+
<>
87+
<Checkbox
88+
label="actions hasNoOffset"
89+
isChecked={hasNoOffset}
90+
onChange={(_event, checked) => toggleOffset(checked)}
91+
aria-label="remove actions offset"
92+
id="toggle-actions-offset"
93+
name="toggle-actions-offset"
94+
/>
95+
<div style={{ marginTop: '15px' }}>
96+
<Card>
97+
<CardHeader actions={{ actions: headerActions, hasNoOffset }}>
98+
<Brand src={pfLogo} alt="PatternFly logo" style={{ width: '300px' }} />
99+
</CardHeader>
100+
<CardTitle subtitle="Subtitle">Title</CardTitle>
101+
<CardBody>Body</CardBody>
102+
<CardFooter>Footer</CardFooter>
103+
</Card>
104+
</div>
105+
</>
106+
);
107+
};

packages/react-docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"test:a11y": "patternfly-a11y --config patternfly-a11y.config"
2424
},
2525
"dependencies": {
26-
"@patternfly/patternfly": "6.5.0-prerelease.12",
26+
"@patternfly/patternfly": "6.5.0-prerelease.14",
2727
"@patternfly/react-charts": "workspace:^",
2828
"@patternfly/react-code-editor": "workspace:^",
2929
"@patternfly/react-core": "workspace:^",

packages/react-icons/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"@fortawesome/free-brands-svg-icons": "^5.15.4",
3434
"@fortawesome/free-regular-svg-icons": "^5.15.4",
3535
"@fortawesome/free-solid-svg-icons": "^5.15.4",
36-
"@patternfly/patternfly": "6.5.0-prerelease.12",
36+
"@patternfly/patternfly": "6.5.0-prerelease.14",
3737
"fs-extra": "^11.3.0",
3838
"tslib": "^2.8.1"
3939
},

0 commit comments

Comments
 (0)