From 9503350cf1c48ee3e45f80f5dcdce763b973a7f3 Mon Sep 17 00:00:00 2001 From: Muse Date: Sat, 14 Nov 2026 15:39:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E8=87=AA=E7=BB=98=20Dropdown=20?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=E5=8E=9F=E7=94=9F=20select=EF=BC=88=E5=A4=96?= =?UTF-8?q?=E8=A7=82=E8=AE=BE=E7=BD=AE=C2=B7=E4=B8=B4=E6=97=B6=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 全应用唯一的原生 select 换为自绘下拉组件:按钮对齐 .input 样式, 展开层沿用应用菜单语言(选中 --accent-weak、悬停 --hover),复用 useDismiss 点别处/Esc 关闭,支持上下键选行。tests/dropdown 1 例。 --- src/components/Dropdown.tsx | 73 +++++++++++++++++++++++++++++++++ src/components/Stage5Modals.tsx | 19 +++++---- src/styles.css | 13 +++++- tests/dropdown.test.tsx | 40 ++++++++++++++++++ 4 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 src/components/Dropdown.tsx create mode 100644 tests/dropdown.test.tsx diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx new file mode 100644 index 0000000..da6009a --- /dev/null +++ b/src/components/Dropdown.tsx @@ -0,0 +1,73 @@ +// 自绘下拉(替代原生 select:原生展开层是系统样式,与应用风格脱节)。 +// 外观对齐 .input;展开层沿用应用菜单语言(面板底、6px 圆角、悬停 --hover、 +// 选中 --accent-weak);点别处/Esc 关闭走统一的 useDismiss;支持上下键选行。 +import { useEffect, useRef, useState } from 'react' +import { useDismiss } from '../hooks/useDismiss' + +export interface DropdownOption { value: string; label: string } + +export function Dropdown({ value, options, onChange, disabled, ariaLabel }: { + value: string + options: DropdownOption[] + onChange: (value: string) => void + disabled?: boolean + ariaLabel?: string +}) { + const [open, setOpen] = useState(false) + const ref = useRef(null) + const menuRef = useRef(null) + useDismiss(ref, open, () => setOpen(false)) + + const current = options.find((o) => o.value === value) ?? options[0] + + // 打开时把焦点放到选中项,便于直接上下键选行 + useEffect(() => { + if (!open) return + const idx = Math.max(0, options.findIndex((o) => o.value === value)) + menuRef.current?.querySelectorAll('button')[idx]?.focus() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open ]) + + const pick = (v: string) => { setOpen(false); onChange(v) } + + const onMenuKey = (e: React.KeyboardEvent) => { + if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return + e.preventDefault() + const buttons = [...(menuRef.current?.querySelectorAll('button') ?? [])] + const idx = buttons.indexOf(document.activeElement as HTMLButtonElement) + const next = e.key === 'ArrowDown' + ? Math.min(buttons.length - 1, idx + 1) + : Math.max(0, idx - 1) + buttons[next]?.focus() + } + + return ( +
+ + {open && ( +
+ {options.map((o) => ( + + ))} +
+ )} +
+ ) +} diff --git a/src/components/Stage5Modals.tsx b/src/components/Stage5Modals.tsx index 7a52709..cf5e96a 100644 --- a/src/components/Stage5Modals.tsx +++ b/src/components/Stage5Modals.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react' import { DOC_THEMES } from '../themes' +import { Dropdown } from './Dropdown' /** 外观与设置(阶段 5):文档主题、当前文档临时主题、自定义 CSS、自动保存间隔。 */ export function AppearanceModal({ docTheme, override, customCss, autosave, autosaveSeconds, hasTab, onDocTheme, onOverride, onCustomCss, onAutosave, onClose }: { @@ -38,18 +39,18 @@ export function AppearanceModal({ docTheme, override, customCss, autosave, autos ))} -