diff --git a/src/components/Switch/Switch.module.css b/src/components/Switch/Switch.module.css new file mode 100644 index 00000000..985dea05 --- /dev/null +++ b/src/components/Switch/Switch.module.css @@ -0,0 +1,87 @@ +/** + * Copyright 2024 Mia srl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +.switchComponent { + display: flex; + flex-direction: column; + gap: var(--spacing-gap-xs, 4px); + + .switchTextWrapper { + display: flex; + align-items: center; + gap: var(--spacing-gap-md, 12px); + color: var(--palette-text-neutral-main, #636363); + } + + .switchDescription { + padding-right: var(--spacing-padding-sm, 8px); + padding-left: calc(var(--spacing-padding-sm, 8px) + var(--spacing-padding-3xl, 48px)); + color: var(--palette-text-neutral-main, #636363); + + &.small { + padding-left: calc(var(--spacing-padding-sm, 8px) + var(--spacing-padding-2xl, 32px)); + } + } + + :global(.mia-platform-switch) { + background: var(--palette-action-secondary-bold, #cdcdcd); + + &:hover { + background: var(--palette-action-secondary-bolder, #acacac); + } + + /* Disabled or loading switch */ + + &:global(.mia-platform-switch-disabled), &:global(.mia-platform-switch-loading) { + opacity: 1; + background: var(--palette-action-disabled-main, #f2f2f2); + + &:hover { + background: var(--palette-action-disabled-main, #f2f2f2); + } + + :global(.mia-platform-switch-loading-icon) { + color: var(--palette-action-disabled-main, #f2f2f2); + } + } + + /* Checked switch */ + + &:global(.mia-platform-switch-checked) { + background: var(--palette-action-primary-default, #1261e4); + + &:hover { + background: var(--palette-action-primary-hover, #1890ff); + } + + /* Disabled or loading checked switch */ + + &:global(.mia-platform-switch-disabled), &:global(.mia-platform-switch-loading){ + background: var(--palette-background-primary-0, #aad1fb); + + &:hover { + background: var(--palette-background-primary-0, #aad1fb); + } + + :global(.mia-platform-switch-loading-icon) { + color: var(--palette-background-primary-0, #aad1fb); + } + } + } + } +} diff --git a/src/components/Switch/Switch.props.ts b/src/components/Switch/Switch.props.ts new file mode 100644 index 00000000..b4ab2319 --- /dev/null +++ b/src/components/Switch/Switch.props.ts @@ -0,0 +1,71 @@ +/** + * Copyright 2024 Mia srl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SwitchChangeEventHandler, SwitchClickEventHandler } from 'antd/es/switch' +import { ReactNode } from 'react' + +import { Size } from './Switch.types' + +export type SwitchProps = { + + /** + * Additional description of the switch, to be rendered below the switch `text`. + * Is ignored if the `text` prop is not provided. + */ + description?: ReactNode, + + /** + * Allows you to control whether the switch is checked. + */ + isChecked?: boolean, + + /** + * Allows you to control whether the switch is disabled. Defaults to false. + */ + isDisabled?: boolean, + + /** + * Allows you to control whether the switch is checked on its first render. Defaults to false. + */ + isInitiallyChecked?: boolean, + + /** + * Allows you to control whether the switch is in loading state. Defaults to false. + */ + isLoading?: boolean, + + /** + * Function that is invoked when the switch state is changed. + */ + onChange?: SwitchChangeEventHandler + + /** + * Function that is invoked when the switch is clicked. + */ + onClick?: SwitchClickEventHandler, + + /** + * Determines the switch size. Can be set to `large` or `small`. Defaults to `large`. + */ + size?: Size, + + /** + * Text to be displayed next to the switch. + */ + text?: ReactNode +} diff --git a/src/components/Switch/Switch.stories.tsx b/src/components/Switch/Switch.stories.tsx new file mode 100644 index 00000000..58b72728 --- /dev/null +++ b/src/components/Switch/Switch.stories.tsx @@ -0,0 +1,125 @@ +/** + * Copyright 2024 Mia srl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Meta, StoryObj } from '@storybook/react' + +import { Size } from './Switch.types' +import { Switch } from '.' +import { defaults } from './Switch' + +const meta = { + component: Switch, + args: { + ...defaults, + }, + argTypes: { + description: { type: 'string' }, + text: { type: 'string' }, + onClick: { control: false }, + onChange: { control: false }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const BasicExample: Story = {} + +export const CheckedOnFirstRender: Story = { + args: { + isInitiallyChecked: true, + }, +} + +export const CheckedState: Story = { + args: { + isChecked: true, + }, +} + +export const Disabled: Story = { + args: { + isDisabled: true, + }, +} + +export const CheckedDisabled: Story = { + args: { + isChecked: true, + isDisabled: true, + }, +} + +export const Loading: Story = { + args: { + isLoading: true, + }, +} + +export const CheckedLoading: Story = { + args: { + isChecked: true, + isLoading: true, + }, +} + +export const WithText: Story = { + args: { + text: 'Switch with text', + }, +} + +export const WithTextAndDescription: Story = { + args: { + text: 'Switch with text', + description: 'This is a description of the switch.', + }, +} + +export const SmallSize: Story = { + args: { + size: Size.Small, + }, +} + +export const SmallSizeWithText: Story = { + args: { + size: Size.Small, + text: 'Switch with text', + }, +} + +export const SmallSizeWithTextAndDescription: Story = { + args: { + size: Size.Small, + text: 'Switch with text', + description: 'This is a description of the switch.', + }, +} + +export const WithOnChange: Story = { + args: { + onChange: (checked) => alert(`Switch is now ${checked ? 'checked' : 'unchecked'}`), + }, +} + +export const WithOnClick: Story = { + args: { + onClick: () => alert('Switch has been clicked'), + }, +} diff --git a/src/components/Switch/Switch.test.tsx b/src/components/Switch/Switch.test.tsx new file mode 100644 index 00000000..7137a27e --- /dev/null +++ b/src/components/Switch/Switch.test.tsx @@ -0,0 +1,128 @@ +/** + * Copyright 2024 Mia srl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render, screen, userEvent, waitFor } from '../../test-utils' +import { Size } from './Switch.types' +import { Switch } from './Switch' + +describe('Switch', () => { + describe('renders states correctly', () => { + it('renders default state', () => { + render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByRole('switch')).not.toBeChecked() + }) + + it('renders checked by default state', () => { + render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByRole('switch')).toBeChecked() + }) + + it('renders checked state', () => { + render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByRole('switch')).toBeChecked() + }) + + it('renders default disabled state', () => { + render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByRole('switch')).toBeDisabled() + }) + + it('renders checked disabled state', () => { + const { asFragment } = render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByRole('switch')).toBeDisabled() + expect(screen.getByRole('switch')).toBeChecked() + expect(asFragment()).toMatchSnapshot() + }) + + it('renders default loading state', () => { + const { asFragment } = render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByRole('switch')).toBeDisabled() + expect(asFragment()).toMatchSnapshot() + }) + + it('renders checked loading state', () => { + const { asFragment } = render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByRole('switch')).toBeDisabled() + expect(asFragment()).toMatchSnapshot() + }) + }) + + describe('renders sizes correctly', () => { + it('renders large size', () => { + render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByRole('switch')).toHaveClass('mia-platform-switch') + expect(screen.getByRole('switch')).not.toHaveClass('mia-platform-switch-small') + }) + + it('renders small size', () => { + render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByRole('switch')).toHaveClass('mia-platform-switch') + expect(screen.getByRole('switch')).toHaveClass('mia-platform-switch-small') + }) + }) + + describe('renders text correctly', () => { + it('renders simple text', () => { + render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByText('Switch')).toBeInTheDocument() + }) + + it('renders text and description', () => { + render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByText('Switch text')).toBeInTheDocument() + expect(screen.getByText('Switch description')).toBeInTheDocument() + }) + + it('ignores description if text is not set', () => { + render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.queryByText('Switch description')).not.toBeInTheDocument() + }) + }) + + describe('performs interactions correctly', () => { + it('calls onClick when the switch is clicked', async() => { + const onClick = jest.fn() + render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByRole('switch')).not.toBeChecked() + userEvent.click(screen.getByRole('switch')) + await waitFor(() => expect(onClick).toHaveBeenCalledTimes(1)) + }) + + it('calls onChange when the switch state changes', async() => { + const onChange = jest.fn() + render() + expect(screen.getByRole('switch')).toBeInTheDocument() + expect(screen.getByRole('switch')).not.toBeChecked() + userEvent.click(screen.getByRole('switch')) + await waitFor(() => expect(onChange).toHaveBeenCalledTimes(1)) + }) + }) +}) diff --git a/src/components/Switch/Switch.theme.ts b/src/components/Switch/Switch.theme.ts new file mode 100644 index 00000000..3c1611dd --- /dev/null +++ b/src/components/Switch/Switch.theme.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2024 Mia srl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ComponentsTheme } from '../ThemeProvider/Ant' +import Theme from '../../themes/schema' + +/** + * Generates a Ant theme configuration for Switch component based on a theme configuration. + * + * @link https://ant.design/components/switch#design-token + * + * @param {Partial} theme - theme configuration. + * @returns {Partial} The generated Switch Ant theme configuration. + */ +export default ({ palette }: Partial): ComponentsTheme['Switch'] => ({ + handleBg: palette?.action?.secondary?.main, + handleShadow: 'transparent', +}) diff --git a/src/components/Switch/Switch.tsx b/src/components/Switch/Switch.tsx new file mode 100644 index 00000000..d28bc48e --- /dev/null +++ b/src/components/Switch/Switch.tsx @@ -0,0 +1,89 @@ +/** + * Copyright 2024 Mia srl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ReactElement, useMemo } from 'react' +import { Switch as AntSwitch } from 'antd' +import type { SwitchSize } from 'antd/es/switch' +import classNames from 'classnames' + +import { BodyS } from '../Typography/BodyX/BodyS' +import { Size } from './Switch.types' +import { SwitchProps } from './Switch.props' +import styles from './Switch.module.css' + +const { + switchComponent, + switchTextWrapper, + switchDescription, + small: smallSwitch, +} = styles + +export const defaults = { + isDisabled: false, + isInitiallyChecked: false, + isLoading: false, + size: Size.Large, +} + +const antSizeRemapping = { + [Size.Large]: 'default' as SwitchSize, + [Size.Small]: 'small' as SwitchSize, +} + +export const Switch = ({ + description, + isChecked, + isInitiallyChecked = defaults.isInitiallyChecked, + isDisabled = defaults.isDisabled, + isLoading = defaults.isLoading, + onChange, + onClick, + size = defaults.size, + text, +} : SwitchProps) : ReactElement => { + const descriptionClassName = useMemo(() => classNames([ + switchDescription, + size === Size.Small && smallSwitch, + ]), [size]) + + return ( +
+
+ + {text && {text}} +
+ { + text && description && ( +
+ {description} +
+ ) + } +
+ ) +} + +Switch.Size = Size diff --git a/src/components/Switch/Switch.types.ts b/src/components/Switch/Switch.types.ts new file mode 100644 index 00000000..bee86338 --- /dev/null +++ b/src/components/Switch/Switch.types.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2023 Mia srl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export enum Size { + Small = 'small', + Large = 'large', +} diff --git a/src/components/Switch/__snapshots__/Switch.test.tsx.snap b/src/components/Switch/__snapshots__/Switch.test.tsx.snap new file mode 100644 index 00000000..21bfd8c0 --- /dev/null +++ b/src/components/Switch/__snapshots__/Switch.test.tsx.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Switch renders states correctly renders checked disabled state 1`] = ` + +
+
+ +
+
+
+`; + +exports[`Switch renders states correctly renders checked loading state 1`] = ` + +
+
+ +
+
+
+`; + +exports[`Switch renders states correctly renders default loading state 1`] = ` + +
+
+ +
+
+
+`; diff --git a/src/components/Switch/index.ts b/src/components/Switch/index.ts new file mode 100644 index 00000000..4094c6fb --- /dev/null +++ b/src/components/Switch/index.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2024 Mia srl + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export { Switch } from './Switch' diff --git a/src/components/ThemeProvider/Ant.tsx b/src/components/ThemeProvider/Ant.tsx index bc6bd4c8..7a2d3390 100644 --- a/src/components/ThemeProvider/Ant.tsx +++ b/src/components/ThemeProvider/Ant.tsx @@ -25,6 +25,7 @@ import DividerTheme from '../Divider/Divider.theme' import FeedbackMessageTheme from '../FeedbackMessage/FeedbackMessage.theme' import InputTheme from '../BaseInput/BaseInput.theme' import MenuTheme from '../Menu/Menu.theme' +import SwitchTheme from '../Switch/Switch.theme' import TableTheme from '../Table/Table.theme' import Theme from '../../themes/schema' import { ThemeProviderProps } from './ThemeProvider.props' @@ -99,6 +100,7 @@ const generateAntTheme = ({ palette, typography, shape, spacing }: Partial