# useTreeFocus

## 使用方法

管理树形控件的 roving tabindex 焦点，以及 WAI-ARIA 树的那套键盘规则。上、下、Home、End 在可见的树节点之间按顺序移动（跳过停用的节点）；右、左带树的语义（展开或收起，去第一个子节点或回父节点）。Enter 和空格激活，直接敲字符会触发 typeahead 跳到以这些字符开头的节点。

打开 `hasRovingTabIndex` 就由这个 hook 管唯一那个 tab 停靠点。不打开就自己管：hook 把焦点移到哪个节点，会通过 `onActiveChange` 告诉你。

```ts
import { useTreeFocus } from '@astryx-vue/core/TreeList'
```

```vue
<script setup lang="ts">
import { useTreeFocus } from '@astryx-vue/core/TreeList'

const { treeRef, handleKeyDown, handleFocus } = useTreeFocus({
  hasRovingTabIndex: true,
  onToggleExpand: id => toggleNode(id),
})
</script>

<template>
  <div ref="treeRef" role="tree" tabindex="-1" @keydown="handleKeyDown" @focus="handleFocus">
    <!-- 树节点 -->
  </div>
</template>
```

## 建议

| 指引 | 做法 |
| --- | --- |
| 推荐 | 用在有层级的树形控件里：把 `onToggleExpand` 接到你的展开状态上；tab 停靠点要么交给 `hasRovingTabIndex`，要么自己在 `onActiveChange` 里挪。 |
| 推荐 | 把 `treeRef` 和 `handleKeyDown` 一起绑到 `role="tree"` 的容器上（`ref="treeRef"` 和 `@keydown="handleKeyDown"`）。 |
| 推荐 | 由 hook 管 tab 停靠点时，把 `handleFocus` 接到容器的 `@focus` 上：点完之后靠它对齐。 |
| 避免 | 用在线性列表（改用 `useListFocus`）或二维网格（改用 `useGridRovingFocus`）上；那些遍历方式和树不一样。 |

## 参数

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `options` | `UseTreeFocusOptions` | — | 树形焦点行为的配置。必传，里面的字段都是可选的。 |
| `options.itemSelector` | `string` | `'[role="treeitem"]'` | 树里可见节点的选择器，按 DOM 顺序。 |
| `options.isItemDisabled` | `(item: HTMLElement) => boolean` | — | 判断某个节点是否停用、导航时要跳过。不传时读 `data-tree-disabled` 和 `aria-disabled`。 |
| `options.getLevel` | `(item: HTMLElement) => number` | — | 读某个节点的层级（从 1 开始）。不传时读 `aria-level` 属性。 |
| `options.onToggleExpand` | `(id: string) => void` | — | 展开或收起指定 id 的节点时调用：右方向键按在收起的父节点上、左方向键按在展开的父节点上、以及父节点自己没有动作时按 Enter 或空格。id 取节点的 `data-tree-id`，没有就取它的 `id` 属性。 |
| `options.onActivate` | `(item: HTMLElement, id: string \| undefined) => boolean \| void` | — | Enter 或空格激活节点时调用。你自己处理了就返回 `true`；返回 `false` 或不返回，hook 会退回去切换展开状态。 |
| `options.onActiveChange` | `(id: string \| undefined) => void` | — | hook 把焦点移到某个节点之后通知。tab 停靠点不归 hook 管时，用它自己挪；也可以用它同步自己的选中状态。 |
| `options.hasRovingTabIndex` | `boolean` | `false` | 为 true 时由 hook 管唯一的 tab 停靠点：给各项打上 `tabindex` 0/-1，挂载时对齐一次，导航时跟着走。挂载时已有的 `tabindex="0"` 会被保留为起点。点了之后想让它对齐，把返回的 `handleFocus` 接上。 |
| `options.typeahead` | `boolean` | `true` | 是否启用 typeahead：敲一个可打印字符，跳到下一个文本以这些字符开头的节点。 |

## 返回值

| 名称 | 类型 | 说明 |
| --- | --- | --- |
| `treeRef` | `Ref` | 绑到树容器（`role="tree"`）上的 ref。 |
| `handleKeyDown` | `(event: KeyboardEvent) => void` | 绑到树容器上的按键处理函数。 |
| `handleFocus` | `(event?: FocusEvent) => void` | 绑到容器 `@focus` 上的处理函数。`hasRovingTabIndex` 打开时靠它把 tab 停靠点对齐，没打开时它什么都不做，所以一直绑着也不会有副作用。 |
| `focusFirst` | `() => void` | 聚焦第一个可用的可见节点。 |
| `focusLast` | `() => void` | 聚焦最后一个可用的可见节点。 |
