feat(component): add slider ui component

This commit is contained in:
Jimmfly
2024-08-14 21:58:11 +08:00
committed by EYHN
parent 9d42db56ca
commit bc77d7a648
5 changed files with 171 additions and 0 deletions

View File

@@ -22,6 +22,7 @@ export * from './ui/popover';
export * from './ui/radio';
export * from './ui/scrollbar';
export * from './ui/skeleton';
export * from './ui/slider';
export * from './ui/switch';
export * from './ui/table';
export * from './ui/tabs';

View File

@@ -0,0 +1,54 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const sliderContainerStyle = style({
position: 'relative',
width: '250px',
height: '16px',
});
export const trackStyle = style({
width: '100%',
height: '1px',
backgroundColor: cssVarV2('layer/border'),
position: 'relative',
display: 'flex',
justifyContent: 'space-between',
});
export const filledTrackStyle = style({
height: '100%',
backgroundColor: cssVarV2('icon/primary'),
borderRadius: '1px',
position: 'absolute',
top: '0',
left: '0',
});
export const thumbStyle = style({
width: '8px',
height: '8px',
backgroundColor: cssVarV2('icon/primary'),
borderRadius: '50%',
position: 'absolute',
top: '50%',
transform: 'translate(-50%, -50%)',
cursor: 'pointer',
});
export const nodeStyle = style({
width: '4px',
height: '4px',
border: '2px solid transparent',
backgroundColor: cssVarV2('layer/border'),
borderRadius: '50%',
position: 'absolute',
top: '50%',
cursor: 'pointer',
transform: 'translate(-50%, -50%)',
selectors: {
'&[data-active="true"]': {
backgroundColor: cssVarV2('icon/primary'),
},
},
});

View File

@@ -0,0 +1 @@
export * from './slider';

View File

@@ -0,0 +1,14 @@
import type { Meta, StoryFn } from '@storybook/react';
import type { SliderProps } from './index';
import { Slider } from './index';
export default {
title: 'UI/Slider',
component: Slider,
} satisfies Meta<typeof Slider>;
const Template: StoryFn<SliderProps> = args => <Slider {...args} />;
export const Default: StoryFn<SliderProps> = Template.bind(undefined);
Default.args = {};

View File

@@ -0,0 +1,101 @@
import type { KeyboardEvent, MouseEvent } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import * as styles from './index.css';
export interface SliderProps {
value?: number;
onChange?: (value: number) => void;
min?: number;
max?: number;
step?: number;
nodes?: number[]; // The values where the nodes should be placed
}
export const Slider = ({
value: propValue = 50,
onChange,
min = 0,
max = 100,
step = 1,
nodes = [20, 40, 60, 80],
}: SliderProps) => {
const [value, setValue] = useState(propValue);
const sliderRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setValue(propValue);
}, [propValue]);
const handleThumbMove = useCallback(
(e: MouseEvent) => {
if (!sliderRef.current) return;
const sliderRect = sliderRef.current.getBoundingClientRect();
const newValue =
((e.clientX - sliderRect.left) / sliderRect.width) * (max - min) + min;
const clampedValue = Math.min(
max,
Math.max(min, Math.round(newValue / step) * step)
);
setValue(clampedValue);
if (onChange) {
onChange(clampedValue);
}
},
[max, min, onChange, step]
);
const handleThumbKeyDown = useCallback(
(e: KeyboardEvent) => {
let newValue = value;
if (e.key === 'ArrowLeft') {
newValue = Math.max(min, value - step);
} else if (e.key === 'ArrowRight') {
newValue = Math.min(max, value + step);
}
setValue(newValue);
if (onChange) {
onChange(newValue);
}
},
[max, min, onChange, step, value]
);
const thumbPosition = useMemo(
() => ((value - min) / (max - min)) * 100,
[max, min, value]
);
return (
<div className={styles.sliderContainerStyle}>
<div
className={styles.trackStyle}
ref={sliderRef}
onMouseDown={handleThumbMove}
>
<div
className={styles.filledTrackStyle}
style={{ width: `${thumbPosition}%` }}
/>
{nodes.map((nodeValue, index) => (
<div
key={index}
className={styles.nodeStyle}
data-active={value >= nodeValue}
style={{
left: `${((nodeValue - min) / (max - min)) * 100}%`,
}}
/>
))}
<div
className={styles.thumbStyle}
style={{ left: `${thumbPosition}%` }}
onMouseDown={e => e.stopPropagation()}
onMouseMove={handleThumbMove}
onKeyDown={handleThumbKeyDown}
tabIndex={0}
/>
</div>
</div>
);
};