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