From 5662b1d4d3d1b586a3e8f243385a39a094333c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Mon, 30 Dec 2024 17:17:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=9E=82=E7=9B=B4SegmentedCo?= =?UTF-8?q?ntrol=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/VSegmentedControl.module.css | 42 +++++++++++++++++ src/components/VSegmentedControl.tsx | 52 +++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/components/VSegmentedControl.module.css create mode 100644 src/components/VSegmentedControl.tsx diff --git a/src/components/VSegmentedControl.module.css b/src/components/VSegmentedControl.module.css new file mode 100644 index 0000000..d3fb0a7 --- /dev/null +++ b/src/components/VSegmentedControl.module.css @@ -0,0 +1,42 @@ +@layer components { + .segmented_control { + display: inline-block; + border-radius: var(--border-radius-xxs); + overflow: hidden; + user-select: none; + .options { + display: flex; + flex-direction: column; + 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; + left: 0; + right: 0; + transition: top 300ms ease, height 300ms ease; + pointer-events: none; + border-radius: var(--border-radius-xxs); + background-color: var(--color-primary-active); + width: 100%; + z-index: 2; + } + } + } +} diff --git a/src/components/VSegmentedControl.tsx b/src/components/VSegmentedControl.tsx new file mode 100644 index 0000000..b358837 --- /dev/null +++ b/src/components/VSegmentedControl.tsx @@ -0,0 +1,52 @@ +import cx from 'clsx'; +import { isEqual, isNil } from 'lodash-es'; +import { useCallback, useRef, useState } from 'react'; +import type { Option } from '../models'; +import styles from './VSegmentedControl.module.css'; + +type VSegmentedControlProps = { + options?: Option[]; + value?: Option['value']; + onChange?: (value: Option['value']) => void; +}; + +export function VSegmentedControl({ options = [], value, onChange }: VSegmentedControlProps) { + const [selected, setSelected] = useState(value ?? options[0].value ?? null); + const [sliderPosition, setSliderPosition] = useState(0); + const [sliderHeight, setSliderHeight] = 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.offsetTop); + setSliderHeight(optionElement.offsetHeight); + } + }, []); + + return ( +
+
+ {options.map((option, index) => ( +
(optionsRef.current[index] = el!)} + onClick={() => handleSelectAction(option.value, index)}> + {option.label} +
+ ))} + {!isNil(selected) && ( +
+ )} +
+
+ ); +}