增加SegmentedControl选择组件。
This commit is contained in:
parent
2a125d9432
commit
927950680c
41
src/components/SegmentedControl.module.css
Normal file
41
src/components/SegmentedControl.module.css
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
@layer components {
|
||||||
|
.segmented_control {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: var(--border-radius-xxs);
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
.option {
|
||||||
|
border-radius: var(--border-radius-xxs);
|
||||||
|
padding: var(--spacing-xxs) var(--spacing-xs);
|
||||||
|
line-height: 1.5em;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 5;
|
||||||
|
transition: background-color 300ms;
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--color-primary-active);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary-hover);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-primary-active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
transition: left 300ms ease, width 300ms ease;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: var(--border-radius-xxs);
|
||||||
|
background-color: var(--color-primary-active);
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
src/components/SegmentedControl.tsx
Normal file
56
src/components/SegmentedControl.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import cx from 'clsx';
|
||||||
|
import { isEqual, isNil } from 'lodash-es';
|
||||||
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
import styles from './SegmentedControl.module.css';
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
label: string;
|
||||||
|
value: string | number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SegmentedConttrolProps = {
|
||||||
|
options?: Option[];
|
||||||
|
value?: Option['value'];
|
||||||
|
onChange?: (value: Option['value']) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SegmentedControl({ options = [], value, onChange }: SegmentedConttrolProps) {
|
||||||
|
const [selected, setSelected] = useState(value ?? options[0].value ?? null);
|
||||||
|
const [sliderPosition, setSliderPosition] = useState(0);
|
||||||
|
const [sliderWidth, setSliderWidth] = useState(0);
|
||||||
|
const sliderRef = useRef<HTMLDivElement>(null);
|
||||||
|
const optionsRef = useRef<HTMLDivElement[]>([]);
|
||||||
|
|
||||||
|
const handleSelectAction = useCallback((option: Option['value'], index: number) => {
|
||||||
|
setSelected(option);
|
||||||
|
onChange?.(option);
|
||||||
|
if (optionsRef.current && optionsRef.current.length > index) {
|
||||||
|
const optionElement = optionsRef.current[index];
|
||||||
|
setSliderPosition(optionElement.offsetLeft);
|
||||||
|
setSliderWidth(optionElement.offsetWidth);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.segmented_control}>
|
||||||
|
<div className={styles.options}>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<div
|
||||||
|
key={`${index}_${option.value}`}
|
||||||
|
className={cx(styles.option, isEqual(selected, option.value) && styles.selected)}
|
||||||
|
ref={(el) => (optionsRef.current[index] = el!)}
|
||||||
|
onClick={() => handleSelectAction(option.value, index)}>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!isNil(selected) && (
|
||||||
|
<div
|
||||||
|
className={styles.slider}
|
||||||
|
ref={sliderRef}
|
||||||
|
style={{ left: `${sliderPosition}px`, width: `${sliderWidth}px` }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user