# useListFocus

## 使用方法

管一维列表里的键盘导航，做法对齐 WAI-ARIA 的菜单、列表框和工具栏模式。方向键可以纵向、横向或两个方向都走；Home/End 到两头；可以选到头绕回；RTL 里左右对调；Escape 用来关闭。工具栏、分段控件、标签条这类「整组只有一个 Tab 停靠点」的组合控件，打开 `hasRovingTabIndex` 交给它管。下拉菜单、工具栏，以及任何一维的可聚焦列表都适用。

`listRef` 绑在容器上，`handleKeyDown` 绑容器的按键，`handleFocus` 绑容器的聚焦事件。`hasRovingTabIndex` 没开时 `handleFocus` 什么都不做，所以可以无脑绑上。

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

## 建议

| 指引 | 实践 |
| --- | --- |
| 推荐 | 工具栏和标签条把 `orientation` 设成 `'horizontal'`，下拉菜单设成 `'vertical'`。 |
| 推荐 | 菜单和下拉菜单传 `onEscape`，在按下 Escape 时把焦点还给触发器。 |
| 推荐 | 工具栏式组合控件要成为单个 Tab 停靠点时，打开 `hasRovingTabIndex`；控件里可能有文本框时再打开 `hasCaretGuard`。 |
| 避免 | 用它做二维网格导航；网格和日历用 `useGridRovingFocus`。 |

## 参数

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `options` | `UseListFocusOptions` | `{}` | 列表焦点行为的配置对象，所有字段都可以不填。 |
| `options.itemSelector` | `string` | `'[role="menuitem"]'` | 列表里可聚焦项的选择器。 |
| `options.boundarySelector` | `string` | — | 列表边界的标志。列表里还嵌着同类列表时用（例如带子菜单弹出层的菜单）。设了之后，取项和按键处理都只在这一层自己的容器里。一般传 `'[role="menu"]'`。 |
| `options.wrap` | `boolean` | `true` | 方向键到头之后是否绕回另一头。 |
| `options.onEscape` | `() => void` | — | 按下 Escape 时调用（例如关闭菜单）。传了就会吃掉这个键（`preventDefault`）；不传则 Escape 交给外层处理。 |
| `options.orientation` | `'horizontal' \| 'vertical' \| 'both'` | `'vertical'` | 导航方向。`'horizontal'` 用左右方向键，`'vertical'` 用上下方向键，`'both'` 四个都收。横向时会看容器的书写方向，RTL 里左右对调。 |
| `options.hasHomeEnd` | `boolean` | `true` | Home/End 是否跳到第一个/最后一个可用项。 |
| `options.hasPaging` | `boolean` | `false` | PageDown/PageUp 是否按视口翻页：PageDown 跳到容器里最后一个完整可见的可用项，在那儿再按一次就往后再翻一屏；PageUp 反过来。两者都不绕回；列表不滚动时它们等于 End/Home。 |
| `options.hasRovingTabIndex` | `boolean` | `false` | 交给它管 roving tabindex：只有一个项是 `tabindex="0"`，其余是 `-1`；项挂载、卸载、切换禁用时它会修好，方向键移动焦点时它跟着挪。为 false 时只移动焦点，不碰 `tabindex`。 |
| `options.hasCaretGuard` | `boolean` | `false` | 为 true 时，嵌在列表里的文本框如果光标不在移动方向的边界上（或者有选中内容），方向键就不抢走；嵌进来的 contenteditable 也永远不抢。列表里的行内编辑因此还能正常用。 |

## 返回值

| 名称 | 类型 | 说明 |
| --- | --- | --- |
| `listRef` | `Ref` | 绑到列表容器元素上。 |
| `handleKeyDown` | `(event: KeyboardEvent) => void` | 按键处理，绑在列表容器上。 |
| `handleFocus` | `(event: FocusEvent) => void` | 聚焦处理，绑在容器上。`hasRovingTabIndex` 打开时用来对齐 tab 停靠点；没打开时什么都不做，所以绑上总是安全的。 |
| `focusItem` | `(index: number) => void` | 按序号聚焦某一项，超出范围会被夹到最近的合法值。 |
| `focusFirst` | `() => boolean` | 聚焦第一个可用项。真的聚焦到了返回 true。 |
| `focusLast` | `() => boolean` | 聚焦最后一个可用项。真的聚焦到了返回 true。 |
| `ownsEvent` | `(event: KeyboardEvent) => boolean` | 这个按键是不是属于当前这一层，而不是共用同一个 `boundarySelector` 的嵌套列表。没配 `boundarySelector` 时恒为 true。调用方自己加按键处理（Enter/Space、typeahead）时用它挡一下。 |
| `getItems` | `() => HTMLElement[]` | 当前这一层、DOM 顺序上的项，已经按 `boundarySelector` 限定过，并去掉了藏起来的项。要算 typeahead 目标就用它，别自己再查一遍。 |
