跳转至

注意:此插件之前名为"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>
Focus is now "trapped" inside this dialog, meaning you can only click/focus elements within this yellow dialog. If you press tab repeatedly, the focus will stay within this dialog.

嵌套对话框

有时你可能需要将一个对话框嵌套在另一个对话框内。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>
Focus is now "trapped" inside this nested dialog. You cannot focus anything inside the outer dialog while this is open. If you close this dialog, focus will be returned to the last known active element.

修饰符

.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>
Dialog Contents

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>
Search results

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>
(Click a button, then use the arrow keys to move left and right)

注意当最后一个按钮获得焦点时,按"右箭头"没有任何效果。让我们添加 .wrap() 方法使焦点"循环":

<div
    @keydown.right="$focus.wrap().next()"
    @keydown.left="$focus.wrap().previous()"
>
    <button>First</button>
    <button>Second</button>
    <button>Third</button>
</div>
(Click a button, then use the arrow keys to move left and right)

现在,让我们添加两个按钮,一个聚焦按钮组中的第一个元素,另一个聚焦最后一个元素:

<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)。