iter(g81): 树组件改用 roving tabindex
This commit is contained in:
@@ -13,6 +13,16 @@ export function tree(props: TreeProps): HTMLElement {
|
||||
const boxes = new Map<string, HTMLInputElement>(); // key → 勾选框
|
||||
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 停点
|
||||
|
||||
/*** 切换 Tab 停点:只有当前行 tabindex=0,其余 -1,Tab 进出树只停一次 ***/
|
||||
const syncTabStop = (key: string | null): void => {
|
||||
tabStop = key;
|
||||
for (const [k, row] of rows) row.tabIndex = k === key ? 0 : -1;
|
||||
};
|
||||
|
||||
/*** 行元素 → key(键盘导航与焦点同步共用) ***/
|
||||
const keyOf = (row: HTMLElement): string | null => [...rows.entries()].find(([, el]) => el === row)?.[0] ?? null;
|
||||
|
||||
/*** 子孙 key 列表(含被禁用项,勾选时再按禁用过滤) ***/
|
||||
const descOf = (node: TreeNode): string[] => (node.children ?? []).flatMap((c) => [c.key, ...descOf(c)]);
|
||||
@@ -60,7 +70,7 @@ export function tree(props: TreeProps): HTMLElement {
|
||||
}
|
||||
const row = h(
|
||||
'div',
|
||||
{ class: 'ua-tree-row', role: 'treeitem', 'aria-level': String(depth + 1), 'aria-expanded': String(on), tabindex: '0' },
|
||||
{ class: 'ua-tree-row', role: 'treeitem', 'aria-level': String(depth + 1), 'aria-expanded': String(on), tabindex: '-1' }, // 由 roving tabStop 决定谁可 Tab
|
||||
box,
|
||||
caret,
|
||||
h('span', { class: 'ua-tree-label' }, node.label),
|
||||
@@ -80,6 +90,7 @@ export function tree(props: TreeProps): HTMLElement {
|
||||
};
|
||||
|
||||
const root = h('div', { class: 'ua-tree', role: 'tree' }, ...props.nodes.map((n) => build(n, 0, null))) as HTMLElement;
|
||||
syncTabStop(props.nodes.find((n) => !n.disabled)?.key ?? null); // 初始停点:首个可用根节点
|
||||
if (props.defaultExpandAll) {
|
||||
const all: string[] = [];
|
||||
const walk = (list: TreeNode[]) => list.forEach((n) => { if (n.children?.length) { all.push(n.key); walk(n.children); } });
|
||||
@@ -113,6 +124,12 @@ 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]'));
|
||||
|
||||
root.addEventListener('focusin', (e: FocusEvent) => {
|
||||
const row = (e.target as HTMLElement).closest('.ua-tree-row') as HTMLElement | null;
|
||||
const key = row ? keyOf(row) : null;
|
||||
if (key && key !== tabStop) syncTabStop(key); // 焦点跟随:下次 Tab 直接回到这行
|
||||
});
|
||||
|
||||
root.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
const row = (e.target as HTMLElement).closest('.ua-tree-row') as HTMLElement | null;
|
||||
if (!row) return; // 卫语句:焦点不在节点行
|
||||
@@ -125,7 +142,7 @@ export function tree(props: TreeProps): HTMLElement {
|
||||
next?.focus(); // 行间移动只改焦点,不改变选中
|
||||
return;
|
||||
}
|
||||
const key = [...rows.entries()].find(([, el]) => el === row)?.[0];
|
||||
const key = keyOf(row);
|
||||
const node = key ? selfOf.get(key) : null;
|
||||
if (!node || node.disabled || !key) return; // 卫语句:找不到或禁用节点
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
|
||||
Reference in New Issue
Block a user