Перейти к содержимому

Focus

Плагин Focus позволяет управлять фокусом на странице.

Установка

Вы можете использовать этот плагин, включив его из тега <script> или установив с помощью менеджера пакетов.

Через CDN

Вы можете подключить CDN-сборку этого плагина с помощью тега <script>, только подключать нужно ДО основного JS-файла Alpine.

<!-- Focus Plugin -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/focus@3/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>

Через менеджер пакетов

Вы можете установить Focus для использования внутри вашей сборки следующим образом:

Окно терминала
npm i @alpinejs/focus

Затем инициализируйте его в своей сборке:

import Alpine from 'alpinejs';
import focus from '@alpinejs/focus';
Alpine.plugin(focus);
Alpine.start();

x-trap

Focus предлагает специальный API для захвата фокуса внутри элемента: директива x-trap.

x-trap принимает выражение в формате JS. Если результат этого выражения равен true, то фокус будет удерживаться внутри этого элемента до тех пор, пока выражение не станет false, после чего фокус будет возвращён туда, где он находился ранее.

Например:

<div x-data="{ open: false }">
<div :class="open && 'opacity-50'">
<button x-on:click="open = true">Открыть диалог</button>
</div>
<div x-show="open" x-trap="open" class="mt-4 space-y-4 p-4 border bg-gray-600" x-on:keyup.escape.window="open = false">
<strong>
<div>Фокус теперь «заперт» внутри этого диалогового окна, то есть вы можете щёлкать/фокусировать элементы только внутри этого диалогового окна. Если вы нажмете Tab несколько раз, фокус останется в этом диалоговом окне.</div>
</strong>
<div>
<input type="text" placeholder="Какой-то ввод...">
</div>
<div>
<input type="text" placeholder="Ещё один ввод...">
</div>
<div>
<button x-on:click="open = false">Закрыть диалог</button>
</div>
</div>
</div>
Фокус теперь «заперт» внутри этого диалогового окна, то есть вы можете щёлкать/фокусировать элементы только внутри этого диалогового окна. Если вы нажмете Tab несколько раз, фокус останется в этом диалоговом окне.

Вложение диалогов

Иногда возникает необходимость вложить один диалог в другой. x-trap делает это тривиальным и обрабатывает автоматически.

x-trap ведет учет новых «пойманных» элементов и сохраняет последний активно сфокусированный элемент. После того, как элемент будет «отловлен», фокус будет возвращён в исходное положение.

Этот механизм является рекурсивным, поэтому можно бесконечно долго удерживать фокус внутри уже захваченного элемента, а затем «разворачивать» каждый элемент последовательно.

Вот вложение в действии:

<div x-data="{ open: false }">
<div :class="open && 'opacity-50'">
<button x-on:click="open = true">Открыть диалог</button>
</div>
<div x-show="open" x-trap="open" class="mt-4 space-y-4 p-4 border bg-yellow-100" x-on:keyup.escape.window="open = false">
<div>
<input type="text" placeholder="Какой-то ввод...">
</div>
<div>
<input type="text" placeholder="Ещё один ввод...">
</div>
<div x-data="{ open: false }">
<div :class="open && 'opacity-50'">
<button x-on:click="open = true">Открыть вложенный диалог</button>
</div>
<div x-show="open" x-trap="open" class="mt-4 space-y-4 p-4 border border-gray-500 bg-yellow-200" x-on:keyup.escape.window="open = false">
<strong>
<div>Фокус теперь «заперт» внутри этого вложенного диалога. Вы не можете сфокусировать что-либо внутри внешнего диалога, пока он открыт. Если вы закроете это диалоговое окно, фокус будет возвращён последнему известному активному элементу.</div>
</strong>
<div>
<input type="text" placeholder="Какой-то ввод...">
</div>
<div>
<input type="text" placeholder="Ещё один ввод...">
</div>
<div>
<button x-on:click="open = false">Закрыть вложенный диалог</button>
</div>
</div>
</div>
<div>
<button x-on:click="open = false">Закрыть диалог</button>
</div>
</div>
</div>
Фокус теперь «заперт» внутри этого вложенного диалога. Вы не можете сфокусировать что-либо внутри внешнего диалога, пока он открыт. Если вы закроете это диалоговое окно, фокус будет возвращён последнему известному активному элементу.

Модификаторы

.inert

При создании таких вещей, как диалоговые окна/модальные окна, рекомендуется скрывать все остальные элементы на странице от программ чтения с экрана при захвате фокуса.

Если добавить .inert к x-trap, то при перехвате фокуса все остальные элементы получат атрибуты 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 x-on:click="open = true">Открыть диалог</button>
<div x-show="open" x-trap.noscroll="open" class="border mt-4 p-4">
<div class="mb-4 text-bold">Содержимое</div>
<p class="mb-4 text-gray-600 text-sm">Обратите внимание, что при открытом диалоговом окне прокрутка на этой странице больше не производится.</p>
<button x-on:click="open = false">Закрыть диалог</button>
</div>
</div>
Содержимое

Обратите внимание, что при открытом диалоговом окне прокрутка на этой странице больше не производится.

.noreturn

Иногда не требуется возвращать фокус на прежнее место. Рассмотрим выпадающий список, который срабатывает при фокусировке на поле ввода, возврат фокуса на поле ввода при закрытии просто вызовет повторное открытие выпадающего списка.

x-trap позволяет отключить это поведение с помощью модификатора .noreturn.

После добавления .noreturn Alpine не будет возвращать фокус при установке x-trap на false.

Например:

<div
x-data="{ open: false }"
x-trap.noreturn="open"
x-on:click.outside="open = false"
x-on:keyup.escape.prevent.stop="open = false"
>
<input type="search" placeholder="найти что-нибудь"
x-on:focus="open = true"
x-on:keyup.escape.prevent="$el.blur()"
/>
<div x-show="open">
<div class="mb-4 text-bold">Результаты поиска</div>
<p class="mb-4 text-gray-600 text-sm">Обратите внимание, что при закрытии этого выпадающего списка фокус не возвращается на поле ввода.</p>
<button x-on:click="open = false">Закрыть диалог</button>
</div>
</div>
Результаты поиска

Обратите внимание, что при закрытии этого выпадающего списка фокус не возвращается на поле ввода.

.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()При получении «next» или «previous» используйте «wrap around» (например, возврат первого элемента, если получен «next» элемент последнего элемента)
getFirst()Получение первого фокусируемого элемента
getLast()Получение последнего фокусируемого элемента
getNext()Получение следующего фокусируемого элемента
getPrevious()Получение предыдущего фокусируемого элемента

Рассмотрим несколько примеров использования этих утилит. Приведённый ниже пример позволяет пользователю управлять фокусом внутри группы кнопок с помощью клавиш со стрелками. Это можно проверить, щелкнув на кнопке, а затем используя клавиши со стрелками для перемещения фокуса:

<div>
<div
x-data
x-on:keydown.right="$focus.next()"
x-on:keydown.left="$focus.previous()"
>
<button>Первая</button>
<button>Вторая</button>
<button>Третья</button>
</div>
(Щёлкните на кнопке, затем используйте клавиши со стрелками для перемещения влево и вправо)
</div>
(Щёлкните на кнопке, затем используйте клавиши со стрелками для перемещения влево и вправо)

Обратите внимание, что при фокусировке последней кнопки, нажатие «стрелки вправо» ничего не сделает. Добавим метод .wrap(), чтобы фокус «оборачивался»:

<div>
<div
x-data
x-on:keydown.right="$focus.wrap().next()"
x-on:keydown.left="$focus.wrap().previous()"
>
<button>Первая</button>
<button>Вторая</button>
<button>Третья</button>
</div>
(Щёлкните на кнопке, затем используйте клавиши со стрелками для перемещения влево и вправо)
</div>
(Щёлкните на кнопке, затем используйте клавиши со стрелками для перемещения влево и вправо)

Теперь добавим две кнопки, одна из которых будет фокусироваться на первом элементе группы кнопок, а другая — на последнем:

<div x-data>
<input type="button" x-on:click="$focus.within($refs.buttons).first()" value="Передать фокус «первому» элементу">
<input type="button" class="mt-2" x-on:click="$focus.within($refs.buttons).last()" value="Передать фокус «последнему» элементу">
<div
x-ref="buttons"
x-on:keydown.right="$focus.wrap().next()"
x-on:keydown.left="$focus.wrap().previous()"
>
<button>Первая</button>
<button>Вторая</button>
<button>Третья</button>
</div>
</div>

Обратите внимание, что для каждой кнопки необходимо добавить метод .within(), чтобы $focus знал, что нужно обращаться к другому элементу (div, оборачивающему кнопки).

Проверка знаний

  1. Как отключить автоматическую фокусировку элемента при захвате фокуса?

  2. Как передать фокус следующему фокусируемому элементу?