From eebfa3f93496f3ee6f3c209577c1c95bc4ff162e Mon Sep 17 00:00:00 2001 From: Ionut Anin Date: Tue, 16 Jan 2024 20:10:26 +0200 Subject: [PATCH 1/2] feat: input and label components (#70) * Add tailwind config and update webpack config * Add colors variables * Add popup variables * Move PostCSS config to webpack config * Add initial button component * Update jest config and setup files * Add initial button tests * Fix error background * Add destructive variant * Update button tests * Add test job to workflows and update CI test script * Fix test name * Input and label components * Update src/components/input.tsx Co-authored-by: Radu-Cristian Popa * Update src/components/label.tsx Co-authored-by: Radu-Cristian Popa * Update src/components/label.tsx Co-authored-by: Radu-Cristian Popa * Update src/components/input.tsx Co-authored-by: Radu-Cristian Popa * Update src/components/__tests__/input.test.tsx Co-authored-by: Radu-Cristian Popa * Update src/components/input.tsx Co-authored-by: Radu-Cristian Popa * Update src/components/input.tsx Co-authored-by: Radu-Cristian Popa * Update src/components/input.tsx Co-authored-by: Radu-Cristian Popa * add input error message, add unit tests, updates based on code review * fix error message position * remove undefined fallback * remove absolute style on error message and change tag * add classes on error message --------- Co-authored-by: Radu-Cristian Popa --- src/components/__tests__/input.test.tsx | 55 ++++++++++++++++++++++++ src/components/icons.tsx | 32 ++++++++++++++ src/components/input.tsx | 56 +++++++++++++++++++++++++ src/components/label.tsx | 23 ++++++++++ src/popup/index.css | 3 +- tailwind.config.ts | 1 + 6 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/components/__tests__/input.test.tsx create mode 100644 src/components/input.tsx create mode 100644 src/components/label.tsx diff --git a/src/components/__tests__/input.test.tsx b/src/components/__tests__/input.test.tsx new file mode 100644 index 00000000..6f66e7a4 --- /dev/null +++ b/src/components/__tests__/input.test.tsx @@ -0,0 +1,55 @@ +import { render } from '@testing-library/react' +import React from 'react' + +import { Input } from '@/components/input' + +describe('Input', () => { + it('should default to `type="text"`', () => { + const { queryByLabelText } = render() + + expect(queryByLabelText('test input')).toBeInTheDocument() + expect(queryByLabelText('test input')).toHaveAttribute('type', 'text') + }) + + it('should not have the `disabled` attribute and `aria-disabled="false"` if `loading` is false', () => { + const { queryByLabelText } = render() + + expect(queryByLabelText('test input')).toBeInTheDocument() + expect(queryByLabelText('test input')).not.toHaveAttribute('disabled') + expect(queryByLabelText('test input')).toHaveAttribute('aria-disabled', 'false') + expect(queryByLabelText('test input')).not.toBeDisabled() + }) + + it('should have the `border-base` class by default', () => { + const { queryByLabelText } = render() + + expect(queryByLabelText('test input')).toBeInTheDocument() + expect(queryByLabelText('test input')).toHaveClass('border-base') + }) + + it('should have the `pl-12` class when the `icon` variant is passed', () => { + const { queryByLabelText } = render(} />) + + expect(queryByLabelText('test input')).toBeInTheDocument() + expect(queryByLabelText('test input')).toHaveClass('pl-12') + }) + + it('should have the `bg-disabled` and `border-transparent` classes when the `disabled` variant is passed', () => { + const { queryByLabelText } = render() + + expect(queryByLabelText('test input')).toBeInTheDocument() + expect(queryByLabelText('test input')).toHaveClass('bg-disabled') + expect(queryByLabelText('test input')).toHaveClass('border-transparent') + }) + + it('should have the `aria-invalid` and `aria-describedby` attributes if errorMessage is present', () => { + const { queryByLabelText, queryByText } = render( + , + ) + + expect(queryByLabelText('test input')).toBeInTheDocument() + expect(queryByLabelText('test input')).toHaveAttribute('aria-invalid') + expect(queryByLabelText('test input')).toHaveAttribute('aria-describedby') + expect(queryByText('some error')).toBeInTheDocument() + }) +}) diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 555791c8..ee34cb21 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -17,3 +17,35 @@ export const Spinner = (props: React.SVGProps) => { ) } + +export const DollarSign = (props: React.SVGProps) => { + return ( + + + + + + + + + + + ) +} diff --git a/src/components/input.tsx b/src/components/input.tsx new file mode 100644 index 00000000..c98ec04e --- /dev/null +++ b/src/components/input.tsx @@ -0,0 +1,56 @@ +import { type VariantProps, cva } from 'class-variance-authority' +import React, { forwardRef } from 'react' + +import { cn } from '@/utils/cn' + +const inputVariants = cva( + [ + 'w-full h-14 rounded-xl border border-2 px-4 text-base text-medium', + 'focus:outline-none focus:border-focus', + 'placeholder-disabled', + ], + + { + variants: { + variant: { + default: 'border-base', + }, + disabled: { + true: 'bg-disabled border-transparent', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +export interface InputProps + extends VariantProps, + React.InputHTMLAttributes { + errorMessage?: string + disabled?: boolean + icon?: React.ReactNode +} + +export const Input = forwardRef(function Input( + { type = 'text', icon, errorMessage, disabled, className, ...props }, + ref, +) { + return ( +
+ {icon &&
{icon}
} + + {errorMessage &&

{errorMessage}

} +
+ ) +}) diff --git a/src/components/label.tsx b/src/components/label.tsx new file mode 100644 index 00000000..94f6063f --- /dev/null +++ b/src/components/label.tsx @@ -0,0 +1,23 @@ +import { type VariantProps, cva } from 'class-variance-authority' +import React, { forwardRef } from 'react' + +import { cn } from '@/utils/cn' + +const labelVariants = cva('text-medium font-medium leading-6 px-2 flex items-center gap-2') + +export interface LabelProps + extends VariantProps, + React.LabelHTMLAttributes { + children: React.ReactNode +} + +export const Label = forwardRef(function Label( + { className, children, ...props }, + ref, +) { + return ( + + ) +}) diff --git a/src/popup/index.css b/src/popup/index.css index e78227f2..ea53da12 100644 --- a/src/popup/index.css +++ b/src/popup/index.css @@ -25,7 +25,8 @@ /* Border colors */ --border-base: 203 213 225; --border-focus: 59 130 246; - + --border-error: 220 38 38; + /* Popup */ --popup-width: 448px; --popup-height: 559px; diff --git a/tailwind.config.ts b/tailwind.config.ts index 62d3ee17..43ba8ef0 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -32,6 +32,7 @@ module.exports = { base: 'rgb(var(--border-base) / )', popup: 'rgb(var(--border-popup) / )', focus: 'rgb(var(--border-focus) / )', + error: 'rgb(var(--border-error) / )', }, }, }, From fe4c099f37cd5d4f1b94ff4a2f85ad6b4a1ec07d Mon Sep 17 00:00:00 2001 From: Ionut Anin Date: Wed, 17 Jan 2024 17:06:55 +0200 Subject: [PATCH 2/2] feat: switch component (#74) * create switch component and add unit tests * Update src/components/switch.tsx Co-authored-by: Radu-Cristian Popa * Update src/components/switch.tsx Co-authored-by: Radu-Cristian Popa * Update src/components/switch.tsx Co-authored-by: Radu-Cristian Popa * updates based on code review * update switch unit tests --------- Co-authored-by: Radu-Cristian Popa --- src/components/__tests__/switch.test.tsx | 69 ++++++++++++++++++++++++ src/components/switch.tsx | 54 +++++++++++++++++++ src/popup/Popup.tsx | 1 + tailwind.config.ts | 1 + 4 files changed, 125 insertions(+) create mode 100644 src/components/__tests__/switch.test.tsx create mode 100644 src/components/switch.tsx diff --git a/src/components/__tests__/switch.test.tsx b/src/components/__tests__/switch.test.tsx new file mode 100644 index 00000000..7c509b11 --- /dev/null +++ b/src/components/__tests__/switch.test.tsx @@ -0,0 +1,69 @@ +import '@testing-library/jest-dom' + +import { fireEvent, render, screen } from '@testing-library/react' +import React from 'react' + +import { Switch } from '@/components/switch' + +describe('Switch', () => { + it('renders without crashing', () => { + render() + expect(screen.getByRole('switch')).toBeInTheDocument() + }) + + it('applies default classes', () => { + render() + const switchElement = screen.getByRole('switch').nextSibling + expect(switchElement).toHaveClass('w-[42px] h-[26px] before:h-5 before:w-5') + }) + + it('applies small size classes when size prop is small', () => { + render() + const switchElement = screen.getByRole('switch').nextSibling + expect(switchElement).toHaveClass('w-9 h-[22px] before:h-4 before:w-4 before:left-[3px]') + }) + + it('forwards ref to input element', () => { + const ref = React.createRef() + render() + expect(ref.current).toBeInstanceOf(HTMLInputElement) + }) + + it('forwards checked prop to input element', () => { + render() + const inputElement = screen.getByRole('switch') + expect(inputElement).toBeChecked() + }) + + it('handles additional props', () => { + render() + const inputElement = screen.getByRole('switch') + expect(inputElement).toHaveAttribute('aria-label', 'Custom Switch') + }) + + it('applies custom class names', () => { + const customClass = 'custom-class' + render() + const switchElement = screen.getByRole('switch').nextSibling + expect(switchElement).toHaveClass(customClass) + }) + + it('toggles switch state when clicked', () => { + render() + const inputElement = screen.getByRole('switch') + expect(inputElement).not.toBeChecked() + + fireEvent.click(inputElement) + expect(inputElement).toBeChecked() + + fireEvent.click(inputElement) + expect(inputElement).not.toBeChecked() + }) + + it('handles additional HTML attributes', () => { + const testId = 'switch-test' + render() + const switchElement = screen.getByTestId(testId) + expect(switchElement).toBeInTheDocument() + }) +}) diff --git a/src/components/switch.tsx b/src/components/switch.tsx new file mode 100644 index 00000000..5771072c --- /dev/null +++ b/src/components/switch.tsx @@ -0,0 +1,54 @@ +import { type VariantProps, cva } from 'class-variance-authority' +import React, { forwardRef } from 'react' + +import { cn } from '@/utils/cn' + +const switchVariants = cva( + [ + 'rounded-full bg-disabled-strong relative cursor-pointer transition-colors duration-300 ease-in-out', + 'before:content-[""] before:absolute before:bg-white before:rounded-full', + 'before:top-1/2 before:transform before:-translate-y-1/2 before:left-[4px]', + 'before:transition-all before:duration-300 before:ease-in-out', + 'peer-checked:before:left-[18px] peer-checked:bg-switch-base', + 'peer-focus:outline peer-focus:outline-2 peer-focus:outline-blue-500', + ], + + { + variants: { + size: { + default: 'w-[42px] h-[26px] before:h-5 before:w-5', + small: [ + 'w-9 h-[22px] before:h-4 before:w-4 before:left-[3px]', + 'peer-checked:before:left-4', + ], + }, + }, + defaultVariants: { + size: 'default', + }, + }, +) + +export interface SwitchProps + extends VariantProps, + React.HTMLAttributes { + checked?: boolean +} + +export const Switch = forwardRef(function Switch( + { size, className, ...props }, + ref, +) { + return ( +