Skip to content

Commit 9fecde9

Browse files
authored
add collapsible card (#35)
1 parent e527a92 commit 9fecde9

File tree

5 files changed

+224
-22
lines changed

5 files changed

+224
-22
lines changed

src/card/Card.tsx

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import React, { CSSProperties, ReactNode } from 'react';
1+
import React, { CSSProperties, ReactNode, useState } from 'react';
22
import { css } from '@emotion/core';
33
import { Text } from '../content';
4+
import { CollapsibleCardTitle } from './CollapsibleCardTitle';
45
import theme from '../theme';
5-
import { cardCSS, headerCSS } from './styles';
6+
import { cardCSS, headerCSS, collapsibleCardCSS } from './styles';
67
import { classNames } from '../utils';
8+
import { useId } from '@react-aria/utils';
79

810
const headerTitleWrapCSS = css`
911
display: flex;
@@ -34,6 +36,8 @@ export type CardProps = {
3436
extra?: ReactNode; // Extra controls on the header
3537
className?: string;
3638
titleExtra?: ReactNode;
39+
collapsible?: boolean;
40+
defaultOpen?: boolean;
3741
};
3842

3943
export function Card({
@@ -45,37 +49,78 @@ export function Card({
4549
extra,
4650
className,
4751
titleExtra,
52+
collapsible,
53+
defaultOpen = true,
4854
}: CardProps) {
49-
const titleEl = (
55+
const [isOpen, setIsOpen] = useState(defaultOpen);
56+
const id = useId();
57+
const contentId = `${id}-content`,
58+
headerId = `${id}-heading`;
59+
const defaultTitle = (
5060
<Text textSize="xlarge" elementType="h3" weight="heavy">
5161
{title}
5262
</Text>
5363
);
64+
const titleEl =
65+
titleExtra != null ? (
66+
<div css={titleWithTitleExtraCSS}>
67+
{defaultTitle}
68+
{titleExtra}
69+
</div>
70+
) : (
71+
defaultTitle
72+
);
73+
const subTitleEl =
74+
subTitle != null ? (
75+
<Text textSize="medium" elementType="h4" color="white70">
76+
{subTitle}
77+
</Text>
78+
) : (
79+
undefined
80+
);
81+
const titleComponent = collapsible ? (
82+
<div css={headerTitleWrapCSS}>
83+
<CollapsibleCardTitle
84+
isOpen={isOpen}
85+
onOpen={() => setIsOpen(!isOpen)}
86+
title={titleEl}
87+
contentId={contentId}
88+
headerId={headerId}
89+
bordered={false}
90+
className="ac-card-collapsible-header"
91+
subTitle={subTitleEl}
92+
/>
93+
</div>
94+
) : (
95+
<div css={headerTitleWrapCSS}>
96+
{titleEl}
97+
{subTitleEl}
98+
</div>
99+
);
54100
return (
55101
<section
56-
css={cardCSS}
102+
css={collapsible ? collapsibleCardCSS : cardCSS}
57103
style={style}
58-
className={classNames('ac-card', className)}
104+
className={classNames('ac-card', className, {
105+
'is-open': isOpen,
106+
})}
59107
>
60108
<header css={headerCSS({ bordered: true })}>
61-
<div css={headerTitleWrapCSS}>
62-
{titleExtra != null ? (
63-
<div css={titleWithTitleExtraCSS}>
64-
{titleEl}
65-
{titleExtra}
66-
</div>
67-
) : (
68-
titleEl
69-
)}
70-
{subTitle && (
71-
<Text textSize="medium" elementType="h4" color="white70">
72-
{subTitle}
73-
</Text>
74-
)}
75-
</div>
109+
{titleComponent}
76110
{extra}
77111
</header>
78-
<div css={bodyCSS} style={bodyStyle}>
112+
<div
113+
css={css(
114+
bodyCSS,
115+
css`
116+
${!isOpen && `display: none;`}
117+
`
118+
)}
119+
style={bodyStyle}
120+
id={contentId}
121+
aria-labelledby={headerId}
122+
aria-hidden={!isOpen}
123+
>
79124
{children}
80125
</div>
81126
</section>

src/card/CollapsibleCardTitle.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React, { ReactNode } from 'react';
2+
import { css } from '@emotion/core';
3+
import { Icon, ArrowDownFill } from '../icon';
4+
import { classNames } from '../utils';
5+
import theme from '../theme';
6+
7+
const titleWrapCSS = css`
8+
display: flex;
9+
flex-direction: column;
10+
& > h3,
11+
& > h4 {
12+
padding: 0;
13+
margin: 0;
14+
}
15+
`;
16+
interface CollapsibleCardTitleProps {
17+
title: ReactNode;
18+
/**
19+
* A unique id for the content of the collapsible card title. Necessary for ally
20+
*/
21+
contentId: string;
22+
isOpen: boolean;
23+
onOpen: () => void;
24+
/**
25+
* A unique id for the header of the collapsible card title. Necessary for ally
26+
*/
27+
headerId: string;
28+
bordered?: boolean;
29+
className?: string;
30+
subTitle: ReactNode;
31+
}
32+
export function CollapsibleCardTitle(props: CollapsibleCardTitleProps) {
33+
const {
34+
onOpen,
35+
isOpen,
36+
title,
37+
className,
38+
contentId,
39+
headerId,
40+
subTitle,
41+
} = props;
42+
return (
43+
<button
44+
id={headerId}
45+
className={classNames(className)}
46+
onClick={onOpen}
47+
aria-controls={contentId}
48+
aria-expanded={isOpen}
49+
>
50+
<Icon
51+
svg={<ArrowDownFill />}
52+
className="ac-card-collapsible__trigger"
53+
css={css`
54+
transition: transform ease var(--collapsible-card-animation-duration);
55+
transform: rotate(180deg);
56+
margin-right: ${theme.spacing.padding8}px;
57+
`}
58+
aria-hidden={true}
59+
/>
60+
<div css={titleWrapCSS}>
61+
{title}
62+
{subTitle}
63+
</div>
64+
</button>
65+
);
66+
}

src/card/styles.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { css } from '@emotion/core';
22
import theme from '../theme';
3+
const cardHeaderHeight = 68;
34

45
export const cardCSS = css`
56
display: flex;
@@ -17,7 +18,7 @@ const headerBorderCSS = css`
1718

1819
export const headerCSS = ({
1920
bordered,
20-
height = 68,
21+
height = cardHeaderHeight,
2122
}: {
2223
bordered: boolean;
2324
height?: number;
@@ -31,3 +32,37 @@ export const headerCSS = ({
3132
height: ${height}px;
3233
${bordered ? headerBorderCSS : ''}
3334
`;
35+
36+
export const collapsibleCardCSS = css`
37+
${cardCSS}
38+
.ac-card-collapsible-header {
39+
padding: 0;
40+
cursor: pointer;
41+
display: block;
42+
width: 100%;
43+
display: flex;
44+
flex-direction: row;
45+
flex: 1 1 auto;
46+
justify-content: space-between;
47+
align-items: center;
48+
appearance: none;
49+
background-color: inherit;
50+
border: 0;
51+
text-align: start;
52+
color: ${theme.colors.text1};
53+
/* remove outline - TODO might need to give a visual cue that this area is in focus */
54+
outline: none;
55+
}
56+
57+
&.is-open {
58+
.ac-card-collapsible__trigger {
59+
transform: rotate(0deg);
60+
}
61+
}
62+
/* shrink the height to the card title so the body is hidden*/
63+
&:not(.is-open) {
64+
height: ${cardHeaderHeight}px !important;
65+
}
66+
67+
--collapsible-card-animation-duration: ${theme.animation.global.duration}ms;
68+
`;

src/icon/Icons.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,20 @@ export const CloseOutline = () => (
317317
</g>
318318
</svg>
319319
);
320+
321+
export const ArrowDownFill = () => (
322+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
323+
<path d="M12 17C11.494 17 11.01 16.767 10.674 16.358L6.46103 11.26C5.95703 10.649 5.85603 9.782 6.20203 9.049C6.50703 8.402 7.11403 8 7.78703 8H16.213C16.886 8 17.493 8.402 17.798 9.049C18.144 9.782 18.043 10.649 17.54 11.259L13.326 16.358C12.99 16.767 12.506 17 12 17" />
324+
<mask
325+
id="mask0_0_2149"
326+
maskUnits="userSpaceOnUse"
327+
x="5"
328+
y="8"
329+
width="14"
330+
height="9"
331+
>
332+
<path d="M12 17C11.494 17 11.01 16.767 10.674 16.358L6.46103 11.26C5.95703 10.649 5.85603 9.782 6.20203 9.049C6.50703 8.402 7.11403 8 7.78703 8H16.213C16.886 8 17.493 8.402 17.798 9.049C18.144 9.782 18.043 10.649 17.54 11.259L13.326 16.358C12.99 16.767 12.506 17 12 17Z" />
333+
</mask>
334+
<g mask="url(#mask0_0_2149)"></g>
335+
</svg>
336+
);

stories/Card.stories.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,45 @@ export const Gallery = () => (
9797
<TabPane name="Tab 2">{null}</TabPane>
9898
</Tabs>
9999
</TabbedCard>
100+
<Card title="Collapsible Title" style={cardStyle} collapsible>
101+
{''}
102+
</Card>
103+
<Card
104+
title="Collapsible Title"
105+
style={cardStyle}
106+
extra={<Button variant="default">Create Dashboard</Button>}
107+
collapsible
108+
>
109+
{''}
110+
</Card>
111+
<Card
112+
title="Collapsible Title"
113+
style={cardStyle}
114+
extra={<Button variant="default">Create Dashboard</Button>}
115+
titleExtra={<InfoTip>Create Dashboard Info Bubble</InfoTip>}
116+
collapsible
117+
>
118+
{''}
119+
</Card>
120+
<Card
121+
title="Collapsible Title"
122+
subTitle="Subtext area"
123+
style={cardStyle}
124+
extra={<Button variant="default">Create Dashboard</Button>}
125+
collapsible
126+
>
127+
{''}
128+
</Card>
129+
<Card
130+
title="Collapsible Title"
131+
subTitle="Subtext area"
132+
style={cardStyle}
133+
extra={<Button variant="default">Create Dashboard</Button>}
134+
titleExtra={<InfoTip>Create Dashboard Info Bubble</InfoTip>}
135+
collapsible
136+
>
137+
{''}
138+
</Card>
100139
</div>
101140
);
102141

0 commit comments

Comments
 (0)