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
Lines changed: 120 additions & 0 deletions
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+
};
Lines changed: 91 additions & 0 deletions
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

Lines changed: 13 additions & 35 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy