Skip to content

Commit 17ef0dc

Browse files
authored
feat: add collapsible support to PanelCard component (calcom#23366)
* feat: add collapsible support to PanelCard component - Add collapsible and defaultCollapsed props to PanelCard - When collapsible is enabled, title becomes clickable with chevron icon - Chevron icon indicates collapse state and rotates on toggle - Body content is hidden when collapsed - Maintains backward compatibility with existing usage - Follows FormCard's collapsible pattern implementation Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> ^ Conflicts: ^ packages/ui/components/card/PanelCard.tsx * update style * address feedback * animate expanding & collapsing panel
1 parent e530ad5 commit 17ef0dc

3 files changed

Lines changed: 102 additions & 37 deletions

File tree

apps/web/public/static/locales/en/common.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3580,6 +3580,8 @@
35803580
"free": "Free",
35813581
"user_email": "User Email",
35823582
"user_name": "User Name",
3583+
"expand_panel": "Expand Panel",
3584+
"collapse_panel": "Collapse Panel",
35833585
"you_have_one_team": "You have one team",
35843586
"consider_consolidating_one_team_org": "Consider setting up an organization to unify billing, admin tools, and analytics across your team.",
35853587
"consider_consolidating_multi_team_org": "Consider setting up an organization to unify billing, admin tools, and analytics across your teams.",

packages/features/insights/components/ChartCard.tsx

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import classNames from "@calcom/ui/classNames";
66
import { PanelCard } from "@calcom/ui/components/card";
77
import { Tooltip } from "@calcom/ui/components/tooltip";
88

9+
type PanelCardProps = React.ComponentProps<typeof PanelCard>;
10+
911
type LegendItem = {
1012
label: string;
1113
color: string; // hex format
@@ -14,27 +16,16 @@ type LegendItem = {
1416
export type LegendSize = "sm" | "default";
1517

1618
export function ChartCard({
17-
title,
18-
subtitle,
19-
cta,
2019
legend,
2120
legendSize,
2221
enabledLegend,
2322
onSeriesToggle,
24-
children,
25-
className,
26-
titleTooltip,
27-
}: {
28-
title: string | ReactNode;
29-
subtitle?: string;
30-
cta?: { label: string; onClick: () => void };
23+
...panelCardProps
24+
}: PanelCardProps & {
3125
legend?: Array<LegendItem>;
3226
legendSize?: LegendSize;
3327
enabledLegend?: Array<LegendItem>;
3428
onSeriesToggle?: (label: string) => void;
35-
className?: string;
36-
titleTooltip?: string;
37-
children: ReactNode;
3829
}) {
3930
const legendComponent =
4031
legend && legend.length > 0 ? (
@@ -43,13 +34,18 @@ export function ChartCard({
4334

4435
return (
4536
<PanelCard
46-
title={title}
47-
subtitle={subtitle}
48-
cta={cta}
49-
headerContent={legendComponent}
50-
className={className}
51-
titleTooltip={titleTooltip}>
52-
{children}
37+
{...panelCardProps}
38+
headerContent={
39+
panelCardProps.headerContent ? (
40+
<div className="flex items-center gap-2">
41+
{panelCardProps.headerContent}
42+
{legendComponent}
43+
</div>
44+
) : (
45+
legendComponent
46+
)
47+
}>
48+
{panelCardProps.children}
5349
</PanelCard>
5450
);
5551
}
Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
"use client";
2+
3+
import { useAutoAnimate } from "@formkit/auto-animate/react";
14
import type { ReactNode } from "react";
5+
import { useId, useState } from "react";
26

7+
import { useLocale } from "@calcom/lib/hooks/useLocale";
38
import classNames from "@calcom/ui/classNames";
49
import { InfoBadge } from "@calcom/ui/components/badge";
510
import { Button } from "@calcom/ui/components/button";
11+
import { Icon } from "@calcom/ui/components/icon";
612

713
export function PanelCard({
814
title,
@@ -12,6 +18,8 @@ export function PanelCard({
1218
className,
1319
titleTooltip,
1420
children,
21+
collapsible = false,
22+
defaultCollapsed = false,
1523
}: {
1624
title: string | ReactNode;
1725
subtitle?: string;
@@ -20,22 +28,75 @@ export function PanelCard({
2028
className?: string;
2129
titleTooltip?: string;
2230
children: ReactNode;
31+
collapsible?: boolean;
32+
defaultCollapsed?: boolean;
2333
}) {
34+
const { t } = useLocale();
35+
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
36+
const contentId = useId();
37+
const titleId = useId();
38+
const [animationParent] = useAutoAnimate<HTMLDivElement>();
39+
40+
const toggleCollapse = () => {
41+
setIsCollapsed((prev) => !prev);
42+
};
43+
44+
const isStringTitle = typeof title === "string";
45+
2446
return (
2547
<div
48+
ref={animationParent}
2649
className={classNames(
27-
"bg-muted group relative flex w-full flex-col items-center rounded-2xl px-1 pb-1",
50+
"bg-muted group relative flex w-full flex-col items-center rounded-2xl px-1",
51+
!isCollapsed && "pb-1",
2852
className
2953
)}>
3054
<div className="flex h-11 w-full shrink-0 items-center justify-between gap-2 px-4">
31-
{typeof title === "string" ? (
32-
<div className="mr-4 flex shrink-0 items-center gap-1">
33-
<h2 className="text-emphasis shrink-0 text-sm font-semibold">{title}</h2>
34-
{titleTooltip && <InfoBadge content={titleTooltip} />}
35-
</div>
36-
) : (
37-
title
38-
)}
55+
<div className="flex shrink-0 items-center gap-1">
56+
{collapsible && (
57+
<Button
58+
size="sm"
59+
variant="icon"
60+
color="minimal"
61+
CustomStartIcon={
62+
<Icon
63+
name="chevron-up"
64+
className={classNames(
65+
"text-default h-4 w-4 transition-transform",
66+
isCollapsed && "rotate-180"
67+
)}
68+
/>
69+
}
70+
onClick={toggleCollapse}
71+
className="text-muted -ml-2"
72+
aria-expanded={!isCollapsed}
73+
aria-controls={contentId}
74+
aria-label={isCollapsed ? t("expand_panel") : t("collapse_panel")}
75+
/>
76+
)}
77+
{isStringTitle ? (
78+
<div className="mr-4 flex shrink-0 items-center gap-1">
79+
<h2 id={titleId} className="text-emphasis shrink-0 text-sm font-semibold">
80+
{collapsible ? (
81+
<button
82+
type="button"
83+
onClick={toggleCollapse}
84+
className="text-left transition-opacity hover:opacity-80"
85+
aria-expanded={!isCollapsed}
86+
aria-controls={contentId}
87+
aria-label={isCollapsed ? t("expand_panel") : t("collapse_panel")}>
88+
{title as string}
89+
</button>
90+
) : (
91+
(title as string)
92+
)}
93+
</h2>
94+
{titleTooltip && <InfoBadge content={titleTooltip} />}
95+
</div>
96+
) : (
97+
title
98+
)}
99+
</div>
39100
<div className="no-scrollbar flex items-center gap-2 overflow-x-auto">
40101
{headerContent}
41102
{cta && (
@@ -45,14 +106,20 @@ export function PanelCard({
45106
)}
46107
</div>
47108
</div>
48-
<div className="bg-default border-muted w-full grow gap-3 rounded-xl border">
49-
{subtitle && (
50-
<h3 className="text-subtle border-muted border-b p-3 text-sm font-medium leading-none">
51-
{subtitle}
52-
</h3>
53-
)}
54-
{children}
55-
</div>
109+
{!(isCollapsed && collapsible) && (
110+
<div
111+
id={contentId}
112+
role="region"
113+
aria-labelledby={isStringTitle ? titleId : undefined}
114+
className="bg-default border-muted w-full grow gap-3 rounded-xl border">
115+
{subtitle && (
116+
<h3 className="text-subtle border-muted border-b p-3 text-sm font-medium leading-none">
117+
{subtitle}
118+
</h3>
119+
)}
120+
{children}
121+
</div>
122+
)}
56123
</div>
57124
);
58125
}

0 commit comments

Comments
 (0)