Часто необходимо знать, с какой областью страницы взаимодействует пользователь. Например, если вы создаете раскрывающийся список, вам нужно знать, когда его закрывать. Наивная реализация заключалась бы в том, чтобы просто слушать клики и проверять, произошло ли это за пределами раскрывающегося элемента. Но мышь — не единственный способ взаимодействия. Человек может использовать клавиатуру для навигации по фокусируемым элементам. А раскрывающийся список может иметь вложенное многоуровневое меню, что затрудняет проверку цели простым щелчком мыши.

В этой статье давайте рассмотрим директиву ActiveZone — подход, который мы использовали в библиотеке компонентов Angular Taiga UI. В нем используются две мои любимые функции Angular: внедрение зависимостей и RxJS. Нам также потребуется глубокое понимание собственных событий DOM. Каким бы абстрактным ни был Angular от простого JavaScript, он по-прежнему опирается на старые добрые веб-API, поэтому важно расширять свои знания о ванильном интерфейсе.
Описание задачи
Базовым примером использования может быть кнопка с выпадающим меню:
<button
[dropdownOpen]="open"
[dropdownContent]="template"
(activeZoneChange)="onActiveZone($event)"
(click)="onClick()"
>
Show menu
<ng-template #template>
<some-menu-component activeZone></some-menu-component>
</ng-template>
</button>
Предположим, меню отображается на портале и не является прямым потомком button. Мы хотим открывать и закрывать меню щелчком по button. А если пользователь щелкнет мышью или переместит фокус с помощью клавиши tab — раскрывающийся список должен закрыться.
Для отслеживания взаимодействия с пользователем мы будем следить главным образом за следующими событиями: focusin, focusout и mousedown.
focus/blurсобытия не всплывают, в отличие отfocusin/focusout
Навигация с помощью клавиатуры может осуществляться только по фокусируемым элементам, тогда как mousedown может быть вызвана, например, выбором пользователем некоторого текста в раскрывающемся списке или взаимодействием с нефокусируемыми элементами на странице. Таким образом, эти три события охватывают почти все, что нас волнует. Но есть несколько сложных случаев, которые мы рассмотрим в дальнейшем.
Потеря фокуса происходит после события
mousedownкак поведение по умолчанию. Если вы вызоветеpreventDefault()по этому событию, фокус останется там, где он есть.