mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 18:02:47 +08:00
feat(component): add slider ui component
This commit is contained in:
@@ -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';
|
||||
|
||||
54
packages/frontend/component/src/ui/slider/index.css.ts
Normal file
54
packages/frontend/component/src/ui/slider/index.css.ts
Normal 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'),
|
||||
},
|
||||
},
|
||||
});
|
||||
1
packages/frontend/component/src/ui/slider/index.ts
Normal file
1
packages/frontend/component/src/ui/slider/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './slider';
|
||||
14
packages/frontend/component/src/ui/slider/slider.stories.tsx
Normal file
14
packages/frontend/component/src/ui/slider/slider.stories.tsx
Normal 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 = {};
|
||||
101
packages/frontend/component/src/ui/slider/slider.tsx
Normal file
101
packages/frontend/component/src/ui/slider/slider.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user