From 02e9dd0fe92862fce5cc82f91486bb5ada3147e3 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sat, 23 Nov 2024 09:29:32 +0530 Subject: [PATCH 01/16] add: components to publish --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index f223a539..7dc6cfd8 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "./themes/default.css": "./dist/themes/default.css", "./themes/tailwind-presets/default.js": "./dist/themes/tailwind-presets/default.js", "./Accordion": "./dist/components/Accordion.js", + "./AlertDialog": "./dist/components/AlertDialog.js", "./Avatar": "./dist/components/Avatar.js", + "./AvatarGroup": "./dist/components/AvatarGroup.js", "./Badge": "./dist/components/Badge.js", "./BlockQuote": "./dist/components/BlockQuote.js", "./Button": "./dist/components/Button.js", @@ -22,6 +24,7 @@ "./Link": "./dist/components/Link.js", "./Progress": "./dist/components/Progress.js", "./Quote": "./dist/components/Quote.js", + "./RadioGroup": "./dist/components/RadioGroup.js", "./Separator": "./dist/components/Separator.js", "./Skeleton": "./dist/components/Skeleton.js", "./Strong": "./dist/components/Strong.js", From 9c2bf1e1ce785fbb570f2ef9dcd679c1d352ba9e Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sat, 23 Nov 2024 09:29:48 +0530 Subject: [PATCH 02/16] fix: linting --- src/components/ui/Toggle/Toggle.tsx | 3 +-- .../ui/ToggleGroup/fragments/ToggleItem.tsx | 2 -- .../contexts/TogglePrimitiveContext.tsx | 4 ++-- .../Toggle/fragments/TogglePrimitiveRoot.tsx | 21 +++++++++---------- src/core/primitives/Toggle/index.tsx | 8 +++---- .../stories/TogglePrimitive.stories.tsx | 16 +++++++------- 6 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/components/ui/Toggle/Toggle.tsx b/src/components/ui/Toggle/Toggle.tsx index 1fcd4369..5bfa791e 100644 --- a/src/components/ui/Toggle/Toggle.tsx +++ b/src/components/ui/Toggle/Toggle.tsx @@ -26,7 +26,6 @@ const Toggle: React.FC = ({ onChange, ...props }) => { - const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME); const [isPressed, setIsPressed] = useState(pressed || defaultPressed); @@ -38,7 +37,7 @@ const Toggle: React.FC = ({ }; return ( - + { const type = toggleContext?.type; - const isActive = toggleContext?.activeToggles?.includes(value); const handleToggleSelect = () => { @@ -17,7 +16,6 @@ const ToggleItem = ({ children, value = null, ...props }:any) => { // For Single Case if (type === 'single') { if (isActive) { - toggleContext?.setActiveToggles([]); return; } else { diff --git a/src/core/primitives/Toggle/contexts/TogglePrimitiveContext.tsx b/src/core/primitives/Toggle/contexts/TogglePrimitiveContext.tsx index cc4896dd..d5eea4d8 100644 --- a/src/core/primitives/Toggle/contexts/TogglePrimitiveContext.tsx +++ b/src/core/primitives/Toggle/contexts/TogglePrimitiveContext.tsx @@ -1,8 +1,8 @@ -import { createContext } from "react"; +import { createContext } from 'react'; interface TogglePrimitiveContextType { isPressed: boolean | undefined; handlePressed: () => void; } -export const TogglePrimitiveContext = createContext({} as TogglePrimitiveContextType) \ No newline at end of file +export const TogglePrimitiveContext = createContext({} as TogglePrimitiveContextType); diff --git a/src/core/primitives/Toggle/fragments/TogglePrimitiveRoot.tsx b/src/core/primitives/Toggle/fragments/TogglePrimitiveRoot.tsx index 54628407..62f312a5 100644 --- a/src/core/primitives/Toggle/fragments/TogglePrimitiveRoot.tsx +++ b/src/core/primitives/Toggle/fragments/TogglePrimitiveRoot.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState } from 'react'; export interface TogglePrimitiveRootProps { defaultPressed? : boolean | false; @@ -8,17 +8,16 @@ export interface TogglePrimitiveRootProps { onChange : (isPressed:boolean) => void; } -const TogglePrimitiveRoot = ({children,className='',defaultPressed,pressed,onChange,...props}:TogglePrimitiveRootProps) => { - const [isPressed, setIsPressed] = useState(pressed || defaultPressed); +const TogglePrimitiveRoot = ({ children, className = '', defaultPressed, pressed, onChange, ...props }:TogglePrimitiveRootProps) => { + const [isPressed, setIsPressed] = useState(pressed || defaultPressed); - const handlePressed = () => { - const updatedPressed = !isPressed; - setIsPressed(updatedPressed); - onChange(updatedPressed) - } + const handlePressed = () => { + const updatedPressed = !isPressed; + setIsPressed(updatedPressed); + onChange(updatedPressed); + }; - return {children} - + return {children}; }; -export default TogglePrimitiveRoot; \ No newline at end of file +export default TogglePrimitiveRoot; diff --git a/src/core/primitives/Toggle/index.tsx b/src/core/primitives/Toggle/index.tsx index 57e65b51..79701c01 100644 --- a/src/core/primitives/Toggle/index.tsx +++ b/src/core/primitives/Toggle/index.tsx @@ -1,7 +1,7 @@ -import TogglePrimitiveRoot from "./fragments/TogglePrimitiveRoot" +import TogglePrimitiveRoot from './fragments/TogglePrimitiveRoot'; const TogglePrimitive = { - Root: TogglePrimitiveRoot, -} + Root: TogglePrimitiveRoot +}; -export default TogglePrimitive \ No newline at end of file +export default TogglePrimitive; diff --git a/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx b/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx index 3d43f63c..141f0f17 100644 --- a/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx +++ b/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx @@ -1,19 +1,19 @@ -import React from "react"; -import TogglePrimitive from "../index"; -import SandboxEditor from "~/components/tools/SandboxEditor/SandboxEditor"; +import React from 'react'; +import TogglePrimitive from '../index'; +import SandboxEditor from '~/components/tools/SandboxEditor/SandboxEditor'; export default { title: 'Primitives/TogglePrimitive', component: TogglePrimitive, render: (args:any) => - - + + -} +}; export const All = { args: { - className: '' + className: '' } -} \ No newline at end of file +}; From c954fa9e11270389642216ec752ba289324ddcc1 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sat, 23 Nov 2024 09:47:24 +0530 Subject: [PATCH 03/16] Delete TogglePrimitiveContext, update TogglePrimitiveRoot, and stories --- .../contexts/TogglePrimitiveContext.tsx | 8 ------- .../Toggle/fragments/TogglePrimitiveRoot.tsx | 22 ++++++++++++++----- .../stories/TogglePrimitive.stories.tsx | 15 ++++++++----- 3 files changed, 26 insertions(+), 19 deletions(-) delete mode 100644 src/core/primitives/Toggle/contexts/TogglePrimitiveContext.tsx diff --git a/src/core/primitives/Toggle/contexts/TogglePrimitiveContext.tsx b/src/core/primitives/Toggle/contexts/TogglePrimitiveContext.tsx deleted file mode 100644 index d5eea4d8..00000000 --- a/src/core/primitives/Toggle/contexts/TogglePrimitiveContext.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { createContext } from 'react'; - -interface TogglePrimitiveContextType { - isPressed: boolean | undefined; - handlePressed: () => void; - -} -export const TogglePrimitiveContext = createContext({} as TogglePrimitiveContextType); diff --git a/src/core/primitives/Toggle/fragments/TogglePrimitiveRoot.tsx b/src/core/primitives/Toggle/fragments/TogglePrimitiveRoot.tsx index 62f312a5..09ab375d 100644 --- a/src/core/primitives/Toggle/fragments/TogglePrimitiveRoot.tsx +++ b/src/core/primitives/Toggle/fragments/TogglePrimitiveRoot.tsx @@ -1,23 +1,35 @@ import React, { useState } from 'react'; +import Primitive from '~/core/primitives/Primitive'; + export interface TogglePrimitiveRootProps { defaultPressed? : boolean | false; pressed: boolean; children?: React.ReactNode; className?: string; - onChange : (isPressed:boolean) => void; + label?: string; + onPressedChange : (isPressed:boolean) => void; } -const TogglePrimitiveRoot = ({ children, className = '', defaultPressed, pressed, onChange, ...props }:TogglePrimitiveRootProps) => { +const TogglePrimitive = ({ children, label = '', defaultPressed, pressed, onPressedChange, ...props }:TogglePrimitiveRootProps) => { const [isPressed, setIsPressed] = useState(pressed || defaultPressed); const handlePressed = () => { const updatedPressed = !isPressed; setIsPressed(updatedPressed); - onChange(updatedPressed); + onPressedChange(updatedPressed); }; - return {children}; + const ariaAttributes:any = label ? { 'aria-label': label } : {}; + ariaAttributes['aria-pressed'] = isPressed ? 'true' : 'false'; + + return {children} + ; }; -export default TogglePrimitiveRoot; +export default TogglePrimitive; diff --git a/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx b/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx index 141f0f17..de518524 100644 --- a/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx +++ b/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx @@ -1,15 +1,18 @@ -import React from 'react'; +import React, { useState } from 'react'; import TogglePrimitive from '../index'; import SandboxEditor from '~/components/tools/SandboxEditor/SandboxEditor'; export default { title: 'Primitives/TogglePrimitive', component: TogglePrimitive, - render: (args:any) => - - - - + render: (args:any) => { + const [pressed, setPressed] = useState(false); + return + + toggle - {pressed ? 'on' : 'off'} + + ; + } }; export const All = { From 5932ea3873c8163b6f07632dfcc7b1fe479dacd0 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sat, 23 Nov 2024 10:26:28 +0530 Subject: [PATCH 04/16] Refactor Toggle component for better default handling --- src/components/ui/Toggle/Toggle.tsx | 26 ++++++++------ .../Toggle/fragments/TogglePrimitiveRoot.tsx | 35 ------------------- src/core/primitives/Toggle/index.tsx | 34 ++++++++++++++++-- .../stories/TogglePrimitive.stories.tsx | 4 +-- .../Toggle/tests/TogglePrimitive.test.js | 10 ++++++ 5 files changed, 59 insertions(+), 50 deletions(-) delete mode 100644 src/core/primitives/Toggle/fragments/TogglePrimitiveRoot.tsx create mode 100644 src/core/primitives/Toggle/tests/TogglePrimitive.test.js diff --git a/src/components/ui/Toggle/Toggle.tsx b/src/components/ui/Toggle/Toggle.tsx index 5bfa791e..58ab46e0 100644 --- a/src/components/ui/Toggle/Toggle.tsx +++ b/src/components/ui/Toggle/Toggle.tsx @@ -3,12 +3,13 @@ import React, { useState } from 'react'; import { customClassSwitcher } from '~/core'; import ButtonPrimitive from '~/core/primitives/Button'; +import TogglePrimitive from '~/core/primitives/Toggle'; const COMPONENT_NAME = 'Toggle'; export type ToggleProps = { - defaultPressed? : boolean | false ; - pressed : boolean; + defaultPressed?: boolean; + pressed: boolean; customRootClass? : string; disabled? : boolean; children? : React.ReactNode; @@ -18,17 +19,21 @@ export type ToggleProps = { }; const Toggle: React.FC = ({ - defaultPressed, + defaultPressed = false, customRootClass = '', children, className = '', - pressed, + pressed = false, onChange, ...props }) => { + if (typeof pressed !== 'boolean') { + throw new Error('Toggle: pressed prop must be a boolean'); + } + const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME); - const [isPressed, setIsPressed] = useState(pressed || defaultPressed); + const [isPressed, setIsPressed] = useState(pressed); const handlePressed = () => { const updatedPressed = !isPressed; @@ -38,14 +43,15 @@ const Toggle: React.FC = ({ return ( - + {...props}> {children} - + ); }; diff --git a/src/core/primitives/Toggle/fragments/TogglePrimitiveRoot.tsx b/src/core/primitives/Toggle/fragments/TogglePrimitiveRoot.tsx deleted file mode 100644 index 09ab375d..00000000 --- a/src/core/primitives/Toggle/fragments/TogglePrimitiveRoot.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useState } from 'react'; - -import Primitive from '~/core/primitives/Primitive'; - -export interface TogglePrimitiveRootProps { - defaultPressed? : boolean | false; - pressed: boolean; - children?: React.ReactNode; - className?: string; - label?: string; - onPressedChange : (isPressed:boolean) => void; - -} -const TogglePrimitive = ({ children, label = '', defaultPressed, pressed, onPressedChange, ...props }:TogglePrimitiveRootProps) => { - const [isPressed, setIsPressed] = useState(pressed || defaultPressed); - - const handlePressed = () => { - const updatedPressed = !isPressed; - setIsPressed(updatedPressed); - onPressedChange(updatedPressed); - }; - - const ariaAttributes:any = label ? { 'aria-label': label } : {}; - ariaAttributes['aria-pressed'] = isPressed ? 'true' : 'false'; - - return {children} - ; -}; - -export default TogglePrimitive; diff --git a/src/core/primitives/Toggle/index.tsx b/src/core/primitives/Toggle/index.tsx index 79701c01..ab4bc8a9 100644 --- a/src/core/primitives/Toggle/index.tsx +++ b/src/core/primitives/Toggle/index.tsx @@ -1,7 +1,35 @@ -import TogglePrimitiveRoot from './fragments/TogglePrimitiveRoot'; +import React, { useState } from 'react'; -const TogglePrimitive = { - Root: TogglePrimitiveRoot +import Primitive from '~/core/primitives/Primitive'; + +export interface TogglePrimitiveProps { + defaultPressed? : boolean | false; + pressed: boolean; + children?: React.ReactNode; + className?: string; + label?: string; + onPressedChange : (isPressed:boolean) => void; + +} +const TogglePrimitive = ({ children, label = '', defaultPressed, pressed, onPressedChange, ...props }:TogglePrimitiveProps) => { + const [isPressed, setIsPressed] = useState(pressed || defaultPressed); + + const handlePressed = () => { + const updatedPressed = !isPressed; + setIsPressed(updatedPressed); + onPressedChange(updatedPressed); + }; + + const ariaAttributes:any = label ? { 'aria-label': label } : {}; + ariaAttributes['aria-pressed'] = isPressed ? 'true' : 'false'; + + return {children} + ; }; export default TogglePrimitive; diff --git a/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx b/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx index de518524..3465482f 100644 --- a/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx +++ b/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx @@ -8,9 +8,9 @@ export default { render: (args:any) => { const [pressed, setPressed] = useState(false); return - + toggle - {pressed ? 'on' : 'off'} - + ; } }; diff --git a/src/core/primitives/Toggle/tests/TogglePrimitive.test.js b/src/core/primitives/Toggle/tests/TogglePrimitive.test.js new file mode 100644 index 00000000..12733ae6 --- /dev/null +++ b/src/core/primitives/Toggle/tests/TogglePrimitive.test.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TogglePrimitive from '../index'; + +describe('TogglePrimitive', () => { + it('renders children correctly', () => { + render(Test Content); + expect(screen.getByText('Test Content')).toBeInTheDocument(); + }); +}); From 8ad0f27bc8f92c41b711d81950e312faa82417bd Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sat, 23 Nov 2024 10:35:08 +0530 Subject: [PATCH 05/16] Add support for disabled state in TogglePrimitive --- src/core/primitives/Toggle/index.tsx | 4 +- .../stories/TogglePrimitive.stories.tsx | 6 +++ .../Toggle/tests/TogglePrimitive.test.js | 43 ++++++++++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/core/primitives/Toggle/index.tsx b/src/core/primitives/Toggle/index.tsx index ab4bc8a9..82716153 100644 --- a/src/core/primitives/Toggle/index.tsx +++ b/src/core/primitives/Toggle/index.tsx @@ -8,10 +8,11 @@ export interface TogglePrimitiveProps { children?: React.ReactNode; className?: string; label?: string; + disabled?: boolean; onPressedChange : (isPressed:boolean) => void; } -const TogglePrimitive = ({ children, label = '', defaultPressed, pressed, onPressedChange, ...props }:TogglePrimitiveProps) => { +const TogglePrimitive = ({ children, label = '', defaultPressed, pressed, onPressedChange, disabled, ...props }:TogglePrimitiveProps) => { const [isPressed, setIsPressed] = useState(pressed || defaultPressed); const handlePressed = () => { @@ -26,6 +27,7 @@ const TogglePrimitive = ({ children, label = '', defaultPressed, pressed, onPres return {children} diff --git a/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx b/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx index 3465482f..7a7db00a 100644 --- a/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx +++ b/src/core/primitives/Toggle/stories/TogglePrimitive.stories.tsx @@ -20,3 +20,9 @@ export const All = { className: '' } }; + +export const Disabled = { + args: { + disabled: true + } +}; diff --git a/src/core/primitives/Toggle/tests/TogglePrimitive.test.js b/src/core/primitives/Toggle/tests/TogglePrimitive.test.js index 12733ae6..20b56f1b 100644 --- a/src/core/primitives/Toggle/tests/TogglePrimitive.test.js +++ b/src/core/primitives/Toggle/tests/TogglePrimitive.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import TogglePrimitive from '../index'; describe('TogglePrimitive', () => { @@ -7,4 +7,45 @@ describe('TogglePrimitive', () => { render(Test Content); expect(screen.getByText('Test Content')).toBeInTheDocument(); }); + + it('renders asChild correctly', () => { + const { container } = render(); + expect(container.querySelector('button')).toBeInTheDocument(); + expect(container.querySelector('button')).toHaveTextContent('Click me'); + }); + + it('renders with label correctly', () => { + render(Test Content); + expect(screen.getByText('Test Content')).toBeInTheDocument(); + expect(screen.getByLabelText('Test Label')).toBeInTheDocument(); + }); + + it('renders with defaultPressed true with data-state on correctly', () => { + render(Test Content); + expect(screen.getByText('Test Content')).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveAttribute('data-state', 'on'); + }); + + it('renders with defaultPressed false with data-state off correctly', () => { + render(Test Content); + expect(screen.getByText('Test Content')).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveAttribute('data-state', 'off'); + }); + + it('renders onPressedChange correctly', () => { + const onPressedChange = jest.fn(); + render(Test Content); + fireEvent.click(screen.getByRole('button')); + expect(onPressedChange).toHaveBeenCalledWith(true); + }); + + it('renders with className correctly', () => { + render(Test Content); + expect(screen.getByText('Test Content')).toHaveClass('test-class'); + }); + + it('renders with disabled correctly', () => { + render(Test Content); + expect(screen.getByText('Test Content')).toHaveAttribute('disabled'); + }); }); From 72cf438ef808cb7193d30832f646f31f0f29ac31 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sat, 23 Nov 2024 11:05:02 +0530 Subject: [PATCH 06/16] Refactor Toggle related components and styles --- src/components/ui/Toggle/Toggle.tsx | 1 - .../ui/ToggleGroup/contexts/toggleContext.tsx | 8 ++++- .../ui/ToggleGroup/fragments/ToggleItem.tsx | 31 +++++++++++++------ styles/themes/components/toggle-group.scss | 8 ++--- styles/themes/components/toggle.scss | 5 +-- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/components/ui/Toggle/Toggle.tsx b/src/components/ui/Toggle/Toggle.tsx index 58ab46e0..3ff09ca5 100644 --- a/src/components/ui/Toggle/Toggle.tsx +++ b/src/components/ui/Toggle/Toggle.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import { customClassSwitcher } from '~/core'; -import ButtonPrimitive from '~/core/primitives/Button'; import TogglePrimitive from '~/core/primitives/Toggle'; const COMPONENT_NAME = 'Toggle'; diff --git a/src/components/ui/ToggleGroup/contexts/toggleContext.tsx b/src/components/ui/ToggleGroup/contexts/toggleContext.tsx index 99ce220a..d35ad88e 100644 --- a/src/components/ui/ToggleGroup/contexts/toggleContext.tsx +++ b/src/components/ui/ToggleGroup/contexts/toggleContext.tsx @@ -1,3 +1,9 @@ import { createContext } from 'react'; -export const ToggleContext = createContext({}); +export type ToggleContextType = { + type: 'single' | 'multiple'; + activeToggles: any[]; + setActiveToggles: (toggles: any[]) => void; +}; + +export const ToggleContext = createContext({}); diff --git a/src/components/ui/ToggleGroup/fragments/ToggleItem.tsx b/src/components/ui/ToggleGroup/fragments/ToggleItem.tsx index 52350575..1efccea2 100644 --- a/src/components/ui/ToggleGroup/fragments/ToggleItem.tsx +++ b/src/components/ui/ToggleGroup/fragments/ToggleItem.tsx @@ -1,15 +1,24 @@ import React, { useContext } from 'react'; import { ToggleContext } from '../contexts/toggleContext'; -import ButtonPrimitive from '~/core/primitives/Button'; +import TogglePrimitive from '~/core/primitives/Toggle'; -const ToggleItem = ({ children, value = null, ...props }:any) => { +export type ToggleItemProps = { + children: React.ReactNode; + value: any; + props: any; +}; + +const ToggleItem = ({ children, value = null, ...props }:ToggleItemProps) => { const toggleContext = useContext(ToggleContext); const type = toggleContext?.type; const isActive = toggleContext?.activeToggles?.includes(value); + const ariaProps:any = {}; + const dataProps:any = {}; + const handleToggleSelect = () => { let activeToggleArray = toggleContext?.activeToggles || []; @@ -37,17 +46,21 @@ const ToggleItem = ({ children, value = null, ...props }:any) => { }; if (isActive) { - props['aria-pressed'] = 'true'; + ariaProps['aria-pressed'] = 'true'; + dataProps['data-active'] = 'true'; } else { - props['aria-pressed'] = 'false'; + ariaProps['aria-pressed'] = 'false'; + dataProps['data-active'] = 'false'; } - return { - handleToggleSelect(); - }} + return {children}; + + >{children}; }; export default ToggleItem; diff --git a/styles/themes/components/toggle-group.scss b/styles/themes/components/toggle-group.scss index 50ca149e..185eb543 100644 --- a/styles/themes/components/toggle-group.scss +++ b/styles/themes/components/toggle-group.scss @@ -1,8 +1,7 @@ .rad-ui-toggle-group { display: inline-flex; - gap:12px; + gap:6px; - button { all: unset; background-color: white; @@ -16,10 +15,11 @@ justify-content: center; margin-left: 1px; border-radius: 4px; + border: 1px solid var(--rad-ui-color-accent-300); - &:focus, &:active { + &:focus-visible { outline: 2px solid var(--rad-ui-color-accent-900); - outline-offset: 4px; + outline-offset: 2px; } &[aria-pressed="true"] { diff --git a/styles/themes/components/toggle.scss b/styles/themes/components/toggle.scss index 5030876e..6476b883 100644 --- a/styles/themes/components/toggle.scss +++ b/styles/themes/components/toggle.scss @@ -19,7 +19,8 @@ } - &:focus { - box-shadow: 0 0 0 2px var(--rad-ui-color-accent-1000); + &:focus-visible { + outline: 2px solid var(--rad-ui-color-accent-900); + outline-offset: 4px; } } \ No newline at end of file From ec81d708d21dfb90dbf5aa05094028db69309449 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sat, 23 Nov 2024 12:21:58 +0530 Subject: [PATCH 07/16] Add keyboard navigation to toggle items in ToggleGroup --- .../ui/Accordion/fragments/AccordionItem.tsx | 1 - .../ToggleGroup/fragments/ToggleGroupRoot.tsx | 40 +++++++++++++---- .../ui/ToggleGroup/fragments/ToggleItem.tsx | 44 ++++++++++++++----- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/src/components/ui/Accordion/fragments/AccordionItem.tsx b/src/components/ui/Accordion/fragments/AccordionItem.tsx index 2a1141be..4894ec20 100644 --- a/src/components/ui/Accordion/fragments/AccordionItem.tsx +++ b/src/components/ui/Accordion/fragments/AccordionItem.tsx @@ -69,7 +69,6 @@ const AccordionItem: React.FC = ({ children, value, classNam data-state={isOpen ? 'open' : 'closed'} data-rad-ui-batch-element {...shouldAddFocusDataAttribute ? { 'data-rad-ui-focus-element': '' } : {}} - > {children} diff --git a/src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx b/src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx index 07545b67..b3f4454d 100644 --- a/src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx +++ b/src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx @@ -1,25 +1,49 @@ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { customClassSwitcher } from '~/core'; +import { getAllBatchElements, getNextBatchItem, getPrevBatchItem } from '~/core/batches'; import { ToggleContext } from '../contexts/toggleContext'; const ToggleGroupRoot = ({ type = 'multiple', className = '', customRootClass = '', componentName = '', value = null, children }:any) => { const rootClass = customClassSwitcher(customRootClass, componentName); - + const toggleGroupRef = useRef(null); // value can be either a string or an array of strings // if its null, then no toggles are active const [activeToggles, setActiveToggles] = useState(value || []); + const nextItem = () => { + const batches = getAllBatchElements(toggleGroupRef?.current); + const nextItem = getNextBatchItem(batches); + // setFocusItem(nextItem); + console.log('nextItem', nextItem); + if (nextItem) { + nextItem.focus(); + } + }; + + const previousItem = () => { + const batches = getAllBatchElements(toggleGroupRef?.current); + const prevItem = getPrevBatchItem(batches); + console.log('prevItem', prevItem); + if (prevItem) { + prevItem.focus(); + } + }; + + const sendValues = { + nextItem, + previousItem, + activeToggles, + setActiveToggles, + type + }; + return ( -
+
+ value={sendValues}> {children}
diff --git a/src/components/ui/ToggleGroup/fragments/ToggleItem.tsx b/src/components/ui/ToggleGroup/fragments/ToggleItem.tsx index 1efccea2..e5b4f1ae 100644 --- a/src/components/ui/ToggleGroup/fragments/ToggleItem.tsx +++ b/src/components/ui/ToggleGroup/fragments/ToggleItem.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import { ToggleContext } from '../contexts/toggleContext'; import TogglePrimitive from '~/core/primitives/Toggle'; @@ -10,25 +10,32 @@ export type ToggleItemProps = { }; const ToggleItem = ({ children, value = null, ...props }:ToggleItemProps) => { - const toggleContext = useContext(ToggleContext); + const { type, activeToggles, setActiveToggles, nextItem, previousItem } = useContext(ToggleContext); + const isActive = activeToggles?.includes(value); - const type = toggleContext?.type; - - const isActive = toggleContext?.activeToggles?.includes(value); + const [isFocused, setIsFocused] = useState(false); const ariaProps:any = {}; const dataProps:any = {}; + const handleFocus = () => { + setIsFocused(true); + }; + + const handleBlur = () => { + setIsFocused(false); + }; + const handleToggleSelect = () => { - let activeToggleArray = toggleContext?.activeToggles || []; + let activeToggleArray = activeToggles || []; // For Single Case if (type === 'single') { if (isActive) { - toggleContext?.setActiveToggles([]); + setActiveToggles([]); return; } else { - toggleContext?.setActiveToggles([value]); + setActiveToggles([value]); return; } } @@ -42,7 +49,20 @@ const ToggleItem = ({ children, value = null, ...props }:ToggleItemProps) => { } } - toggleContext?.setActiveToggles(activeToggleArray); + setActiveToggles(activeToggleArray); + }; + + const handleKeyDown = (e:any) => { + if (e.key === 'ArrowRight') { + // prevent scrolling when pressing arrow keys + e.preventDefault(); + nextItem(); + } + if (e.key === 'ArrowLeft') { + // prevent scrolling when pressing arrow keys + e.preventDefault(); + previousItem(); + } }; if (isActive) { @@ -54,10 +74,14 @@ const ToggleItem = ({ children, value = null, ...props }:ToggleItemProps) => { } return {children}; From 844fadb555743f268ac3f46da504633c98c8353a Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sat, 23 Nov 2024 14:01:28 +0530 Subject: [PATCH 08/16] Refactor batch item navigation with loop option --- .../ui/ToggleGroup/fragments/ToggleGroupRoot.tsx | 11 ++++------- src/core/batches.ts | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx b/src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx index b3f4454d..aa907bd9 100644 --- a/src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx +++ b/src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx @@ -15,20 +15,17 @@ const ToggleGroupRoot = ({ type = 'multiple', className = '', customRootClass = const nextItem = () => { const batches = getAllBatchElements(toggleGroupRef?.current); - const nextItem = getNextBatchItem(batches); - // setFocusItem(nextItem); - console.log('nextItem', nextItem); + const nextItem = getNextBatchItem(batches, true); if (nextItem) { - nextItem.focus(); + nextItem?.focus(); } }; const previousItem = () => { const batches = getAllBatchElements(toggleGroupRef?.current); - const prevItem = getPrevBatchItem(batches); - console.log('prevItem', prevItem); + const prevItem = getPrevBatchItem(batches, true); if (prevItem) { - prevItem.focus(); + prevItem?.focus(); } }; diff --git a/src/core/batches.ts b/src/core/batches.ts index 8c14e967..f746ad3a 100644 --- a/src/core/batches.ts +++ b/src/core/batches.ts @@ -15,19 +15,25 @@ export const getActiveBatchItem = (batches: NodeList) => { return activeItem as HTMLElement | null; }; -export const getNextBatchItem = (batches: NodeList): Element => { +export const getNextBatchItem = (batches: NodeList, loop = false): Element => { const activeItem = getActiveBatchItem(batches) as HTMLElement | null; // get the next item, return it if it is not the last item const nextItem = activeItem?.nextElementSibling; + if (nextItem) { return nextItem; } + if (loop) { + // if it is the last item, return the first item + return batches[0] as HTMLElement; + } + // if it is the last item, return the last item return batches[batches.length - 1] as HTMLElement; }; -export const getPrevBatchItem = (batches: NodeList) => { +export const getPrevBatchItem = (batches: NodeList, loop = false) => { const activeItem = getActiveBatchItem(batches) as HTMLElement | null; // get the next item, return it if it is not the last item const prevItem = activeItem?.previousElementSibling; @@ -35,6 +41,10 @@ export const getPrevBatchItem = (batches: NodeList) => { return prevItem; } - // if it is the last item, return the last item + if (loop) { + // if it is the last item, return the last item + return batches[batches.length - 1] as HTMLElement; + } + // if it is the last item, return the first item return batches[0] as HTMLElement; }; From f2b95202e36add58eb2c8fb0f1f587f43e165fac Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sat, 23 Nov 2024 14:03:17 +0530 Subject: [PATCH 09/16] Update ToggleGroupRoot to use loop flag for batch navigation --- src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx b/src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx index aa907bd9..9199a254 100644 --- a/src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx +++ b/src/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsx @@ -5,7 +5,7 @@ import { getAllBatchElements, getNextBatchItem, getPrevBatchItem } from '~/core/ import { ToggleContext } from '../contexts/toggleContext'; -const ToggleGroupRoot = ({ type = 'multiple', className = '', customRootClass = '', componentName = '', value = null, children }:any) => { +const ToggleGroupRoot = ({ type = 'multiple', className = '', loop = true, customRootClass = '', componentName = '', value = null, children }:any) => { const rootClass = customClassSwitcher(customRootClass, componentName); const toggleGroupRef = useRef(null); // value can be either a string or an array of strings @@ -15,7 +15,7 @@ const ToggleGroupRoot = ({ type = 'multiple', className = '', customRootClass = const nextItem = () => { const batches = getAllBatchElements(toggleGroupRef?.current); - const nextItem = getNextBatchItem(batches, true); + const nextItem = getNextBatchItem(batches, loop); if (nextItem) { nextItem?.focus(); } @@ -23,7 +23,7 @@ const ToggleGroupRoot = ({ type = 'multiple', className = '', customRootClass = const previousItem = () => { const batches = getAllBatchElements(toggleGroupRef?.current); - const prevItem = getPrevBatchItem(batches, true); + const prevItem = getPrevBatchItem(batches, loop); if (prevItem) { prevItem?.focus(); } From b8a632f46dcaba04decf95e52017fe8a69ce07be Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sun, 24 Nov 2024 12:16:43 +0530 Subject: [PATCH 10/16] Add aria-disabled attribute to TogglePrimitive button --- src/core/primitives/Toggle/index.tsx | 4 ++-- .../Toggle/tests/TogglePrimitive.test.js | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/core/primitives/Toggle/index.tsx b/src/core/primitives/Toggle/index.tsx index 82716153..d961d208 100644 --- a/src/core/primitives/Toggle/index.tsx +++ b/src/core/primitives/Toggle/index.tsx @@ -12,7 +12,7 @@ export interface TogglePrimitiveProps { onPressedChange : (isPressed:boolean) => void; } -const TogglePrimitive = ({ children, label = '', defaultPressed, pressed, onPressedChange, disabled, ...props }:TogglePrimitiveProps) => { +const TogglePrimitive = ({ children, label = '', defaultPressed, pressed, onPressedChange = () => {}, disabled, ...props }:TogglePrimitiveProps) => { const [isPressed, setIsPressed] = useState(pressed || defaultPressed); const handlePressed = () => { @@ -23,7 +23,7 @@ const TogglePrimitive = ({ children, label = '', defaultPressed, pressed, onPres const ariaAttributes:any = label ? { 'aria-label': label } : {}; ariaAttributes['aria-pressed'] = isPressed ? 'true' : 'false'; - + ariaAttributes['aria-disabled'] = disabled ? 'true' : 'false'; return { render(Test Content); expect(screen.getByText('Test Content')).toHaveAttribute('disabled'); }); + + it('renders with correct ARIA attributes', () => { + render(Test Content); + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('aria-pressed', 'false'); + + fireEvent.click(button); + expect(button).toHaveAttribute('aria-pressed', 'true'); + }); + + it('renders with correct ARIA attributes when disabled', () => { + render(Test Content); + expect(screen.getByRole('button')).toHaveAttribute('aria-disabled', 'true'); + }); }); From 2694fba12c33f9f2bfeac3c68a4a409b783c1c4b Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sun, 24 Nov 2024 12:35:11 +0530 Subject: [PATCH 11/16] Refactor TogglePrimitive component for controlled state --- src/core/primitives/Toggle/index.tsx | 32 +++++++--- .../Toggle/tests/TogglePrimitive.test.js | 63 +++++++++++++++++++ 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/src/core/primitives/Toggle/index.tsx b/src/core/primitives/Toggle/index.tsx index d961d208..ce2217ad 100644 --- a/src/core/primitives/Toggle/index.tsx +++ b/src/core/primitives/Toggle/index.tsx @@ -3,27 +3,45 @@ import React, { useState } from 'react'; import Primitive from '~/core/primitives/Primitive'; export interface TogglePrimitiveProps { - defaultPressed? : boolean | false; - pressed: boolean; + defaultPressed?: boolean; + pressed?: boolean; children?: React.ReactNode; className?: string; label?: string; disabled?: boolean; - onPressedChange : (isPressed:boolean) => void; - + onPressedChange: (isPressed: boolean) => void; } -const TogglePrimitive = ({ children, label = '', defaultPressed, pressed, onPressedChange = () => {}, disabled, ...props }:TogglePrimitiveProps) => { - const [isPressed, setIsPressed] = useState(pressed || defaultPressed); + +const TogglePrimitive = ({ + children, + label = '', + defaultPressed = false, + pressed: controlledPressed, + onPressedChange = () => {}, + disabled, + ...props +}: TogglePrimitiveProps) => { + const [uncontrolledPressed, setUncontrolledPressed] = useState(defaultPressed); + + const isControlled = controlledPressed !== undefined; + const isPressed = isControlled ? controlledPressed : uncontrolledPressed; const handlePressed = () => { + if (disabled) { + return; + } + const updatedPressed = !isPressed; - setIsPressed(updatedPressed); + if (!isControlled) { + setUncontrolledPressed(updatedPressed); + } onPressedChange(updatedPressed); }; const ariaAttributes:any = label ? { 'aria-label': label } : {}; ariaAttributes['aria-pressed'] = isPressed ? 'true' : 'false'; ariaAttributes['aria-disabled'] = disabled ? 'true' : 'false'; + return { render(Test Content); expect(screen.getByRole('button')).toHaveAttribute('aria-disabled', 'true'); }); + + it('renders in controlled mode correctly', () => { + const onPressedChange = jest.fn(); + const { rerender } = render( + + Test Content + + ); + expect(screen.getByRole('button')).toHaveAttribute('data-state', 'off'); + + // Click should trigger onPressedChange but not change state directly + fireEvent.click(screen.getByRole('button')); + expect(onPressedChange).toHaveBeenCalledWith(true); + expect(screen.getByRole('button')).toHaveAttribute('data-state', 'off'); + + // State should only change when pressed prop changes + rerender( + + Test Content + + ); + expect(screen.getByRole('button')).toHaveAttribute('data-state', 'on'); + }); + + it('handles multiple clicks correctly in uncontrolled mode', () => { + render(Test Content); + const button = screen.getByRole('button'); + + fireEvent.click(button); + expect(button).toHaveAttribute('data-state', 'on'); + + fireEvent.click(button); + expect(button).toHaveAttribute('data-state', 'off'); + }); + + it('prevents state change when disabled', () => { + const onPressedChange = jest.fn(); + render( + + Test Content + + ); + + fireEvent.click(screen.getByRole('button')); + expect(onPressedChange).not.toHaveBeenCalled(); + expect(screen.getByRole('button')).toHaveAttribute('data-state', 'off'); + }); + + it('maintains controlled state after multiple clicks', () => { + const onPressedChange = jest.fn(); + render( + + Test Content + + ); + const button = screen.getByRole('button'); + + fireEvent.click(button); + fireEvent.click(button); + + expect(onPressedChange).toHaveBeenCalledTimes(2); + expect(button).toHaveAttribute('data-state', 'off'); + }); }); From cae77cd4371387da347de94de9dbb048cd260820 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sun, 24 Nov 2024 12:43:02 +0530 Subject: [PATCH 12/16] Add keyboard interaction handling to TogglePrimitive --- src/core/primitives/Toggle/index.tsx | 8 +++++++ .../Toggle/tests/TogglePrimitive.test.js | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/core/primitives/Toggle/index.tsx b/src/core/primitives/Toggle/index.tsx index ce2217ad..64e73af7 100644 --- a/src/core/primitives/Toggle/index.tsx +++ b/src/core/primitives/Toggle/index.tsx @@ -38,12 +38,20 @@ const TogglePrimitive = ({ onPressedChange(updatedPressed); }; + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + handlePressed(); + } + }; + const ariaAttributes:any = label ? { 'aria-label': label } : {}; ariaAttributes['aria-pressed'] = isPressed ? 'true' : 'false'; ariaAttributes['aria-disabled'] = disabled ? 'true' : 'false'; return { expect(onPressedChange).toHaveBeenCalledTimes(2); expect(button).toHaveAttribute('data-state', 'off'); }); + + it('handles keyboard interactions correctly', () => { + const onPressedChange = jest.fn(); + render(Test Content); + const button = screen.getByRole('button'); + + // Initial state check + expect(button).toHaveAttribute('data-state', 'off'); + + // Test Space key + fireEvent.click(button); // Just simulate the click directly + expect(button).toHaveAttribute('data-state', 'on'); + expect(onPressedChange).toHaveBeenCalledWith(true); + + // Test Enter key + fireEvent.click(button); + expect(button).toHaveAttribute('data-state', 'off'); + expect(onPressedChange).toHaveBeenCalledWith(false); + + // Test that other keys don't trigger the toggle + fireEvent.keyDown(button, { key: 'A' }); + expect(button).toHaveAttribute('data-state', 'off'); + expect(onPressedChange).toHaveBeenCalledTimes(2); + }); }); From dc91df187b3bec3c637782a21d6611a8f62dddf4 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sun, 24 Nov 2024 12:43:57 +0530 Subject: [PATCH 13/16] Add keyboard event handling in TogglePrimitive --- src/core/primitives/Toggle/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/primitives/Toggle/index.tsx b/src/core/primitives/Toggle/index.tsx index 64e73af7..db80f59b 100644 --- a/src/core/primitives/Toggle/index.tsx +++ b/src/core/primitives/Toggle/index.tsx @@ -39,6 +39,8 @@ const TogglePrimitive = ({ }; const handleKeyDown = (event: React.KeyboardEvent) => { + // TODO: Should these be handled by the browser? + // Or should we add these functionalities inside the ButtonPrimitive? if (event.key === ' ' || event.key === 'Enter') { event.preventDefault(); handlePressed(); From 1d0d724f4f2e7db4a78eff347cefe52bb19a28d4 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sun, 24 Nov 2024 12:48:28 +0530 Subject: [PATCH 14/16] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/core/primitives/Toggle/tests/TogglePrimitive.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/primitives/Toggle/tests/TogglePrimitive.test.js b/src/core/primitives/Toggle/tests/TogglePrimitive.test.js index 42f36b4a..6729b57d 100644 --- a/src/core/primitives/Toggle/tests/TogglePrimitive.test.js +++ b/src/core/primitives/Toggle/tests/TogglePrimitive.test.js @@ -46,7 +46,7 @@ describe('TogglePrimitive', () => { it('renders with disabled correctly', () => { render(Test Content); - expect(screen.getByText('Test Content')).toHaveAttribute('disabled'); + expect(screen.getByRole('button')).toHaveAttribute('disabled'); }); it('renders with correct ARIA attributes', () => { From cf3588501e7eaef07d16196aac898883f8a8b3e0 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Sun, 24 Nov 2024 12:50:39 +0530 Subject: [PATCH 15/16] Update src/core/batches.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/core/batches.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/batches.ts b/src/core/batches.ts index f746ad3a..655e5133 100644 --- a/src/core/batches.ts +++ b/src/core/batches.ts @@ -17,7 +17,7 @@ export const getActiveBatchItem = (batches: NodeList) => { export const getNextBatchItem = (batches: NodeList, loop = false): Element => { const activeItem = getActiveBatchItem(batches) as HTMLElement | null; - // get the next item, return it if it is not the last item + // Try to get the next sibling element const nextItem = activeItem?.nextElementSibling; if (nextItem) { @@ -25,11 +25,11 @@ export const getNextBatchItem = (batches: NodeList, loop = false): Element => { } if (loop) { - // if it is the last item, return the first item + // When at the end and looping is enabled, return the first item return batches[0] as HTMLElement; } - // if it is the last item, return the last item + // When at the end and looping is disabled, stay on the last item return batches[batches.length - 1] as HTMLElement; }; From 972e3e787a667926c5393c3044e1f9ec923b780a Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Tue, 26 Nov 2024 08:36:03 +0530 Subject: [PATCH 16/16] Update TogglePrimitive test for key events handling --- src/core/primitives/Toggle/tests/TogglePrimitive.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/primitives/Toggle/tests/TogglePrimitive.test.js b/src/core/primitives/Toggle/tests/TogglePrimitive.test.js index 6729b57d..17d7e3db 100644 --- a/src/core/primitives/Toggle/tests/TogglePrimitive.test.js +++ b/src/core/primitives/Toggle/tests/TogglePrimitive.test.js @@ -135,12 +135,14 @@ describe('TogglePrimitive', () => { expect(button).toHaveAttribute('data-state', 'off'); // Test Space key - fireEvent.click(button); // Just simulate the click directly + fireEvent.keyDown(button, { key: ' ' }); + fireEvent.keyUp(button, { key: ' ' }); expect(button).toHaveAttribute('data-state', 'on'); expect(onPressedChange).toHaveBeenCalledWith(true); // Test Enter key - fireEvent.click(button); + fireEvent.keyDown(button, { key: 'Enter' }); + fireEvent.keyUp(button, { key: 'Enter' }); expect(button).toHaveAttribute('data-state', 'off'); expect(onPressedChange).toHaveBeenCalledWith(false);