Skip to content
Open
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
16 changes: 16 additions & 0 deletions .changeset/cold-terms-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@alfalab/core-components-slider': major
'@alfalab/core-components': major
---

- Добавлена возможность динамического отображения маркеров слайдера в зависимости от состояния (скрытие при точном совпадении позиции, изменение цвета при прохождении)

- Добавлены новые пропсы для управления точками на слайдере:
- `dots` - включение/отключение отображения точек
- `dotsSlider` - тип отображения точек ('step' | 'custom')
- `customDots` - массив значений для произвольного размещения точек
- `showNumbers` - включение/отключение отображения чисел под точками
- `hideCustomDotsNumbers` - скрытие чисел только для кастомных точек

- Исправлена работа `showNumbers` для режима `dotsSlider='step'`
- Добавлены стили для disabled состояния слайдера (черный цвет кноба и активной части)
4 changes: 2 additions & 2 deletions packages/slider-input/src/Component.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ describe('SliderInput', () => {
values: [1, 2, 3],
};

const { queryByText } = render(<SliderInput pips={pips} />);
const { container } = render(<SliderInput pips={pips} />);

pips.values.map((value) => expect(queryByText(value.toString())).toBeInTheDocument());
expect(container.querySelector('.noUi-pips')).toBeInTheDocument();
});

it('should render info', () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/slider-input/src/Component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export const SliderInput = forwardRef<HTMLInputElement, SliderInputProps>(
const { value: inputValue } = event.target as HTMLInputElement;
const validValue = getValidInputValue(inputValue);

const getEventPayloadValue = (payload: number | '') => {
const getEventPayloadValue = (payload: number) => {
if (payload > max) {
return max;
}
Expand All @@ -232,12 +232,12 @@ export const SliderInput = forwardRef<HTMLInputElement, SliderInputProps>(
if (lockLimit) {
if (onChange) {
onChange(null, {
value: getEventPayloadValue(validValue),
value: getEventPayloadValue(validValue as number),
});
}
if (onInputChange) {
onInputChange(null, {
value: getEventPayloadValue(validValue),
value: getEventPayloadValue(validValue as number),
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ exports[`SliderInput should match snapshot 1`] = `
</div>
</div>
<div
class="component slider size-48 size-2 noUi-target noUi-ltr noUi-horizontal noUi-txt-dir-ltr noUi-state-tap"
class="component slider size-48 size-2 dotsDisabled noUi-target noUi-ltr noUi-horizontal noUi-txt-dir-ltr noUi-state-tap"
>
<div
class="noUi-base"
Expand Down
1 change: 1 addition & 0 deletions packages/slider/src/Component.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Slider } from './index';
describe('Slider', () => {
it('should match snapshot', () => {
const { container } = render(<Slider step={1} />);

expect(container).toMatchSnapshot();
});

Expand Down
201 changes: 67 additions & 134 deletions packages/slider/src/Component.tsx
Original file line number Diff line number Diff line change
@@ -1,119 +1,14 @@
import React, { type FC, useEffect, useRef } from 'react';
import React, { type FC, useEffect, useMemo, useRef } from 'react';
import cn from 'classnames';
import noUiSlider, { type API, type Options } from 'nouislider';
import noUiSlider, { type API } from 'nouislider';

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

type SubRange = number | [number] | [number, number];
type RangeOptions = {
min: SubRange;
max: SubRange;
[key: string]: SubRange;
};

type PipsType = -1 | 0 | 1 | 2;

type Pips = {
mode: 'range' | 'steps' | 'positions' | 'count' | 'values';
values: number | number[];
filter?: (value: number, type: PipsType) => PipsType;
format?: {
to: (value: number) => string | number;
from?: (value: string) => number | false;
};
stepped?: boolean;
};
import { useSliderMarkers } from './hooks';
import { type SliderProps } from './types';
import { createPipsConfig, updateMarkerClasses } from './utils';

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

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

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

/**
* Отображение подписей
* https://refreshless.com/nouislider/pips/
*/
pips?: Pips;

/**
* Настройка шагов
* https://refreshless.com/nouislider/pips/#section-range
*/
range?: RangeOptions;

/**
* Флаг точной привязки к range
* https://refreshless.com/nouislider/examples/#section-skipping
*/
snap?: boolean;

/**
* Значение слайдера
*/
value?: number;

/**
* Второе значение слайдера (значение второго ползунка)
* если передать ValueTo, то слайдер будет работать как range
*/
valueTo?: number;

/**
* Заблокированное состояние
*/
disabled?: boolean;

/**
* Дополнительный класс
*/
className?: string;

/**
* Поведение ползунка
*/
behaviour?: 'unconstrained-tap' | 'tap';

/**
* Размер
* @description s, m deprecated, используйте вместо них 2, 4 соответственно
*/
size?: 's' | 'm' | 2 | 4;

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

/**
* @deprecated
* Обработчик начала перетаскивания ползунка
*/
onStart?: () => void;

/**
* Обработчик окончания перетаскивания ползунка
* @description https://refreshless.com/nouislider/events-callbacks/#section-change
*/
onEnd?: () => void;

/**
* Идентификатор для систем автоматизированного тестирования
*/
dataTestId?: string;
};
import styles from './index.module.css';

export const SIZE_TO_CLASSNAME_MAP = {
const SIZE_TO_CLASSNAME_MAP = {
s: 'size-2',
m: 'size-4',
2: 'size-2',
Expand All @@ -131,6 +26,11 @@ export const Slider: FC<SliderProps> = ({
behaviour = 'tap',
range = { min, max },
size = 2,
dots = false,
dotsSlider = 'step',
customDots,
showNumbers = true,
hideCustomDotsNumbers = false,
className,
onChange,
onStart,
Expand All @@ -141,9 +41,35 @@ export const Slider: FC<SliderProps> = ({
const sliderRef = useRef<(HTMLDivElement & { noUiSlider: API }) | null>(null);
const busyRef = useRef<boolean>(false);
const hasValueTo = valueTo !== undefined;
const { values } = pips || {};
const hasCustomDotsSlider = dotsSlider === 'custom';
const shouldCreatePipsConfig = pips || customDots?.length;

const getSlider = () => sliderRef.current?.noUiSlider;

const pipsConfig = useMemo(() => {
const configParams = {
dotsSlider,
pips,
pipsValues: Array.isArray(values) ? values : [],
customDots: Array.isArray(customDots) ? customDots : [],
showNumbers,
hideCustomDotsNumbers,
};

return createPipsConfig(configParams);
}, [values, pips, dotsSlider, customDots, showNumbers, hideCustomDotsNumbers]);

const { updateMarkersState, createSlideHandler } = useSliderMarkers({
sliderRef,
hasValueTo,
value,
valueTo,
min,
max,
onChange,
});

useEffect(() => {
if (!sliderRef.current) return;

Expand All @@ -152,7 +78,7 @@ export const Slider: FC<SliderProps> = ({
connect: valueTo ? true : [true, false],
behaviour,
step,
pips: pips as Options['pips'],
pips: shouldCreatePipsConfig ? pipsConfig : undefined,
range,
snap,
});
Expand Down Expand Up @@ -194,12 +120,12 @@ export const Slider: FC<SliderProps> = ({
{
step,
range,
pips: pips as Options['pips'],
pips: shouldCreatePipsConfig ? pipsConfig : undefined,
snap,
},
true,
);
}, [pips, range, snap, step]);
}, [pipsConfig, shouldCreatePipsConfig, range, snap, step]);

useEffect(() => {
const slider = getSlider();
Expand All @@ -219,31 +145,38 @@ export const Slider: FC<SliderProps> = ({

if (!slider) return;

const handler = () => {
if (onChange) {
if (hasValueTo) {
const sliderValues = slider.get() as string[];
const from = Number(sliderValues[0]);
const to = Number(sliderValues[1]);

if (from <= to) {
onChange({ value: from, valueTo: to });
} else {
onChange({ value: to, valueTo: from });
}
} else {
onChange({ value: Number(slider.get()) });
}
}
};
const handler = createSlideHandler(slider);

slider.off('slide');
slider.on('slide', handler);
}, [onChange, hasValueTo]);

if (hasValueTo) {
updateMarkersState(value, valueTo);
} else {
updateMarkersState(value);
}
}, [onChange, hasValueTo, value, valueTo, createSlideHandler, updateMarkersState]);

useEffect(() => {
const pipsValues = Array.isArray(values) ? values : [];

if (!hasCustomDotsSlider || !pipsValues.length || !sliderRef.current) return;

updateMarkerClasses({
sliderElement: sliderRef.current,
pipsValues,
customDots,
});
}, [customDots, dotsSlider, hasCustomDotsSlider, values]);

return (
<div
className={cn(styles.component, className, styles[SIZE_TO_CLASSNAME_MAP[size]])}
className={cn(styles.component, className, styles[SIZE_TO_CLASSNAME_MAP[size]], {
[styles.numbersDisabled]: hasCustomDotsSlider && !customDots?.length,
[styles.hideLargePips]: hasCustomDotsSlider,
[styles.dotsDisabled]: !dots,
[styles.disabled]: disabled,
})}
ref={sliderRef}
data-test-id={dataTestId}
{...{ disabled }}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
exports[`Slider should match snapshot 1`] = `
<div>
<div
class="component size-2 noUi-target noUi-ltr noUi-horizontal noUi-txt-dir-ltr noUi-state-tap"
class="component size-2 dotsDisabled noUi-target noUi-ltr noUi-horizontal noUi-txt-dir-ltr noUi-state-tap"
>
<div
class="noUi-base"
Expand Down
1 change: 1 addition & 0 deletions packages/slider/src/component.screenshots.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('Slider | main props', () => {
componentName: 'Slider',
knobs: {
value: [0, 50, 100],
disabled: [false, true],
},
size: { width: 200, height: 30 },
}),
Expand Down
7 changes: 6 additions & 1 deletion packages/slider/src/docs/Component.stories.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Meta, Story, Markdown } from '@storybook/addon-docs';
import { number, select } from '@storybook/addon-knobs';
import { number, select, boolean, object } from '@storybook/addon-knobs';
import { ComponentHeader, Tabs } from 'storybook/blocks';
import { Slider } from '@alfalab/core-components-slider';

Expand All @@ -25,6 +25,11 @@ import Changelog from '../../CHANGELOG.md?raw';
step={number('step', 1)}
size={select('size', [2, 4], 2)}
behaviour={select('behaviour', ['unconstrained-tap', 'tap'], 'tap')}
dots={boolean('dots', true)}
showNumbers={boolean('showNumbers', true)}
dotsSlider={select('dotsSlider', ['step', 'custom'], 'step')}
customDots={object('customDots', [25, 50, 75])}
hideCustomDotsNumbers={boolean('hideCustomDotsNumbers', false)}
/>
);
})}
Expand Down
Loading