iter(g166): 下拉选项支持分组标题

This commit is contained in:
HIsheR
2026-10-26 16:13:05 +08:00
parent e1b7de6f20
commit a80644c35b
8 changed files with 81 additions and 16 deletions
+7 -3
View File
@@ -73,10 +73,14 @@ export function multiSelect(props: MultiSelectProps): HTMLElement {
}
tagWrap.replaceChildren(...tags); // 只换标签,输入框保持挂载(重建会丢焦点与输入内容)
input.placeholder = cur.length ? '' : props.placeholder ?? '请选择'; // 有标签时不挤占占位文案
const rows = visible().map((o, i) => {
const rows: HTMLElement[] = []; // 候选行:分组小标题与选项混排(小标题不可聚焦,不占 active 下标)
let prevGroup = ''; // 上一个候选的分组名,只在组首插一次标题
visible().forEach((o, i) => {
if (o.group && o.group !== prevGroup) rows.push(h('li', { class: 'ua-multiselect-group', role: 'presentation' }, o.group));
prevGroup = o.group ?? ''; // 无分组的候选打断连续段,后续同名组会重新起标题
const on = cur.includes(o.value);
const dead = o.disabled === true || (full && !on); // 到上限:未选项不可再点
return h(
rows.push(h(
'li',
{
class: () => `ua-multiselect-opt${on ? ' is-selected' : ''}${active() === i ? ' is-active' : ''}${dead ? ' is-disabled' : ''}`,
@@ -86,7 +90,7 @@ export function multiSelect(props: MultiSelectProps): HTMLElement {
},
h('span', { class: 'ua-multiselect-check', 'aria-hidden': 'true' }, on ? '✓' : ''),
h('span', {}, o.label),
);
));
});
if (!rows.length) rows.push(h('li', { class: 'ua-multiselect-empty' }, '无匹配项'));
list.replaceChildren(...rows);
+19 -1
View File
@@ -60,6 +60,19 @@ export function select(props: SelectProps): HTMLElement {
opt.label,
),
);
// —— 分组:相邻同组选项合成一段,组内首项前插小标题;记下区间供过滤时整组显隐 ——
const nodes: HTMLElement[] = []; // 弹层内容序列(小标题与选项混排)
const runs: { el: HTMLElement; from: number; to: number }[] = []; // 每段标题及其首尾选项下标
props.options.forEach((opt, i) => {
let run = runs[runs.length - 1] ?? null;
if (opt.group && (!run || run.to !== i - 1)) { // 新起一段:标题插在本组首项之前
const el = h('div', { class: 'ua-option-group', role: 'presentation' }, opt.group);
run = { el, from: i, to: i };
runs.push(run);
nodes.push(el);
} else if (opt.group && run) run.to = i; // 同组续接:只拉长区间
nodes.push(options[i]);
});
const search = h('input', {
type: 'search',
class: 'ua-select-search',
@@ -75,7 +88,7 @@ export function select(props: SelectProps): HTMLElement {
'div',
{ class: () => `ua-select-pop${open() ? ' ua-open' : ''}${place() === 'top' ? ' is-flip' : ''}`, role: 'listbox' },
props.searchable ? search : null,
...options,
...nodes,
blank,
);
@@ -129,6 +142,11 @@ export function select(props: SelectProps): HTMLElement {
el.hidden = !on; // 不匹配即折叠,键盘只在可见项之间移动
if (on) shown += 1;
});
for (const run of runs) { // 整组都无命中时连小标题一起藏,不留孤零零的分组名
let any = false;
for (let i = run.from; i <= run.to; i++) if (match(i)) { any = true; break; }
run.el.hidden = !any;
}
blank.hidden = shown > 0 || !q; // 仅「有关键字且无命中」时才提示
});
effect(() => {
+2
View File
@@ -75,6 +75,7 @@ export interface OptionItem {
value: string; // 选项值(提交/匹配用)
label: string; // 选项文案(展示用)
disabled?: boolean; // tabs / segmented 消费:禁用该选项
group?: string; // select 消费:分组小标题,相邻同组选项合成一段(组内首项前插标题)
}
/*** 分段控制器入参 ***/
@@ -687,6 +688,7 @@ export interface CheckOption {
value: string; // 选项值
label: string; // 显示文案
disabled?: boolean; // 单项禁用:不可勾选,也不计入全选
group?: string; // 分组小标题:相邻同组选项合成一段(组内首项前插标题),过滤后整组无命中则标题一并隐藏
}
/*** 多选组入参 ***/