# useFocusTrap

## 使用方法

`useFocusTrap` 就像一个看不见的小管家，遵循 WAI-ARIA 规范，负责把键盘的光标焦点规整地关在指定的弹窗或容器里。

当弹窗打开并且焦点陷阱激活时，用户不断按下 Tab 键切换焦点，如果按到了容器里的最后一个按钮，再按 Tab 键就会悄悄绕回容器的第一个元素，焦点永远在弹窗内部循环，绝对不会不小心跳到背景网页上。打开陷阱时，它还会把光标焦点送进容器内部：优先定位到 `initialFocus` 指定的元素上，如果没有指定就会自动停留在第一个可以聚焦的元素上。

当弹窗关闭或陷阱解除时，它会把焦点原路还给刚才点开它的那个元素，确保键盘操作流程顺畅完整。焦点从没进过容器（比如焦点一直留在输入框上的下拉列表），或者已经被挪到别处时，就不还。用鼠标或手指关掉的，焦点回到触发器上但不画焦点环；用键盘关掉的照常画。

Tab 键只在焦点真的在容器里面时才会被拦下，浮层开着、焦点合理地留在外面时，整个页面照样能用 Tab。程序把初始焦点放在容器自己身上时，按 Tab 会进入里面的循环。输入法正在拼字时按的 Escape 是取消拼字，不会触发 `onEscape`。在容器外面用鼠标点击不会被拦截，方便实现点击遮罩空白处关闭弹窗。

**什么时候用：**
- 编写自定义的模态对话框（Modal）、抽屉（Drawer）等全屏遮罩浮层，需要限制键盘焦点不能跑到弹窗外面时。

**什么时候不要用：**
- 像 Tooltip（文字提示）、下拉菜单（DropdownMenu）这类普通的浮层，请不要把焦点强行关在里面；它们需要的是轻量的点击外部关闭，而不是把焦点牢牢锁死。
- 如果已经使用了官方的 [Dialog](/components/dialog) 或 [AlertDialog](/components/alert-dialog)，它们底层已经内置好了完备的焦点管理逻辑，无需再手动调用 `useFocusTrap`。

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

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

const containerRef = ref<HTMLElement | null>(null)
const isOpen = ref(false)

const trap = useFocusTrap({
  container: containerRef,
  isActive: isOpen,
  onEscape: () => { isOpen.value = false },
})
</script>

<template>
  <div ref="containerRef" role="dialog">
    <button @click="trap.focusFirst()">
      第一个输入项
    </button>
  </div>
</template>
```

## 建议

| 指引 | 做法 |
| --- | --- |
| 推荐 | 打开对话框或模态时调 `focusFirst()`，把焦点送进被关住的区域。 |
| 推荐 | 传 `onEscape`，容器里按下 Escape 时关掉对话框。 |
| 推荐 | 关闭后的焦点还原用内置的就行；只有确实要送别处时，才自己再写一套。 |
| 避免 | 用在 Tooltip、下拉菜单这类非模态内容上；它们要的是轻关闭，不是关住焦点。 |

## 参数

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `options` | `UseFocusTrapOptions` | `{}` | 焦点陷阱的配置对象。里面的字段都是可选的。 |
| `options.container` | `Ref` | — | 要关住焦点的元素。模板里绑同一个模板引用。不传也行，hook 自己建一个，从返回值的 `containerRef` 取。 |
| `options.isActive` | `MaybeRefOrGetter<boolean>` | — | 陷阱当前是否生效。打开时装上监听并把焦点送进去，关闭时摘掉监听并把焦点还回去。`isEnabled` 是同一个开关的另一个名字，任一为真即生效。 |
| `options.isEnabled` | `MaybeRefOrGetter<boolean>` | — | 和 `isActive` 是同一个开关，习惯哪个名字就用哪个。两个都传没意义。 |
| `options.onEscape` | `() => void` | — | 容器里按下 Escape 时调用。陷阱不吞这个键：旁边的轻关闭处理照样收得到。 |
| `options.initialFocus` | `() => HTMLElement \| null` | — | 陷阱打开时先聚焦哪个元素。返回 `null` 就退回第一个可聚焦元素。 |

## 返回值

| 名称 | 类型 | 说明 |
| --- | --- | --- |
| `containerRef` | `Ref` | 绑到要关住焦点的容器上的 ref。就是你传进来的 `container`，没传时是 hook 自己建的那个。 |
| `focusFirst` | `() => void` | 聚焦容器里的第一个可聚焦元素。 |
| `focusLast` | `() => void` | 聚焦容器里的最后一个可聚焦元素。 |
| `update` | `() => void` | 留给需要在子元素变化后重新测量的调用方。它什么都不做：可聚焦元素在每次按键时都会重新读。 |
