注意:此插件之前名为"Trap"。Trap 的功能已合并到此插件中,并增加了额外功能。你可以将 Trap 替换为 Focus,无需任何破坏性更改。
Focus 插件
Alpine 的 Focus 插件允许你管理页面上的焦点。
此插件内部大量使用了开源工具:Tabbable。非常感谢该团队为此问题提供了急需的解决方案。
安装
你可以通过 <script> 标签引入或通过 NPM 安装来使用此插件:
通过 CDN
你可以将此插件的 CDN 构建版本作为 <script> 标签引入,只需确保在 Alpine 核心 JS 文件之前引入。
<!-- Alpine 插件 -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine 核心 -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
通过 NPM
你可以从 NPM 安装 Focus 以在打包中使用:
npm install @alpinejs/focus
然后从你的打包工具中初始化它:
import Alpine from 'alpinejs'
import focus from '@alpinejs/focus'
Alpine.plugin(focus)
...
x-trap
Focus 提供了一个用于在元素内锁定焦点的专用 API:x-trap 指令。
x-trap 接受一个 JS 表达式。如果该表达式的结果为 true,则焦点将被锁定在该元素内,直到表达式变为 false,此时焦点将返回到之前的位置。
例如:
<div x-data="{ open: false }">
<button @click="open = true">Open Dialog</button>
<span x-show="open" x-trap="open">
<p>...</p>
<input type="text" placeholder="Some input...">
<input type="text" placeholder="Some other input...">
<button @click="open = false">Close Dialog</button>
</span>
</div>
嵌套对话框
有时你可能需要将一个对话框嵌套在另一个对话框内。x-trap 使这变得非常简单,并会自动处理。
x-trap 会跟踪新"锁定"的元素,并存储最后活跃聚焦的元素。一旦元素被"解锁",焦点将返回到最初的位置。
这种机制是递归的,因此你可以在一个已被锁定的元素内无限次地锁定焦点,然后逐个"解锁"每个元素。
以下是嵌套的实际演示:
<div x-data="{ open: false }">
<button @click="open = true">Open Dialog</button>
<span x-show="open" x-trap="open">
...
<div x-data="{ open: false }">
<button @click="open = true">Open Nested Dialog</button>
<span x-show="open" x-trap="open">
...
<button @click="open = false">Close Nested Dialog</button>
</span>
</div>
<button @click="open = false">Close Dialog</button>
</span>
</div>
修饰符
.inert
在构建对话框/模态框时,建议在锁定焦点时将页面上所有其他元素对屏幕阅读器隐藏。
通过在 x-trap 上添加 .inert,当焦点被锁定时,页面上所有其他元素将收到 aria-hidden="true" 属性,当焦点锁定被禁用时,这些属性也将被移除。
<!-- 当 `open` 为 `false` 时: -->
<body x-data="{ open: false }">
<div x-trap.inert="open" ...>
...
</div>
<div>
...
</div>
</body>
<!-- 当 `open` 为 `true` 时: -->
<body x-data="{ open: true }">
<div x-trap.inert="open" ...>
...
</div>
<div aria-hidden="true">
...
</div>
</body>
.noscroll
在使用 Alpine 构建对话框/模态框时,建议在对话框打开时禁用周围内容的滚动。
x-trap 允许你通过 .noscroll 修饰符自动实现这一点。
通过添加 .noscroll,Alpine 将移除页面滚动条,并在对话框打开时阻止用户滚动页面。
例如:
<div x-data="{ open: false }">
<button @click="open = true">Open Dialog</button>
<div x-show="open" x-trap.noscroll="open">
Dialog Contents
<button @click="open = false">Close Dialog</button>
</div>
</div>
Notice how you can no longer scroll on this page while this dialog is open.
.noreturn
有时你可能不希望焦点返回到之前的位置。考虑一个在输入框获得焦点时触发的下拉菜单,关闭时将焦点返回到输入框只会再次触发下拉菜单打开。
x-trap 允许你通过 .noreturn 修饰符禁用此行为。
通过添加 .noreturn,当 x-trap 评估为 false 时,Alpine 将不会返回焦点。
例如:
<div x-data="{ open: false }" x-trap.noreturn="open">
<input type="search" placeholder="search for something" />
<div x-show="open">
Search results
<button @click="open = false">Close</button>
</div>
</div>
Notice when closing this dropdown, focus is not returned to the input.
.noautofocus
默认情况下,当 x-trap 将焦点锁定在某个元素内时,它会聚焦该元素中的第一个可聚焦元素。这是一个合理的默认行为,但有时你可能希望禁用此行为,在 x-trap 启用时不自动聚焦任何元素。
通过添加 .noautofocus,Alpine 在锁定焦点时将不会自动聚焦任何元素。
$focus
此插件提供了许多用于管理页面内焦点的实用工具。这些工具通过 $focus 魔法方法暴露出来。
| 属性 | 描述 |
|---|---|
focus(el) |
聚焦传入的元素(内部处理各种烦扰:使用 nextTick 等) |
focusable(el) |
检测一个元素是否可聚焦 |
focusables() |
获取当前元素内所有"可聚焦"的元素 |
focused() |
获取页面上当前聚焦的元素 |
lastFocused() |
获取页面上最后聚焦的元素 |
within(el) |
指定将 $focus 魔法限定在哪个元素内(默认为当前元素) |
first() |
聚焦第一个可聚焦元素 |
last() |
聚焦最后一个可聚焦元素 |
next() |
聚焦下一个可聚焦元素 |
previous() |
聚焦上一个可聚焦元素 |
noscroll() |
防止滚动到即将被聚焦的元素 |
wrap() |
在获取"下一个"或"上一个"时使用"循环"(例如,如果获取最后一个元素的"下一个"元素,则返回第一个元素) |
getFirst() |
获取第一个可聚焦元素 |
getLast() |
获取最后一个可聚焦元素 |
getNext() |
获取下一个可聚焦元素 |
getPrevious() |
获取上一个可聚焦元素 |
让我们看几个使用这些工具的示例。下面的示例允许用户使用方向键控制按钮组内的焦点。你可以点击一个按钮,然后使用方向键来移动焦点:
<div
@keydown.right="$focus.next()"
@keydown.left="$focus.previous()"
>
<button>First</button>
<button>Second</button>
<button>Third</button>
</div>
注意当最后一个按钮获得焦点时,按"右箭头"没有任何效果。让我们添加 .wrap() 方法使焦点"循环":
<div
@keydown.right="$focus.wrap().next()"
@keydown.left="$focus.wrap().previous()"
>
<button>First</button>
<button>Second</button>
<button>Third</button>
</div>
现在,让我们添加两个按钮,一个聚焦按钮组中的第一个元素,另一个聚焦最后一个元素:
<button @click="$focus.within($refs.buttons).first()">Focus "First"</button>
<button @click="$focus.within($refs.buttons).last()">Focus "Last"</button>
<div
x-ref="buttons"
@keydown.right="$focus.wrap().next()"
@keydown.left="$focus.wrap().previous()"
>
<button>First</button>
<button>Second</button>
<button>Third</button>
</div>
注意我们需要为每个按钮添加 .within() 方法,以便 $focus 知道将作用域限定到一个不同的元素(包裹按钮的 div)。