iter(g38): select 支持 searchable 搜索过滤

This commit is contained in:
HIsheR
2026-10-26 11:50:00 +08:00
parent 587ea92edb
commit 82909c5ed1
10 changed files with 110 additions and 26 deletions
+8
View File
@@ -660,6 +660,14 @@
"acceptance": "packages/tools/cli/recipes.md 作为唯一来源,ua docs 时追加到 llms-full.txt 末尾;缺少文件时不报错;npm run check 全绿", "acceptance": "packages/tools/cli/recipes.md 作为唯一来源,ua docs 时追加到 llms-full.txt 末尾;缺少文件时不报错;npm run check 全绿",
"status": "done", "status": "done",
"notes": "自补:Agent 文档;配方文件缺失时静默跳过" "notes": "自补:Agent 文档;配方文件缺失时静默跳过"
},
{
"id": "g38",
"title": "select 支持 searchable 搜索",
"motivation": "选项超过十个时只能靠键入首字母快跳,看不到筛掉的候选;业务常自己再包一层搜索框,交互割裂",
"acceptance": "select({ searchable: true }) 弹层顶部带搜索框,打开即聚焦、实时包含匹配、无命中显示「无匹配选项」,↑↓/Home/End 只在可见项间移动;README 分组行补齐新组件;npm run check 全绿",
"status": "open",
"notes": "自补:既有组件可用性细节"
} }
] ]
} }
+9 -3
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "ui-arch-registry-index/0.1", "$schema": "ui-arch-registry-index/0.1",
"generatedAt": "2026-10-26T03:47:28.088Z", "generatedAt": "2026-10-26T03:49:26.217Z",
"repoRoot": ".", "repoRoot": ".",
"packages": [ "packages": [
{ {
@@ -149,7 +149,7 @@
"slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值", "slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值",
"checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用", "checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用",
"switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用", "switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用",
"select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用,空间不足自动翻转,键入字母快跳选项", "select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用,空间不足自动翻转,键入字母快跳选项;searchable 开启弹层搜索框",
"menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭,空间不足自动翻转,键入字母快跳菜单项", "menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭,空间不足自动翻转,键入字母快跳菜单项",
"flip(trigger, panel, preferred):浮层上下翻转判定,按触发器与面板的视口几何决定 preferred 方位放不下时是否翻转,返回实际生效方位 'top' | 'bottom'", "flip(trigger, panel, preferred):浮层上下翻转判定,按触发器与面板的视口几何决定 preferred 方位放不下时是否翻转,返回实际生效方位 'top' | 'bottom'",
"radioGroup(props):单选组,options + value 信号双向绑定,disabled 整组禁用", "radioGroup(props):单选组,options + value 信号双向绑定,disabled 整组禁用",
@@ -615,6 +615,11 @@
"t": "string", "t": "string",
"d": "未选中时触发器显示的占位文案" "d": "未选中时触发器显示的占位文案"
}, },
{
"n": "searchable",
"t": "boolean",
"d": "弹层顶部带搜索框,按标签包含匹配过滤;缺省 false"
},
{ {
"n": "disabled", "n": "disabled",
"t": "boolean | Signal<boolean>", "t": "boolean | Signal<boolean>",
@@ -633,7 +638,8 @@
"创建信号并传入 value;未传时组件内部自持状态", "创建信号并传入 value;未传时组件内部自持状态",
"点击触发器展开弹层,点选项写回信号并收起;点外部或按 Esc 也会收起", "点击触发器展开弹层,点选项写回信号并收起;点外部或按 Esc 也会收起",
"选中项在弹层中用淡色弱化标记,不抢视觉焦点", "选中项在弹层中用淡色弱化标记,不抢视觉焦点",
"弹层展开时按 Esc 只收起下拉并回焦触发器;若下拉在弹窗/抽屉内,再按一次 Esc 才关外层(逐层关闭)" "弹层展开时按 Esc 只收起下拉并回焦触发器;若下拉在弹窗/抽屉内,再按一次 Esc 才关外层(逐层关闭)",
"searchable: true 时打开即聚焦搜索框,输入实时过滤;↑↓/Home/End 只在可见项间移动,无命中显示「无匹配选项」"
] ]
}, },
{ {
+1 -1
View File
@@ -17,7 +17,7 @@ npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全
accordion · alert · avatar · badge · breadcrumb · button · carousel · card · checkbox · dataTable · descriptions · divider · drawer · empty · form · image · inputNumber · list · menu · modal · notification · pagination · popover · progress · radioGroup · rate · result · segmented · select · skeleton · slider · spinner · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · tree · upload accordion · alert · avatar · badge · breadcrumb · button · carousel · card · checkbox · dataTable · descriptions · divider · drawer · empty · form · image · inputNumber · list · menu · modal · notification · pagination · popover · progress · radioGroup · rate · result · segmented · select · skeleton · slider · spinner · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · tree · upload
<!-- /ua:components --> <!-- /ua:components -->
分组:表单(button / textField / inputNumber / textarea / checkbox / switchButton / select / radioGroup / slider / rate / segmented / form / formItem)、反馈(alert / toast / spinner / progress / skeleton / badge / empty / statistic)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper)、数据(dataTable / descriptions / tag / avatar / card)、其他(divider / timeline)。 分组:表单(button / textField / inputNumber / textarea / checkbox / switchButton / select / radioGroup / slider / rate / segmented / upload / form + formItem)、反馈(alert / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree)、数据展示(dataTable / descriptions / card / list / timeline / tag / avatar / image / carousel)、其他(divider)。
## 目录 ## 目录
+2
View File
@@ -181,6 +181,7 @@ const menuPick = signal('');
const listPick = signal(''); // 列表点击选中的条目标题 const listPick = signal(''); // 列表点击选中的条目标题
const slideIndex = signal(0); // 轮播当前下标(外部受控演示) const slideIndex = signal(0); // 轮播当前下标(外部受控演示)
const treePick = signal('core'); // 树形选中节点 key const treePick = signal('core'); // 树形选中节点 key
const cityPick = signal(''); // 可搜索下拉的选中城市
const pickedFiles = signal(''); // 文件选择结果(名称串) const pickedFiles = signal(''); // 文件选择结果(名称串)
// —— 禁用态演示:开关信号同时绑定 switch 的 checked 与 checkbox 的 disabled —— // —— 禁用态演示:开关信号同时绑定 switch 的 checked 与 checkbox 的 disabled ——
@@ -556,6 +557,7 @@ const content = h(
'div', 'div',
{ class: 'row' }, { class: 'row' },
segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }, { value: 'month', label: '月' }], value: range, size: 'sm' }), segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }, { value: 'month', label: '月' }], value: range, size: 'sm' }),
select({ options: [{ value: 'bj', label: '北京' }, { value: 'sh', label: '上海' }, { value: 'gz', label: '广州' }, { value: 'sz', label: '深圳' }, { value: 'hz', label: '杭州' }, { value: 'cd', label: '成都' }], value: cityPick, searchable: true, placeholder: '可搜索城市' }),
segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }], value: range, size: 'sm', disabled: locked }), segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }], value: range, size: 'sm', disabled: locked }),
h('span', { class: 'out' }, () => (locked() ? '第二个分段已禁用(同上「锁定」开关)' : '第二个分段可用')), h('span', { class: 'out' }, () => (locked() ? '第二个分段已禁用(同上「锁定」开关)' : '第二个分段可用')),
), ),
+4 -2
View File
@@ -1,6 +1,6 @@
# @ui-arch 组件库 Agent 文档(无图形版) # @ui-arch 组件库 Agent 文档(无图形版)
> 自动生成于 2026-10-26T03:47:28.088Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 自动生成于 2026-10-26T03:49:26.217Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
## 快速上手 ## 快速上手
@@ -259,6 +259,7 @@ const night = signal(true); switchButton({ label: '夜间模式', checked: night
- options(必填):OptionItem[] —— 选项列表,每项 { value, label } - options(必填):OptionItem[] —— 选项列表,每项 { value, label }
- value:Signal<string> —— 选中值信号,双向绑定 - value:Signal<string> —— 选中值信号,双向绑定
- placeholder:string —— 未选中时触发器显示的占位文案 - placeholder:string —— 未选中时触发器显示的占位文案
- searchable:boolean —— 弹层顶部带搜索框,按标签包含匹配过滤;缺省 false
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步;禁用时触发器置灰且不展开弹层 - disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步;禁用时触发器置灰且不展开弹层
- onChange(回调):(value: string) => void —— 选中回调 - onChange(回调):(value: string) => void —— 选中回调
@@ -275,6 +276,7 @@ const city = signal(''); select({ options: [{ value: 'bj', label: '北京' }], v
3. 点击触发器展开弹层,点选项写回信号并收起;点外部或按 Esc 也会收起 3. 点击触发器展开弹层,点选项写回信号并收起;点外部或按 Esc 也会收起
4. 选中项在弹层中用淡色弱化标记,不抢视觉焦点 4. 选中项在弹层中用淡色弱化标记,不抢视觉焦点
5. 弹层展开时按 Esc 只收起下拉并回焦触发器;若下拉在弹窗/抽屉内,再按一次 Esc 才关外层(逐层关闭) 5. 弹层展开时按 Esc 只收起下拉并回焦触发器;若下拉在弹窗/抽屉内,再按一次 Esc 才关外层(逐层关闭)
6. searchable: true 时打开即聚焦搜索框,输入实时过滤;↑↓/Home/End 只在可见项间移动,无命中显示「无匹配选项」
现场演示:apps/docs/demo.html#sec-form 现场演示:apps/docs/demo.html#sec-form
@@ -1175,7 +1177,7 @@ API 速览:
- slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值 - slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用 - checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用 - switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用,空间不足自动翻转,键入字母快跳选项 - select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用,空间不足自动翻转,键入字母快跳选项;searchable 开启弹层搜索框
- menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭,空间不足自动翻转,键入字母快跳菜单项 - menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭,空间不足自动翻转,键入字母快跳菜单项
- flip(trigger, panel, preferred):浮层上下翻转判定,按触发器与面板的视口几何决定 preferred 方位放不下时是否翻转,返回实际生效方位 'top' | 'bottom' - flip(trigger, panel, preferred):浮层上下翻转判定,按触发器与面板的视口几何决定 preferred 方位放不下时是否翻转,返回实际生效方位 'top' | 'bottom'
- radioGroup(props):单选组,options + value 信号双向绑定,disabled 整组禁用 - radioGroup(props):单选组,options + value 信号双向绑定,disabled 整组禁用
+1 -1
View File
@@ -54,7 +54,7 @@
- slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值 - slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用 - checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用 - switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用,空间不足自动翻转,键入字母快跳选项 - select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用,空间不足自动翻转,键入字母快跳选项;searchable 开启弹层搜索框
- menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭,空间不足自动翻转,键入字母快跳菜单项 - menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭,空间不足自动翻转,键入字母快跳菜单项
- flip(trigger, panel, preferred):浮层上下翻转判定,按触发器与面板的视口几何决定 preferred 方位放不下时是否翻转,返回实际生效方位 'top' | 'bottom' - flip(trigger, panel, preferred):浮层上下翻转判定,按触发器与面板的视口几何决定 preferred 方位放不下时是否翻转,返回实际生效方位 'top' | 'bottom'
- radioGroup(props):单选组,options + value 信号双向绑定,disabled 整组禁用 - radioGroup(props):单选组,options + value 信号双向绑定,disabled 整组禁用
+53 -15
View File
@@ -2,15 +2,19 @@ import { effect, h, signal } from '@ui-arch/frame';
import type { SelectProps } from './types.js'; import type { SelectProps } from './types.js';
import { flip } from './position.js'; import { flip } from './position.js';
/*** 下拉选择:自绘弹层替代原生 select,value 信号双向绑定;支持 ↑↓/Home/End 键盘导航,点选项 / 点外部 / Esc 关闭 ***/ /*** 下拉选择:自绘弹层替代原生 select,value 信号双向绑定;searchable 时弹层顶部带搜索框,支持 ↑↓/Home/End 导航与 Esc 关闭 ***/
export function select(props: SelectProps): HTMLElement { export function select(props: SelectProps): HTMLElement {
// —— 状态集中声明 —— // —— 状态集中声明 ——
const open = signal(false); // 弹层开合 const open = signal(false); // 弹层开合
const place = signal<'top' | 'bottom'>('bottom'); // 实际生效方位:打开时按视口空间自动翻转 const place = signal<'top' | 'bottom'>('bottom'); // 实际生效方位:打开时按视口空间自动翻转
const query = signal(''); // 搜索关键字(仅 searchable 时使用)
const value = props.value ?? signal(''); // 选中值;未传信号则内部持有 const value = props.value ?? signal(''); // 选中值;未传信号则内部持有
const labels = props.options.map((o) => o.label); // 文案缓存,供搜索与键入快跳共用
const labelOf = (v: string) => props.options.find((o) => o.value === v)?.label ?? ''; const labelOf = (v: string) => props.options.find((o) => o.value === v)?.label ?? '';
const current = () => labelOf(value()); // 触发器显示文案(响应式) const current = () => labelOf(value()); // 触发器显示文案(响应式)
const match = (i: number) => !query() || labels[i].toLowerCase().includes(query().toLowerCase()); // 关键字包含匹配
const visibleIdx = () => props.options.map((_, i) => i).filter(match); // 当前可见选项的下标序列
const pick = (v: string) => { const pick = (v: string) => {
value(v); value(v);
@@ -41,49 +45,83 @@ export function select(props: SelectProps): HTMLElement {
type: 'button', type: 'button',
class: () => `ua-option${opt.value === value() ? ' ua-option-selected' : ''}`, class: () => `ua-option${opt.value === value() ? ' ua-option-selected' : ''}`,
role: 'option', role: 'option',
'aria-selected': () => String(opt.value === value()), 'aria-selected': () => String(value() === opt.value),
onClick: () => pick(opt.value), onClick: () => pick(opt.value),
}, },
opt.label, opt.label,
), ),
); );
const pop = h('div', { class: () => `ua-select-pop${open() ? ' ua-open' : ''}${place() === 'top' ? ' is-flip' : ''}`, role: 'listbox' }, ...options); const search = h('input', {
type: 'search',
class: 'ua-select-search',
placeholder: '搜索…',
'aria-label': '搜索选项',
oninput: (e: Event) => query((e.target as HTMLInputElement).value), // 输入即过滤
onkeydown: (e: KeyboardEvent) => {
if (e.key === 'ArrowDown') { e.preventDefault(); options[visibleIdx()[0] ?? 0]?.focus(); } // ↓ 从搜索框进选项
},
}) as HTMLInputElement;
const blank = h('div', { class: 'ua-select-blank' }, '无匹配选项'); // 过滤空结果提示
const pop = h(
'div',
{ class: () => `ua-select-pop${open() ? ' ua-open' : ''}${place() === 'top' ? ' is-flip' : ''}`, role: 'listbox' },
props.searchable ? search : null,
...options,
blank,
);
const root = h('div', { class: () => `ua-select${open() ? ' ua-open' : ''}` }, trigger, pop); // 根带开合类,供箭头旋转 const root = h('div', { class: () => `ua-select${open() ? ' ua-open' : ''}` }, trigger, pop); // 根带开合类,供箭头旋转
// —— 键入快跳缓冲 —— // —— 键入快跳缓冲(非 searchable 时生效,逻辑与 menu 一致)——
let typed = ''; // 最近键入的字母串,用于前缀匹配 let typed = ''; // 最近键入的字母串,用于前缀匹配
let typedAt = 0; // 最近键入时间戳,超 1 秒重置缓冲 let typedAt = 0; // 最近键入时间戳,超 1 秒重置缓冲
const jumpByTyped = (ch: string): void => { const jumpByTyped = (ch: string): void => {
const now = Date.now(); const now = Date.now();
typed = now - typedAt > 1000 ? ch : typed + ch; // 超时重新起词,否则追加 typed = now - typedAt > 1000 ? ch : typed + ch; // 超时重新起词,否则追加
typedAt = now; typedAt = now;
const at = options.indexOf(document.activeElement as HTMLElement); // 当前焦点序号,作为环形搜索起点 const list = visibleIdx();
const order = options.map((_, i) => (at + 1 + i) % options.length); // 从下一项开始环形排列的序号 const at = list.findIndex((i) => options[i] === document.activeElement); // 当前焦点在可见集中的序号
const hit = order.find((i) => props.options[i].label.toLowerCase().startsWith(typed.toLowerCase())); // 前缀匹配(忽略大小写) const order = list.length ? list.map((_, k) => list[(at + 1 + k) % list.length]) : []; // 从下一项开始环形排列
const hit = order.find((i) => labels[i].toLowerCase().startsWith(typed.toLowerCase())); // 前缀匹配(忽略大小写)
if (hit != null) options[hit]?.focus(); // 命中即聚焦;无命中留在原地 if (hit != null) options[hit]?.focus(); // 命中即聚焦;无命中留在原地
}; };
root.addEventListener('keydown', (e: KeyboardEvent) => { root.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') { e.stopPropagation(); open(false); trigger.focus(); return; } // Esc 只关本层并回焦触发器 if (e.key === 'Escape') { e.stopPropagation(); open(false); trigger.focus(); return; } // Esc 只关本层并回焦触发器
const list = visibleIdx();
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault(); // 阻止页面滚动 e.preventDefault(); // 阻止页面滚动
if (!open()) { open(true); options[0]?.focus(); return; } // 未展开:展开并聚焦首个选项 if (!open()) { open(true); options[list[0] ?? 0]?.focus(); return; } // 未展开:展开并聚焦首个可见选项
const idx = options.indexOf(document.activeElement as HTMLElement); // 展开中:在选项间环形移动 const at = list.findIndex((i) => options[i] === document.activeElement); // 展开中:在可见选项间环形移动
const dir = e.key === 'ArrowDown' ? 1 : -1; const dir = e.key === 'ArrowDown' ? 1 : -1;
const next = (idx + dir + options.length) % options.length; options[list[(at + dir + list.length) % list.length]]?.focus();
options[next]?.focus();
return; return;
} }
if (e.key === 'Home' && open()) { e.preventDefault(); options[0]?.focus(); return; } // Home 跳首项 if (e.key === 'Home' && open()) { e.preventDefault(); options[list[0]]?.focus(); return; } // Home 跳首个可见项
if (e.key === 'End' && open()) { e.preventDefault(); options[options.length - 1]?.focus(); return; } // End 跳末项 if (e.key === 'End' && open()) { e.preventDefault(); options[list[list.length - 1]]?.focus(); return; } // End 跳末个可见项
if (open() && e.key.length === 1) jumpByTyped(e.key); // 可打印字符:键入快跳(Home/End/方向键已提前接管) if (open() && !props.searchable && e.key.length === 1) jumpByTyped(e.key); // 可打印字符:键入快跳(搜索模式由输入框接管)
}); });
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
if (root.contains(e.target as Node)) return; // 卫语句:组件内点击不处理 if (root.contains(e.target as Node)) return; // 卫语句:组件内点击不处理
open(false); // 点外部关闭 open(false); // 点外部关闭
}); });
effect(() => open()); // 建立依赖,保证开合信号生效 effect(() => {
const q = query();
let shown = 0;
options.forEach((el, i) => {
const on = match(i);
el.hidden = !on; // 不匹配即折叠,键盘只在可见项之间移动
if (on) shown += 1;
});
blank.hidden = shown > 0 || !q; // 仅「有关键字且无命中」时才提示
});
effect(() => {
if (open() && props.searchable) {
query(''); // 每次打开重置关键字
search.value = '';
requestAnimationFrame(() => search.focus()); // 搜索模式:打开即聚焦输入框
}
});
effect(() => { if (open()) place(flip(trigger, pop, 'bottom')); }); // 每次打开重算方位:空间不足自动翻转 effect(() => { if (open()) place(flip(trigger, pop, 'bottom')); }); // 每次打开重算方位:空间不足自动翻转
return root; return root;
} }
+1
View File
@@ -98,6 +98,7 @@ export interface SelectProps {
value?: Signal<string>; // 选中值信号,双向绑定 value?: Signal<string>; // 选中值信号,双向绑定
placeholder?: string; // 未选中时的占位项文案 placeholder?: string; // 未选中时的占位项文案
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步 disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
searchable?: boolean; // 弹层顶部带搜索框,按标签包含匹配过滤;缺省 false
onChange?: (value: string) => void; // 变更回调 onChange?: (value: string) => void; // 变更回调
} }
+8 -2
View File
@@ -20,7 +20,7 @@
"slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值", "slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值",
"checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用", "checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用",
"switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用", "switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用",
"select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用,空间不足自动翻转,键入字母快跳选项", "select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用,空间不足自动翻转,键入字母快跳选项;searchable 开启弹层搜索框",
"menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭,空间不足自动翻转,键入字母快跳菜单项", "menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭,空间不足自动翻转,键入字母快跳菜单项",
"flip(trigger, panel, preferred):浮层上下翻转判定,按触发器与面板的视口几何决定 preferred 方位放不下时是否翻转,返回实际生效方位 'top' | 'bottom'", "flip(trigger, panel, preferred):浮层上下翻转判定,按触发器与面板的视口几何决定 preferred 方位放不下时是否翻转,返回实际生效方位 'top' | 'bottom'",
"radioGroup(props):单选组,options + value 信号双向绑定,disabled 整组禁用", "radioGroup(props):单选组,options + value 信号双向绑定,disabled 整组禁用",
@@ -486,6 +486,11 @@
"t": "string", "t": "string",
"d": "未选中时触发器显示的占位文案" "d": "未选中时触发器显示的占位文案"
}, },
{
"n": "searchable",
"t": "boolean",
"d": "弹层顶部带搜索框,按标签包含匹配过滤;缺省 false"
},
{ {
"n": "disabled", "n": "disabled",
"t": "boolean | Signal<boolean>", "t": "boolean | Signal<boolean>",
@@ -504,7 +509,8 @@
"创建信号并传入 value;未传时组件内部自持状态", "创建信号并传入 value;未传时组件内部自持状态",
"点击触发器展开弹层,点选项写回信号并收起;点外部或按 Esc 也会收起", "点击触发器展开弹层,点选项写回信号并收起;点外部或按 Esc 也会收起",
"选中项在弹层中用淡色弱化标记,不抢视觉焦点", "选中项在弹层中用淡色弱化标记,不抢视觉焦点",
"弹层展开时按 Esc 只收起下拉并回焦触发器;若下拉在弹窗/抽屉内,再按一次 Esc 才关外层(逐层关闭)" "弹层展开时按 Esc 只收起下拉并回焦触发器;若下拉在弹窗/抽屉内,再按一次 Esc 才关外层(逐层关闭)",
"searchable: true 时打开即聚焦搜索框,输入实时过滤;↑↓/Home/End 只在可见项间移动,无命中显示「无匹配选项」"
] ]
}, },
{ {
+21
View File
@@ -294,6 +294,27 @@
transform: none; transform: none;
visibility: visible; visibility: visible;
} }
.ua-select-search {
width: 100%;
margin-bottom: 4px;
padding: 6px 10px;
border: 1px solid var(--ua-color-border);
border-radius: var(--ua-radius-sm);
background: var(--ua-color-bg);
color: var(--ua-color-text);
font-size: var(--ua-font-size-sm);
outline: none;
}
.ua-select-search:focus {
border-color: var(--ua-color-accent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ua-color-accent) 22%, transparent);
}
.ua-select-blank {
padding: 8px 10px;
font-size: var(--ua-font-size-sm);
color: var(--ua-color-text-muted);
text-align: center;
}
.ua-option { .ua-option {
display: block; display: block;
width: 100%; width: 100%;