diff --git a/packages/frappe-ui-react/src/components/buttonGroup/buttonGroup.stories.tsx b/packages/frappe-ui-react/src/components/buttonGroup/buttonGroup.stories.tsx new file mode 100644 index 00000000..fc74b080 --- /dev/null +++ b/packages/frappe-ui-react/src/components/buttonGroup/buttonGroup.stories.tsx @@ -0,0 +1,204 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { ArrowUpDown, ChevronDown, ListFilter, SmilePlus } from "lucide-react"; + +import ButtonGroup from "./buttonGroup"; +import type { ButtonGroupProps } from "./types"; + +export default { + title: "Components/ButtonGroup", + component: ButtonGroup, + tags: ["autodocs"], + argTypes: { + className: { + control: "text", + description: "Additional CSS classes for the button group container", + }, + buttons: { + control: "object", + description: "Array of button configurations", + }, + size: { + control: { type: "select", options: ["sm", "md", "lg", "xl", "2xl"] }, + description: "Size of the buttons in the group", + }, + variant: { + control: { + type: "select", + options: ["solid", "subtle", "outline", "ghost"], + }, + description: "Variant style of the buttons", + }, + theme: { + control: { type: "select", options: ["gray", "blue", "green", "red"] }, + description: "Theme color of the buttons", + }, + }, + parameters: { docs: { source: { type: "dynamic" } }, layout: "centered" }, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + buttons: [ + { + id: "btn-1", + label: "Group by", + }, + { + id: "btn-2", + label: "Sort", + }, + { + id: "btn-3", + label: "Filters", + }, + ], + size: "sm", + variant: "subtle", + }, + render: (args) => ( +
+ +
+ ), +}; + +export const IconSubtle: Story = { + args: { + buttons: [ + { + id: "btn-1", + icon: "phone", + }, + { + id: "btn-2", + icon: "mail", + }, + { + id: "btn-3", + icon: "external-link", + }, + ], + size: "sm", + variant: "subtle", + theme: "gray", + }, + render: (args) => ( +
+ +
+ ), +}; + +export const IconMixedVariant: Story = { + args: { + buttons: [ + { + id: "btn-1", + icon: "corner-up-left", + }, + { + id: "btn-2", + icon: "map-pin", + }, + { + id: "btn-3", + variant: "subtle", + icon: () => , + }, + { + id: "btn-4", + icon: "more-horizontal", + }, + ], + size: "sm", + variant: "ghost", + theme: "gray", + }, + render: (args) => ( +
+ +
+ ), +}; + +export const IconWithLabelSubtle: Story = { + args: { + buttons: [ + { + id: "btn-1", + label: "Save view", + iconLeft: "plus", + }, + { + id: "btn-2", + label: "Sort", + iconLeft: () => , + }, + { + id: "btn-3", + label: "Filter", + iconLeft: () => , + iconRight: () => , + }, + { + id: "btn-4", + label: "Column", + iconLeft: "columns", + }, + { + id: "btn-5", + icon: "more-horizontal", + }, + ], + size: "md", + variant: "subtle", + theme: "gray", + }, + render: (args) => ( +
+ +
+ ), +}; + +export const IconWithLabelOutline: Story = { + args: { + buttons: [ + { + id: "btn-1", + label: "Save view", + iconLeft: "plus", + }, + { + id: "btn-2", + label: "Sort", + iconLeft: () => , + }, + { + id: "btn-3", + label: "Filter", + iconLeft: () => , + iconRight: () => , + }, + { + id: "btn-4", + label: "Column", + iconLeft: "columns", + }, + { + id: "btn-5", + icon: "more-horizontal", + }, + ], + size: "md", + variant: "outline", + theme: "gray", + }, + render: (args) => ( +
+ +
+ ), +}; diff --git a/packages/frappe-ui-react/src/components/buttonGroup/buttonGroup.tsx b/packages/frappe-ui-react/src/components/buttonGroup/buttonGroup.tsx new file mode 100644 index 00000000..c75a41a8 --- /dev/null +++ b/packages/frappe-ui-react/src/components/buttonGroup/buttonGroup.tsx @@ -0,0 +1,34 @@ +/** + * External dependencies. + */ +import clsx from "clsx"; + +/** + * Internal dependencies. + */ +import Button from "../button/button"; +import type { ButtonGroupProps } from "./types"; + +const ButtonGroup = ({ + buttons, + className, + size, + variant, + theme, +}: ButtonGroupProps) => { + return ( +
+ {buttons.map((buttonProps, index) => ( +
+ ); +}; + +export default ButtonGroup; diff --git a/packages/frappe-ui-react/src/components/buttonGroup/index.ts b/packages/frappe-ui-react/src/components/buttonGroup/index.ts new file mode 100644 index 00000000..223e4db1 --- /dev/null +++ b/packages/frappe-ui-react/src/components/buttonGroup/index.ts @@ -0,0 +1,2 @@ +export { default as ButtonGroup } from "./buttonGroup"; +export * from "./types"; diff --git a/packages/frappe-ui-react/src/components/buttonGroup/tests/buttonGroup.tsx b/packages/frappe-ui-react/src/components/buttonGroup/tests/buttonGroup.tsx new file mode 100644 index 00000000..f2a0e7fc --- /dev/null +++ b/packages/frappe-ui-react/src/components/buttonGroup/tests/buttonGroup.tsx @@ -0,0 +1,64 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import ButtonGroup from "../buttonGroup"; + +describe("ButtonGroup Component", () => { + const defaultButtons = [ + { label: "Button 1", onClick: jest.fn() }, + { label: "Button 2", onClick: jest.fn() }, + ]; + + const renderButtonGroup = (props = {}) => { + return render(); + }; + + it("renders multiple buttons", () => { + renderButtonGroup(); + expect(screen.getByText("Button 1")).toBeInTheDocument(); + expect(screen.getByText("Button 2")).toBeInTheDocument(); + }); + + it("applies shared props to all buttons", () => { + renderButtonGroup({ size: "sm", variant: "outline", theme: "red" }); + + const button1 = screen.getByText("Button 1").closest("button"); + const button2 = screen.getByText("Button 2").closest("button"); + + expect(button1).toHaveClass("h-7"); + expect(button1).toHaveClass("text-red-700"); + expect(button1).toHaveClass("border-outline-red-1"); + + expect(button2).toHaveClass("h-7"); + expect(button2).toHaveClass("text-red-700"); + expect(button2).toHaveClass("border-outline-red-1"); + }); + + it("applies custom global className", () => { + const { container } = renderButtonGroup({ + className: "custom-group-class", + }); + // ButtonGroup wraps buttons in a div + expect(container.firstChild).toHaveClass("custom-group-class"); + expect(container.firstChild).toHaveClass("flex"); + expect(container.firstChild).toHaveClass("gap-1"); + }); + + it("handles click events on individual buttons", () => { + const handleClick1 = jest.fn(); + const handleClick2 = jest.fn(); + + const buttons = [ + { label: "Action 1", onClick: handleClick1 }, + { label: "Action 2", onClick: handleClick2 }, + ]; + + render(); + + fireEvent.click(screen.getByText("Action 1")); + expect(handleClick1).toHaveBeenCalledTimes(1); + expect(handleClick2).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByText("Action 2")); + expect(handleClick2).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/frappe-ui-react/src/components/buttonGroup/types.ts b/packages/frappe-ui-react/src/components/buttonGroup/types.ts new file mode 100644 index 00000000..c50b3858 --- /dev/null +++ b/packages/frappe-ui-react/src/components/buttonGroup/types.ts @@ -0,0 +1,14 @@ +import type { + ButtonProps, + ButtonSize, + ButtonTheme, + ButtonVariant, +} from "../button/types"; + +export interface ButtonGroupProps { + buttons: (ButtonProps & { id?: string })[]; + className?: string; + size?: ButtonSize; + variant?: ButtonVariant; + theme?: ButtonTheme; +} diff --git a/packages/frappe-ui-react/src/components/index.ts b/packages/frappe-ui-react/src/components/index.ts index a1a7101c..957c49a4 100644 --- a/packages/frappe-ui-react/src/components/index.ts +++ b/packages/frappe-ui-react/src/components/index.ts @@ -4,6 +4,7 @@ export * from "./avatar"; export * from "./badge"; export * from "./breadcrumbs"; export * from "./button"; +export * from "./buttonGroup"; export * from "./calendar"; export * from "./charts"; export * from "./checkbox";