feat: add collapsible support to PanelCard component (#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
This commit is contained in:
@@ -3580,6 +3580,8 @@
|
||||
"free": "Free",
|
||||
"user_email": "User Email",
|
||||
"user_name": "User Name",
|
||||
"expand_panel": "Expand Panel",
|
||||
"collapse_panel": "Collapse Panel",
|
||||
"you_have_one_team": "You have one team",
|
||||
"consider_consolidating_one_team_org": "Consider setting up an organization to unify billing, admin tools, and analytics across your team.",
|
||||
"consider_consolidating_multi_team_org": "Consider setting up an organization to unify billing, admin tools, and analytics across your teams.",
|
||||
|
||||
@@ -6,6 +6,8 @@ import classNames from "@calcom/ui/classNames";
|
||||
import { PanelCard } from "@calcom/ui/components/card";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
|
||||
type PanelCardProps = React.ComponentProps<typeof PanelCard>;
|
||||
|
||||
type LegendItem = {
|
||||
label: string;
|
||||
color: string; // hex format
|
||||
@@ -14,27 +16,16 @@ type LegendItem = {
|
||||
export type LegendSize = "sm" | "default";
|
||||
|
||||
export function ChartCard({
|
||||
title,
|
||||
subtitle,
|
||||
cta,
|
||||
legend,
|
||||
legendSize,
|
||||
enabledLegend,
|
||||
onSeriesToggle,
|
||||
children,
|
||||
className,
|
||||
titleTooltip,
|
||||
}: {
|
||||
title: string | ReactNode;
|
||||
subtitle?: string;
|
||||
cta?: { label: string; onClick: () => void };
|
||||
...panelCardProps
|
||||
}: PanelCardProps & {
|
||||
legend?: Array<LegendItem>;
|
||||
legendSize?: LegendSize;
|
||||
enabledLegend?: Array<LegendItem>;
|
||||
onSeriesToggle?: (label: string) => void;
|
||||
className?: string;
|
||||
titleTooltip?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const legendComponent =
|
||||
legend && legend.length > 0 ? (
|
||||
@@ -43,13 +34,18 @@ export function ChartCard({
|
||||
|
||||
return (
|
||||
<PanelCard
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
cta={cta}
|
||||
headerContent={legendComponent}
|
||||
className={className}
|
||||
titleTooltip={titleTooltip}>
|
||||
{children}
|
||||
{...panelCardProps}
|
||||
headerContent={
|
||||
panelCardProps.headerContent ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{panelCardProps.headerContent}
|
||||
{legendComponent}
|
||||
</div>
|
||||
) : (
|
||||
legendComponent
|
||||
)
|
||||
}>
|
||||
{panelCardProps.children}
|
||||
</PanelCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import type { ReactNode } from "react";
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useId, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { InfoBadge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
|
||||
export function PanelCard({
|
||||
title,
|
||||
@@ -12,6 +18,8 @@ export function PanelCard({
|
||||
className,
|
||||
titleTooltip,
|
||||
children,
|
||||
collapsible = false,
|
||||
defaultCollapsed = false,
|
||||
}: {
|
||||
title: string | ReactNode;
|
||||
subtitle?: string;
|
||||
@@ -20,22 +28,75 @@ export function PanelCard({
|
||||
className?: string;
|
||||
titleTooltip?: string;
|
||||
children: ReactNode;
|
||||
collapsible?: boolean;
|
||||
defaultCollapsed?: boolean;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
||||
const contentId = useId();
|
||||
const titleId = useId();
|
||||
const [animationParent] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
const toggleCollapse = () => {
|
||||
setIsCollapsed((prev) => !prev);
|
||||
};
|
||||
|
||||
const isStringTitle = typeof title === "string";
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={animationParent}
|
||||
className={classNames(
|
||||
"bg-muted group relative flex w-full flex-col items-center rounded-2xl px-1 pb-1",
|
||||
"bg-muted group relative flex w-full flex-col items-center rounded-2xl px-1",
|
||||
!isCollapsed && "pb-1",
|
||||
className
|
||||
)}>
|
||||
<div className="flex h-11 w-full shrink-0 items-center justify-between gap-2 px-4">
|
||||
{typeof title === "string" ? (
|
||||
<div className="mr-4 flex shrink-0 items-center gap-1">
|
||||
<h2 className="text-emphasis shrink-0 text-sm font-semibold">{title}</h2>
|
||||
{titleTooltip && <InfoBadge content={titleTooltip} />}
|
||||
</div>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{collapsible && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="icon"
|
||||
color="minimal"
|
||||
CustomStartIcon={
|
||||
<Icon
|
||||
name="chevron-up"
|
||||
className={classNames(
|
||||
"text-default h-4 w-4 transition-transform",
|
||||
isCollapsed && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
}
|
||||
onClick={toggleCollapse}
|
||||
className="text-muted -ml-2"
|
||||
aria-expanded={!isCollapsed}
|
||||
aria-controls={contentId}
|
||||
aria-label={isCollapsed ? t("expand_panel") : t("collapse_panel")}
|
||||
/>
|
||||
)}
|
||||
{isStringTitle ? (
|
||||
<div className="mr-4 flex shrink-0 items-center gap-1">
|
||||
<h2 id={titleId} className="text-emphasis shrink-0 text-sm font-semibold">
|
||||
{collapsible ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleCollapse}
|
||||
className="text-left transition-opacity hover:opacity-80"
|
||||
aria-expanded={!isCollapsed}
|
||||
aria-controls={contentId}
|
||||
aria-label={isCollapsed ? t("expand_panel") : t("collapse_panel")}>
|
||||
{title as string}
|
||||
</button>
|
||||
) : (
|
||||
(title as string)
|
||||
)}
|
||||
</h2>
|
||||
{titleTooltip && <InfoBadge content={titleTooltip} />}
|
||||
</div>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</div>
|
||||
<div className="no-scrollbar flex items-center gap-2 overflow-x-auto">
|
||||
{headerContent}
|
||||
{cta && (
|
||||
@@ -45,14 +106,20 @@ export function PanelCard({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-default border-muted w-full grow gap-3 rounded-xl border">
|
||||
{subtitle && (
|
||||
<h3 className="text-subtle border-muted border-b p-3 text-sm font-medium leading-none">
|
||||
{subtitle}
|
||||
</h3>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
{!(isCollapsed && collapsible) && (
|
||||
<div
|
||||
id={contentId}
|
||||
role="region"
|
||||
aria-labelledby={isStringTitle ? titleId : undefined}
|
||||
className="bg-default border-muted w-full grow gap-3 rounded-xl border">
|
||||
{subtitle && (
|
||||
<h3 className="text-subtle border-muted border-b p-3 text-sm font-medium leading-none">
|
||||
{subtitle}
|
||||
</h3>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user