feat(components): 添加 HSegmengts 组件
- 实现了一个新的 Segments 组件,支持水平和垂直方向的选项切换 - 组件具有选择状态和动态指示器样式 - 支持通过 props 传递选项、初始值和变更事件处理函数 - 优化了组件的性能和可维护性
This commit is contained in:
109
src/components/Segments.tsx
Normal file
109
src/components/Segments.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import cx from 'clsx';
|
||||||
|
import { isNotNil } from 'es-toolkit';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
Index,
|
||||||
|
JSX,
|
||||||
|
mergeProps,
|
||||||
|
Show,
|
||||||
|
} from 'solid-js';
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label: string | JSX.Element;
|
||||||
|
value: string | number | symbol | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SegmentsProps {
|
||||||
|
name?: string;
|
||||||
|
options: Option[];
|
||||||
|
value?: Option['value'];
|
||||||
|
direction?: 'horizontal' | 'vertical';
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (value: Option['value'] | undefined) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HSegmengts: Component<SegmentsProps> = (props) => {
|
||||||
|
const mProps = mergeProps<SegmentsProps[]>(
|
||||||
|
{
|
||||||
|
options: [],
|
||||||
|
disabled: false,
|
||||||
|
direction: 'horizontal',
|
||||||
|
},
|
||||||
|
props,
|
||||||
|
);
|
||||||
|
const optionRefs: HTMLDivElement[] = [];
|
||||||
|
|
||||||
|
const originalValue = createMemo(() => mProps.value);
|
||||||
|
const [selected, setSelected] = createSignal<Option['value'] | undefined>(undefined);
|
||||||
|
const [indicatorTop, setIndicatorTop] = createSignal(0);
|
||||||
|
const [indicatorLeft, setIndicatorLeft] = createSignal(0);
|
||||||
|
const [indicatorHeight, setIndicatorHeight] = createSignal(0);
|
||||||
|
const [indicatorWidth, setIndicatorWidth] = createSignal(0);
|
||||||
|
const indicatorStyle = createMemo(() => ({
|
||||||
|
top: `${indicatorTop()}px`,
|
||||||
|
left: `${indicatorLeft()}px`,
|
||||||
|
height: `${indicatorHeight()}px`,
|
||||||
|
width: `${indicatorWidth()}px`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (isNotNil(originalValue()) && originalValue() !== selected()) {
|
||||||
|
setSelected(originalValue());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
createEffect(() => {
|
||||||
|
const selectedIndex = mProps.options?.findIndex((option) => option.value === selected());
|
||||||
|
if (isNotNil(selectedIndex) && optionRefs.length > 0 && optionRefs.length > selectedIndex) {
|
||||||
|
const optionElement = optionRefs[selectedIndex];
|
||||||
|
if (isNotNil(optionElement)) {
|
||||||
|
setIndicatorTop(optionElement.offsetTop);
|
||||||
|
setIndicatorLeft(optionElement.offsetLeft);
|
||||||
|
setIndicatorHeight(optionElement.offsetHeight);
|
||||||
|
setIndicatorWidth(optionElement.offsetWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSelect = (value: Option['value']) => {
|
||||||
|
setSelected(value);
|
||||||
|
mProps.onChange?.(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="inline-block overflow-hidden rounded-sm bg-neutral p-1 select-none">
|
||||||
|
<div
|
||||||
|
class={cx(
|
||||||
|
'relative flex',
|
||||||
|
mProps.direction === 'horizontal' ? 'flex-row items-center' : 'flex-col',
|
||||||
|
)}>
|
||||||
|
<Index each={mProps.options}>
|
||||||
|
{(option, index) => (
|
||||||
|
<div
|
||||||
|
ref={(el) => (optionRefs[index] = el)}
|
||||||
|
class={cx(
|
||||||
|
'z-[5] cursor-pointer rounded-sm px-2 py-1',
|
||||||
|
selected() === option().value
|
||||||
|
? 'text-on-primary-surface hover:bg-primary-surface-hover/45'
|
||||||
|
: 'text-on-surface hover:bg-surface/35',
|
||||||
|
)}
|
||||||
|
onClick={() => handleSelect(option().value)}>
|
||||||
|
{option().label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Index>
|
||||||
|
<Show when={isNotNil(selected())}>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute z-[2] rounded-sm bg-primary-surface transition-[top,left,height,width] duration-200 ease-in-out"
|
||||||
|
style={indicatorStyle()}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={isNotNil(mProps.name)}>
|
||||||
|
<input type="hidden" name={mProps.name} value={selected()} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
Reference in New Issue
Block a user