Skip to content

Commit d989854

Browse files
SOV-4411: Update Slider component with Ranged functionality (#997)
* chore: update slider component * Create cuddly-cougars-unite.md * chore: update BalancedRange component * chore: naming updates * chore: update isSimple description * chore: updates from comments
1 parent 6d7e340 commit d989854

8 files changed

Lines changed: 280 additions & 17 deletions

File tree

.changeset/cuddly-cougars-unite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sovryn/ui": patch
3+
---
4+
5+
SOV-4411: Update Slider component with Ranged functionality

apps/frontend/src/app/5_pages/MarketMakingPage/components/BobDepositModal/components/PriceRange/components/BalancedRange/BalancedRange.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@ export const BalancedRange: FC<BalancedRangeProps> = ({ pool }) => {
5353
}, [rangeWidth, setMaximumPrice, setMinimumPrice, currentPrice]);
5454

5555
const onRangeChange = useCallback(
56-
(value: number) => {
57-
setMinimumPrice(calculateBoundedPrice(true, value, currentPrice));
58-
setMaximumPrice(calculateBoundedPrice(false, value, currentPrice));
59-
setRangeWidth(value);
56+
(value: number | number[]) => {
57+
if (typeof value === 'number') {
58+
setMinimumPrice(calculateBoundedPrice(true, value, currentPrice));
59+
setMaximumPrice(calculateBoundedPrice(false, value, currentPrice));
60+
setRangeWidth(value);
61+
}
6062
},
6163
[setMaximumPrice, setMinimumPrice, setRangeWidth, currentPrice],
6264
);
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
.track {
22
@apply h-1 bg-gray-50 rounded;
3+
}
34

4-
& .track-0 {
5-
@apply h-1 bg-gray-50 rounded;
6-
}
7-
8-
&.track-1 {
9-
@apply h-1 rounded bg-primary-30;
5+
.rangeSlider {
6+
& .track {
7+
&:nth-child(1), &:nth-child(3) {
8+
@apply bg-gray-50 !important;
9+
}
1010
}
1111
}
1212

1313
.thumb {
1414
@apply h-2.5 w-2.5 bg-sov-white rounded-full -top-[3px];
15+
&:focus {
16+
@apply outline-none;
17+
}
1518
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { Story, Meta } from '@storybook/react';
2+
3+
import React, { ComponentProps, useCallback, useEffect, useState } from 'react';
4+
5+
import { Slider } from './Slider';
6+
7+
export default {
8+
title: 'Atoms/Slider',
9+
component: Slider,
10+
} as Meta;
11+
12+
const Template: Story<ComponentProps<typeof Slider>> = args => {
13+
const [value, setValue] = useState(args.value || 0);
14+
15+
useEffect(() => {
16+
setValue(args.value || 0);
17+
}, [args.value]);
18+
19+
const handleChange = useCallback(
20+
(newValue: number | number[], thumbIndex: number) => {
21+
setValue(newValue);
22+
if (args.onChange) {
23+
args.onChange(newValue, thumbIndex);
24+
}
25+
},
26+
[args],
27+
);
28+
29+
return <Slider {...args} value={value} onChange={handleChange} />;
30+
};
31+
32+
const RangeSliderTemplate: Story<ComponentProps<typeof Slider>> = ({
33+
value: initialValue,
34+
...args
35+
}) => {
36+
const [value, setValue] = useState<number | number[]>(
37+
initialValue || [30, 70],
38+
);
39+
40+
const handleChange = (newValue: number | number[], thumbIndex: number) => {
41+
if (Array.isArray(newValue)) {
42+
setValue(newValue);
43+
}
44+
if (args.onChange) args.onChange(newValue, thumbIndex);
45+
};
46+
47+
const handleAfterChange = (
48+
newValue: number | number[],
49+
thumbIndex: number,
50+
) => {
51+
if (Array.isArray(newValue)) {
52+
setValue(newValue);
53+
}
54+
if (args.onAfterChange) args.onAfterChange(newValue, thumbIndex);
55+
};
56+
57+
return (
58+
<Slider
59+
{...args}
60+
value={value}
61+
onChange={handleChange}
62+
onAfterChange={handleAfterChange}
63+
/>
64+
);
65+
};
66+
67+
export const Basic = Template.bind({});
68+
Basic.args = {
69+
value: 50,
70+
dataAttribute: 'slider-basic',
71+
};
72+
73+
Basic.argTypes = {
74+
value: {
75+
control: 'number',
76+
description: 'The value of the slider',
77+
},
78+
min: {
79+
control: 'number',
80+
description: 'The minimum value of the slider',
81+
},
82+
max: {
83+
control: 'number',
84+
description: 'The maximum value of the slider',
85+
},
86+
step: {
87+
control: 'number',
88+
description:
89+
'Value to be added or subtracted on each step the slider makes. Must be greater than zero. max - min should be evenly divisible by the step value',
90+
},
91+
onChange: {
92+
action: 'onChange',
93+
description:
94+
'Callback called on every value change. The function will be called with two arguments, the first being the new value(s) the second being thumb index',
95+
},
96+
onAfterChange: {
97+
action: 'onAfterChange',
98+
description:
99+
'Callback called only after moving a thumb has ended. The callback will only be called if the action resulted in a change. The function will be called with two arguments, the first being the result value(s) the second being thumb index',
100+
},
101+
disabled: {
102+
control: 'boolean',
103+
description: "If true the thumbs can't be moved",
104+
},
105+
className: {
106+
control: 'text',
107+
description: 'The class name to apply to the Slider',
108+
},
109+
thumbClassName: {
110+
control: 'text',
111+
description: 'The css class set on each thumb node',
112+
},
113+
trackClassName: {
114+
control: 'text',
115+
description:
116+
'The css class set on the tracks between the thumbs. In addition track fragment will receive a numbered css class of the form {trackClassName}-{i}, e.g. track-0, track-1, ...',
117+
},
118+
thumbActiveClassName: {
119+
control: 'text',
120+
description: 'The css class set on the thumb that is currently being moved',
121+
},
122+
dataAttribute: {
123+
control: 'text',
124+
description: 'The data attribute to apply to the Slider',
125+
},
126+
isSimple: {
127+
control: 'boolean',
128+
description: 'If false applies the rangeSlider class to the Slider',
129+
},
130+
};
131+
132+
export const RangeSlider = RangeSliderTemplate.bind({});
133+
RangeSlider.args = {
134+
value: [20, 80],
135+
step: 5,
136+
thumbClassName: 'bg-primary',
137+
trackClassName: 'bg-primary',
138+
dataAttribute: 'slider-double',
139+
isSimple: false,
140+
};
141+
142+
RangeSlider.argTypes = {
143+
...Basic.argTypes,
144+
};
145+
146+
export const DRangeSlider = RangeSliderTemplate.bind({});
147+
DRangeSlider.args = {
148+
value: [20, 80],
149+
step: 10,
150+
className: 'w-96 m-auto',
151+
thumbClassName: 'bg-success ring-2 ring-success',
152+
thumbActiveClassName: 'outline-2 outline-sov-white',
153+
trackClassName: 'bg-success',
154+
dataAttribute: 'slider-styled',
155+
isSimple: false,
156+
};
157+
158+
DRangeSlider.argTypes = {
159+
...Basic.argTypes,
160+
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import '@testing-library/jest-dom/extend-expect';
2+
import { render, screen } from '@testing-library/react';
3+
4+
import React from 'react';
5+
6+
import { Slider } from './Slider';
7+
8+
beforeAll(() => {
9+
global.ResizeObserver = class {
10+
observe() {}
11+
unobserve() {}
12+
disconnect() {}
13+
};
14+
});
15+
16+
describe('Slider', () => {
17+
it('renders the slider with default props', () => {
18+
render(<Slider />);
19+
const slider = screen.getByRole('slider');
20+
expect(slider).toBeInTheDocument();
21+
});
22+
23+
it('applies the rangeSlider class when isSimple is false', () => {
24+
const { container } = render(<Slider isSimple={false} />);
25+
const slider = container.firstChild;
26+
expect(slider).toHaveClass('rangeSlider');
27+
});
28+
29+
it('applies custom class names to thumbs and tracks', () => {
30+
const { container } = render(
31+
<Slider thumbClassName="custom-thumb" trackClassName="custom-track" />,
32+
);
33+
34+
const thumb = container.querySelector('.thumb');
35+
const track = container.querySelector('.track');
36+
37+
expect(thumb).toHaveClass('custom-thumb');
38+
expect(track).toHaveClass('custom-track');
39+
});
40+
41+
it('renders correctly range slider and custom class names', () => {
42+
const { container } = render(
43+
<Slider
44+
isSimple={false}
45+
thumbClassName="custom-thumb"
46+
trackClassName="custom-track"
47+
/>,
48+
);
49+
50+
const slider = container.firstChild;
51+
expect(slider).toHaveClass('rangeSlider');
52+
53+
const thumb = container.querySelector('.thumb');
54+
const track = container.querySelector('.track');
55+
56+
expect(thumb).toHaveClass('custom-thumb');
57+
expect(track).toHaveClass('custom-track');
58+
});
59+
});
Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,54 @@
11
import React, { FC } from 'react';
22

3+
import classNames from 'classnames';
34
import ReactSlider from 'react-slider';
45

6+
import { applyDataAttr } from '../../utils';
57
import styles from './Slider.module.css';
68

79
type SliderProps = {
810
max?: number;
911
min?: number;
1012
step?: number;
11-
onAfterChange?: (value: number, thumbIndex: number) => void;
12-
onChange?: (value: number, thumbIndex: number) => void;
13-
value?: number;
13+
value?: number | number[];
14+
isSimple?: boolean;
15+
onChange?: (value: number | number[], thumbIndex: number) => void;
16+
disabled?: boolean;
17+
className?: string;
18+
dataAttribute?: string;
19+
onAfterChange?: (value: number | number[], thumbIndex: number) => void;
20+
thumbClassName?: string;
21+
trackClassName?: string;
22+
thumbActiveClassName?: string;
1423
};
1524

16-
export const Slider: FC<SliderProps> = props => (
25+
export const Slider: FC<SliderProps> = ({
26+
max = 100,
27+
min = 0,
28+
step = 1,
29+
value,
30+
onChange,
31+
isSimple = true,
32+
disabled = false,
33+
className,
34+
dataAttribute,
35+
onAfterChange,
36+
thumbClassName,
37+
trackClassName,
38+
thumbActiveClassName,
39+
}) => (
1740
<ReactSlider
18-
thumbClassName={styles.thumb}
19-
trackClassName={styles.track}
20-
{...props}
41+
max={max}
42+
min={min}
43+
step={step}
44+
value={value}
45+
disabled={disabled}
46+
onChange={onChange}
47+
className={classNames(!isSimple && styles.rangeSlider, className)}
48+
onAfterChange={onAfterChange}
49+
thumbClassName={classNames(styles.thumb, thumbClassName)}
50+
trackClassName={classNames(styles.track, trackClassName)}
51+
thumbActiveClassName={thumbActiveClassName}
52+
{...applyDataAttr(dataAttribute)}
2153
/>
2254
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Slider';

packages/ui/src/1_atoms/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ export * from './DynamicValue';
1414
export * from './Lottie';
1515
export * from './ErrorBadge';
1616
export * from './Toggle';
17+
export * from './Slider';
1718
export * from './Bar/Bar';
1819
export * from './Slider/Slider';

0 commit comments

Comments
 (0)