iter(g93): 树形组件支持按标签搜索
This commit is contained in:
@@ -14,6 +14,8 @@ export function tree(props: TreeProps): HTMLElement {
|
||||
const selfOf = new Map<string, TreeNode>(); // key → 节点对象
|
||||
const parentOf = new Map<string, string | null>(); // key → 父 key(根为 null)
|
||||
let tabStop: string | null = null; // roving tabindex:整棵树只保留一个 Tab 停点
|
||||
const keyword = signal(''); // 搜索词:非空时按标签过滤,命中节点保留父链
|
||||
const nodeEls = new Map<string, HTMLElement>(); // key → 节点根(行 + 子容器),整枝显隐用
|
||||
|
||||
/*** 切换 Tab 停点:只有当前行 tabindex=0,其余 -1,Tab 进出树只停一次 ***/
|
||||
const syncTabStop = (key: string | null): void => {
|
||||
@@ -86,7 +88,24 @@ export function tree(props: TreeProps): HTMLElement {
|
||||
parentOf.set(node.key, parent);
|
||||
if (!node.children?.length) caret.classList.add('ua-tree-caret-leaf'); // 叶子箭头置空位占位
|
||||
const kids = h('div', { class: 'ua-tree-children', role: 'group' }, ...(node.children ?? []).map((c) => build(c, depth + 1, node.key)));
|
||||
return h('div', { class: 'ua-tree-node' }, row, kids) as HTMLElement;
|
||||
const nodeEl = h('div', { class: 'ua-tree-node' }, row, kids) as HTMLElement;
|
||||
nodeEls.set(node.key, nodeEl);
|
||||
return nodeEl;
|
||||
};
|
||||
|
||||
/*** 命中集合:自身标签含关键词或子孙含关键词(父链整条保留,避免结果断在中间) ***/
|
||||
const hitting = (): Set<string> => {
|
||||
const q = keyword().trim().toLowerCase();
|
||||
const set = new Set<string>();
|
||||
if (!q) return set; // 卫语句:无关键词不过滤
|
||||
const walk = (node: TreeNode): boolean => {
|
||||
let hit = node.label.toLowerCase().includes(q);
|
||||
for (const c of node.children ?? []) if (walk(c)) hit = true; // 子孙命中同样保留本节点
|
||||
if (hit) set.add(node.key);
|
||||
return hit;
|
||||
};
|
||||
props.nodes.forEach(walk);
|
||||
return set;
|
||||
};
|
||||
|
||||
const root = h('div', { class: 'ua-tree', role: 'tree' }, ...props.nodes.map((n) => build(n, 0, null))) as HTMLElement;
|
||||
@@ -102,15 +121,20 @@ export function tree(props: TreeProps): HTMLElement {
|
||||
const open = expanded();
|
||||
const sel = selected();
|
||||
const picked = checked();
|
||||
const hits = hitting(); // 搜索命中集合;空集合表示未过滤
|
||||
const filtering = keyword().trim().length > 0;
|
||||
for (const [key, row] of rows) {
|
||||
const on = open.includes(key);
|
||||
const shown = !filtering || hits.has(key);
|
||||
const nodeEl = nodeEls.get(key);
|
||||
if (nodeEl) nodeEl.hidden = !shown; // 不命中整枝隐藏,连带子节点退出键盘遍历
|
||||
row.classList.toggle('ua-tree-open', on);
|
||||
row.classList.toggle('ua-tree-selected', key === sel);
|
||||
row.setAttribute('aria-selected', String(key === sel));
|
||||
row.setAttribute('aria-expanded', String(on));
|
||||
toggles.get(key)?.classList.toggle('ua-tree-caret-open', on); // 箭头随展开态旋转
|
||||
const kids = row.nextElementSibling as HTMLElement | null;
|
||||
if (kids) kids.hidden = !on; // 收起即隐藏子容器
|
||||
if (kids) kids.hidden = filtering ? !shown : !on; // 搜索时命中枝强制展开,否则跟随展开态
|
||||
const box = boxes.get(key);
|
||||
if (!box) continue; // 卫语句:非多选模式没有勾选框
|
||||
const self = selfOf.get(key);
|
||||
@@ -122,7 +146,7 @@ export function tree(props: TreeProps): HTMLElement {
|
||||
|
||||
/*** 当前可见的节点行(收起分支的子节点不参与 ↑↓ 遍历) ***/
|
||||
const visibleRows = (): HTMLElement[] =>
|
||||
Array.from(root.querySelectorAll<HTMLElement>('.ua-tree-row')).filter((el) => !el.closest('.ua-tree-children[hidden]'));
|
||||
Array.from(root.querySelectorAll<HTMLElement>('.ua-tree-row')).filter((el) => !el.closest('[hidden]')); // 收起或搜索隐藏的枝都不参与 ↑↓ 遍历
|
||||
|
||||
root.addEventListener('focusin', (e: FocusEvent) => {
|
||||
const row = (e.target as HTMLElement).closest('.ua-tree-row') as HTMLElement | null;
|
||||
@@ -155,5 +179,13 @@ export function tree(props: TreeProps): HTMLElement {
|
||||
if (e.key === 'ArrowLeft' && expanded().includes(key)) { e.preventDefault(); toggle(key); } // ← 收起
|
||||
});
|
||||
|
||||
return root;
|
||||
if (!props.searchable) return root;
|
||||
const search = h('input', {
|
||||
type: 'search',
|
||||
class: 'ua-tree-search',
|
||||
placeholder: '搜索节点…',
|
||||
'aria-label': '搜索节点',
|
||||
oninput: (e: Event) => keyword((e.target as HTMLInputElement).value),
|
||||
}) as HTMLInputElement;
|
||||
return h('div', { class: 'ua-tree-box' }, search, root); // 搜索框放在 role=tree 之外,语义不混
|
||||
}
|
||||
|
||||
@@ -487,6 +487,7 @@ export interface TreeProps {
|
||||
expanded?: Signal<string[]>; // 展开的 key 集合;不传则内部维护
|
||||
selected?: Signal<string>; // 选中 key 信号;不传则内部维护
|
||||
defaultExpandAll?: boolean; // 首屏展开全部分支;缺省 false
|
||||
searchable?: boolean; // 顶部搜索框:按标签过滤并保留命中节点的父链;缺省 false
|
||||
multi?: boolean; // 是否显示勾选框并开启多选;缺省 false
|
||||
checked?: Signal<string[]>; // 勾选 key 集合(multi 时生效);不传则内部维护
|
||||
onSelect?: (node: TreeNode) => void; // 选中回调(点击或 Enter/空格)
|
||||
|
||||
@@ -1977,6 +1977,11 @@
|
||||
"t": "boolean",
|
||||
"d": "首屏展开全部分支;缺省 false"
|
||||
},
|
||||
{
|
||||
"n": "searchable",
|
||||
"t": "boolean",
|
||||
"d": "顶部搜索框:按标签过滤,命中节点保留父链并自动展开;缺省 false"
|
||||
},
|
||||
{
|
||||
"n": "onSelect",
|
||||
"t": "(node: TreeNode) => void",
|
||||
@@ -2007,7 +2012,8 @@
|
||||
"键盘:↑↓ 在可见节点间移动、Home/End 到首末行(只移动焦点),Enter/空格 选中、→ 展开、← 收起;disabled 节点点击与键盘都不响应",
|
||||
"defaultExpandAll 首屏展开全部分支(外部已给 expanded 初值时以外部为准)",
|
||||
"multi: true 时每行前置勾选框:勾父级下钻全选子孙(跳过禁用项),子孙全选后父级自动上勾、部分勾选显示半选",
|
||||
"勾选框点击不触发选中(stopPropagation),键盘在多选模式下 Enter/空格改为切换勾选"
|
||||
"勾选框点击不触发选中(stopPropagation),键盘在多选模式下 Enter/空格改为切换勾选",
|
||||
"节点多时开 searchable:输入即过滤,命中节点连同父链一起显示,隐藏的枝不参与方向键遍历"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2375,6 +2375,24 @@ button.ua-step-dot:hover {
|
||||
.ua-tree-children[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.ua-tree-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ua-space-2);
|
||||
}
|
||||
.ua-tree-search {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--ua-color-border);
|
||||
border-radius: var(--ua-radius-md);
|
||||
background: var(--ua-color-bg);
|
||||
color: var(--ua-color-text);
|
||||
font-size: var(--ua-font-size-sm);
|
||||
}
|
||||
.ua-tree-search:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--ua-color-accent);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 18%, transparent);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ua-tree-row,
|
||||
.ua-tree-caret {
|
||||
|
||||
Reference in New Issue
Block a user