Skip to content

Commit

Permalink
feat(primitive): add button component
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexGarrixen committed Dec 16, 2023
1 parent 81ccb72 commit 6d4d267
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 0 deletions.
45 changes: 45 additions & 0 deletions src/components/primitives/button/__tests__/button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createRef } from "react";
import { describe, it } from "@jest/globals";
import { fireEvent, render } from "@testing-library/react";

import { Button } from "../button";

describe("Primitives / Button", () => {
it("Correct rendering and unmount", () => {
const screen = render(<Button />);

expect(() => screen.unmount()).not.toThrow();
});

it("Successful ref forwarding", () => {
const ref = createRef<HTMLButtonElement>();

render(<Button ref={ref} />);

expect(ref.current).not.toBeUndefined();
});

it("Verify that it is disabled", () => {
const screen = render(<Button disabled />);
const button = screen.getByRole("button") as HTMLInputElement;

expect(button).toBeDisabled();
});

it("Should trigger onClick function", () => {
const mockHandler = jest.fn();
const screen = render(<Button onClick={mockHandler} />);
const button = screen.getByRole("button");

fireEvent.click(button);

expect(mockHandler).toHaveBeenCalled();
});

it("Verify that you have the correct type attribute", () => {
const screen = render(<Button type="submit" />);
const button = screen.getByRole("button");

expect(button).toHaveAttribute("type", "submit");
});
});
78 changes: 78 additions & 0 deletions src/components/primitives/button/button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { StoryObj, Meta } from "@storybook/react";

import { css } from "@root/styled-system/css";
import { stack } from "@root/styled-system/patterns";

import { Button } from "./button";

export default {
title: "Primitives/Button",
tags: ["autodocs"],
decorators: [
(Story) => (
<div className={css({ p: "4" })}>
<Story />
</div>
),
],
} satisfies Meta<typeof Button>;

type Story = StoryObj<typeof Button>;

export const Preview: Story = {
name: "Default Preview",
args: {
children: "Sample Text",
},
render: (args) => <Button {...args} />,
};

export const Sizes: Story = {
render: () => (
<div className={stack({ gap: "3", direction: "row", flexWrap: "wrap" })}>
<Button size="sm">Small</Button>
<Button>Medium</Button>
</div>
),
};

export const RecipeVariantProps: Story = {
render: () => (
<div className={stack({ gap: "3", direction: "row", flexWrap: "wrap" })}>
<Button>Outline</Button>
<Button variant="solid">Solid</Button>
<div className={css({ backgroundColor: "bg-primary-solid", p: "2" })}>
<Button variant="solid-white">Solid White</Button>
</div>
</div>
),
};

export const OnlyIcon: Story = {
render: () => (
<div className={stack({ gap: "3", direction: "row", flexWrap: "wrap" })}>
<Button isIconOnly size="sm">
<svg height="20" viewBox="0 0 24 24" width="20">
<g fill="none" fillRule="nonzero">
<path d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002-.071.035-.02.004-.014-.004-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01-.017.428.005.02.01.013.104.074.015.004.012-.004.104-.074.012-.016.004-.017-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113-.013.002-.185.093-.01.01-.003.011.018.43.005.012.008.007.201.093c.012.004.023 0 .029-.008l.004-.014-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014-.034.614c0 .012.007.02.017.024l.015-.002.201-.093.01-.008.004-.011.017-.43-.003-.012-.01-.01-.184-.092Z" />
<path
d="M21 5.18V17a4 4 0 1 1-2-3.465V9.181L9 10.847V18c0 .06-.005.117-.015.174A3.5 3.5 0 1 1 7 15.337v-8.49a2 2 0 0 1 1.671-1.973l10-1.666A2 2 0 0 1 21 5.18ZM5.5 17a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3ZM17 15a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm2-9.82L9 6.847V8.82l10-1.667V5.18Z"
fill="#09244BFF"
/>
</g>
</svg>
</Button>
<Button isIconOnly>
<svg height="24" viewBox="0 0 24 24" width="24">
<g fill="none" fillRule="nonzero">
<path d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002-.071.035-.02.004-.014-.004-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01-.017.428.005.02.01.013.104.074.015.004.012-.004.104-.074.012-.016.004-.017-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113-.013.002-.185.093-.01.01-.003.011.018.43.005.012.008.007.201.093c.012.004.023 0 .029-.008l.004-.014-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014-.034.614c0 .012.007.02.017.024l.015-.002.201-.093.01-.008.004-.011.017-.43-.003-.012-.01-.01-.184-.092Z" />
<path
d="M21 5.18V17a4 4 0 1 1-2-3.465V9.181L9 10.847V18c0 .06-.005.117-.015.174A3.5 3.5 0 1 1 7 15.337v-8.49a2 2 0 0 1 1.671-1.973l10-1.666A2 2 0 0 1 21 5.18ZM5.5 17a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3ZM17 15a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm2-9.82L9 6.847V8.82l10-1.667V5.18Z"
fill="#09244BFF"
/>
</g>
</svg>
</Button>
</div>
),
};
25 changes: 25 additions & 0 deletions src/components/primitives/button/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { ButtonHTMLAttributes } from "react";

import { forwardRef } from "react";
import { button } from "@root/styled-system/recipes";

import type { RecipeVariantProps } from "@root/styled-system/css";

type ButtonElementProps = ButtonHTMLAttributes<HTMLButtonElement>;
type RecipeProps = RecipeVariantProps<typeof button>;
type Props = RecipeProps & Omit<ButtonElementProps, keyof RecipeProps>;

export const Button = forwardRef<HTMLButtonElement, Props>(
({ children, size, variant, className, isIconOnly, ...props }, ref) => {
const classes = button({ size, variant, isIconOnly });

return (
// eslint-disable-next-line react/button-has-type
<button ref={ref} className={`${classes} ${className ?? ""}`} {...props}>
{children}
</button>
);
},
);

Button.displayName = "Button";
71 changes: 71 additions & 0 deletions theme/recipes/button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { defineRecipe } from "@pandacss/dev";

export default defineRecipe({
className: "button",
base: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
fontWeight: "700",
cursor: "pointer",
},
variants: {
size: {
sm: {
height: "8",
textStyle: "body-sm",
px: "3",
rounded: "md",
},
md: {
height: "10",
textStyle: "body-base",
px: "4",
rounded: "lg",
},
},
variant: {
outline: {
borderWidth: "1px",
borderColor: "border-primary",
borderStyle: "solid",
backgroundColor: "transparent",
color: "text-primary",
},
solid: {
backgroundColor: "bg-primary-solid",
color: "white",
},
"solid-white": {
backgroundColor: "white",
color: "text-primary",
},
},
isIconOnly: {
true: {
rounded: "999px!",
px: "0!",
},
},
},
compoundVariants: [
{
isIconOnly: true,
size: "md",
css: {
width: "10!",
},
},
{
isIconOnly: true,
size: "sm",
css: {
width: "8!",
},
},
],
defaultVariants: {
size: "md",
variant: "outline",
},
});
2 changes: 2 additions & 0 deletions theme/recipes/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import inputRecipe from "./input";
import sliderRecipe from "./slider";
import buttonRecipe from "./button";

export const recipes = {
input: inputRecipe,
button: buttonRecipe,
};

export const slotRecipes = {
Expand Down

0 comments on commit 6d4d267

Please sign in to comment.