Skip to content

Commit

Permalink
feat(core-components-slider): add component (#285)
Browse files Browse the repository at this point in the history
* feat(core-components-slider): add component

* feat(core-components-slider): change cursor

* fix(core-components-slider): add missing deps

* fix(core-components-slider): pr fixes
  • Loading branch information
reme3d2y committed Oct 7, 2020
1 parent f94f526 commit 57a645e
Show file tree
Hide file tree
Showing 8 changed files with 481 additions and 0 deletions.
23 changes: 23 additions & 0 deletions packages/slider/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@alfalab/core-components-slider",
"version": "1.0.0",
"description": "Slider component",
"keywords": [],
"license": "ISC",
"main": "dist/index.js",
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"classnames": "^2.2.6",
"react": "^16.9.0",
"react-dom": "^16.9.0"
},
"dependencies": {
"@alfalab/hooks": "^0.9.0",
"react-merge-refs": "^1.1.0"
}
}
61 changes: 61 additions & 0 deletions packages/slider/src/Component.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
import { action } from '@storybook/addon-actions';
import { text, select, boolean, number } from '@storybook/addon-knobs';
import { Container, Row, Col } from 'storybook/blocks/grid';
import { ComponentHeader } from 'storybook/blocks/component-header';
import Icon from '@alfalab/icons-glyph/StarMIcon';

import { Slider } from './Component';
import { name, version } from '../package.json';


<Meta
title='Компоненты'
component={Slider}
/>


<!-- Canvas -->

<Story name='Slider'>
{React.createElement(() => {
const [value, setValue] = React.useState(50);
const handleChange = (event, { value }) => setValue(value);
return (
<Slider
value={value}
onChange={handleChange}
min={number('min', 0)}
max={number('max', 100)}
step={number('step', 1)}
/>
);
})}
</Story>


<!-- Docs -->

<ComponentHeader
name='Slider'
version={version}
package='@alfalab/core-components-slider'
stage={0}
design='https://www.figma.com/file/hqSP3L6qu8UcL3sf18Su1M/Web-Components?node-id=11780%3A5392'
/>

```tsx
import { Slider } from '@alfalab/core-components-slider';
```

<Preview>
{React.createElement(() => {
const [value, setValue] = React.useState(50);
const handleChange = (event, { value }) => setValue(value);
return (
<Slider value={value} onChange={handleChange} />
);
})}
</Preview>

<Props of={Slider} />
112 changes: 112 additions & 0 deletions packages/slider/src/Component.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';

import { Slider } from './index';

describe('Slider', () => {
it('should match snapshot', () => {
const { container } = render(<Slider />);
expect(container).toMatchSnapshot();
});

it('should set `data-test-id` attribute', () => {
const dataTestId = 'test-id';
const { getByTestId } = render(<Slider dataTestId={dataTestId} />);

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

it('should forward ref to input element', () => {
const ref = jest.fn();
const dataTestId = 'test-id';

render(<Slider ref={ref} dataTestId={dataTestId} />);

expect(ref.mock.calls[0][0].tagName).toBe('INPUT');
});

it('should set `className` class to root', () => {
const className = 'test-class';
const { container } = render(<Slider className={className} />);

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

it('should set min, max correctly', () => {
const dataTestId = 'test-id';
const min = 5;
const max = 50;
const { getByRole } = render(<Slider min={min} max={max} dataTestId={dataTestId} />);
const slider = getByRole('slider') as HTMLInputElement;
const progress = getByRole('progressbar') as HTMLProgressElement;

expect(slider.min).toBe(min.toString());
expect(slider.max).toBe(max.toString());

expect(progress.max).toBe(max - min);
});

it('should set step = 1 if (max - min) % step !== 0', () => {
const dataTestId = 'test-id';
const step = 3;
const { getByRole } = render(
<Slider min={0} max={7} step={step} dataTestId={dataTestId} />,
);
const slider = getByRole('slider') as HTMLInputElement;

expect(slider.step).toBeFalsy();
});

it('should set value correctly', () => {
const dataTestId = 'test-id';
const value = 10;
const min = 5;
const max = 50;
const step = 5;
const { getByRole } = render(
<Slider value={value} min={min} max={max} step={step} dataTestId={dataTestId} />,
);

expect((getByRole('slider') as HTMLInputElement).value).toBe(value.toString());
expect((getByRole('progressbar') as HTMLProgressElement).value).toBe(value - min);
});

it('should set out of bounds value in range [min, max]', () => {
const dataTestId = 'test-id';
const min = 5;
const max = 55;
const valueBelowMinimum = 4;
const valueAboveMaximum = 56;

const { getByRole, rerender } = render(
<Slider value={valueBelowMinimum} min={min} max={max} dataTestId={dataTestId} />,
);

expect((getByRole('slider') as HTMLInputElement).value).toBe(min.toString());
expect((getByRole('progressbar') as HTMLProgressElement).value).toBe(0);

rerender(<Slider value={valueAboveMaximum} min={min} max={max} dataTestId={dataTestId} />);

expect((getByRole('slider') as HTMLInputElement).value).toBe(max.toString());
expect((getByRole('progressbar') as HTMLProgressElement).value).toBe(max - min);
});

it('should call `onChange` prop', () => {
const cb = jest.fn();
const dataTestId = 'test-id';
const value = 10;
const { getByRole } = render(<Slider onChange={cb} dataTestId={dataTestId} />);

const input = getByRole('slider') as HTMLInputElement;

fireEvent.change(input, { target: { value } });

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

it('should unmount without errors', () => {
const { unmount } = render(<Slider />);

expect(unmount).not.toThrowError();
});
});
106 changes: 106 additions & 0 deletions packages/slider/src/Component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { forwardRef, useRef, InputHTMLAttributes, useCallback, ChangeEvent } from 'react';
import cn from 'classnames';
import mergeRefs from 'react-merge-refs';
import { useFocus } from '@alfalab/hooks';

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

type NativeProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
'min' | 'max' | 'step' | 'value' | 'type'
>;

export type SliderProps = NativeProps & {
/**
* Мин. допустимое число
*/
min?: number;

/**
* Макс. допустимое число
*/
max?: number;

/**
* Шаг (должен нацело делить отрезок между мин и макс)
*/
step?: number;

/**
* Значение инпута
*/
value?: number;

/**
* Обработчик поля ввода
*/
onChange?: (event: ChangeEvent<HTMLInputElement>, payload: { value: number }) => void;

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

export const Slider = forwardRef<HTMLInputElement, SliderProps>(
(
{ min = 0, max = 100, step = 1, value = 0, className, onChange, dataTestId, ...restProps },
ref,
) => {
const inputRef = useRef<HTMLInputElement>(null);

const [focused] = useFocus(inputRef, 'keyboard');

const range = max - min;
const dividedWithoutRemainder = range % step === 0;
const validValue = Math.max(min, Math.min(value, max));

const rangeProps = {
className: cn(styles.range, { [styles.focused]: focused }),
type: 'range',
min,
max,
value: validValue,
step: dividedWithoutRemainder ? step : undefined,
};

const progressProps = {
className: styles.progress,
max: range,
value: validValue - min,
};

const handleInputChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
if (onChange) {
onChange(event, { value: +event.target.value });
}
},
[onChange],
);

return (
<div className={cn(styles.component, className)} data-test-id={dataTestId}>
<div className={styles.rangeWrapper}>
<input
{...rangeProps}
{...restProps}
ref={mergeRefs([ref, inputRef])}
onChange={handleInputChange}
/>
</div>
<progress {...progressProps} />
</div>
);
},
);

/**
* Для отображения в сторибуке
*/
Slider.defaultProps = {
min: 0,
max: 100,
step: 1,
value: 0,
};
27 changes: 27 additions & 0 deletions packages/slider/src/__snapshots__/Component.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Slider should match snapshot 1`] = `
<div>
<div
class="component"
>
<div
class="rangeWrapper"
>
<input
class="range"
max="100"
min="0"
step="1"
type="range"
value="0"
/>
</div>
<progress
class="progress"
max="100"
value="0"
/>
</div>
</div>
`;

0 comments on commit 57a645e

Please sign in to comment.