Skip to content

Commit

Permalink
Radio group implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Diana Fulga committed Jan 9, 2024
1 parent 8386c32 commit 0551530
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 34 deletions.
9 changes: 8 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@
"react/jsx-props-no-spreading": "off",
"import/prefer-default-export": "off",
"jsx-a11y/anchor-is-valid": "warn",
"jsx-a11y/no-noninteractive-tabindex": "warn",
"jsx-a11y/no-noninteractive-tabindex": [
"warn",
{
"tags": [],
"roles": ["tabpanel"],
"allowExpressionValues": true
}
],
"jsx-a11y/tabindex-no-positive": "warn",
"jsx-a11y/click-events-have-key-events": "warn",
"jsx-a11y/no-static-element-interactions": "warn",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
},
"devDependencies": {
"@interledger/http-signature-utils": "^2.0.0",
"@tailwindcss/forms": "^0.5.7",
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",
"@types/chrome": "^0.0.244",
Expand Down
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

104 changes: 104 additions & 0 deletions src/components/__tests__/radio-group-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { fireEvent, render } from '@testing-library/react'
import React from 'react'

import { Radio, RadioGroup } from '../radio-group'

describe('RadioGroup', () => {
const radioItems = [
{ label: 'Option 1', value: 'option1', checked: true },
{ label: 'Option 2', value: 'option2' },
]

it('should have the `flex-row` class when the `inline` variant is passed', () => {
const { queryByRole } = render(
<RadioGroup variant="inline" items={radioItems} name="radioName" />,
)

expect(queryByRole('tabpanel')).toBeInTheDocument()
expect(queryByRole('tabpanel')).toHaveClass('flex-row')
})

it('renders radio group correctly with items', () => {
const { getByRole } = render(<RadioGroup items={radioItems} name="radioGroup" />)

const radioGroup = getByRole('tabpanel')
expect(radioGroup).toBeInTheDocument()
expect(radioGroup.childNodes.length).toBe(2) // Ensure two radio buttons are rendered
})

it('renders radio group with no element checked by default', () => {
const radioItemsNotChecked = [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
]
const { getByLabelText } = render(<RadioGroup items={radioItemsNotChecked} name="radioGroup" />)

const firstRadioButton = getByLabelText('Option 1')
const secondRadioButton = getByLabelText('Option 2')

expect(firstRadioButton).not.toBeChecked()
expect(secondRadioButton).not.toBeChecked()
})

it('handles keyboard navigation', () => {
const { getByLabelText } = render(<RadioGroup items={radioItems} name="radioGroup" />)

const radioGroup = getByLabelText('Option 1')
fireEvent.keyDown(radioGroup, { key: 'ArrowRight', code: 'ArrowRight' })
let secondRadioButton = getByLabelText('Option 2')
expect(secondRadioButton).toBeChecked()

fireEvent.keyDown(radioGroup, { key: 'ArrowLeft', code: 'ArrowLeft' })
let firstRadioButton = getByLabelText('Option 1')
expect(firstRadioButton).toBeChecked()

fireEvent.keyDown(radioGroup, { key: 'ArrowUp', code: 'ArrowUp' })
secondRadioButton = getByLabelText('Option 2')
expect(secondRadioButton).toBeChecked()

fireEvent.keyDown(radioGroup, { key: 'ArrowDown', code: 'ArrowDown' })
firstRadioButton = getByLabelText('Option 1')
expect(firstRadioButton).toBeChecked()
})

it('changes selection on arrow keys', () => {
const { getByLabelText } = render(<RadioGroup items={radioItems} name="radioGroup" />)

const radioGroup = getByLabelText('Option 1')
fireEvent.keyDown(radioGroup, { key: 'ArrowRight', code: 'ArrowRight' })
fireEvent.keyDown(radioGroup, { key: 'Enter', code: 'Enter' })
const secondRadioButton = getByLabelText('Option 2')
expect(secondRadioButton).toBeChecked()
})

it('changes selection on clicking radio buttons', () => {
const { getByLabelText } = render(<RadioGroup items={radioItems} name="radioGroup" />)

const secondRadioButton = getByLabelText('Option 2')
fireEvent.click(secondRadioButton)
expect(secondRadioButton).toBeChecked()
})
})

describe('Radio', () => {
it('renders radio button correctly with label', () => {
const { getByLabelText } = render(<Radio label="Option 1" value="option1" name="radioGroup" />)

const radioButton = getByLabelText('Option 1')
expect(radioButton).toBeInTheDocument()
expect(radioButton).toHaveAttribute('type', 'radio')
expect(radioButton).not.toBeChecked()

fireEvent.click(radioButton)
expect(radioButton).toBeChecked()
})

it('renders disabled radio button', () => {
const { getByLabelText } = render(
<Radio label="Option 1" value="option1" name="radioGroup" disabled />,
)

const radioButton = getByLabelText('Option 1')
expect(radioButton).toBeDisabled()
})
})
128 changes: 128 additions & 0 deletions src/components/radio-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { type VariantProps, cva } from 'class-variance-authority'
import React, { useEffect, useMemo, useState } from 'react'

import { cn } from '@/utils/cn'

export interface RadioProps {
checked?: boolean
label?: string
value: string
name: string
id?: string
disabled?: boolean
onChange?: any
}

export const Radio = ({
label,
id,
name,
value,
disabled,
onChange,
checked,
}: RadioProps): JSX.Element => {
const inputId = id || `id-${name}-${value}`

return (
<div className="flex items-center">
<input
id={inputId}
type="radio"
disabled={disabled}
value={value}
name={name}
className="hidden"
onChange={onChange}
checked={checked}
/>

<label htmlFor={inputId} className="flex items-center">
<span className="w-6 h-6 inline-block rounded-full border-2 border-base" />
{label ? <p className="text-base text-medium leading-6 ms-2">{label}</p> : ''}
</label>
</div>
)
}

const radioGroupVariants = cva(['flex gap-3'], {
variants: {
variant: {
default: 'flex-col',
inline: 'flex-row',
},
fullWidth: {
true: 'w-full',
},
},
defaultVariants: {
variant: 'default',
},
})

export interface RadioGroupProps
extends VariantProps<typeof radioGroupVariants>,
React.InputHTMLAttributes<HTMLInputElement> {
disabled?: boolean
items: Omit<RadioProps, 'name'>[]
name: string
}

export const RadioGroup = ({
items,
variant,
name,
fullWidth,
disabled,
className,
}: RadioGroupProps) => {
const checkedItem = useMemo(() => items.findIndex(item => item.checked), [items])
const [selected, setSelected] = useState(checkedItem)

const handleKeyDown = (event: any) => {
if (event.code === 'ArrowRight' || event.code === 'ArrowDown') {
event.preventDefault()

const nextIndex = (selected + 1) % items.length
setSelected(nextIndex)
} else if (event.code === 'ArrowLeft' || event.code === 'ArrowUp') {
event.preventDefault()

const prevIndex = selected > 0 ? selected - 1 : items.length - 1
setSelected(prevIndex)
}
}

useEffect(() => {
const handleKeyPress = (event: any) => {
if (event.target.type === 'radio' && event.key === 'Enter') {
setSelected(Number(event.target.value))
}
}

document.addEventListener('keypress', handleKeyPress)
return () => {
document.removeEventListener('keypress', handleKeyPress)
}
}, [])

return (
//eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<div
tabIndex={0}
className={cn(radioGroupVariants({ variant, fullWidth }), className, 'outline-none')}
onKeyDown={handleKeyDown}
role="tabpanel">
{items.map((item, index) => (
<Radio
key={`key-${name}-${item.value}`}
{...item}
name={name}
disabled={disabled}
checked={selected === index}
onChange={() => setSelected(index)}
/>
))}
</div>
)
}
56 changes: 25 additions & 31 deletions src/manifest/chrome.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
{
"name": "__MSG_appName__",
"version": "1.0.1",
"manifest_version": 2,
"description": "__MSG_appDescription__",
"icons": {
"34": "assets/icons/icon-34.png",
"128": "assets/icons/icon-128.png"
},
"default_locale": "en",
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
"js": ["content/content.js"]
}
],
"background": {
"scripts": ["background/background.js"]
},
"permissions": ["tabs", "storage"],
"browser_action": {
"default_icon": "assets/icons/icon-34.png",
"default_title": "Web Monetization",
"default_popup": "popup/index.html"
},
"web_accessible_resources": [
"assets/*",
"content/*",
"options/*",
"popup/*",
"background/*"
]
"name": "__MSG_appName__",
"version": "1.0.1",
"manifest_version": 2,
"description": "__MSG_appDescription__",
"icons": {
"34": "assets/icons/icon-34.png",
"128": "assets/icons/icon-128.png"
},
"default_locale": "en",
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
"js": ["content/content.js"]
}
],
"background": {
"scripts": ["background/background.js"]
},
"permissions": ["tabs", "storage"],
"browser_action": {
"default_icon": "assets/icons/icon-34.png",
"default_title": "Web Monetization",
"default_popup": "popup/index.html"
},
"web_accessible_resources": ["assets/*", "content/*", "options/*", "popup/*", "background/*"]
}
10 changes: 9 additions & 1 deletion src/popup/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,17 @@
/* Border colors */
--border-base: 203 213 225;
--border-focus: 59 130 246;

/* Popup */
--popup-width: 448px;
--popup-height: 559px;
}
}

input[type='radio']:checked + label span,
input[type='radio']:checked + span {
@apply bg-primary;
background-color: bg-primary;
box-shadow: 0px 0px 0px 4px white inset;
border-color: #3490dc;
}
5 changes: 4 additions & 1 deletion tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ module.exports = {
popup: 'rgb(var(--border-popup) / <alpha-value>)',
focus: 'rgb(var(--border-focus) / <alpha-value>)',
},
boxShadow: {
inset: 'red',
},
},
},
plugins: [],
plugins: [require('@tailwindcss/forms')],
} satisfies Config

0 comments on commit 0551530

Please sign in to comment.