From 055153041a769470432edb9348b34a7a7b0dd423 Mon Sep 17 00:00:00 2001 From: Diana Fulga Date: Tue, 9 Jan 2024 13:19:44 +0200 Subject: [PATCH 1/5] Radio group implementation --- .eslintrc.json | 9 +- package.json | 1 + pnpm-lock.yaml | 17 +++ src/components/__tests__/radio-group-test.tsx | 104 ++++++++++++++ src/components/radio-group.tsx | 128 ++++++++++++++++++ src/manifest/chrome.json | 56 ++++---- src/popup/index.css | 10 +- tailwind.config.ts | 5 +- 8 files changed, 296 insertions(+), 34 deletions(-) create mode 100644 src/components/__tests__/radio-group-test.tsx create mode 100644 src/components/radio-group.tsx diff --git a/.eslintrc.json b/.eslintrc.json index ca9b8c95..01f96c60 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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", diff --git a/package.json b/package.json index 5512379c..af341fd5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6e04cd0..b8cd354c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,9 @@ devDependencies: '@interledger/http-signature-utils': specifier: ^2.0.0 version: 2.0.0 + '@tailwindcss/forms': + specifier: ^0.5.7 + version: 0.5.7(tailwindcss@3.4.0) '@testing-library/jest-dom': specifier: ^6.1.3 version: 6.1.5(@types/jest@29.5.11)(jest@29.7.0) @@ -1193,6 +1196,15 @@ packages: '@sinonjs/commons': 3.0.0 dev: true + /@tailwindcss/forms@0.5.7(tailwindcss@3.4.0): + resolution: {integrity: sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1' + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 3.4.0(ts-node@10.9.2) + dev: true + /@testing-library/dom@9.3.3: resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==} engines: {node: '>=14'} @@ -5692,6 +5704,11 @@ packages: engines: {node: '>=4'} dev: true + /mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + dev: true + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: diff --git a/src/components/__tests__/radio-group-test.tsx b/src/components/__tests__/radio-group-test.tsx new file mode 100644 index 00000000..ed222721 --- /dev/null +++ b/src/components/__tests__/radio-group-test.tsx @@ -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( + , + ) + + expect(queryByRole('tabpanel')).toBeInTheDocument() + expect(queryByRole('tabpanel')).toHaveClass('flex-row') + }) + + it('renders radio group correctly with items', () => { + const { getByRole } = render() + + 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() + + 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() + + 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() + + 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() + + const secondRadioButton = getByLabelText('Option 2') + fireEvent.click(secondRadioButton) + expect(secondRadioButton).toBeChecked() + }) +}) + +describe('Radio', () => { + it('renders radio button correctly with label', () => { + const { getByLabelText } = render() + + 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( + , + ) + + const radioButton = getByLabelText('Option 1') + expect(radioButton).toBeDisabled() + }) +}) diff --git a/src/components/radio-group.tsx b/src/components/radio-group.tsx new file mode 100644 index 00000000..33d3dacc --- /dev/null +++ b/src/components/radio-group.tsx @@ -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 ( +
+ + + +
+ ) +} + +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, + React.InputHTMLAttributes { + disabled?: boolean + items: Omit[] + 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 +
+ {items.map((item, index) => ( + setSelected(index)} + /> + ))} +
+ ) +} diff --git a/src/manifest/chrome.json b/src/manifest/chrome.json index 68de6a36..8805a11e 100644 --- a/src/manifest/chrome.json +++ b/src/manifest/chrome.json @@ -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://*/*", ""], - "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://*/*", ""], + "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/*"] } diff --git a/src/popup/index.css b/src/popup/index.css index e78227f2..a73f03b9 100644 --- a/src/popup/index.css +++ b/src/popup/index.css @@ -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; +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 62d3ee17..4fb6bec9 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -33,7 +33,10 @@ module.exports = { popup: 'rgb(var(--border-popup) / )', focus: 'rgb(var(--border-focus) / )', }, + boxShadow: { + inset: 'red', + }, }, }, - plugins: [], + plugins: [require('@tailwindcss/forms')], } satisfies Config From 45042a8fdc5b9b1f33fda83c0f1340012e3eac39 Mon Sep 17 00:00:00 2001 From: Diana Fulga Date: Mon, 22 Jan 2024 11:55:57 +0200 Subject: [PATCH 2/5] Implement feedback --- src/components/radio-group.tsx | 23 ++++++++++++++++++----- tailwind.config.ts | 3 --- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/components/radio-group.tsx b/src/components/radio-group.tsx index 33d3dacc..3415d771 100644 --- a/src/components/radio-group.tsx +++ b/src/components/radio-group.tsx @@ -11,6 +11,7 @@ export interface RadioProps { id?: string disabled?: boolean onChange?: any + noSelected?: boolean } export const Radio = ({ @@ -21,11 +22,22 @@ export const Radio = ({ disabled, onChange, checked, + noSelected, }: RadioProps): JSX.Element => { const inputId = id || `id-${name}-${value}` + const divId = `div-${inputId}` + + useEffect(() => { + if (checked) document.getElementById(divId)?.focus() + }, [checked, divId]) return ( -
+