Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(web-react): Introduce UNSTABLE_Toggle component #DS-1346 #1545

Merged
merged 1 commit into from
Jul 22, 2024
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
2 changes: 2 additions & 0 deletions packages/web-react/scripts/entryPoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const entryPoints = [
{ dirs: ['components', 'UNSTABLE_PartnerLogo'] },
{ dirs: ['components', 'UNSTABLE_ProductLogo'] },
{ dirs: ['components', 'UNSTABLE_Slider'] },
{ dirs: ['components', 'UNSTABLE_Toggle'] },
{ dirs: ['components', 'UNSTABLE_Truncate'] },
{ dirs: ['components', 'VisuallyHidden'] },
];

Expand Down
110 changes: 110 additions & 0 deletions packages/web-react/src/components/UNSTABLE_Toggle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# UNSTABLE Toggle

> ⚠️ This component is UNSTABLE. It may significantly change at any point in the future.
> Please use it with caution.
Toggle is a form control that allows users to switch between two states.

## Basic Usage

The Toggle component implements the HTML [checkbox input][mdn-checkbox] element. It uses
the native input element and styles it to look like a toggle switch.

```jsx
import { UNSTABLE_Toggle } from '@lmc-eu/spirit-web-react/components';

<UNSTABLE_Toggle id="toggle-default" label="Toggle Label" />;
```

## Indicators

If you need to indicate the state of the toggle, you can add the `hasIndicators` prop. This will add a visual indicators to the toggle switch.

```jsx
<UNSTABLE_Toggle id="toggle-indicators" label="Toggle Label" hasIndicators />
```

## Required

Add the `isRequired` prop to mark it as required.

```jsx
<UNSTABLE_Toggle id="toggle-required" label="Toggle Label" isRequired />
```

## Hidden Label

```jsx
<UNSTABLE_Toggle id="toggle-hidden-label" label="Toggle Label" isLabelHidden />
```

## Fluid

```jsx
<UNSTABLE_Toggle id="toggle-fluid" label="Toggle Label" isFluid />
```

## Helper Text

```jsx
<UNSTABLE_Toggle id="toggle-helper-text" label="Toggle Label" helperText="Helper text" />
```

## Validation States

Validation states can be presented by prop `validationState`. See Validation state [dictionary][dictionary-validation].

```jsx
<UNSTABLE_Toggle id="toggle-success" label="Toggle Label" validationState="success" />
<UNSTABLE_Toggle
id="toggle-warning"
label="Toggle Label"
validationText="Validation text"
validationState="warning"
isChecked
/>
<UNSTABLE_Toggle
id="toggle-danger"
label="Toggle Label"
validationText={['First validation text', 'Second validation text']}
validationState="danger"
/>
```

## Disabled State

You can add `isDisabled` prop to disable Toggle.

```jsx
<UNSTABLE_Toggle id="toggle-disabled" label="Toggle Label" isDisabled />
```

## API
curdaj marked this conversation as resolved.
Show resolved Hide resolved

| Name | Type | Default | Required | Description |
| ----------------- | ---------------------------------------------- | ------- | -------- | ---------------------------------------------------- |
| `autoComplete` | `string` | - || [Automated assistance in filling][autocomplete-attr] |
| `hasIndicators` | boolean | `false` || Whether has visual indicators |
| `helperText` | string | - || Helper text |
| `id` | string | - || Input and label identification |
| `isChecked` | boolean | `false` || Whether is toggle checked |
| `isDisabled` | boolean | `false` || Whether is toggle disabled |
| `isFluid` | boolean | `false` || Whether is toggle fluid |
| `isLabelHidden` | boolean | `false` || Whether is label hidden |
| `label` | string | - || Label text |
| `name` | string | - || Input name |
| `onChange` | (event: ChangeEvent<HTMLInputElement>) => void | - || Change event handler |
| `ref` | `ForwardedRef<HTMLInputElement>` | - || Input element reference |
| `validationState` | [Validation dictionary][dictionary-validation] | - || Type of validation state |
| `validationText` | `string` \| `string[]` | - || Validation text |

The components accept [additional attributes][readme-additional-attributes].
If you need more control over the styling of a component, you can use [style props][readme-style-props]
and [escape hatches][readme-escape-hatches].

[autocomplete-attr]: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
[dictionary-validation]: https://github.com/lmc-eu/spirit-design-system/blob/main/docs/DICTIONARIES.md#validation
[mdn-checkbox]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox
[readme-additional-attributes]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#additional-attributes
[readme-escape-hatches]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#escape-hatches
[readme-style-props]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#style-props
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import classNames from 'classnames';
import React, { ForwardedRef, forwardRef, useState } from 'react';
import { useStyleProps } from '../../hooks';
import { SpiritToggleProps } from '../../types';
import { HelperText, useAriaIds, ValidationText } from '../Field';
import { useToggleStyleProps } from './useToggleStyleProps';

/* We need an exception for components exported with forwardRef */
/* eslint no-underscore-dangle: ['error', { allow: ['_UNSTABLE_Toggle'] }] */
/* eslint-disable-next-line camelcase */
const _UNSTABLE_Toggle = (props: SpiritToggleProps, ref: ForwardedRef<HTMLInputElement>) => {
const { classProps, props: modifiedProps } = useToggleStyleProps(props);
const {
'aria-describedby': ariaDescribedBy = '',
id,
isDisabled,
isChecked = false,
isRequired,
label,
helperText,
onChange = () => {},
validationState,
validationText,
...restProps
} = modifiedProps;

const { styleProps, props: otherProps } = useStyleProps(restProps);

const [ids, register] = useAriaIds(ariaDescribedBy);
const [checked, setChecked] = useState(isChecked);

const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked);
onChange(event);
};

return (
<label {...styleProps} htmlFor={id} className={classNames(classProps.root, styleProps.className)}>
<span className={classProps.text}>
<span className={classProps.label}>{label}</span>
<HelperText
className={classProps.helperText}
elementType="span"
id={`${id}__helperText`}
registerAria={register}
helperText={helperText}
/>
{validationState && (
<ValidationText
className={classProps.validationText}
id={`${id}__validationText`}
validationText={validationText}
registerAria={register}
/>
)}
</span>
<input
{...otherProps}
aria-describedby={ids.join(' ')}
type="checkbox"
id={id}
className={classProps.input}
disabled={isDisabled}
checked={checked}
required={isRequired}
onChange={handleOnChange}
ref={ref}
/>
</label>
);
};

export const UNSTABLE_Toggle = forwardRef<HTMLInputElement, SpiritToggleProps>(_UNSTABLE_Toggle);

export default UNSTABLE_Toggle;
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest';
import { validationStatePropsTest } from '../../../../tests/providerTests/dictionaryPropsTest';
import { requiredPropsTest } from '../../../../tests/providerTests/requiredPropsTest';
import { restPropsTest } from '../../../../tests/providerTests/restPropsTest';
import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest';
import UNSTABLE_Toggle from '../UNSTABLE_Toggle';

describe('UNSTABLE_Toggle', () => {
classNamePrefixProviderTest(UNSTABLE_Toggle, 'UNSTABLE_Toggle');

stylePropsTest(UNSTABLE_Toggle);

restPropsTest(UNSTABLE_Toggle, 'input');

validationStatePropsTest(UNSTABLE_Toggle, 'UNSTABLE_Toggle--');

requiredPropsTest(UNSTABLE_Toggle, 'checkbox', 'id', 'example-id');

it('should have correct className', () => {
render(<UNSTABLE_Toggle id="test-toggle" label="Toggle Label" />);

expect(screen.getByRole('checkbox').parentElement).toHaveClass('UNSTABLE_Toggle');
});

it('should have label classname', () => {
render(<UNSTABLE_Toggle id="test-toggle" label="Toggle Label" />);

const label = screen.getByText('Toggle Label');

expect(label).toHaveClass('UNSTABLE_Toggle__label');
expect(label).toContainHTML('label');
});

it('should have label with required classname', () => {
render(<UNSTABLE_Toggle id="test-toggle" label="Toggle Label" isRequired />);

const label = screen.getByText('Toggle Label');

expect(label).toHaveClass('UNSTABLE_Toggle__label');
expect(label).toHaveClass('UNSTABLE_Toggle__label--required');
});

it('should have hidden classname', () => {
render(<UNSTABLE_Toggle id="test-toggle" label="Toggle Label" isLabelHidden />);

const label = screen.getByText('Toggle Label');

expect(label).toHaveClass('UNSTABLE_Toggle__label');
expect(label).toHaveClass('UNSTABLE_Toggle__label--hidden');
curdaj marked this conversation as resolved.
Show resolved Hide resolved
});

it('should have input classname', () => {
render(<UNSTABLE_Toggle id="test-toggle" label="Toggle Label" />);

expect(screen.getByRole('checkbox')).toHaveClass('UNSTABLE_Toggle__input');
});

it('should have helper text with correct classname', () => {
render(<UNSTABLE_Toggle id="test-toggle" label="Toggle Label" helperText="Helper Text" />);

const helperText = screen.getByText('Helper Text');

expect(helperText).toBeInTheDocument();
expect(helperText).toHaveClass('UNSTABLE_Toggle__helperText');
});

it('should have correct attribute when checked', () => {
render(<UNSTABLE_Toggle id="test-toggle" label="Toggle Label" isChecked />);

expect(screen.getByRole('checkbox')).toBeChecked();
});

it('should have correct attribute when disabled', () => {
render(<UNSTABLE_Toggle id="test-toggle" label="Toggle Label" isDisabled />);

expect(screen.getByRole('checkbox')).toBeDisabled();
});

it('should have correct classname if fluid', () => {
render(<UNSTABLE_Toggle id="test-toggle" label="Toggle Label" isFluid />);

const checkbox = screen.getByRole('checkbox');

expect(checkbox.parentElement).toHaveClass('UNSTABLE_Toggle');
expect(checkbox.parentElement).toHaveClass('UNSTABLE_Toggle--fluid');
});

it('should have indicators classname', () => {
render(<UNSTABLE_Toggle id="test-toggle" label="Toggle Label" hasIndicators />);

const checkbox = screen.getByRole('checkbox');

expect(checkbox).toHaveClass('UNSTABLE_Toggle__input');
expect(checkbox).toHaveClass('UNSTABLE_Toggle__input--indicators');
});

it('should change the state of the checkbox when clicked', () => {
render(<UNSTABLE_Toggle id="test-toggle" label="Toggle Label" />);

const checkbox = screen.getByRole('checkbox');

expect(checkbox).not.toBeChecked();

fireEvent.click(checkbox);

expect(checkbox).toBeChecked();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { renderHook } from '@testing-library/react';
import { ValidationStates } from '../../../constants';
import { SpiritToggleProps } from '../../../types';
import { useToggleStyleProps } from '../useToggleStyleProps';

describe('useToggleStyleProps', () => {
it('should return defaults', () => {
const props = { id: 'toggle', label: 'text' };
const { result } = renderHook(() => useToggleStyleProps(props));

expect(result.current.classProps).toEqual({
root: 'UNSTABLE_Toggle',
text: 'UNSTABLE_Toggle__text',
input: 'UNSTABLE_Toggle__input',
label: 'UNSTABLE_Toggle__label',
helperText: 'UNSTABLE_Toggle__helperText',
validationText: 'UNSTABLE_Toggle__validationText',
});
});

it('should return hidden label', () => {
const props = { id: 'toggle', label: 'text', isLabelHidden: true } as SpiritToggleProps;
const { result } = renderHook(() => useToggleStyleProps(props));

expect(result.current.classProps.label).toBe('UNSTABLE_Toggle__label UNSTABLE_Toggle__label--hidden');
});

it('should return disabled', () => {
const props = { id: 'toggle', label: 'text', isDisabled: true } as SpiritToggleProps;
const { result } = renderHook(() => useToggleStyleProps(props));

expect(result.current.classProps.root).toBe('UNSTABLE_Toggle UNSTABLE_Toggle--disabled');
});

it.each([Object.values(ValidationStates)])('should return field with %s', (state) => {
const props = { validationState: state } as SpiritToggleProps;
const { result } = renderHook(() => useToggleStyleProps(props));

expect(result.current.classProps.root).toBe(`UNSTABLE_Toggle UNSTABLE_Toggle--${state}`);
});

it('should return fluid', () => {
const props = { id: 'toggle', label: 'text', isFluid: true } as SpiritToggleProps;
const { result } = renderHook(() => useToggleStyleProps(props));

expect(result.current.classProps.root).toBe('UNSTABLE_Toggle UNSTABLE_Toggle--fluid');
});

it('should return required', () => {
const props = { id: 'toggle', label: 'text', isRequired: true } as SpiritToggleProps;
const { result } = renderHook(() => useToggleStyleProps(props));

expect(result.current.classProps.label).toBe('UNSTABLE_Toggle__label UNSTABLE_Toggle__label--required');
});

it('should return input with indicators', () => {
const props = { id: 'toggle', label: 'text', hasIndicators: true } as SpiritToggleProps;
const { result } = renderHook(() => useToggleStyleProps(props));

expect(result.current.classProps.input).toBe('UNSTABLE_Toggle__input UNSTABLE_Toggle__input--indicators');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { UNSTABLE_Toggle } from '..';
curdaj marked this conversation as resolved.
Show resolved Hide resolved

const ToggleDefault = () => (
<>
<UNSTABLE_Toggle id="toggle-default" label="Toggle Label" name="default" />
<UNSTABLE_Toggle id="toggle-default-checked" label="Toggle Label" name="default" isChecked />
</>
);

export default ToggleDefault;
Loading
Loading