Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SOV-4411: Update Slider component with Ranged functionality #997

Merged
merged 6 commits into from
Sep 10, 2024
Merged
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
5 changes: 5 additions & 0 deletions .changeset/cuddly-cougars-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sovryn/ui": patch
---

SOV-4411: Update Slider component with Ranged functionality
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ export const BalancedRange: FC<BalancedRangeProps> = ({ pool }) => {
}, [rangeWidth, setMaximumPrice, setMinimumPrice, currentPrice]);

const onRangeChange = useCallback(
(value: number) => {
setMinimumPrice(calculateBoundedPrice(true, value, currentPrice));
setMaximumPrice(calculateBoundedPrice(false, value, currentPrice));
setRangeWidth(value);
(value: number | number[]) => {
if (typeof value === 'number') {
setMinimumPrice(calculateBoundedPrice(true, value, currentPrice));
setMaximumPrice(calculateBoundedPrice(false, value, currentPrice));
setRangeWidth(value);
}
},
[setMaximumPrice, setMinimumPrice, setRangeWidth, currentPrice],
);
Expand Down
15 changes: 9 additions & 6 deletions packages/ui/src/1_atoms/Slider/Slider.module.css
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
.track {
@apply h-1 bg-gray-50 rounded;
}

& .track-0 {
@apply h-1 bg-gray-50 rounded;
}

&.track-1 {
@apply h-1 rounded bg-primary-30;
.rangeSlider {
& .track {
&:nth-child(1), &:nth-child(3) {
@apply bg-gray-50 !important;
}
}
}

.thumb {
@apply h-2.5 w-2.5 bg-sov-white rounded-full -top-[3px];
&:focus {
@apply outline-none;
}
}
160 changes: 160 additions & 0 deletions packages/ui/src/1_atoms/Slider/Slider.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Story, Meta } from '@storybook/react';

import React, { ComponentProps, useCallback, useEffect, useState } from 'react';

import { Slider } from './Slider';

export default {
title: 'Atoms/Slider',
component: Slider,
} as Meta;

const Template: Story<ComponentProps<typeof Slider>> = args => {
const [value, setValue] = useState(args.value || 0);

useEffect(() => {
setValue(args.value || 0);
}, [args.value]);

const handleChange = useCallback(
(newValue: number | number[], thumbIndex: number) => {
setValue(newValue);
if (args.onChange) {
args.onChange(newValue, thumbIndex);
}
},
[args],
);

return <Slider {...args} value={value} onChange={handleChange} />;
};

const RangeSliderTemplate: Story<ComponentProps<typeof Slider>> = ({
value: initialValue,
...args
}) => {
const [value, setValue] = useState<number | number[]>(
initialValue || [30, 70],
);

const handleChange = (newValue: number | number[], thumbIndex: number) => {
if (Array.isArray(newValue)) {
setValue(newValue);
}
if (args.onChange) args.onChange(newValue, thumbIndex);
};

const handleAfterChange = (
newValue: number | number[],
thumbIndex: number,
) => {
if (Array.isArray(newValue)) {
setValue(newValue);
}
if (args.onAfterChange) args.onAfterChange(newValue, thumbIndex);
};

return (
<Slider
{...args}
value={value}
onChange={handleChange}
onAfterChange={handleAfterChange}
/>
);
};

export const Basic = Template.bind({});
Basic.args = {
value: 50,
dataAttribute: 'slider-basic',
};

Basic.argTypes = {
value: {
control: 'number',
description: 'The value of the slider',
},
min: {
control: 'number',
description: 'The minimum value of the slider',
},
max: {
control: 'number',
description: 'The maximum value of the slider',
},
step: {
control: 'number',
description:
'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',
},
onChange: {
action: 'onChange',
description:
'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',
},
onAfterChange: {
action: 'onAfterChange',
description:
'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',
},
disabled: {
control: 'boolean',
description: "If true the thumbs can't be moved",
},
className: {
control: 'text',
description: 'The class name to apply to the Slider',
},
thumbClassName: {
control: 'text',
description: 'The css class set on each thumb node',
},
trackClassName: {
control: 'text',
description:
'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, ...',
},
thumbActiveClassName: {
control: 'text',
description: 'The css class set on the thumb that is currently being moved',
},
dataAttribute: {
control: 'text',
description: 'The data attribute to apply to the Slider',
},
isSimple: {
control: 'boolean',
description: 'If false applies the rangeSlider class to the Slider',
},
};

export const RangeSlider = RangeSliderTemplate.bind({});
RangeSlider.args = {
value: [20, 80],
step: 5,
thumbClassName: 'bg-primary',
trackClassName: 'bg-primary',
dataAttribute: 'slider-double',
isSimple: false,
};

RangeSlider.argTypes = {
...Basic.argTypes,
};

export const DRangeSlider = RangeSliderTemplate.bind({});
DRangeSlider.args = {
value: [20, 80],
step: 10,
className: 'w-96 m-auto',
thumbClassName: 'bg-success ring-2 ring-success',
thumbActiveClassName: 'outline-2 outline-sov-white',
trackClassName: 'bg-success',
dataAttribute: 'slider-styled',
isSimple: false,
};

DRangeSlider.argTypes = {
...Basic.argTypes,
};
59 changes: 59 additions & 0 deletions packages/ui/src/1_atoms/Slider/Slider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import '@testing-library/jest-dom/extend-expect';
import { render, screen } from '@testing-library/react';

import React from 'react';

import { Slider } from './Slider';

beforeAll(() => {
global.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
});

describe('Slider', () => {
it('renders the slider with default props', () => {
render(<Slider />);
const slider = screen.getByRole('slider');
expect(slider).toBeInTheDocument();
});

it('applies the rangeSlider class when isSimple is false', () => {
const { container } = render(<Slider isSimple={false} />);
const slider = container.firstChild;
expect(slider).toHaveClass('rangeSlider');
});

it('applies custom class names to thumbs and tracks', () => {
const { container } = render(
<Slider thumbClassName="custom-thumb" trackClassName="custom-track" />,
);

const thumb = container.querySelector('.thumb');
const track = container.querySelector('.track');

expect(thumb).toHaveClass('custom-thumb');
expect(track).toHaveClass('custom-track');
});

it('renders correctly range slider and custom class names', () => {
const { container } = render(
<Slider
isSimple={false}
thumbClassName="custom-thumb"
trackClassName="custom-track"
/>,
);

const slider = container.firstChild;
expect(slider).toHaveClass('rangeSlider');

const thumb = container.querySelector('.thumb');
const track = container.querySelector('.track');

expect(thumb).toHaveClass('custom-thumb');
expect(track).toHaveClass('custom-track');
});
});
46 changes: 39 additions & 7 deletions packages/ui/src/1_atoms/Slider/Slider.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,54 @@
import React, { FC } from 'react';

import classNames from 'classnames';
import ReactSlider from 'react-slider';

import { applyDataAttr } from '../../utils';
import styles from './Slider.module.css';

type SliderProps = {
max?: number;
min?: number;
step?: number;
onAfterChange?: (value: number, thumbIndex: number) => void;
onChange?: (value: number, thumbIndex: number) => void;
value?: number;
value?: number | number[];
isSimple?: boolean;
onChange?: (value: number | number[], thumbIndex: number) => void;
disabled?: boolean;
className?: string;
dataAttribute?: string;
onAfterChange?: (value: number | number[], thumbIndex: number) => void;
thumbClassName?: string;
trackClassName?: string;
thumbActiveClassName?: string;
};

export const Slider: FC<SliderProps> = props => (
export const Slider: FC<SliderProps> = ({
max = 100,
min = 0,
step = 1,
value,
onChange,
isSimple = true,
disabled = false,
className,
dataAttribute,
onAfterChange,
thumbClassName,
trackClassName,
thumbActiveClassName,
}) => (
<ReactSlider
thumbClassName={styles.thumb}
trackClassName={styles.track}
{...props}
max={max}
min={min}
step={step}
value={value}
disabled={disabled}
onChange={onChange}
className={classNames(!isSimple && styles.rangeSlider, className)}
onAfterChange={onAfterChange}
thumbClassName={classNames(styles.thumb, thumbClassName)}
trackClassName={classNames(styles.track, trackClassName)}
thumbActiveClassName={thumbActiveClassName}
{...applyDataAttr(dataAttribute)}
/>
);
1 change: 1 addition & 0 deletions packages/ui/src/1_atoms/Slider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Slider';
1 change: 1 addition & 0 deletions packages/ui/src/1_atoms/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export * from './DynamicValue';
export * from './Lottie';
export * from './ErrorBadge';
export * from './Toggle';
export * from './Slider';
export * from './Bar/Bar';
export * from './Slider/Slider';
Loading