From 1b34bc8761e119b4d011f1726e9b781e0ed3cca8 Mon Sep 17 00:00:00 2001 From: hasan-deriv Date: Mon, 16 Oct 2023 13:04:53 +0800 Subject: [PATCH] feat: button component --- package-lock.json | 48 ++++++-- package.json | 3 +- src/App.tsx | 5 +- .../ui/button/__test__/button.test.tsx | 112 ++++++++++++++++++ src/components/ui/button/index.tsx | 87 ++++++++++++++ tailwind.config.js | 2 +- 6 files changed, 247 insertions(+), 10 deletions(-) create mode 100644 src/components/ui/button/__test__/button.test.tsx create mode 100644 src/components/ui/button/index.tsx diff --git a/package-lock.json b/package-lock.json index cd86d9a7d2..d20af60855 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "smarttrader", "version": "0.0.0", "dependencies": { + "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "react": "^18.2.0", @@ -379,7 +380,6 @@ "version": "7.23.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -658,6 +658,41 @@ "node": ">= 8" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -976,13 +1011,13 @@ "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.2.21", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1002,7 +1037,7 @@ "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "devOptional": true }, "node_modules/@types/semver": { "version": "7.5.2", @@ -1859,7 +1894,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true + "devOptional": true }, "node_modules/data-urls": { "version": "4.0.0", @@ -4255,8 +4290,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", diff --git a/package.json b/package.json index 664cbe62b6..eb7b3ec4e3 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,9 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { - "clsx": "^2.0.0", + "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", + "clsx": "^2.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "tailwind-merge": "^1.14.0", diff --git a/src/App.tsx b/src/App.tsx index c01508007b..060063eda8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,14 @@ import ErrorBoundary from 'Components/common/error-boundary'; import Layout from 'Components/layout'; +import { Button } from 'Components/ui/button'; import AuthProvider from 'Contexts/authProvider'; const App = () => ( - App + + + ); diff --git a/src/components/ui/button/__test__/button.test.tsx b/src/components/ui/button/__test__/button.test.tsx new file mode 100644 index 0000000000..4d91a6cb74 --- /dev/null +++ b/src/components/ui/button/__test__/button.test.tsx @@ -0,0 +1,112 @@ +import { describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event' +import { Button } from '..'; + +describe("Button component", () => { + it('should render the button with children', async () => { + render(); + const btnElement = screen.getByText("Button"); + expect(btnElement).toBeInTheDocument(); + }); + + it("should call the onClick handler when button is clicked", async () => { + const onClickMock = vi.fn(); + render(); + const btnElement = screen.getByText("Button"); + await userEvent.click(btnElement) + expect(onClickMock).toHaveBeenCalled() + }) + + it("should apply the default styling when no props are passed", () => { + render(); + const btnElement = screen.getByText("Button"); + expect(btnElement).toHaveClass("bg-primary", "text-white", "text-base") + }); + + it("should apply contained secondary styling when color prop is 'secondary' and variant prop is 'contained'", () => { + render(); + const btnElement = screen.getByText("Button"); + expect(btnElement).toHaveClass("bg-secondary") + }) + + it("should apply outlined primary styling when color prop is 'primary' and variant prop is 'outlined'", () => { + render(); + const btnElement = screen.getByText("Button"); + expect(btnElement).toHaveClass("bg-transparent", "border-primary") + }) + + it("should apply outlined secondary styling when color prop is 'secondary' and variant prop is 'outlined'", () => { + render(); + const btnElement = screen.getByText("Button"); + expect(btnElement).toHaveClass("bg-transparent", "border-secondary") + }) + + it("should apply outlined secondary styling when color prop is 'secondary' and variant prop is 'outlined'", () => { + render(); + const btnElement = screen.getByText("Button"); + expect(btnElement).toHaveClass("bg-transparent", "border-secondary") + }) + + it("should apply small size style when size prop is 'sm'", () => { + render(); + const btnElement = screen.getByText("Button"); + expect(btnElement).toHaveClass("px-3") + }) + + it("should apply medium size style when size prop is 'md'", () => { + render(); + const btnElement = screen.getByText("Button"); + expect(btnElement).toHaveClass("text-[0.875rem]") + }) + + it("should have few same classes for size 'default' and 'md'", () => { + render(
+ + +
); + const defaultBtnElement = screen.getByText("Default Button"); + const mediumBtnElement = screen.getByText("Medium Button"); + + const commonClass = "min-h-[2rem]" + expect(defaultBtnElement).toHaveClass(commonClass) + expect(mediumBtnElement).toHaveClass(commonClass) + }) + + it("should have same padding block classe for all size", () => { + render(
+ + + +
); + const defaultBtnElement = screen.getByText("Default Button"); + const mediumBtnElement = screen.getByText("Medium Button"); + const smallBtnElement = screen.getByText("Small Button"); + + const commonClass = "py-1" + expect(defaultBtnElement).toHaveClass(commonClass) + expect(mediumBtnElement).toHaveClass(commonClass) + expect(smallBtnElement).toHaveClass(commonClass) + }) + + it('should apply the fullwidth styling when fullwidth prop is true', () => { + render() + const btnElement = screen.getByText("Full Width Button"); + expect(btnElement).toHaveClass("w-full"); + }); + + it('should apply custom class and styles correctly', () => { + render( + + ); + const btnElement = screen.getByText("Button"); + expect(btnElement).toHaveClass("custom-class"); + expect(btnElement).toHaveStyle({ fontSize: "20px" }); + }); + + it('should render a disabled button when disabled prop is true', () => { + const { getByText } = render(); + const button = getByText('Click me'); + expect(button).toBeDisabled(); + }); +}) \ No newline at end of file diff --git a/src/components/ui/button/index.tsx b/src/components/ui/button/index.tsx new file mode 100644 index 0000000000..3467fc34c9 --- /dev/null +++ b/src/components/ui/button/index.tsx @@ -0,0 +1,87 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "Utils/cn" + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded font-bold border border-solid transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + contained: "text-white", + outlined: "bg-transparent text-prominent" + }, + color: { + primary: "border-primary hover:border-primary-hover focus:border-primary-hover", + secondary: "border-secondary", + success: "border-success", + }, + size: { + default: "text-base", + sm: "text-[0.75rem] px-3", + md: "text-[0.875rem]", + }, + fullwidth: { + true: "flex w-full", + }, + }, + compoundVariants: [ + { + variant: "contained", + color: "primary", + className: "bg-primary hover:bg-primary-hover focus:bg-primary-hover" + }, + { + variant: "contained", + color: "secondary", + className: "bg-secondary" + }, + { + variant: "outlined", + color: "primary", + className: "hover:bg-primary-hover focus:bg-primary-hover" + }, + { + variant: "outlined", + color: "secondary", + className: "hover:bg-secondary/[0.08] focus:bg-secondary/[0.08]" + }, + { + size: ["default", "md"], + className: "px-5 min-h-[2rem] min-w-[3.5rem]" + }, + { + size: ["default", "md", "sm"], + className: "py-1" + } + ], + defaultVariants: { + variant: "contained", + color: "primary", + size: "default", + fullwidth: false + }, + } +) + +export interface ButtonProps + extends Omit, "color">, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, color, fullwidth, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/tailwind.config.js b/tailwind.config.js index 0211d09d04..cea2a22329 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -27,7 +27,7 @@ module.exports = { 100: "#e6e9e9", 200: "#d6d6d6" }, - prominent:" #333333", + prominent: "#333333", "colored-barrier": "#008000", active: "#d6dadb", danger: "#ec3f3f",