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

В этой статье давайте рассмотрим директиву 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() по этому событию, фокус останется там, где он есть.