Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/community/components/form/toggle/toggle.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import Toggle, { ToggleProps } from './toggle';
const meta: Meta<ToggleProps> = {
component: Toggle,
title: 'Community/Form/Toggle',
parameters: {
status: {
type: ['deprecated', 'ExistsInTediReady'],
},
},
};

export default meta;
Expand Down
167 changes: 167 additions & 0 deletions src/tedi/components/form/toggle/toggle.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
@mixin toggle-variant($color, $type) {
@if $type == filled {
--toggle-bg-default: var(--form-toggle-#{$color}-inactive-default);
--toggle-bg-hover: var(--form-toggle-#{$color}-inactive-hover);
--toggle-bg-active: var(--form-toggle-#{$color}-inactive-active);
--toggle-bg-checked: var(--form-toggle-#{$color}-active-default);
--toggle-bg-checked-hover: var(--form-toggle-#{$color}-active-hover);
--toggle-bg-checked-active: var(--form-toggle-#{$color}-active-active);
--toggle-indicator-default: var(--form-toggle-#{$color}-inactive-indicator);
--toggle-indicator-checked: var(--form-toggle-#{$color}-active-indicator);
--toggle-icon-default: var(--form-toggle-#{$color}-inactive-icon);
--toggle-icon-checked: var(--form-toggle-#{$color}-active-icon);
}

@if $type == outlined {
--toggle-border-default: var(--form-toggle-#{$color}-inactive-default);
--toggle-border-hover: var(--form-toggle-#{$color}-inactive-hover);
--toggle-border-active: var(--form-toggle-#{$color}-inactive-active);
--toggle-border-checked: var(--form-toggle-#{$color}-active-default);
--toggle-border-checked-hover: var(--form-toggle-#{$color}-active-hover);
--toggle-border-checked-active: var(--form-toggle-#{$color}-active-active);
--toggle-indicator-default: var(--form-toggle-#{$color}-inactive-default);
--toggle-indicator-checked: var(--form-toggle-#{$color}-active-default);
--toggle-icon-default: var(--form-toggle-#{$color}-inactive-icon-outlined);
--toggle-icon-checked: var(--form-toggle-#{$color}-active-icon-outlined);
}
}

.tedi-toggle {
position: relative;
display: block;

&--default {
--toggle-indicator: var(--form-toggle-default-indicator);

width: var(--form-toggle-default-width);
height: var(--form-toggle-default-height);

.tedi-toggle__slider {
width: var(--form-toggle-default-indicator);
height: var(--form-toggle-default-indicator);
}
}

&--large {
--toggle-indicator: var(--form-toggle-large-indicator);

width: var(--form-toggle-large-width);
height: var(--form-toggle-large-height);

.tedi-toggle__slider {
width: var(--form-toggle-large-indicator);
height: var(--form-toggle-large-indicator);
}
}

&--primary-filled {
@include toggle-variant(primary, filled);
}

&--primary-outlined {
@include toggle-variant(primary, outlined);
}

&--colored-filled {
@include toggle-variant(colored, filled);
}

&--colored-outlined {
@include toggle-variant(colored, outlined);
}

&__input {
width: 100%;
height: 100%;
margin: 0;
appearance: none;
cursor: pointer;
background: var(--toggle-bg-default);
border: var(--tedi-borders-01) solid var(--toggle-border-default);
border-radius: var(--form-toggle-radius);
transition: background 0.15s ease, border-color 0.15s ease;

&:hover:not(:disabled) {
background: var(--toggle-bg-hover);
border-color: var(--toggle-border-hover);
}

&:active:not(:disabled) {
background: var(--toggle-bg-active);
border-color: var(--toggle-border-active);
}

&:checked {
background: var(--toggle-bg-checked);
border-color: var(--toggle-border-checked);

&:hover:not(:disabled) {
background: var(--toggle-bg-checked-hover);
border-color: var(--toggle-border-checked-hover);
}

&:active:not(:disabled) {
background: var(--toggle-bg-checked-active);
border-color: var(--toggle-border-checked-active);
}
}

&:focus-visible {
outline: calc(2 * var(--borders-01)) solid var(--form-toggle-primary-active-default);
outline-offset: var(--borders-01);
}

&:checked + .tedi-toggle__slider {
left: calc(100% - var(--toggle-indicator) - var(--form-toggle-padding));
color: var(--toggle-icon-checked);
background-color: var(--toggle-indicator-checked);
transform: translateY(-50%);

.tedi-toggle__icon {
stroke: var(--toggle-icon-checked);
}
}
}

&__slider {
position: absolute;
top: 50%;
left: var(--form-toggle-padding);
display: flex;
align-items: center;
justify-content: center;
color: var(--toggle-icon-default);
pointer-events: none;
background-color: var(--toggle-indicator-default);
border-radius: 50%;
transition: left 0.17s ease-out, background 0.15s ease;
transform: translateY(-50%);

.tedi-toggle__icon {
stroke: var(--toggle-icon-default);
}
}

&--disabled {
cursor: not-allowed;
opacity: 0.5;

.tedi-toggle__input {
cursor: not-allowed;
}
}

&__control {
display: flex;
gap: var(--layout-grid-gutters-08);
align-items: center;

&--label-left {
flex-direction: row;
}

&--label-right {
flex-direction: row-reverse;
}
}
}
157 changes: 157 additions & 0 deletions src/tedi/components/form/toggle/toggle.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import Toggle, { ToggleProps } from './toggle';

import '@testing-library/jest-dom';

describe('Toggle component', () => {
const defaultProps: ToggleProps = {
id: 'test-toggle',
label: 'Enable notifications',
};

it('renders with default properties', () => {
render(<Toggle {...defaultProps} />);

const input = screen.getByRole('switch');
expect(input).toBeInTheDocument();
expect(input).toHaveAttribute('id', 'test-toggle');
expect(input).toHaveAttribute('type', 'checkbox');
});

it('renders the label correctly', () => {
render(<Toggle {...defaultProps} />);

const label = screen.getByText(/enable notifications/i);
expect(label).toBeInTheDocument();
});

it('renders hidden label when hideLabel is true', () => {
render(<Toggle {...defaultProps} hideLabel />);

const label = screen.getByText(/enable notifications/i);
expect(label).toBeInTheDocument();
expect(label.closest('label')).toHaveClass('tedi-toggle__label');
});

it('calls onChange when toggled', async () => {
const handleChange = jest.fn();
const user = userEvent.setup();

render(<Toggle {...defaultProps} onChange={handleChange} />);

const toggle = screen.getByRole('switch');
await user.click(toggle);

expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith(true);
});

it('respects controlled checked state', () => {
const { rerender } = render(<Toggle {...defaultProps} checked={false} onChange={jest.fn()} />);

let toggle = screen.getByRole('switch');
expect(toggle).not.toBeChecked();

rerender(<Toggle {...defaultProps} checked={true} onChange={jest.fn()} />);
toggle = screen.getByRole('switch');
expect(toggle).toBeChecked();
});

it('renders as uncontrolled with defaultChecked', () => {
render(<Toggle {...defaultProps} defaultChecked />);

const toggle = screen.getByRole('switch');
expect(toggle).toBeChecked();
});

it('disables the toggle when disabled prop is true', () => {
render(<Toggle {...defaultProps} disabled />);

const toggle = screen.getByRole('switch');
expect(toggle).toBeDisabled();
});

it('disables the toggle and prevents onChange when isLoading is true', async () => {
const handleChange = jest.fn();
const user = userEvent.setup();

render(<Toggle {...defaultProps} isLoading onChange={handleChange} />);

const toggle = screen.getByRole('switch');
expect(toggle).toBeDisabled();

await user.click(toggle);
expect(handleChange).not.toHaveBeenCalled();
});

it('shows loading spinner when isLoading is true', () => {
render(<Toggle {...defaultProps} isLoading />);

const spinner = screen.getByTestId('tedi-spinner');
expect(spinner).toBeInTheDocument();
});

it('shows lock icon when icon prop is true', () => {
render(<Toggle {...defaultProps} icon size="large" />);

const icon = screen.getByRole('img', { hidden: true });
expect(icon).toBeInTheDocument();
});

it('renders label on the right when labelPosition="right"', () => {
render(<Toggle {...defaultProps} labelPosition="right" />);

const control = screen.getByRole('switch').closest('.tedi-toggle__control');
expect(control).toHaveClass('tedi-toggle__control--label-right');
});

it('renders label on the left by default', () => {
render(<Toggle {...defaultProps} />);

const control = screen.getByRole('switch').closest('.tedi-toggle__control');
expect(control).toHaveClass('tedi-toggle__control--label-left');
});

it('renders helper text when provided', () => {
render(<Toggle {...defaultProps} helper={{ text: 'This is a helper message', type: 'hint' }} />);

const helper = screen.getByText(/this is a helper message/i);
expect(helper).toBeInTheDocument();
});

it('applies correct size classes', () => {
const { rerender } = render(<Toggle {...defaultProps} size="default" />);

const toggleContainer = screen.getByRole('switch').closest('.tedi-toggle');
expect(toggleContainer).toHaveClass('tedi-toggle--default');

rerender(<Toggle {...defaultProps} size="large" />);
expect(toggleContainer).toHaveClass('tedi-toggle--large');
});

it('applies variant classes correctly', () => {
render(<Toggle {...defaultProps} color="colored" type="outlined" />);

const toggleContainer = screen.getByRole('switch').closest('.tedi-toggle');
expect(toggleContainer).toHaveClass('tedi-toggle--colored-outlined');
});

it('applies active class when checked', () => {
render(<Toggle {...defaultProps} checked onChange={jest.fn()} />);
const toggleContainer = screen.getByRole('switch').closest('.tedi-toggle');
expect(toggleContainer).toHaveClass('tedi-toggle--active');
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('applies disabled class when disabled or loading', () => {
const { rerender } = render(<Toggle {...defaultProps} disabled />);

let toggleContainer = screen.getByRole('switch').closest('.tedi-toggle');
expect(toggleContainer).toHaveClass('tedi-toggle--disabled');

rerender(<Toggle {...defaultProps} isLoading />);
toggleContainer = screen.getByRole('switch').closest('.tedi-toggle');
expect(toggleContainer).toHaveClass('tedi-toggle--disabled');
});
});
Loading
Loading