From ba286787d0f0961f5766e8e5e9e157e3b85679c0 Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Mon, 8 Jan 2024 09:52:17 +0100 Subject: [PATCH 01/12] Start implementation of Toggle and ToggleGroup component --- src/components/index.ts | 1 + src/components/toggles/index.ts | 2 + src/components/toggles/toggle/index.ts | 1 + .../toggles/toggle/toggle.stories.tsx | 54 ++++++++++++++++ src/components/toggles/toggle/toggle.test.tsx | 18 ++++++ src/components/toggles/toggle/toggle.tsx | 60 ++++++++++++++++++ src/components/toggles/toggleContext/index.ts | 1 + .../toggles/toggleContext/toggleContext.ts | 18 ++++++ src/components/toggles/toggleGroup/index.ts | 2 + .../toggles/toggleGroup/toggleGroup.api.ts | 20 ++++++ .../toggleGroup/toggleGroup.stories.tsx | 63 +++++++++++++++++++ .../toggles/toggleGroup/toggleGroup.tsx | 12 ++++ 12 files changed, 252 insertions(+) create mode 100644 src/components/toggles/index.ts create mode 100644 src/components/toggles/toggle/index.ts create mode 100644 src/components/toggles/toggle/toggle.stories.tsx create mode 100644 src/components/toggles/toggle/toggle.test.tsx create mode 100644 src/components/toggles/toggle/toggle.tsx create mode 100644 src/components/toggles/toggleContext/index.ts create mode 100644 src/components/toggles/toggleContext/toggleContext.ts create mode 100644 src/components/toggles/toggleGroup/index.ts create mode 100644 src/components/toggles/toggleGroup/toggleGroup.api.ts create mode 100644 src/components/toggles/toggleGroup/toggleGroup.stories.tsx create mode 100644 src/components/toggles/toggleGroup/toggleGroup.tsx diff --git a/src/components/index.ts b/src/components/index.ts index b7b735e28..f987a1063 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -10,3 +10,4 @@ export * from './progress'; export * from './spinner'; export * from './switch'; export * from './tag'; +export * from './toggles'; diff --git a/src/components/toggles/index.ts b/src/components/toggles/index.ts new file mode 100644 index 000000000..71ade5c71 --- /dev/null +++ b/src/components/toggles/index.ts @@ -0,0 +1,2 @@ +export * from './toggle'; +export * from './toggleGroup'; diff --git a/src/components/toggles/toggle/index.ts b/src/components/toggles/toggle/index.ts new file mode 100644 index 000000000..60eeec61b --- /dev/null +++ b/src/components/toggles/toggle/index.ts @@ -0,0 +1 @@ +export { Toggle, type IToggleProps, type ToggleValue } from './toggle'; diff --git a/src/components/toggles/toggle/toggle.stories.tsx b/src/components/toggles/toggle/toggle.stories.tsx new file mode 100644 index 000000000..621bd2603 --- /dev/null +++ b/src/components/toggles/toggle/toggle.stories.tsx @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; +import { ToggleGroup } from '../toggleGroup'; +import { Toggle, type IToggleProps } from './toggle'; + +const meta: Meta = { + title: 'components/Toggles/Toggle', + component: Toggle, + tags: ['autodocs'], + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/jfKRr1V9evJUp1uBeyP3Zz/v1.0.0?type=design&node-id=9778-14&mode=design&t=eAkFH3nzSllOw9zp-4', + }, + }, +}; + +type Story = StoryObj; + +const DefaultComponent = (props: IToggleProps) => { + const [value, setValue] = useState(); + + const handleChange = (value: string | undefined) => setValue(value); + + return ( + + + + ); +}; + +/** + * Default usage example of the Toggle component. + */ +export const Default: Story = { + render: (props) => , + args: { + value: 'value', + label: 'Label', + }, +}; + +/** + * Disabled Toggle component. + */ +export const Disabled: Story = { + render: (props) => , + args: { + disabled: true, + label: 'Disabled', + }, +}; + +export default meta; diff --git a/src/components/toggles/toggle/toggle.test.tsx b/src/components/toggles/toggle/toggle.test.tsx new file mode 100644 index 000000000..26c17ef5c --- /dev/null +++ b/src/components/toggles/toggle/toggle.test.tsx @@ -0,0 +1,18 @@ +import { render } from '@testing-library/react'; +import { Toggle, type IToggleProps } from './toggle'; + +describe(' component', () => { + const createTestComponent = (props?: Partial) => { + const completeProps: IToggleProps = { + label: 'label', + value: 'value', + ...props, + }; + + return ; + }; + + it('TODO', () => { + render(createTestComponent()); + }); +}); diff --git a/src/components/toggles/toggle/toggle.tsx b/src/components/toggles/toggle/toggle.tsx new file mode 100644 index 000000000..12d3b7178 --- /dev/null +++ b/src/components/toggles/toggle/toggle.tsx @@ -0,0 +1,60 @@ +import classNames from 'classnames'; +import type { ComponentProps } from 'react'; +import { useToggleContext } from '../toggleContext'; + +export type ToggleValue = string | number; + +export interface IToggleProps + extends Omit, 'value'> { + /** + * Value of the toggle. + */ + value: TValue; + /** + * Label of the toggle. + */ + label: string; +} + +export const Toggle = (props: IToggleProps) => { + const { className, label, value, disabled, ...otherProps } = props; + + const { value: currentValue, onChange, isMultiSelect } = useToggleContext(); + + const isActive = + isMultiSelect && Array.isArray(currentValue) ? currentValue.includes(value) : value === currentValue; + + const toggleClasses = classNames( + 'flex h-10 items-center rounded-[40px] border border-neutral-100 px-4', // Default + 'focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset', // Focus state + 'hover:enabled:border-neutral-200 hover:enabled:shadow-primary-md', // Hover state + { 'bg-neutral-0 text-neutral-600': !isActive && !disabled }, // Default state + { 'bg-neutral-100 text-neutral-800': isActive && !disabled }, // Active state + { 'bg-neutral-100 text-neutral-300': disabled }, // Disabled state + className, + ); + + const handleToggleClick = () => { + if (isMultiSelect) { + const parsedSelection = Array.isArray(currentValue) + ? currentValue + : currentValue != null + ? [currentValue] + : []; + + const newSelection = parsedSelection.includes(value) + ? parsedSelection.filter((field) => field !== value) + : parsedSelection.concat(value); + + onChange(newSelection); + } else { + onChange(currentValue === value ? undefined : value); + } + }; + + return ( + + ); +}; diff --git a/src/components/toggles/toggleContext/index.ts b/src/components/toggles/toggleContext/index.ts new file mode 100644 index 000000000..838502bb7 --- /dev/null +++ b/src/components/toggles/toggleContext/index.ts @@ -0,0 +1 @@ +export { ToggleContextProvider, useToggleContext, type IToggleContext } from './toggleContext'; diff --git a/src/components/toggles/toggleContext/toggleContext.ts b/src/components/toggles/toggleContext/toggleContext.ts new file mode 100644 index 000000000..ef1560000 --- /dev/null +++ b/src/components/toggles/toggleContext/toggleContext.ts @@ -0,0 +1,18 @@ +import { createContext, useContext } from 'react'; +import { type IToggleGroupProps } from '../toggleGroup/toggleGroup.api'; + +export type IToggleContext = Pick; + +export const toggleContext = createContext(null); + +export const ToggleContextProvider = toggleContext.Provider; + +export const useToggleContext = (): IToggleContext => { + const values = useContext(toggleContext); + + if (values == null) { + throw new Error('useToggleContext: hook must be used inside a ToggleContextProvider to work properly'); + } + + return values; +}; diff --git a/src/components/toggles/toggleGroup/index.ts b/src/components/toggles/toggleGroup/index.ts new file mode 100644 index 000000000..beca149bb --- /dev/null +++ b/src/components/toggles/toggleGroup/index.ts @@ -0,0 +1,2 @@ +export { ToggleGroup } from './toggleGroup'; +export type { IToggleGroupProps, ToggleActiveValue } from './toggleGroup.api'; diff --git a/src/components/toggles/toggleGroup/toggleGroup.api.ts b/src/components/toggles/toggleGroup/toggleGroup.api.ts new file mode 100644 index 000000000..8dc68eeaf --- /dev/null +++ b/src/components/toggles/toggleGroup/toggleGroup.api.ts @@ -0,0 +1,20 @@ +import type { ComponentProps } from 'react'; +import type { ToggleValue } from '../toggle'; + +export type ToggleActiveValue = TValue | TValue[] | undefined; + +export interface IToggleGroupProps + extends Omit, 'onChange'> { + /** + * Allows multiple toggles to be selected at the same time when set to true. + */ + isMultiSelect?: boolean; + /** + * Current value of the toggle selection. + */ + value: TValue; + /** + * Callback called on toggle selection change. + */ + onChange: (value: TValue) => void; +} diff --git a/src/components/toggles/toggleGroup/toggleGroup.stories.tsx b/src/components/toggles/toggleGroup/toggleGroup.stories.tsx new file mode 100644 index 000000000..437ef830f --- /dev/null +++ b/src/components/toggles/toggleGroup/toggleGroup.stories.tsx @@ -0,0 +1,63 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; +import { Toggle } from '../toggle'; +import { ToggleGroup } from './toggleGroup'; +import { type IToggleGroupProps } from './toggleGroup.api'; + +const meta: Meta = { + title: 'components/Toggles/ToggleGroup', + component: Toggle, + tags: ['autodocs'], + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/jfKRr1V9evJUp1uBeyP3Zz/v1.0.0?type=design&node-id=9778-14&mode=design&t=eAkFH3nzSllOw9zp-4', + }, + }, +}; + +type Story = StoryObj; + +const DefaultComponent = (props: Omit) => { + const [value, setValue] = useState(); + + const handleChange = (value: string | undefined) => setValue(value); + + return ( + + + + + + ); +}; + +/** + * Default usage example of the ToggleGroup component. + */ +export const Default: Story = { + render: ({ value, onChange, ...props }) => , +}; + +const MultiSelectComponent = (props: Omit) => { + const [value, setValue] = useState(); + + const handleChange = (value: string[] | undefined) => setValue(value); + + return ( + + + + + + ); +}; + +/** + * ToggleGroup component used with multiple selection. + */ +export const MultiSelect: Story = { + render: ({ value, onChange, ...props }) => , +}; + +export default meta; diff --git a/src/components/toggles/toggleGroup/toggleGroup.tsx b/src/components/toggles/toggleGroup/toggleGroup.tsx new file mode 100644 index 000000000..2c6e04917 --- /dev/null +++ b/src/components/toggles/toggleGroup/toggleGroup.tsx @@ -0,0 +1,12 @@ +import { ToggleContextProvider } from '../toggleContext'; +import type { IToggleGroupProps, ToggleActiveValue } from './toggleGroup.api'; + +export const ToggleGroup = (props: IToggleGroupProps) => { + const { value, onChange, isMultiSelect, ...otherProps } = props; + + return ( + void }}> +
+ + ); +}; From 3c7edb039449c0c8e7055109d66c4ea683dda381 Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Mon, 8 Jan 2024 12:49:16 +0100 Subject: [PATCH 02/12] Implement tests for toggle-group and toggle-context components --- .../toggles/toggle/toggle.stories.tsx | 10 +++--- .../toggleContext/toggleContext.test.tsx | 32 +++++++++++++++++++ .../toggleGroup/toggleGroup.stories.tsx | 14 +++----- .../toggles/toggleGroup/toggleGroup.test.tsx | 25 +++++++++++++++ .../toggles/toggleGroup/toggleGroup.tsx | 13 ++++++-- src/test/index.ts | 1 + src/test/setup.ts | 4 +++ src/test/utils/index.ts | 1 + src/test/utils/testLogger.ts | 26 +++++++++++++++ 9 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 src/components/toggles/toggleContext/toggleContext.test.tsx create mode 100644 src/components/toggles/toggleGroup/toggleGroup.test.tsx create mode 100644 src/test/index.ts create mode 100644 src/test/utils/index.ts create mode 100644 src/test/utils/testLogger.ts diff --git a/src/components/toggles/toggle/toggle.stories.tsx b/src/components/toggles/toggle/toggle.stories.tsx index 621bd2603..5be8236d2 100644 --- a/src/components/toggles/toggle/toggle.stories.tsx +++ b/src/components/toggles/toggle/toggle.stories.tsx @@ -17,13 +17,11 @@ const meta: Meta = { type Story = StoryObj; -const DefaultComponent = (props: IToggleProps) => { +const ToggleComponent = (props: IToggleProps) => { const [value, setValue] = useState(); - const handleChange = (value: string | undefined) => setValue(value); - return ( - + ); @@ -33,7 +31,7 @@ const DefaultComponent = (props: IToggleProps) => { * Default usage example of the Toggle component. */ export const Default: Story = { - render: (props) => , + render: (props) => , args: { value: 'value', label: 'Label', @@ -44,7 +42,7 @@ export const Default: Story = { * Disabled Toggle component. */ export const Disabled: Story = { - render: (props) => , + render: (props) => , args: { disabled: true, label: 'Disabled', diff --git a/src/components/toggles/toggleContext/toggleContext.test.tsx b/src/components/toggles/toggleContext/toggleContext.test.tsx new file mode 100644 index 000000000..a0ec5a357 --- /dev/null +++ b/src/components/toggles/toggleContext/toggleContext.test.tsx @@ -0,0 +1,32 @@ +import { renderHook } from '@testing-library/react'; +import type { ReactNode } from 'react'; +import { testLogger } from '../../../test'; +import { ToggleContextProvider, useToggleContext, type IToggleContext } from './toggleContext'; + +describe('useToggleContext hook', () => { + const createHookWrapper = (values?: Partial) => + function toggleContextWrapper(props: { children?: ReactNode }) { + const completeValues: IToggleContext = { + value: undefined, + onChange: jest.fn(), + ...values, + }; + + return {props.children}; + }; + + it('throws error when not used with a ToggleContext provider ', () => { + testLogger.suppressErrors(); + expect(() => renderHook(() => useToggleContext())).toThrowError(); + }); + + it('returns the ToggleContext values', () => { + const values: IToggleContext = { + value: 'selected-toggle', + onChange: jest.fn(), + isMultiSelect: true, + }; + const { result } = renderHook(() => useToggleContext(), { wrapper: createHookWrapper(values) }); + expect(result.current).toEqual(values); + }); +}); diff --git a/src/components/toggles/toggleGroup/toggleGroup.stories.tsx b/src/components/toggles/toggleGroup/toggleGroup.stories.tsx index 437ef830f..2e70f67f3 100644 --- a/src/components/toggles/toggleGroup/toggleGroup.stories.tsx +++ b/src/components/toggles/toggleGroup/toggleGroup.stories.tsx @@ -21,10 +21,8 @@ type Story = StoryObj; const DefaultComponent = (props: Omit) => { const [value, setValue] = useState(); - const handleChange = (value: string | undefined) => setValue(value); - return ( - + @@ -42,13 +40,11 @@ export const Default: Story = { const MultiSelectComponent = (props: Omit) => { const [value, setValue] = useState(); - const handleChange = (value: string[] | undefined) => setValue(value); - return ( - - - - + + + + ); }; diff --git a/src/components/toggles/toggleGroup/toggleGroup.test.tsx b/src/components/toggles/toggleGroup/toggleGroup.test.tsx new file mode 100644 index 000000000..9eede2176 --- /dev/null +++ b/src/components/toggles/toggleGroup/toggleGroup.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from '@testing-library/react'; +import { Toggle } from '../toggle'; +import { ToggleGroup } from './toggleGroup'; +import type { IToggleGroupProps } from './toggleGroup.api'; + +describe(' component', () => { + const createTestComponent = (props?: Partial) => { + const completeProps: IToggleGroupProps = { + value: undefined, + onChange: jest.fn(), + ...props, + }; + + return ; + }; + + it('renders the children components', () => { + const children = [ + , + , + ]; + render(createTestComponent({ children })); + expect(screen.getAllByRole('button')).toHaveLength(children.length); + }); +}); diff --git a/src/components/toggles/toggleGroup/toggleGroup.tsx b/src/components/toggles/toggleGroup/toggleGroup.tsx index 2c6e04917..d94a0c233 100644 --- a/src/components/toggles/toggleGroup/toggleGroup.tsx +++ b/src/components/toggles/toggleGroup/toggleGroup.tsx @@ -1,12 +1,19 @@ +import classNames from 'classnames'; +import { useMemo } from 'react'; import { ToggleContextProvider } from '../toggleContext'; import type { IToggleGroupProps, ToggleActiveValue } from './toggleGroup.api'; export const ToggleGroup = (props: IToggleGroupProps) => { - const { value, onChange, isMultiSelect, ...otherProps } = props; + const { value, onChange, isMultiSelect, className, ...otherProps } = props; + + const contextValues = useMemo( + () => ({ isMultiSelect, value, onChange: onChange as (value: ToggleActiveValue) => void }), + [isMultiSelect, value, onChange], + ); return ( - void }}> -
+ +
); }; diff --git a/src/test/index.ts b/src/test/index.ts new file mode 100644 index 000000000..04bca77e0 --- /dev/null +++ b/src/test/index.ts @@ -0,0 +1 @@ +export * from './utils'; diff --git a/src/test/setup.ts b/src/test/setup.ts index 8f2609b7b..5ed1ddd41 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -3,3 +3,7 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; +import { testLogger } from './utils'; + +// Setup debug logger +testLogger.setup(); diff --git a/src/test/utils/index.ts b/src/test/utils/index.ts new file mode 100644 index 000000000..d22fedcd8 --- /dev/null +++ b/src/test/utils/index.ts @@ -0,0 +1 @@ +export * from './testLogger'; diff --git a/src/test/utils/testLogger.ts b/src/test/utils/testLogger.ts new file mode 100644 index 000000000..5d11a8f01 --- /dev/null +++ b/src/test/utils/testLogger.ts @@ -0,0 +1,26 @@ +/* eslint-disable no-console */ +class TestLogger { + private shouldSuppressErrors = false; + private originalConsoleError = console.error; + + setup = () => { + beforeEach(() => { + console.error = jest.fn((...params) => { + if (!this.shouldSuppressErrors) { + this.originalConsoleError.apply(console, params); + } + }); + }); + + afterEach(() => { + this.shouldSuppressErrors = false; + console.error = this.originalConsoleError; + }); + }; + + suppressErrors = () => { + this.shouldSuppressErrors = true; + }; +} + +export const testLogger = new TestLogger(); From 028ee06efef1bed78204f4c52568e4ff34a08c91 Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Mon, 8 Jan 2024 13:11:37 +0100 Subject: [PATCH 03/12] Cleanup implementation --- jest.config.js | 2 +- src/components/toggles/toggle/toggle.test.tsx | 17 ++++++++++++++--- src/test/setup.ts | 2 +- src/test/utils/index.ts | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/jest.config.js b/jest.config.js index 1c285286b..aae6dfa82 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,7 @@ const config = { testEnvironment: 'jsdom', collectCoverageFrom: ['./src/**/*.{ts,tsx}'], - coveragePathIgnorePatterns: ['.d.ts', '.api.ts', 'index.ts', '.stories.tsx'], + coveragePathIgnorePatterns: ['.d.ts', '.api.ts', 'index.ts', '.stories.tsx', './src/test/*'], setupFilesAfterEnv: ['/src/test/setup.ts'], transform: { '^.+\\.svg$': '/src/test/svgTransform.js', diff --git a/src/components/toggles/toggle/toggle.test.tsx b/src/components/toggles/toggle/toggle.test.tsx index 26c17ef5c..282ff7829 100644 --- a/src/components/toggles/toggle/toggle.test.tsx +++ b/src/components/toggles/toggle/toggle.test.tsx @@ -1,15 +1,26 @@ import { render } from '@testing-library/react'; +import { ToggleContextProvider, type IToggleContext } from '../toggleContext'; import { Toggle, type IToggleProps } from './toggle'; describe(' component', () => { - const createTestComponent = (props?: Partial) => { + const createTestComponent = (values?: { props?: Partial; context?: Partial }) => { const completeProps: IToggleProps = { label: 'label', value: 'value', - ...props, + ...values?.props, }; - return ; + const completeContext: IToggleContext = { + value: undefined, + onChange: jest.fn(), + ...values?.context, + }; + + return ( + + + + ); }; it('TODO', () => { diff --git a/src/test/setup.ts b/src/test/setup.ts index 5ed1ddd41..7ff1a6b29 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -5,5 +5,5 @@ import '@testing-library/jest-dom'; import { testLogger } from './utils'; -// Setup debug logger +// Setup test logger testLogger.setup(); diff --git a/src/test/utils/index.ts b/src/test/utils/index.ts index d22fedcd8..2b5e62a84 100644 --- a/src/test/utils/index.ts +++ b/src/test/utils/index.ts @@ -1 +1 @@ -export * from './testLogger'; +export { testLogger } from './testLogger'; From 596ec21554e5376a76befe3a6f1fbe3edef6d066 Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Mon, 8 Jan 2024 13:12:14 +0100 Subject: [PATCH 04/12] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b170382c3..fa1a1dbfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added -- Implement `Card`, `CardSummary`, and `Switch` components +- Implement `Card`, `CardSummary`, `Switch`, `Toggle` and `ToggleGroup` components ### Changed From 09e78c63ff7dd0fa6c91685f0498bc242f0187b3 Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Mon, 8 Jan 2024 13:33:05 +0100 Subject: [PATCH 05/12] Implement tests for Toggle component --- src/components/toggles/toggle/toggle.test.tsx | 81 ++++++++++++++++++- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/src/components/toggles/toggle/toggle.test.tsx b/src/components/toggles/toggle/toggle.test.tsx index 282ff7829..c0dae87ac 100644 --- a/src/components/toggles/toggle/toggle.test.tsx +++ b/src/components/toggles/toggle/toggle.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { ToggleContextProvider, type IToggleContext } from '../toggleContext'; import { Toggle, type IToggleProps } from './toggle'; @@ -23,7 +23,82 @@ describe(' component', () => { ); }; - it('TODO', () => { - render(createTestComponent()); + it('renders a button with the specified label', () => { + const label = 'Toggle Label'; + const props = { label }; + render(createTestComponent({ props })); + expect(screen.getByRole('button', { name: label })).toBeInTheDocument(); + }); + + it('renders the toggle as active when active value matches toggle value', () => { + const activeValue = 'active-value'; + const toggleValue = activeValue; + const props = { value: toggleValue }; + const context = { value: activeValue }; + render(createTestComponent({ props, context })); + expect(screen.getByRole('button').className).toContain('text-neutral-800'); + }); + + it('renders the toggle as active when active value includes toggle value on multi-select variant', () => { + const activeValues = ['first', 'second']; + const toggleValue = activeValues[0]; + const props = { value: toggleValue }; + const context = { isMultiSelect: true, value: activeValues }; + render(createTestComponent({ props, context })); + expect(screen.getByRole('button').className).toContain('text-neutral-800'); + }); + + it('renders the button as disabled when the disabled prop is set to true', () => { + const disabled = true; + const props = { disabled }; + render(createTestComponent({ props })); + expect(screen.getByRole('button')).toBeDisabled(); + }); + + it('correctly updates the active value on toggle click', () => { + const onChange = jest.fn(); + const toggleValue = 'test'; + const context = { onChange }; + const props = { value: toggleValue }; + const { rerender } = render(createTestComponent({ props, context })); + + fireEvent.click(screen.getByRole('button')); + expect(onChange).toHaveBeenCalledWith(toggleValue); + + const newContext = { onChange, value: toggleValue }; + rerender(createTestComponent({ props, context: newContext })); + + fireEvent.click(screen.getByRole('button')); + expect(onChange).toHaveBeenCalledWith(undefined); + }); + + it('correctly updates the active values on toggle click on multi-select variant', () => { + const onChange = jest.fn(); + const activeValues = ['first', 'second']; + const toggleValue = 'third'; + const context = { isMultiSelect: true, onChange, value: activeValues }; + const props = { value: toggleValue }; + const { rerender } = render(createTestComponent({ props, context })); + + fireEvent.click(screen.getByRole('button')); + const expectedNewValues = [...activeValues, toggleValue]; + expect(onChange).toHaveBeenCalledWith(expectedNewValues); + + const newContext = { ...context, value: expectedNewValues }; + rerender(createTestComponent({ props, context: newContext })); + + fireEvent.click(screen.getByRole('button')); + expect(onChange).toHaveBeenCalledWith(activeValues); + }); + + it('correclty updates the active values when initial value is undefined on multi-select variant', () => { + const onChange = jest.fn(); + const toggleValue = 'first'; + const context = { isMultiSelect: true, onChange }; + const props = { value: toggleValue }; + render(createTestComponent({ props, context })); + + fireEvent.click(screen.getByRole('button')); + expect(onChange).toHaveBeenCalledWith([toggleValue]); }); }); From 7be19571be92ca6ef0515ec61b8ec47927a36607 Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Mon, 8 Jan 2024 14:57:05 +0100 Subject: [PATCH 06/12] Update testLogger.ts --- src/test/utils/testLogger.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/test/utils/testLogger.ts b/src/test/utils/testLogger.ts index 5d11a8f01..75142116f 100644 --- a/src/test/utils/testLogger.ts +++ b/src/test/utils/testLogger.ts @@ -3,13 +3,15 @@ class TestLogger { private shouldSuppressErrors = false; private originalConsoleError = console.error; + private testErrorLogger = jest.fn((...params) => { + if (!this.shouldSuppressErrors) { + this.originalConsoleError.apply(console, params); + } + }); + setup = () => { beforeEach(() => { - console.error = jest.fn((...params) => { - if (!this.shouldSuppressErrors) { - this.originalConsoleError.apply(console, params); - } - }); + console.error = this.testErrorLogger; }); afterEach(() => { From c6cb98430f55378031a25685dcbf36766fbf489d Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Wed, 10 Jan 2024 10:12:54 +0100 Subject: [PATCH 07/12] Fix toggle group layout --- src/components/toggles/toggleGroup/toggleGroup.stories.tsx | 2 ++ src/components/toggles/toggleGroup/toggleGroup.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/toggles/toggleGroup/toggleGroup.stories.tsx b/src/components/toggles/toggleGroup/toggleGroup.stories.tsx index 2e70f67f3..669d2b638 100644 --- a/src/components/toggles/toggleGroup/toggleGroup.stories.tsx +++ b/src/components/toggles/toggleGroup/toggleGroup.stories.tsx @@ -26,6 +26,8 @@ const DefaultComponent = (props: Omit) + + ); }; diff --git a/src/components/toggles/toggleGroup/toggleGroup.tsx b/src/components/toggles/toggleGroup/toggleGroup.tsx index d94a0c233..afb559a52 100644 --- a/src/components/toggles/toggleGroup/toggleGroup.tsx +++ b/src/components/toggles/toggleGroup/toggleGroup.tsx @@ -13,7 +13,7 @@ export const ToggleGroup = (props: IToggleGrou return ( -
+
); }; From c046357362d9a46f24b916d86c9bd66fe6a4ce16 Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Wed, 10 Jan 2024 14:00:14 +0100 Subject: [PATCH 08/12] Start updating toggle component to use radix-ui primitive --- package.json | 1 + .../toggles/toggle/toggle.stories.tsx | 31 +++++++++--- src/components/toggles/toggle/toggle.tsx | 44 ++++------------- src/components/toggles/toggleContext/index.ts | 1 - .../toggleContext/toggleContext.test.tsx | 32 ------------- .../toggles/toggleContext/toggleContext.ts | 18 ------- .../toggles/toggleGroup/toggleGroup.api.ts | 20 -------- .../toggleGroup/toggleGroup.stories.tsx | 29 ++++++++---- .../toggles/toggleGroup/toggleGroup.test.tsx | 17 ++++--- .../toggles/toggleGroup/toggleGroup.tsx | 47 ++++++++++++++----- yarn.lock | 2 +- 11 files changed, 100 insertions(+), 142 deletions(-) delete mode 100644 src/components/toggles/toggleContext/index.ts delete mode 100644 src/components/toggles/toggleContext/toggleContext.test.tsx delete mode 100644 src/components/toggles/toggleContext/toggleContext.ts delete mode 100644 src/components/toggles/toggleGroup/toggleGroup.api.ts diff --git a/package.json b/package.json index 384a48a7d..615ffceb6 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "dependencies": { "@radix-ui/react-progress": "^1.0.0", "@radix-ui/react-switch": "^1.0.0", + "@radix-ui/react-toggle-group": "^1.0.0", "classnames": "^2.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" diff --git a/src/components/toggles/toggle/toggle.stories.tsx b/src/components/toggles/toggle/toggle.stories.tsx index 5be8236d2..b91c3a635 100644 --- a/src/components/toggles/toggle/toggle.stories.tsx +++ b/src/components/toggles/toggle/toggle.stories.tsx @@ -17,21 +17,36 @@ const meta: Meta = { type Story = StoryObj; -const ToggleComponent = (props: IToggleProps) => { +/** + * Default usage example of the Toggle component. + */ +export const Default: Story = { + render: (props) => ( + + + + ), + args: { + value: 'value', + label: 'Label', + }, +}; + +const ControllerComponent = (props: IToggleProps) => { const [value, setValue] = useState(); return ( - + ); }; /** - * Default usage example of the Toggle component. + * Controlled usage example of the Toggle component. */ -export const Default: Story = { - render: (props) => , +export const Controlled: Story = { + render: (props) => , args: { value: 'value', label: 'Label', @@ -42,7 +57,11 @@ export const Default: Story = { * Disabled Toggle component. */ export const Disabled: Story = { - render: (props) => , + render: (props) => ( + + + + ), args: { disabled: true, label: 'Disabled', diff --git a/src/components/toggles/toggle/toggle.tsx b/src/components/toggles/toggle/toggle.tsx index 12d3b7178..816b0fb7a 100644 --- a/src/components/toggles/toggle/toggle.tsx +++ b/src/components/toggles/toggle/toggle.tsx @@ -1,60 +1,34 @@ +import { ToggleGroupItem as RadixToggle } from '@radix-ui/react-toggle-group'; import classNames from 'classnames'; import type { ComponentProps } from 'react'; -import { useToggleContext } from '../toggleContext'; -export type ToggleValue = string | number; - -export interface IToggleProps - extends Omit, 'value'> { +export interface IToggleProps extends Omit, 'ref'> { /** * Value of the toggle. */ - value: TValue; + value: string; /** * Label of the toggle. */ label: string; } -export const Toggle = (props: IToggleProps) => { +export const Toggle: React.FC = (props) => { const { className, label, value, disabled, ...otherProps } = props; - const { value: currentValue, onChange, isMultiSelect } = useToggleContext(); - - const isActive = - isMultiSelect && Array.isArray(currentValue) ? currentValue.includes(value) : value === currentValue; - const toggleClasses = classNames( 'flex h-10 items-center rounded-[40px] border border-neutral-100 px-4', // Default 'focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset', // Focus state 'hover:enabled:border-neutral-200 hover:enabled:shadow-primary-md', // Hover state - { 'bg-neutral-0 text-neutral-600': !isActive && !disabled }, // Default state - { 'bg-neutral-100 text-neutral-800': isActive && !disabled }, // Active state - { 'bg-neutral-100 text-neutral-300': disabled }, // Disabled state + 'data-[state=off]:enabled:bg-neutral-0 data-[state=off]:enabled:text-neutral-600', // Default state + 'data-[state=on]:enabled:bg-neutral-100 data-[state=on]:enabled:text-neutral-800', // Active state + 'disabled:bg-neutral-100 disabled:text-neutral-300', // Disabled state className, ); - const handleToggleClick = () => { - if (isMultiSelect) { - const parsedSelection = Array.isArray(currentValue) - ? currentValue - : currentValue != null - ? [currentValue] - : []; - - const newSelection = parsedSelection.includes(value) - ? parsedSelection.filter((field) => field !== value) - : parsedSelection.concat(value); - - onChange(newSelection); - } else { - onChange(currentValue === value ? undefined : value); - } - }; - return ( - + ); }; diff --git a/src/components/toggles/toggleContext/index.ts b/src/components/toggles/toggleContext/index.ts deleted file mode 100644 index 838502bb7..000000000 --- a/src/components/toggles/toggleContext/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ToggleContextProvider, useToggleContext, type IToggleContext } from './toggleContext'; diff --git a/src/components/toggles/toggleContext/toggleContext.test.tsx b/src/components/toggles/toggleContext/toggleContext.test.tsx deleted file mode 100644 index a0ec5a357..000000000 --- a/src/components/toggles/toggleContext/toggleContext.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import type { ReactNode } from 'react'; -import { testLogger } from '../../../test'; -import { ToggleContextProvider, useToggleContext, type IToggleContext } from './toggleContext'; - -describe('useToggleContext hook', () => { - const createHookWrapper = (values?: Partial) => - function toggleContextWrapper(props: { children?: ReactNode }) { - const completeValues: IToggleContext = { - value: undefined, - onChange: jest.fn(), - ...values, - }; - - return {props.children}; - }; - - it('throws error when not used with a ToggleContext provider ', () => { - testLogger.suppressErrors(); - expect(() => renderHook(() => useToggleContext())).toThrowError(); - }); - - it('returns the ToggleContext values', () => { - const values: IToggleContext = { - value: 'selected-toggle', - onChange: jest.fn(), - isMultiSelect: true, - }; - const { result } = renderHook(() => useToggleContext(), { wrapper: createHookWrapper(values) }); - expect(result.current).toEqual(values); - }); -}); diff --git a/src/components/toggles/toggleContext/toggleContext.ts b/src/components/toggles/toggleContext/toggleContext.ts deleted file mode 100644 index ef1560000..000000000 --- a/src/components/toggles/toggleContext/toggleContext.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createContext, useContext } from 'react'; -import { type IToggleGroupProps } from '../toggleGroup/toggleGroup.api'; - -export type IToggleContext = Pick; - -export const toggleContext = createContext(null); - -export const ToggleContextProvider = toggleContext.Provider; - -export const useToggleContext = (): IToggleContext => { - const values = useContext(toggleContext); - - if (values == null) { - throw new Error('useToggleContext: hook must be used inside a ToggleContextProvider to work properly'); - } - - return values; -}; diff --git a/src/components/toggles/toggleGroup/toggleGroup.api.ts b/src/components/toggles/toggleGroup/toggleGroup.api.ts deleted file mode 100644 index 8dc68eeaf..000000000 --- a/src/components/toggles/toggleGroup/toggleGroup.api.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ComponentProps } from 'react'; -import type { ToggleValue } from '../toggle'; - -export type ToggleActiveValue = TValue | TValue[] | undefined; - -export interface IToggleGroupProps - extends Omit, 'onChange'> { - /** - * Allows multiple toggles to be selected at the same time when set to true. - */ - isMultiSelect?: boolean; - /** - * Current value of the toggle selection. - */ - value: TValue; - /** - * Callback called on toggle selection change. - */ - onChange: (value: TValue) => void; -} diff --git a/src/components/toggles/toggleGroup/toggleGroup.stories.tsx b/src/components/toggles/toggleGroup/toggleGroup.stories.tsx index 669d2b638..7ceb6d7c1 100644 --- a/src/components/toggles/toggleGroup/toggleGroup.stories.tsx +++ b/src/components/toggles/toggleGroup/toggleGroup.stories.tsx @@ -1,8 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { useState } from 'react'; import { Toggle } from '../toggle'; -import { ToggleGroup } from './toggleGroup'; -import { type IToggleGroupProps } from './toggleGroup.api'; +import { ToggleGroup, type IToggleGroupProps } from './toggleGroup'; const meta: Meta = { title: 'components/Toggles/ToggleGroup', @@ -18,11 +17,23 @@ const meta: Meta = { type Story = StoryObj; -const DefaultComponent = (props: Omit) => { +/** + * Default usage example of the ToggleGroup component. + */ +export const Default: Story = { + render: (props) => ( + + + + + ), +}; + +const ControlledComponent = (props: Omit) => { const [value, setValue] = useState(); return ( - + @@ -33,13 +44,13 @@ const DefaultComponent = (props: Omit) }; /** - * Default usage example of the ToggleGroup component. + * Controlled usage example of the ToggleGroup component. */ -export const Default: Story = { - render: ({ value, onChange, ...props }) => , +export const Controlled: Story = { + render: ({ value, onChange, isMultiSelect, ...props }) => , }; -const MultiSelectComponent = (props: Omit) => { +const MultiSelectComponent = (props: Omit) => { const [value, setValue] = useState(); return ( @@ -55,7 +66,7 @@ const MultiSelectComponent = (props: Omit , + render: ({ value, onChange, isMultiSelect, ...props }) => , }; export default meta; diff --git a/src/components/toggles/toggleGroup/toggleGroup.test.tsx b/src/components/toggles/toggleGroup/toggleGroup.test.tsx index 9eede2176..814d0a64b 100644 --- a/src/components/toggles/toggleGroup/toggleGroup.test.tsx +++ b/src/components/toggles/toggleGroup/toggleGroup.test.tsx @@ -1,17 +1,16 @@ import { render, screen } from '@testing-library/react'; import { Toggle } from '../toggle'; -import { ToggleGroup } from './toggleGroup'; -import type { IToggleGroupProps } from './toggleGroup.api'; +import { ToggleGroup, type IToggleGroupBaseProps, type IToggleGroupProps } from './toggleGroup'; describe(' component', () => { - const createTestComponent = (props?: Partial) => { - const completeProps: IToggleGroupProps = { - value: undefined, - onChange: jest.fn(), - ...props, - }; + const createTestComponent = (props: Partial = {}) => { + if (props?.isMultiSelect) { + return ; + } - return ; + const { isMultiSelect, ...otherProps } = props as IToggleGroupBaseProps; + + return ; }; it('renders the children components', () => { diff --git a/src/components/toggles/toggleGroup/toggleGroup.tsx b/src/components/toggles/toggleGroup/toggleGroup.tsx index afb559a52..e7b87c9b0 100644 --- a/src/components/toggles/toggleGroup/toggleGroup.tsx +++ b/src/components/toggles/toggleGroup/toggleGroup.tsx @@ -1,19 +1,44 @@ +import { ToggleGroup as RadixToggleGroup } from '@radix-ui/react-toggle-group'; import classNames from 'classnames'; -import { useMemo } from 'react'; -import { ToggleContextProvider } from '../toggleContext'; -import type { IToggleGroupProps, ToggleActiveValue } from './toggleGroup.api'; +import type { ComponentProps } from 'react'; -export const ToggleGroup = (props: IToggleGroupProps) => { +export type ToggleGroupValue = TMulti extends true ? string[] | undefined : string | undefined; + +export interface IToggleGroupBaseProps + extends Omit, 'value' | 'onChange' | 'defaultValue' | 'ref' | 'dir'> { + /** + * Allows multiple toggles to be selected at the same time when set to true. + */ + isMultiSelect: TMulti; + /** + * Current value of the toggle selection. + */ + value?: ToggleGroupValue; + /** + * Callback called on toggle selection change. + */ + onChange?: (value: ToggleGroupValue) => void; +} + +export type IToggleGroupProps = IToggleGroupBaseProps | IToggleGroupBaseProps; + +export const ToggleGroup = (props: IToggleGroupProps) => { const { value, onChange, isMultiSelect, className, ...otherProps } = props; + const classes = classNames('flex flex-row flex-wrap gap-2 md:gap-3', className); - const contextValues = useMemo( - () => ({ isMultiSelect, value, onChange: onChange as (value: ToggleActiveValue) => void }), - [isMultiSelect, value, onChange], - ); + if (isMultiSelect === true) { + return ( + + ); + } return ( - -
- + ); }; diff --git a/yarn.lock b/yarn.lock index 574f3146b..09b0caee2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1886,7 +1886,7 @@ "@radix-ui/react-use-previous" "1.0.1" "@radix-ui/react-use-size" "1.0.1" -"@radix-ui/react-toggle-group@1.0.4": +"@radix-ui/react-toggle-group@1.0.4", "@radix-ui/react-toggle-group@^1.0.0": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz#f5b5c8c477831b013bec3580c55e20a68179d6ec" integrity sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A== From 0e35a37ac10ae5e3bd0f750a26432eb209aeba21 Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Wed, 10 Jan 2024 16:19:20 +0100 Subject: [PATCH 09/12] Fix and update tests --- src/components/toggles/toggle/index.ts | 2 +- src/components/toggles/toggle/toggle.test.tsx | 93 +++---------------- src/components/toggles/toggleGroup/index.ts | 3 +- .../toggles/toggleGroup/toggleGroup.test.tsx | 40 +++++++- 4 files changed, 53 insertions(+), 85 deletions(-) diff --git a/src/components/toggles/toggle/index.ts b/src/components/toggles/toggle/index.ts index 60eeec61b..488ec5560 100644 --- a/src/components/toggles/toggle/index.ts +++ b/src/components/toggles/toggle/index.ts @@ -1 +1 @@ -export { Toggle, type IToggleProps, type ToggleValue } from './toggle'; +export { Toggle, type IToggleProps } from './toggle'; diff --git a/src/components/toggles/toggle/toggle.test.tsx b/src/components/toggles/toggle/toggle.test.tsx index c0dae87ac..1a94eedf8 100644 --- a/src/components/toggles/toggle/toggle.test.tsx +++ b/src/components/toggles/toggle/toggle.test.tsx @@ -1,104 +1,37 @@ import { fireEvent, render, screen } from '@testing-library/react'; -import { ToggleContextProvider, type IToggleContext } from '../toggleContext'; +import { ToggleGroup } from '../toggleGroup'; import { Toggle, type IToggleProps } from './toggle'; describe(' component', () => { - const createTestComponent = (values?: { props?: Partial; context?: Partial }) => { + const createTestComponent = (props?: Partial) => { const completeProps: IToggleProps = { label: 'label', value: 'value', - ...values?.props, - }; - - const completeContext: IToggleContext = { - value: undefined, - onChange: jest.fn(), - ...values?.context, + ...props, }; return ( - + - + ); }; it('renders a button with the specified label', () => { const label = 'Toggle Label'; - const props = { label }; - render(createTestComponent({ props })); - expect(screen.getByRole('button', { name: label })).toBeInTheDocument(); - }); - - it('renders the toggle as active when active value matches toggle value', () => { - const activeValue = 'active-value'; - const toggleValue = activeValue; - const props = { value: toggleValue }; - const context = { value: activeValue }; - render(createTestComponent({ props, context })); - expect(screen.getByRole('button').className).toContain('text-neutral-800'); - }); - - it('renders the toggle as active when active value includes toggle value on multi-select variant', () => { - const activeValues = ['first', 'second']; - const toggleValue = activeValues[0]; - const props = { value: toggleValue }; - const context = { isMultiSelect: true, value: activeValues }; - render(createTestComponent({ props, context })); - expect(screen.getByRole('button').className).toContain('text-neutral-800'); + render(createTestComponent({ label })); + expect(screen.getByRole('radio', { name: label })).toBeInTheDocument(); }); it('renders the button as disabled when the disabled prop is set to true', () => { const disabled = true; - const props = { disabled }; - render(createTestComponent({ props })); - expect(screen.getByRole('button')).toBeDisabled(); + render(createTestComponent({ disabled })); + expect(screen.getByRole('radio')).toBeDisabled(); }); - it('correctly updates the active value on toggle click', () => { - const onChange = jest.fn(); - const toggleValue = 'test'; - const context = { onChange }; - const props = { value: toggleValue }; - const { rerender } = render(createTestComponent({ props, context })); - - fireEvent.click(screen.getByRole('button')); - expect(onChange).toHaveBeenCalledWith(toggleValue); - - const newContext = { onChange, value: toggleValue }; - rerender(createTestComponent({ props, context: newContext })); - - fireEvent.click(screen.getByRole('button')); - expect(onChange).toHaveBeenCalledWith(undefined); - }); - - it('correctly updates the active values on toggle click on multi-select variant', () => { - const onChange = jest.fn(); - const activeValues = ['first', 'second']; - const toggleValue = 'third'; - const context = { isMultiSelect: true, onChange, value: activeValues }; - const props = { value: toggleValue }; - const { rerender } = render(createTestComponent({ props, context })); - - fireEvent.click(screen.getByRole('button')); - const expectedNewValues = [...activeValues, toggleValue]; - expect(onChange).toHaveBeenCalledWith(expectedNewValues); - - const newContext = { ...context, value: expectedNewValues }; - rerender(createTestComponent({ props, context: newContext })); - - fireEvent.click(screen.getByRole('button')); - expect(onChange).toHaveBeenCalledWith(activeValues); - }); - - it('correclty updates the active values when initial value is undefined on multi-select variant', () => { - const onChange = jest.fn(); - const toggleValue = 'first'; - const context = { isMultiSelect: true, onChange }; - const props = { value: toggleValue }; - render(createTestComponent({ props, context })); - - fireEvent.click(screen.getByRole('button')); - expect(onChange).toHaveBeenCalledWith([toggleValue]); + it('renders the toggle as active when clicked', () => { + render(createTestComponent()); + fireEvent.click(screen.getByRole('radio')); + expect(screen.getByRole('radio').className).toContain('text-neutral-800'); }); }); diff --git a/src/components/toggles/toggleGroup/index.ts b/src/components/toggles/toggleGroup/index.ts index beca149bb..2b2b96dc0 100644 --- a/src/components/toggles/toggleGroup/index.ts +++ b/src/components/toggles/toggleGroup/index.ts @@ -1,2 +1 @@ -export { ToggleGroup } from './toggleGroup'; -export type { IToggleGroupProps, ToggleActiveValue } from './toggleGroup.api'; +export { ToggleGroup, type IToggleGroupProps } from './toggleGroup'; diff --git a/src/components/toggles/toggleGroup/toggleGroup.test.tsx b/src/components/toggles/toggleGroup/toggleGroup.test.tsx index 814d0a64b..a2f9dd6c4 100644 --- a/src/components/toggles/toggleGroup/toggleGroup.test.tsx +++ b/src/components/toggles/toggleGroup/toggleGroup.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { Toggle } from '../toggle'; import { ToggleGroup, type IToggleGroupBaseProps, type IToggleGroupProps } from './toggleGroup'; @@ -19,6 +19,42 @@ describe(' component', () => { , ]; render(createTestComponent({ children })); - expect(screen.getAllByRole('button')).toHaveLength(children.length); + expect(screen.getAllByRole('radio')).toHaveLength(children.length); + }); + + it('correctly updates the active value on toggle click', () => { + const onChange = jest.fn(); + const value = 'test'; + const children = []; + const { rerender } = render(createTestComponent({ onChange, children })); + + fireEvent.click(screen.getByRole('radio')); + expect(onChange).toHaveBeenCalledWith(value); + + rerender(createTestComponent({ value, onChange, children })); + + fireEvent.click(screen.getByRole('radio')); + expect(onChange).toHaveBeenCalledWith(''); + }); + + it('correctly updates the active values on toggle click on multi-select variant', () => { + const onChange = jest.fn(); + const isMultiSelect = true; + const firstValue = 'first'; + const secondValue = 'second'; + const children = [ + , + , + ]; + const { rerender } = render(createTestComponent({ onChange, children, isMultiSelect })); + + fireEvent.click(screen.getByRole('button', { name: firstValue })); + const newValue = [firstValue]; + expect(onChange).toHaveBeenCalledWith(newValue); + + rerender(createTestComponent({ value: newValue, onChange, children, isMultiSelect })); + + fireEvent.click(screen.getByRole('button', { name: secondValue })); + expect(onChange).toHaveBeenCalledWith([...newValue, secondValue]); }); }); From 79e879125fd804ce958a5f4f4621425c7b1bb83c Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Wed, 10 Jan 2024 17:04:36 +0100 Subject: [PATCH 10/12] Update Figma links --- src/components/toggles/toggle/toggle.stories.tsx | 2 +- src/components/toggles/toggleGroup/toggleGroup.stories.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/toggles/toggle/toggle.stories.tsx b/src/components/toggles/toggle/toggle.stories.tsx index b91c3a635..857327cc1 100644 --- a/src/components/toggles/toggle/toggle.stories.tsx +++ b/src/components/toggles/toggle/toggle.stories.tsx @@ -10,7 +10,7 @@ const meta: Meta = { parameters: { design: { type: 'figma', - url: 'https://www.figma.com/file/jfKRr1V9evJUp1uBeyP3Zz/v1.0.0?type=design&node-id=9778-14&mode=design&t=eAkFH3nzSllOw9zp-4', + url: 'https://www.figma.com/file/jfKRr1V9evJUp1uBeyP3Zz/v1.0.0?type=design&node-id=9778-14&mode=dev', }, }, }; diff --git a/src/components/toggles/toggleGroup/toggleGroup.stories.tsx b/src/components/toggles/toggleGroup/toggleGroup.stories.tsx index 7ceb6d7c1..29ea653fb 100644 --- a/src/components/toggles/toggleGroup/toggleGroup.stories.tsx +++ b/src/components/toggles/toggleGroup/toggleGroup.stories.tsx @@ -10,7 +10,7 @@ const meta: Meta = { parameters: { design: { type: 'figma', - url: 'https://www.figma.com/file/jfKRr1V9evJUp1uBeyP3Zz/v1.0.0?type=design&node-id=9778-14&mode=design&t=eAkFH3nzSllOw9zp-4', + url: 'https://www.figma.com/file/jfKRr1V9evJUp1uBeyP3Zz/v1.0.0?type=design&node-id=11857-23553&mode=dev', }, }, }; From b8315f5aac0f6c2222937daffb5bf42b0bd8569d Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Wed, 10 Jan 2024 17:12:12 +0100 Subject: [PATCH 11/12] Fix test naming --- src/components/toggles/toggle/toggle.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/toggles/toggle/toggle.test.tsx b/src/components/toggles/toggle/toggle.test.tsx index 1a94eedf8..9042c54a3 100644 --- a/src/components/toggles/toggle/toggle.test.tsx +++ b/src/components/toggles/toggle/toggle.test.tsx @@ -17,13 +17,13 @@ describe(' component', () => { ); }; - it('renders a button with the specified label', () => { + it('renders a toggle with the specified label', () => { const label = 'Toggle Label'; render(createTestComponent({ label })); expect(screen.getByRole('radio', { name: label })).toBeInTheDocument(); }); - it('renders the button as disabled when the disabled prop is set to true', () => { + it('renders the toggle as disabled when the disabled prop is set to true', () => { const disabled = true; render(createTestComponent({ disabled })); expect(screen.getByRole('radio')).toBeDisabled(); From c8e85009bdf4fdb2b3a69b40320ae0dd4f785c26 Mon Sep 17 00:00:00 2001 From: Ruggero Cino Date: Thu, 11 Jan 2024 10:34:40 +0100 Subject: [PATCH 12/12] Update Toggle documentation, fix ToggleGroup stories --- src/components/toggles/toggle/toggle.tsx | 5 +++++ src/components/toggles/toggleGroup/toggleGroup.stories.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/toggles/toggle/toggle.tsx b/src/components/toggles/toggle/toggle.tsx index 816b0fb7a..8881449c8 100644 --- a/src/components/toggles/toggle/toggle.tsx +++ b/src/components/toggles/toggle/toggle.tsx @@ -13,6 +13,11 @@ export interface IToggleProps extends Omit, 'ref'> { label: string; } +/** + * The Toggle component is a button that handles the "on" and "off" states. + * + * **NOTE**: The component must be used inside a `` component in order to work properly. + */ export const Toggle: React.FC = (props) => { const { className, label, value, disabled, ...otherProps } = props; diff --git a/src/components/toggles/toggleGroup/toggleGroup.stories.tsx b/src/components/toggles/toggleGroup/toggleGroup.stories.tsx index 29ea653fb..0b980779d 100644 --- a/src/components/toggles/toggleGroup/toggleGroup.stories.tsx +++ b/src/components/toggles/toggleGroup/toggleGroup.stories.tsx @@ -3,9 +3,9 @@ import { useState } from 'react'; import { Toggle } from '../toggle'; import { ToggleGroup, type IToggleGroupProps } from './toggleGroup'; -const meta: Meta = { +const meta: Meta = { title: 'components/Toggles/ToggleGroup', - component: Toggle, + component: ToggleGroup, tags: ['autodocs'], parameters: { design: {