From 34584ff9ef112a5d95a23f2d5a58e845a1968cef Mon Sep 17 00:00:00 2001 From: Vixalie Date: Thu, 14 Aug 2025 23:01:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(components):=20=E6=B7=BB=E5=8A=A0=20Select?= =?UTF-8?q?=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了一个功能完善的 Select 下拉选择组件 - 支持简单选项和分组选项两种类型 - 提供正常、下划线和沉浸三种样式变体 - 具备单选和受控组件功能 - 优化了可访问性和交互体验 --- src/components/Select.tsx | 207 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 src/components/Select.tsx diff --git a/src/components/Select.tsx b/src/components/Select.tsx new file mode 100644 index 0000000..d2beedc --- /dev/null +++ b/src/components/Select.tsx @@ -0,0 +1,207 @@ +import { Icon } from '@iconify-icon/solid'; +import cx from 'clsx'; +import { isNil, isNotNil } from 'es-toolkit'; +import { + Component, + createEffect, + createMemo, + createSignal, + Index, + JSX, + Match, + mergeProps, + onMount, + Show, + Switch, +} from 'solid-js'; +import { Dynamic, Portal } from 'solid-js/web'; +import Divider from './Divider'; +import ScrollArea from './ScrollArea'; + +interface Option { + label: JSX.Element; + value: string | number | symbol | boolean; +} + +interface SimpleOptionsProps { + options: Option[]; + active?: Option['value']; + onClick?: (option: Option) => void; +} + +const SimpleOptions: Component = (props) => { + const handleClick = (value: Option['value']) => { + props.onClick?.(value); + }; + + return ( +
+ + {(option) => ( +
handleClick(option().value)}> + {option().label} +
+ )} +
+
+ ); +}; + +interface GroupedOptionsProps { + options: Record; + active?: Option['value']; + onClick?: (value: Option['value']) => void; +} + +const GroupedOptions: Component = (props) => { + const entries = createMemo(() => Object.entries(props.options)); + const handleClick = (value: Option['value']) => { + props.onClick?.(value); + }; + + return ( +
+ + {(entry) => ( +
+ {entry()[0]} + +
+ + {(option) => ( +
handleClick(option().value)}> + {option().label} +
+ )} +
+
+
+ )} +
+
+ ); +}; + +interface SelectProps { + variant?: 'normal' | 'underlined' | 'immersive'; + placeholder?: string; + name?: string; + options: Option[] | Record; + value?: Option['value']; + defaultValue?: Option['value']; + disabled?: boolean; + onChange?: (value: Option['value']) => void; +} + +const Select: Component = (props) => { + const mProps = mergeProps( + { + variant: 'normal', + placeholder: '', + options: [], + disabled: false, + }, + props, + ); + + let trigger: HTMLDivElement; + let optionFrame: HTMLDivElement; + + const [selected, setSelected] = createSignal(undefined); + const [optionVisible, setOptionVisible] = createSignal(false); + + onMount(() => { + if (isNotNil(mProps.defaultValue)) { + setSelected(mProps.defaultValue); + } + }); + createEffect(() => { + if (isNotNil(mProps.value) && mProps.value !== selected()) { + setSelected(mProps.value); + } + }); + const displayLabel = createMemo(() => { + const placeholderContent = {mProps.placeholder}; + if (isNil(selected())) { + return placeholderContent; + } + + if (Array.isArray(mProps.options)) { + const selectedOption = mProps.options.find((option) => option.value === selected()); + return selectedOption?.label ?? placeholderContent; + } else { + for (const [, entry] of Object.entries(mProps.options)) { + const option = entry.find((opt) => opt.value === selected()); + if (isNotNil(option)) { + return option.label; + } + } + return placeholderContent; + } + }); + + return ( +
+
+
+ +
+ +
+ + +
+ + }> + + + + + +
+
+
+ + + +
+ ); +}; + +export default Select;