Skip to content

Commit

Permalink
feat: switch component (#74)
Browse files Browse the repository at this point in the history
* create switch component and add unit tests

* Update src/components/switch.tsx

Co-authored-by: Radu-Cristian Popa <[email protected]>

* Update src/components/switch.tsx

Co-authored-by: Radu-Cristian Popa <[email protected]>

* Update src/components/switch.tsx

Co-authored-by: Radu-Cristian Popa <[email protected]>

* updates based on code review

* update switch unit tests

---------

Co-authored-by: Radu-Cristian Popa <[email protected]>
  • Loading branch information
ionutanin and raducristianpopa authored Jan 17, 2024
1 parent eebfa3f commit fe4c099
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 0 deletions.
69 changes: 69 additions & 0 deletions src/components/__tests__/switch.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<Switch />)
expect(screen.getByRole('switch')).toBeInTheDocument()
})

it('applies default classes', () => {
render(<Switch />)
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(<Switch size="small" />)
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<HTMLInputElement>()
render(<Switch ref={ref} />)
expect(ref.current).toBeInstanceOf(HTMLInputElement)
})

it('forwards checked prop to input element', () => {
render(<Switch checked />)
const inputElement = screen.getByRole('switch')
expect(inputElement).toBeChecked()
})

it('handles additional props', () => {
render(<Switch aria-label="Custom Switch" />)
const inputElement = screen.getByRole('switch')
expect(inputElement).toHaveAttribute('aria-label', 'Custom Switch')
})

it('applies custom class names', () => {
const customClass = 'custom-class'
render(<Switch className={customClass} />)
const switchElement = screen.getByRole('switch').nextSibling
expect(switchElement).toHaveClass(customClass)
})

it('toggles switch state when clicked', () => {
render(<Switch />)
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(<Switch data-testid={testId} />)
const switchElement = screen.getByTestId(testId)
expect(switchElement).toBeInTheDocument()
})
})
54 changes: 54 additions & 0 deletions src/components/switch.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof switchVariants>,
React.HTMLAttributes<HTMLInputElement> {
checked?: boolean
}

export const Switch = forwardRef<HTMLInputElement, SwitchProps>(function Switch(
{ size, className, ...props },
ref,
) {
return (
<label>
<input
role="switch"
ref={ref}
type="checkbox"
{...props}
className="peer absolute opacity-0 -translate-x-[100%] pointer-events-none"
/>
<div className={cn(switchVariants({ size }), className)} />
</label>
)
})
1 change: 1 addition & 0 deletions src/popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { runtime } from 'webextension-polyfill'

import PopupFooter from '@/components/Popup/PopupFooter'
import PopupHeader from '@/components/Popup/PopupHeader'
import { Switch } from '@/components/switch'
import { sendMessage, sendMessageToActiveTab } from '@/utils/sendMessages'

const Success = runtime.getURL('assets/images/web-monetization-success.svg')
Expand Down
1 change: 1 addition & 0 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
'button-base': 'rgb(var(--bg-button-base) / <alpha-value>)',
'button-base-hover': 'rgb(var(--bg-button-base-hover) / <alpha-value>)',
'switch-base': 'rgb(var(--bg-switch-base) / <alpha-value>)',
'disabled-strong': 'rgb(var(--bg-disabled-strong) / <alpha-value>)',
'disabled-base': 'rgb(var(--bg-disabled-base) / <alpha-value>)',
'disabled-base-hover': 'rgb(var(--bg-disabled-base-hover) / <alpha-value>)',
},
Expand Down

0 comments on commit fe4c099

Please sign in to comment.