Skip to content

Commit 8aec4f2

Browse files
gcp-cherry-pick-bot[bot]jaaydenhEmyrk
authored
chore: create collapsible summary component (cherry-pick #16705) (#16794)
Cherry-picked chore: create collapsible summary component (#16705) This is based on the Figma designs here: https://www.figma.com/design/WfqIgsTFXN2BscBSSyXWF8/Coder-kit?node-id=507-1525&m=dev --------- Co-authored-by: Steven Masley <stevenmasley@gmail.com> Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com> Co-authored-by: Steven Masley <stevenmasley@gmail.com>
1 parent e54e31e commit 8aec4f2

File tree

3 files changed

+224
-35
lines changed

3 files changed

+224
-35
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { Button } from "../Button/Button";
3+
import { CollapsibleSummary } from "./CollapsibleSummary";
4+
5+
const meta: Meta<typeof CollapsibleSummary> = {
6+
title: "components/CollapsibleSummary",
7+
component: CollapsibleSummary,
8+
args: {
9+
label: "Advanced options",
10+
children: (
11+
<>
12+
<div className="p-2 border border-border rounded-md border-solid">
13+
Option 1
14+
</div>
15+
<div className="p-2 border border-border rounded-md border-solid">
16+
Option 2
17+
</div>
18+
<div className="p-2 border border-border rounded-md border-solid">
19+
Option 3
20+
</div>
21+
</>
22+
),
23+
},
24+
};
25+
26+
export default meta;
27+
type Story = StoryObj<typeof CollapsibleSummary>;
28+
29+
export const Default: Story = {};
30+
31+
export const DefaultOpen: Story = {
32+
args: {
33+
defaultOpen: true,
34+
},
35+
};
36+
37+
export const MediumSize: Story = {
38+
args: {
39+
size: "md",
40+
},
41+
};
42+
43+
export const SmallSize: Story = {
44+
args: {
45+
size: "sm",
46+
},
47+
};
48+
49+
export const CustomClassName: Story = {
50+
args: {
51+
className: "text-blue-500 font-bold",
52+
},
53+
};
54+
55+
export const ManyChildren: Story = {
56+
args: {
57+
defaultOpen: true,
58+
children: (
59+
<>
60+
{Array.from({ length: 10 }).map((_, i) => (
61+
<div
62+
key={`option-${i + 1}`}
63+
className="p-2 border border-border rounded-md border-solid"
64+
>
65+
Option {i + 1}
66+
</div>
67+
))}
68+
</>
69+
),
70+
},
71+
};
72+
73+
export const NestedCollapsible: Story = {
74+
args: {
75+
defaultOpen: true,
76+
children: (
77+
<>
78+
<div className="p-2 border border-border rounded-md border-solid">
79+
Option 1
80+
</div>
81+
<CollapsibleSummary label="Nested options" size="sm">
82+
<div className="p-2 border border-border rounded-md border-solid">
83+
Nested Option 1
84+
</div>
85+
<div className="p-2 border border-border rounded-md border-solid">
86+
Nested Option 2
87+
</div>
88+
</CollapsibleSummary>
89+
<div className="p-2 border border-border rounded-md border-solid">
90+
Option 3
91+
</div>
92+
</>
93+
),
94+
},
95+
};
96+
97+
export const ComplexContent: Story = {
98+
args: {
99+
defaultOpen: true,
100+
children: (
101+
<div className="p-4 border border-border rounded-md bg-surface-secondary">
102+
<h3 className="text-lg font-bold mb-2">Complex Content</h3>
103+
<p className="mb-4">
104+
This is a more complex content example with various elements.
105+
</p>
106+
<div className="flex gap-2">
107+
<Button>Action 1</Button>
108+
<Button>Action 2</Button>
109+
</div>
110+
</div>
111+
),
112+
},
113+
};
114+
115+
export const LongLabel: Story = {
116+
args: {
117+
label:
118+
"This is a very long label that might wrap or cause layout issues if not handled properly",
119+
},
120+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { type VariantProps, cva } from "class-variance-authority";
2+
import { ChevronRightIcon } from "lucide-react";
3+
import { type FC, type ReactNode, useState } from "react";
4+
import { cn } from "utils/cn";
5+
6+
const collapsibleSummaryVariants = cva(
7+
`flex items-center gap-1 p-0 bg-transparent border-0 text-inherit cursor-pointer
8+
transition-colors text-content-secondary hover:text-content-primary font-medium
9+
whitespace-nowrap`,
10+
{
11+
variants: {
12+
size: {
13+
md: "text-sm",
14+
sm: "text-xs",
15+
},
16+
},
17+
defaultVariants: {
18+
size: "md",
19+
},
20+
},
21+
);
22+
23+
export interface CollapsibleSummaryProps
24+
extends VariantProps<typeof collapsibleSummaryVariants> {
25+
/**
26+
* The label to display for the collapsible section
27+
*/
28+
label: string;
29+
/**
30+
* The content to show when expanded
31+
*/
32+
children: ReactNode;
33+
/**
34+
* Whether the section is initially expanded
35+
*/
36+
defaultOpen?: boolean;
37+
/**
38+
* Optional className for the button
39+
*/
40+
className?: string;
41+
/**
42+
* The size of the component
43+
*/
44+
size?: "md" | "sm";
45+
}
46+
47+
export const CollapsibleSummary: FC<CollapsibleSummaryProps> = ({
48+
label,
49+
children,
50+
defaultOpen = false,
51+
className,
52+
size,
53+
}) => {
54+
const [isOpen, setIsOpen] = useState(defaultOpen);
55+
56+
return (
57+
<div className="flex flex-col gap-4">
58+
<button
59+
className={cn(
60+
collapsibleSummaryVariants({ size }),
61+
isOpen && "text-content-primary",
62+
className,
63+
)}
64+
type="button"
65+
onClick={() => {
66+
setIsOpen((v) => !v);
67+
}}
68+
>
69+
<div
70+
className={cn(
71+
"flex items-center justify-center transition-transform duration-200",
72+
isOpen ? "rotate-90" : "rotate-0",
73+
)}
74+
>
75+
<ChevronRightIcon
76+
className={cn(
77+
"p-0.5",
78+
size === "sm" ? "size-icon-xs" : "size-icon-sm",
79+
)}
80+
/>
81+
</div>
82+
<span className="sr-only">
83+
({isOpen ? "Hide" : "Show"}) {label}
84+
</span>
85+
<span className="[&:first-letter]:uppercase">{label}</span>
86+
</button>
87+
88+
{isOpen && <div className="flex flex-col gap-4">{children}</div>}
89+
</div>
90+
);
91+
};

site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx

+13-35
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Checkbox from "@mui/material/Checkbox";
33
import Tooltip from "@mui/material/Tooltip";
44
import type { SlimRole } from "api/typesGenerated";
55
import { Button } from "components/Button/Button";
6+
import { CollapsibleSummary } from "components/CollapsibleSummary/CollapsibleSummary";
67
import {
78
HelpTooltip,
89
HelpTooltipContent,
@@ -159,41 +160,18 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
159160
/>
160161
))}
161162
{advancedRoles.length > 0 && (
162-
<>
163-
<button
164-
className={cn([
165-
"flex items-center gap-1 p-0 bg-transparent border-0 text-inherit text-sm cursor-pointer",
166-
"transition-colors text-content-secondary hover:text-content-primary font-medium whitespace-nowrap",
167-
isAdvancedOpen && "text-content-primary",
168-
])}
169-
type="button"
170-
onClick={() => {
171-
setIsAdvancedOpen((v) => !v);
172-
}}
173-
>
174-
{isAdvancedOpen ? (
175-
<ChevronDownIcon className="size-icon-sm p-0.5" />
176-
) : (
177-
<ChevronRightIcon className="size-icon-sm p-0.5" />
178-
)}
179-
<span className="sr-only">
180-
({isAdvancedOpen ? "Hide" : "Show advanced"})
181-
</span>
182-
<span className="[&:first-letter]:uppercase">Advanced</span>
183-
</button>
184-
185-
{isAdvancedOpen &&
186-
advancedRoles.map((role) => (
187-
<Option
188-
key={role.name}
189-
onChange={handleChange}
190-
isChecked={selectedRoleNames.has(role.name)}
191-
value={role.name}
192-
name={role.display_name || role.name}
193-
description={roleDescriptions[role.name] ?? ""}
194-
/>
195-
))}
196-
</>
163+
<CollapsibleSummary label="advanced" defaultOpen={isAdvancedOpen}>
164+
{advancedRoles.map((role) => (
165+
<Option
166+
key={role.name}
167+
onChange={handleChange}
168+
isChecked={selectedRoleNames.has(role.name)}
169+
value={role.name}
170+
name={role.display_name || role.name}
171+
description={roleDescriptions[role.name] ?? ""}
172+
/>
173+
))}
174+
</CollapsibleSummary>
197175
)}
198176
</div>
199177
</fieldset>

0 commit comments

Comments
 (0)