Skip to content

Commit

Permalink
Feat(web-react): Introduce UNSTABLE_Toggle component #DS-1346
Browse files Browse the repository at this point in the history
  • Loading branch information
curdaj committed Jul 22, 2024
1 parent 1e3235e commit b20d633
Show file tree
Hide file tree
Showing 22 changed files with 660 additions and 0 deletions.
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

| 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');
});

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 '..';

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

0 comments on commit b20d633

Please sign in to comment.