-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): add SplitButton component (#27)
Introduces a new SplitButton component, combining a primary action button with a dropdown menu for additional options. This component enhances user interface flexibility by providing quick access to a main action while offering related secondary actions in a compact design. The implementation includes various customization options such as different variants, tones, sizes, and icon support, along with Storybook documentation for easy integration and testing.
- Loading branch information
1 parent
9edb37b
commit 4752ed2
Showing
5 changed files
with
304 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
import { SplitButton, type SplitButtonProps } from "@synopsisapp/symbiosis-ui"; | ||
|
||
const meta: Meta<typeof SplitButton> = { | ||
title: "Components/SplitButton", | ||
component: SplitButton, | ||
tags: ["autodocs"], | ||
argTypes: { | ||
variant: { | ||
control: { type: "select" }, | ||
options: ["primary", "outline", "ghost"], | ||
description: "Visual style variant of the button", | ||
table: { | ||
type: { summary: "string" }, | ||
defaultValue: { summary: "primary" }, | ||
}, | ||
}, | ||
tone: { | ||
control: { type: "select" }, | ||
options: ["default", "destructive", "monochrome-light", "monochrome-dark"], | ||
description: "Color tone of the button", | ||
table: { | ||
type: { summary: "string" }, | ||
defaultValue: { summary: "default" }, | ||
}, | ||
}, | ||
size: { | ||
control: { type: "select" }, | ||
options: ["small-200", "small-100", "base"], | ||
description: "Size of the button", | ||
table: { | ||
type: { summary: "string" }, | ||
defaultValue: { summary: "base" }, | ||
}, | ||
}, | ||
leftIcon: { | ||
control: { type: "text" }, | ||
description: "Icon to display on the left side of the button", | ||
}, | ||
rightIcon: { | ||
control: { type: "text" }, | ||
description: "Icon to display on the right side of the button", | ||
}, | ||
isDisabled: { | ||
control: { type: "boolean" }, | ||
description: "Whether the button is disabled", | ||
}, | ||
isLoading: { | ||
control: { type: "boolean" }, | ||
description: "Whether the button is in a loading state", | ||
}, | ||
}, | ||
}; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof SplitButton>; | ||
|
||
const defaultItems = [ | ||
{ | ||
text: "Option 1", | ||
icon: "edit", | ||
onSelect: () => console.log("Option 1 selected"), | ||
}, | ||
{ | ||
text: "Option 2", | ||
icon: "close", | ||
onSelect: () => console.log("Option 2 selected"), | ||
}, | ||
{ | ||
text: "Section Title", | ||
isSectionTitle: true, | ||
isSeparated: true, | ||
}, | ||
{ | ||
text: "Option 3", | ||
icon: "symbiosis-chevron-down", | ||
onSelect: () => console.log("Option 3 selected"), | ||
}, | ||
]; | ||
|
||
export const Default: Story = { | ||
args: { | ||
label: "Split Button", | ||
variant: "primary", | ||
tone: "default", | ||
size: "base", | ||
isDisabled: false, | ||
isLoading: false, | ||
leftIcon: "edit", | ||
items: defaultItems as SplitButtonProps["items"], | ||
}, | ||
}; | ||
|
||
export const DifferentVariants: Story = { | ||
render: (args) => ( | ||
<div className="flex flex-col gap-4 items-start"> | ||
<SplitButton {...args} variant="primary" /> | ||
<SplitButton {...args} variant="outline" /> | ||
<SplitButton {...args} variant="ghost" /> | ||
</div> | ||
), | ||
args: { | ||
...Default.args, | ||
}, | ||
}; | ||
|
||
export const DifferentTones: Story = { | ||
render: (args) => ( | ||
<div className="flex flex-col gap-4 items-start"> | ||
<SplitButton {...args} tone="default" /> | ||
<SplitButton {...args} tone="destructive" /> | ||
<div className="bg-red-200 p-2"> | ||
(custom background container to showcase) | ||
<SplitButton {...args} tone="monochrome-light" /> | ||
</div> | ||
<SplitButton {...args} tone="monochrome-dark" /> | ||
<SplitButton {...args} layout="fullwidth" tone="destructive" /> | ||
<SplitButton {...args} layout="fullwidth" tone="monochrome-light" /> | ||
<SplitButton {...args} layout="fullwidth" tone="monochrome-dark" /> | ||
</div> | ||
), | ||
args: { | ||
...Default.args, | ||
}, | ||
}; | ||
|
||
export const DifferentSizes: Story = { | ||
render: (args) => ( | ||
<div className="flex flex-col gap-4 items-start"> | ||
<SplitButton {...args} size="small-200" /> | ||
<SplitButton {...args} size="small-100" /> | ||
<SplitButton {...args} size="base" /> | ||
</div> | ||
), | ||
args: { | ||
...Default.args, | ||
}, | ||
}; | ||
|
||
export const WithIcons: Story = { | ||
render: (args) => ( | ||
<div className="flex flex-col gap-4 items-start"> | ||
<SplitButton {...args} leftIcon="edit" /> | ||
<SplitButton {...args} rightIcon="google-play" /> | ||
<SplitButton {...args} leftIcon="edit" rightIcon="google-play" /> | ||
</div> | ||
), | ||
args: { | ||
...Default.args, | ||
}, | ||
}; | ||
|
||
export const States: Story = { | ||
render: (args) => ( | ||
<div className="flex flex-col gap-4 items-start"> | ||
<SplitButton {...args} isDisabled={true} /> | ||
<SplitButton {...args} isLoading={true} /> | ||
</div> | ||
), | ||
args: { | ||
...Default.args, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
|
||
import { Button } from "../Button"; | ||
import { cn } from "../../utils/cn"; | ||
import { Dropdown, DropdownMenuLabel, DropdownSimpleItem } from "../Dropdown"; | ||
import { IconButton } from "../IconButton"; | ||
import { iconButtonLeftBorderIconVariant } from "./styles"; | ||
import type { SplitButtonProps } from "./types"; | ||
|
||
export const SplitButton = ({ | ||
label, | ||
variant = "primary", | ||
size = "base", | ||
leftIcon, | ||
rightIcon, | ||
layout = "normal", | ||
type = "button", | ||
className, | ||
tone = "default", | ||
items, | ||
...restProps | ||
}: SplitButtonProps) => { | ||
return ( | ||
<div className="flex"> | ||
<Button | ||
variant={variant} | ||
layout={layout} | ||
size={size} | ||
leftIcon={leftIcon} | ||
rightIcon={rightIcon} | ||
type={type} | ||
className={cn("rounded-tr-none rounded-br-none", className)} | ||
tone={tone} | ||
label={label} | ||
{...restProps} | ||
/> | ||
<Dropdown.Root> | ||
<Dropdown.Trigger> | ||
<IconButton | ||
icon="symbiosis-chevron-down" | ||
variant={variant} | ||
size={size} | ||
isDisabled={restProps.isDisabled} | ||
tone={tone} | ||
renderAs="div" | ||
className={cn( | ||
"rounded-tl-none rounded-bl-none border-l", | ||
iconButtonLeftBorderIconVariant({ | ||
variant, | ||
tone, | ||
}), | ||
)} | ||
/> | ||
</Dropdown.Trigger> | ||
<Dropdown.Portal> | ||
<Dropdown.Content> | ||
{items.map((item) => { | ||
return ( | ||
<> | ||
{item.isSeparated && <Dropdown.Separator />} | ||
{item.isSectionTitle ? ( | ||
<DropdownMenuLabel key={`label-${item.text}`} label={item.text} /> | ||
) : ( | ||
<DropdownSimpleItem | ||
key={`item-${item.text}`} | ||
text={item.text} | ||
onSelect={() => { | ||
item.onSelect?.(); | ||
}} | ||
icon={item.icon} | ||
/> | ||
)} | ||
</> | ||
); | ||
})} | ||
</Dropdown.Content> | ||
</Dropdown.Portal> | ||
</Dropdown.Root> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { cva } from "class-variance-authority"; | ||
|
||
export const iconButtonLeftBorderIconVariant = cva([], { | ||
variants: { | ||
variant: { | ||
primary: [], | ||
outline: ["border-l-transparent"], | ||
ghost: ["border-l-transparent"], | ||
}, | ||
tone: { | ||
default: [], | ||
destructive: [], | ||
"monochrome-light": [], | ||
"monochrome-dark": [], | ||
}, | ||
}, | ||
compoundVariants: [ | ||
{ | ||
variant: "primary", | ||
tone: "default", | ||
className: ["border-l-mainColors-light-400 hover:border-l-mainColors-light-400"], | ||
}, | ||
{ | ||
variant: "primary", | ||
tone: "destructive", | ||
className: ["border-l-white hover:border-l-white"], | ||
}, | ||
{ | ||
variant: "primary", | ||
tone: "monochrome-light", | ||
className: ["border-l-slate-600 hover:border-l-slate-600"], | ||
}, | ||
{ | ||
variant: "primary", | ||
tone: "monochrome-dark", | ||
className: ["border-l-white hover:border-l-white"], | ||
}, | ||
], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import type { ButtonVariant, BaseProps } from "../Button/types"; | ||
import type { IconProps } from "../Icon/types"; | ||
|
||
export type SplitButtonItemProps = { | ||
text: string; | ||
content?: React.ReactNode; | ||
icon?: IconProps["name"]; | ||
onSelect?: () => void; | ||
wrapperClassName?: string; | ||
isSeparated?: boolean; | ||
isSectionTitle?: boolean; | ||
}; | ||
|
||
export type SplitButtonProps = Omit<BaseProps, "renderAs" | "variant"> & { | ||
items: SplitButtonItemProps[]; | ||
variant: Exclude<ButtonVariant, "link">; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters