mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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>
This commit is contained in:
co-authored by
Steven Masley
parent
cccdf1ecac
commit
464fccd807
@@ -0,0 +1,120 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Button } from "../Button/Button";
|
||||
import { CollapsibleSummary } from "./CollapsibleSummary";
|
||||
|
||||
const meta: Meta<typeof CollapsibleSummary> = {
|
||||
title: "components/CollapsibleSummary",
|
||||
component: CollapsibleSummary,
|
||||
args: {
|
||||
label: "Advanced options",
|
||||
children: (
|
||||
<>
|
||||
<div className="p-2 border border-border rounded-md border-solid">
|
||||
Option 1
|
||||
</div>
|
||||
<div className="p-2 border border-border rounded-md border-solid">
|
||||
Option 2
|
||||
</div>
|
||||
<div className="p-2 border border-border rounded-md border-solid">
|
||||
Option 3
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CollapsibleSummary>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const DefaultOpen: Story = {
|
||||
args: {
|
||||
defaultOpen: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const MediumSize: Story = {
|
||||
args: {
|
||||
size: "md",
|
||||
},
|
||||
};
|
||||
|
||||
export const SmallSize: Story = {
|
||||
args: {
|
||||
size: "sm",
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomClassName: Story = {
|
||||
args: {
|
||||
className: "text-blue-500 font-bold",
|
||||
},
|
||||
};
|
||||
|
||||
export const ManyChildren: Story = {
|
||||
args: {
|
||||
defaultOpen: true,
|
||||
children: (
|
||||
<>
|
||||
{Array.from({ length: 10 }).map((_, i) => (
|
||||
<div
|
||||
key={`option-${i + 1}`}
|
||||
className="p-2 border border-border rounded-md border-solid"
|
||||
>
|
||||
Option {i + 1}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const NestedCollapsible: Story = {
|
||||
args: {
|
||||
defaultOpen: true,
|
||||
children: (
|
||||
<>
|
||||
<div className="p-2 border border-border rounded-md border-solid">
|
||||
Option 1
|
||||
</div>
|
||||
<CollapsibleSummary label="Nested options" size="sm">
|
||||
<div className="p-2 border border-border rounded-md border-solid">
|
||||
Nested Option 1
|
||||
</div>
|
||||
<div className="p-2 border border-border rounded-md border-solid">
|
||||
Nested Option 2
|
||||
</div>
|
||||
</CollapsibleSummary>
|
||||
<div className="p-2 border border-border rounded-md border-solid">
|
||||
Option 3
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const ComplexContent: Story = {
|
||||
args: {
|
||||
defaultOpen: true,
|
||||
children: (
|
||||
<div className="p-4 border border-border rounded-md bg-surface-secondary">
|
||||
<h3 className="text-lg font-bold mb-2">Complex Content</h3>
|
||||
<p className="mb-4">
|
||||
This is a more complex content example with various elements.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button>Action 1</Button>
|
||||
<Button>Action 2</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const LongLabel: Story = {
|
||||
args: {
|
||||
label:
|
||||
"This is a very long label that might wrap or cause layout issues if not handled properly",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
const collapsibleSummaryVariants = cva(
|
||||
`flex items-center gap-1 p-0 bg-transparent border-0 text-inherit cursor-pointer
|
||||
transition-colors text-content-secondary hover:text-content-primary font-medium
|
||||
whitespace-nowrap`,
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
md: "text-sm",
|
||||
sm: "text-xs",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface CollapsibleSummaryProps
|
||||
extends VariantProps<typeof collapsibleSummaryVariants> {
|
||||
/**
|
||||
* The label to display for the collapsible section
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* The content to show when expanded
|
||||
*/
|
||||
children: ReactNode;
|
||||
/**
|
||||
* Whether the section is initially expanded
|
||||
*/
|
||||
defaultOpen?: boolean;
|
||||
/**
|
||||
* Optional className for the button
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The size of the component
|
||||
*/
|
||||
size?: "md" | "sm";
|
||||
}
|
||||
|
||||
export const CollapsibleSummary: FC<CollapsibleSummaryProps> = ({
|
||||
label,
|
||||
children,
|
||||
defaultOpen = false,
|
||||
className,
|
||||
size,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<button
|
||||
className={cn(
|
||||
collapsibleSummaryVariants({ size }),
|
||||
isOpen && "text-content-primary",
|
||||
className,
|
||||
)}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsOpen((v) => !v);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center transition-transform duration-200",
|
||||
isOpen ? "rotate-90" : "rotate-0",
|
||||
)}
|
||||
>
|
||||
<ChevronRightIcon
|
||||
className={cn(
|
||||
"p-0.5",
|
||||
size === "sm" ? "size-icon-xs" : "size-icon-sm",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<span className="sr-only">
|
||||
({isOpen ? "Hide" : "Show"}) {label}
|
||||
</span>
|
||||
<span className="[&:first-letter]:uppercase">{label}</span>
|
||||
</button>
|
||||
|
||||
{isOpen && <div className="flex flex-col gap-4">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import Checkbox from "@mui/material/Checkbox";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import type { SlimRole } from "api/typesGenerated";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { CollapsibleSummary } from "components/CollapsibleSummary/CollapsibleSummary";
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipContent,
|
||||
@@ -159,41 +160,18 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
|
||||
/>
|
||||
))}
|
||||
{advancedRoles.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
className={cn([
|
||||
"flex items-center gap-1 p-0 bg-transparent border-0 text-inherit text-sm cursor-pointer",
|
||||
"transition-colors text-content-secondary hover:text-content-primary font-medium whitespace-nowrap",
|
||||
isAdvancedOpen && "text-content-primary",
|
||||
])}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsAdvancedOpen((v) => !v);
|
||||
}}
|
||||
>
|
||||
{isAdvancedOpen ? (
|
||||
<ChevronDownIcon className="size-icon-sm p-0.5" />
|
||||
) : (
|
||||
<ChevronRightIcon className="size-icon-sm p-0.5" />
|
||||
)}
|
||||
<span className="sr-only">
|
||||
({isAdvancedOpen ? "Hide" : "Show advanced"})
|
||||
</span>
|
||||
<span className="[&:first-letter]:uppercase">Advanced</span>
|
||||
</button>
|
||||
|
||||
{isAdvancedOpen &&
|
||||
advancedRoles.map((role) => (
|
||||
<Option
|
||||
key={role.name}
|
||||
onChange={handleChange}
|
||||
isChecked={selectedRoleNames.has(role.name)}
|
||||
value={role.name}
|
||||
name={role.display_name || role.name}
|
||||
description={roleDescriptions[role.name] ?? ""}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<CollapsibleSummary label="advanced" defaultOpen={isAdvancedOpen}>
|
||||
{advancedRoles.map((role) => (
|
||||
<Option
|
||||
key={role.name}
|
||||
onChange={handleChange}
|
||||
isChecked={selectedRoleNames.has(role.name)}
|
||||
value={role.name}
|
||||
name={role.display_name || role.name}
|
||||
description={roleDescriptions[role.name] ?? ""}
|
||||
/>
|
||||
))}
|
||||
</CollapsibleSummary>
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
Reference in New Issue
Block a user