Skip to content

Commit

Permalink
feat(switch): add new component
Browse files Browse the repository at this point in the history
* feat(switch): add new component

* fix(switch): fix styles

* fix(switch): remove tap highlight

* fix(switch): fix version

* feat(switch): add additional props to onChange payload

* refactor(switch): refactor & fixes

* refactor(switch): remove value from onChange payload

* fix(*): rename storybook fields

* feat(switch): more customizable switch
  • Loading branch information
reme3d2y committed Apr 21, 2020
1 parent ff62fac commit 4e23acd
Show file tree
Hide file tree
Showing 8 changed files with 673 additions and 0 deletions.
18 changes: 18 additions & 0 deletions src/switch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@alfalab/core-components-switch",
"version": "1.0.0",
"description": "",
"gitHead": "f054fef20200664c65e2501ef1f916c555cdf05d",
"keywords": [],
"license": "ISC",
"main": "index.js",
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"react": "^16.9.0"
},
"dependencies": {
"classnames": "^2.2.6"
}
}
101 changes: 101 additions & 0 deletions src/switch/src/Component.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { useState } from 'react';
import cn from 'classnames';

import { withKnobs, text, boolean } from '@storybook/addon-knobs';

import styles from '../../../.storybook/styles.css';

import { Switch } from './Component';
import { Button } from '../../button/src';

export default {
title: 'Common|Switch',
component: Switch,
decorators: [withKnobs],
};

export const SwitchStory = () => {
const [checked, setChecked] = useState(false);

const handleChange = () => {
setChecked(!checked);
};

return (
<React.Fragment>
<div className={cn(styles.row)}>
<div className={cn(styles.col)}>
<Switch
checked={checked}
disabled={boolean('disabled', false)}
reversed={boolean('reversed', false)}
className={text('className', '')}
dataTestId={text('dataTestId', '')}
label={text('label', 'Условие тоггла')}
hint={text('hint', 'Описание пункта')}
name='switch'
value='value'
onChange={handleChange}
/>
</div>
</div>

<div className={cn(styles.row)}>
<div className={cn(styles.col)}>
<Switch
checked={checked}
disabled={boolean('disabled', false)}
reversed={boolean('reversed', false)}
className={text('className', '')}
dataTestId={text('dataTestId', '')}
name='switch3'
value='value3'
onChange={handleChange}
/>
</div>
</div>

<div className={cn(styles.row)}>
<div className={cn(styles.col)}>
<Switch
checked={checked}
disabled={boolean('disabled', false)}
reversed={boolean('reversed', false)}
className={text('className', '')}
dataTestId={text('dataTestId', '')}
name='switch3'
value='value3'
label='Согласен'
hint={
<span>
вы соглашаетесь с <Button view='ghost'>условиями</Button>
</span>
}
onChange={handleChange}
/>
</div>
</div>

<div className={cn(styles.row)}>
<div className={cn(styles.col)}>
<Switch
checked={checked}
disabled={boolean('disabled', false)}
reversed={true}
className={text('className', '')}
dataTestId={text('dataTestId', '')}
name='switch3'
value='value3'
label='reversed'
onChange={handleChange}
/>
</div>
</div>
</React.Fragment>
);
};

SwitchStory.story = {
name: 'Switch',
parameters: {},
};
111 changes: 111 additions & 0 deletions src/switch/src/Component.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';

import { Switch } from './index';

describe('Switch', () => {
describe('Snapshots tests', () => {
it('should match snapshot', () => {
expect(render(<Switch label='label' hint='hint' />)).toMatchSnapshot();
});

it('should render only switch if no content', () => {
expect(render(<Switch />)).toMatchSnapshot();
});
});

describe('Classes tests', () => {
it('should set custom class', () => {
const className = 'custom-class';
const { container } = render(<Switch className={className} />);

expect(container.firstElementChild).toHaveClass(className);
});

it('should set `checked` class', () => {
const { container } = render(<Switch checked={true} />);

expect(container.firstElementChild).toHaveClass('checked');
});

it('should set `disabled` class', () => {
const { container } = render(<Switch disabled={true} />);

expect(container.firstElementChild).toHaveClass('disabled');
});
it('should set `reversed` class', () => {
const { container } = render(<Switch reversed={true} />);

expect(container.firstElementChild).toHaveClass('reversed');
});
});

describe('Attributes tests', () => {
it('should set `data-test-id` atribute', () => {
const dataTestId = 'test-id';
const { getByTestId } = render(<Switch dataTestId={dataTestId} />);

expect(getByTestId(dataTestId).tagName).toBe('INPUT');
});

it('should set disabled attribute', () => {
const dataTestId = 'test-id';
const { getByTestId } = render(<Switch disabled={true} dataTestId={dataTestId} />);

expect(getByTestId(dataTestId)).toBeDisabled();
});

it('should set checked attribute', () => {
const dataTestId = 'test-id';
const { getByTestId } = render(<Switch checked={true} dataTestId={dataTestId} />);

expect(getByTestId(dataTestId)).toBeChecked();
});
});

describe('Render tests', () => {
test('should unmount without errors', () => {
const { unmount } = render(<Switch />);

expect(unmount).not.toThrowError();
});
});

describe('Interaction tests', () => {
test('should call `onChange` prop if not disabled', () => {
const cb = jest.fn();

const { container } = render(<Switch onChange={cb} checked={true} />);

if (container.firstElementChild) {
fireEvent.click(container.firstElementChild);
}

expect(cb).toBeCalledTimes(1);
});

test('should not call `onChange` prop if disabled', () => {
const cb = jest.fn();

const { container } = render(<Switch onChange={cb} disabled={true} />);

if (container.firstElementChild) {
fireEvent.click(container.firstElementChild);
}

expect(cb).not.toBeCalled();
});

test('should not call `onChange` prop if disabled and checked', () => {
const cb = jest.fn();

const { container } = render(<Switch onChange={cb} checked={true} disabled={true} />);

if (container.firstElementChild) {
fireEvent.click(container.firstElementChild);
}

expect(cb).not.toBeCalled();
});
});
});
99 changes: 99 additions & 0 deletions src/switch/src/Component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React, { InputHTMLAttributes, useCallback, ChangeEvent, ReactNode } from 'react';
import cn from 'classnames';

import styles from './index.module.css';

export type SwitchProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
'type' | 'hint' | 'onChange'
> & {
/**
* Управление состоянием вкл/выкл компонента
*/
checked?: boolean;

/**
* Текст подписи к переключателю
*/
label?: ReactNode;

/**
* Текст подсказки снизу
*/
hint?: ReactNode;

/**
* Переключатель будет отрисован справа от контента
*/
reversed?: boolean;

/**
* Обработчик переключения компонента
*/
onChange?: (
event?: ChangeEvent<HTMLInputElement>,
payload?: {
checked: boolean;
name: InputHTMLAttributes<HTMLInputElement>['name'];
},
) => void;

/**
* Идентификатор для систем автоматизированного тестирования
*/
dataTestId?: string;
};

export const Switch = ({
reversed = false,
checked = false,
disabled,
label,
hint,
name,
value,
className,
onChange,
dataTestId,
...restProps
}: SwitchProps) => {
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
if (onChange) {
onChange(e, { checked: e.target.checked, name });
}
},
[onChange, name],
);

return (
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label
className={cn(styles.component, className, {
[styles.disabled]: disabled,
[styles.checked]: checked,
[styles.reversed]: reversed,
})}
>
<input
type='checkbox'
onChange={handleChange}
disabled={disabled}
checked={checked}
name={name}
value={value}
data-test-id={dataTestId}
{...restProps}
/>

<span className={styles.switch} />

{(label || hint) && (
<div className={styles.content}>
{label && <span className={styles.label}>{label}</span>}
{hint && <span className={styles.hint}>{hint}</span>}
</div>
)}
</label>
);
};

0 comments on commit 4e23acd

Please sign in to comment.