From 927950680c761bb781032fce0fb1e841f0d61235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Mon, 30 Dec 2024 14:25:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0SegmentedControl=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E7=BB=84=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SegmentedControl.module.css | 41 ++++++++++++++++ src/components/SegmentedControl.tsx | 56 ++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/components/SegmentedControl.module.css create mode 100644 src/components/SegmentedControl.tsx diff --git a/src/components/SegmentedControl.module.css b/src/components/SegmentedControl.module.css new file mode 100644 index 0000000..77512fb --- /dev/null +++ b/src/components/SegmentedControl.module.css @@ -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; + } + } + } +} diff --git a/src/components/SegmentedControl.tsx b/src/components/SegmentedControl.tsx new file mode 100644 index 0000000..65db4b8 --- /dev/null +++ b/src/components/SegmentedControl.tsx @@ -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(null); + const optionsRef = useRef([]); + + 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 ( +
+
+ {options.map((option, index) => ( +
(optionsRef.current[index] = el!)} + onClick={() => handleSelectAction(option.value, index)}> + {option.label} +
+ ))} + {!isNil(selected) && ( +
+ )} +
+
+ ); +}