Работа с фокусами

Система фокусов в Wasaby (UI/Focus) расширяет и дополняет нативную систему фокусов.

Основные возможности

Можно пользоваться нативной системой фокусов: нативными событиями фокусов (focus, blur, focusin, focusout), нативным методом HTML-элемента focus (вместо него рекомендуется использовать UI/Focus:focus как его более продвинутую замену).

Настройка атрибута tabindex аналогична нативной.

Клик по области фокусирует область (если не было специально настроено иначе). Таким образом можно фокусировать области кликом и начинать управление с клавиатуры по месту клика.

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

Возможность настроить зацикливание обхода по Tab директивой ws-tab-cycling.

Возможность запрета фокусировки директивой ws-no-focus.

Поддержка событий активации (activated, deactivated) на контролах, чтобы реагировать на перемещение фокуса.

Умная активация методом wasaby-контрола activate с поддержкой отключения показа экранной клавиатуры и поддержкой отключения автоподскролла к фокусируемому элементу.

Автофокусировка при загрузке страницы и переходах в SPA режиме.

Восстановление фокуса по умолчанию в динамических интерфейсах.

Настройка порядка обхода по Tab

Определение фокусируемости элемента

Элементы при обходе по Tab фокусируются согласно стандарту HTML. Фокусироваться будут:

  • элементы, реагирующие на нажатия клавиш. Такие элементы по умолчанию фокусируются и дополнительно указывать им tabindex не надо (по умолчанию браузер считает tabindex = 0).
  • <button> — выполнение действия кнопки по Enter.
  • <input> — ввод с клавиатуры.
  • <checkbox> — выставление флажка по пробелу.
  • <a> — переход по ссылке по Enter.
  • и т.п.
  • Элемент с настроенным табиндексом будет сфокусирован, потому что tabindex >= 0.
<div tabindex="0">

Остальные элементы будут пропущены.

Принцип обхода по Tab

Нажатие на Tab запускает поиск элемента, который будет сфокусирован. Поиск следующего элемента запустится от текущего элемента в пределах шаблона, в котором он расположен.

Шаблон контрола Example:

<div>
   <input name="input_1"/>
   <input name="input_2"/>
   <input name="input_3"/>
</div>

Обход по Tab в данном примере: input_1, input_2, input_3.

В обходе по Tab содержимого шаблона участвуют как элементы, так и вставленные в шаблоне контролы. Когда в процессе обхода попадается контрол, шаблон этого контрола рекурсивно обходится по такому же принципу. Когда обход контрола завершен, продолжается обход текущего шаблона.

Шаблон контрола Example:

<div>
   <input name="input_2"/>
   <input name="input_3"/>
</div>

Шаблон контрола Container:

<div>
   <input name="input_1"/>
   <Example/>
   <input name="input_4"/>
</div>

Опишем порядок обхода по Tab в данном примере:

  1. Если фокус стоит на input_1 и был нажат Tab, следующим в порядке обхода будет найден контрол Example. В рамках шаблона Container поиск приостанавливается и запускается поиск в рамках шаблона Example. В нем будет найден и сфокусирован элемент input_2.
  2. Если фокус стоит на input_2, нажатие на Tab приведет к поиску элемента в шаблоне Example. Следующим элементом будет найден input_3.
  3. Если фокус стоит на input_3, при нажатии на Tab в шаблоне Example элемента найдено не будет, поэтому поиск продолжит поиск в родительском шаблоне, и поищет элементы, расположенные в шаблоне Container после Example (его мы только что обошли). Будет найден и сфокусирован input_4.
  4. Если в шаблоне элемент будет не найден, аналогичным образом запустится поиск элемента у родительских шаблонов. Следующий за input_4 элемент будет искаться в родительском для Container шаблоне.

Аналогичным образом обходится и вставленный шаблон через конструкцию ws:partial.

Если в шаблоне контрола не найдено элементов для обхода по Tab, контрол будет пропущен.

Шаблон контрола Example:

<div>
   <span>Text</span>
</div>

Шаблон контрола Container:

<div>
   <input name="input_1"/>
   <Example/>
   <input name="input_2"/>
</div>

Если фокус стоит на input_1, при нажатии на Tab фокус переместится сразу на input_2, так как в контроле Example нечего фокусировать.

Настройка табиндексов в шаблонах

Порядок обхода осуществляется согласно настроенным табиндексам. Порядок обхода с учетом табиндексов в пределах шаблона соответствует стандарту HTML.

Шаблон контрола Example:

<div>
   <input name="input_1" tabindex="2"/>
   <input name="input_2" tabindex="-1"/>
   <input name="input_3" tabindex="1"/>
</div>

Обход по Tab в данном примере: input_3, input_1.

В основном tabindex нужно указывать, чтобы задать порядок обхода (tabindex >= 0), либо чтобы исключить элемент/контрол из обхода (tabindex == -1).

Шаблон контрола Example:

<div>
   <input name="input_2"/>
</div>

Шаблон контрола Container:

<div>
   <input name="input_1"/>
   <Example attr:tabindex="-1"/>
   <input name="input_3"/>
</div>

Обход по Tab в данном примере: input_1, input_3.

Контекстная зависимость табиндексов

Контекстная зависимость табиндексов позволяет задавать табиндексы в пределах шаблона не боясь, что их значения пересекаются со значениями других шаблонов.

Шаблон контрола Example1:

<div>
   <input name="input_2" tabindex="1"/>
   <input name="input_3" tabindex="2"/>
</div>

Шаблон контрола Example2:

<div>
   <input name="input_4" tabindex="1"/>
   <input name="input_5" tabindex="2"/>
</div>

Шаблон контрола Container:

<div>
   <input name="input_1"/>
   <Example1 attr:tabindex="1"/>
   <Example2 attr:tabindex="2"/>
   <input name="input_6"/>
</div>

Example1 и Example2 имеют изолированную настройку табиндексов в своих шаблонах. При обходе по Tab сначала будут пройдены все элементы шаблона Example1, а затем все элементы шаблона Example2. Обход будет следующим: input_2, input_3, input_4, input_5, input_1, input_6. Значение полей ввода input_1 и input_6 не указано. В таком случае, согласно стандарту html, значение табиндекса для тега input равно 0.

Зацикливание обхода по Tab

Чтобы добиться зацикливания обхода по Tab в пределах элемента, достаточно воспользоваться атрибутом ws-tab-cycling.

Шаблон контрола Example:

<div ws-tab-cycling="true">
   <input name="input_1"/>
   <input name="input_2"/>
</div>

Обход по Tab: input_1, input_2, input_1, input_2, input_1, input_2, ...

Таким же образом можно настроить контрол по месту использования.

Шаблон контрола Example:

<div>
   <input name="input_2"/>
   <input name="input_3"/>
</div>

Шаблон контрола Container:

<div>
   <input name="input_1"/>
   <Example attr:ws-tab-cycling="true"/>
   <input name="input_4"/>
</div>

Обход по Tab: input_1, input_2, input_3, input_2, input_3, input_2, input_3, ...

Настройка контролов, реагирующих на нажатия клавиш

Если в шаблоне контрола нечего фокусировать, обход по Tab проигнорирует контрол.

Необходимо обеспечить, чтобы внутри контрола что-то могло сфокусироваться, чтобы контролом можно было управлять с клавиатуры.

Шаблон контрола Controls.ImprovedList — списка, между строками которого можно перемещаться клавишами стрелок вверх и вниз

<div on:keydown="moveMarker()">
   <Controls.List items="{{items}}" name="list"/>
   <div name="fakeFocusElem" tabindex="0"></div>
</div>

Контроллер Controls.ImprovedList:

...
moveMarker: function(event) {
   if (event.nativeEvent.keyCode === Keys.Up) 
      this._children.list.moveMarker('up');
   if (event.nativeEvent.keyCode === Keys.Down)
      this._children.list.moveMarker('down');
}

При обходе по Tab в контроле Controls.ImprovedList будет найден и сфокусирован элемент fakeFocusElem. Нажатия клавиш начнут перехватываться обработчиком moveMarker.

Запуск активации контрола

Прежде чем говорить об активации, сначала нужно ввести само понятие активности.

Определение понятия "физический родитель контрола"

Контрол_1 является физическим родителем по отношению к Контролу_2, если Контрол_2 вставляется в шаблоне Контрола_1.

Шаблон контрола Example:

<Controls.buttons:Button name="Add"/>

Физическим родителем контрола Add будет контрол Example.

В данном случае рассматривается именно использование контрола внутри другого, а не объявление.

Шаблон контрола Example:

<Controls.scroll:Container name="scrollContainer">
   <ws:content>
      <Controls.list:View name="listView"/>
   </ws:content>
</Controls.scroll:Container>

Шаблон контрола Controls/scroll:Container:

...
<ws:partial template="{{ _options.content }}"/>
...

В данном случае listView объявляется в контроле Example, но используется в scrollContainer. Физическим родителем listView будет scrollContainer, физическим родителем scrollContainer будет Example.

Определение понятия "опенер контрола" (opener)

Контрол_1 является опенером по отношению к Контролу_2, если для Контрола_2 опция opener равняется Контролу_1.

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

Таким образом, опенеры корректируют структуру контролов так, что она перестает учитывать только DOM-дерево, но также начинает учитывать логическую связь между контролами.

Обычно логической связью соединяется попап и открывающий его контрол.

Определение понятия "активность" контрола

Контрол активен, если выполняется одно из условий:

  1. Один из элементов в его шаблоне — в фокусе.
  2. Он является физическим родителем контрола, который активен и у которого нет опенера.
  3. Он является опенером контрола, который активен.

То есть если элемент контрола 1-1-1 будет в фокусе, активными будут:

  • контрол 1-1-1 (по 1 признаку)
  • контрол 1-1-2 (по 3 признаку)
  • контрол 1-1 (по 2 признаку)
  • контрол 1-2 (по 3 признаку)
  • контрол 1 (по 2 признаку)

Метод wasaby-контрола activate для запуска активации

Иногда возникает необходимость перевести фокус программно. Программный перевод фокуса затрудняется поиском элемента, который должен принять на себя фокус. Метод activate предоставляет высокоуровневый способ найти и сфокусировать элемент внутри выбранного контрола. Метод является частью API базового контрола UI/Base:Control.

Метод activate предполагает, что контрол уже был построен. Так что на этапе _beforeMount звать метод нельзя.

Автофокусировка отобразившейся области

В рамках системы фокусов метод wasaby-контрола activate зовется только для запуска автофокусировки.

Автофокусировка запускается:

  • При загрузке страницы — вызов activate для корневого контрола страницы.
  • При переходе на страницу в SPA-режиме — вызов activate для корневого контрола страницы.
  • При открытии попапа — вызов activate для попапа.

Алгоритм активации

Алгоритм сначала определяет, какие контролы помечены атрибутом ws-autofocus. Пытается рекурсивно выполнить метод wasaby-контрола activate на этих контролах.

В случае, когда контрол с ws-autofocus не найден, происходит поиск элемента с учетом контекстной зависимости табиндексов. Если элемент получается найти, он фокусируется и функция возвращает значение true.

Если элемент не получается найти — производится попытка сфокусировать хотя бы корневой элемент области, для который был вызван метод wasaby-контрола activate. Если и корневой элемент не получается сфокусировать, activate возвращает значение false.

ws-autofocus следует использовать только для корректировки поиска элемента при автофокусировке. ws-autofocus ищется в шаблоне контрола и во всех дочерних контролах тоже. То есть достаточно указать ws-autofocus только контролу, в который должен уходить фокус при автофокусировке.

Шаблон контрола Document:

<html>
   <head>...</head>
   <body>
      <MyControls.Container/>
   </body>
</html>

Шаблон контрола MyControls.Container:

<div>
   <MyControls.Accordeon/>
   <MyControls.Header/>
   <MyControls.Browser/>
   <MyControls.Footer/>
</div>

Шаблон контрола MyControls.Browser:

<div>
   <MyControls.Button on:click="createElement()"/>
   <MyControls.Search attr:ws-autofocus="true"/>
   <MyControls.List/>
</div>

Таким образом MyControls.Search будет фокусироваться при загрузке страницы.

API метода activate

control.activate(config), где config — необязательный аргумент. Это объект, который может содержать параметры:

  • enableScreenKeyboard — в случае значения true, на мобильных устройствах разрешается фокусировка полей ввода и соответствующее отображение экранной клавиатуры. в случае значения false, на мобильных устройствах запрещается фокусировка полей ввода (фокусироваться будет контейнер). По умолчанию значение равно false.
  • enableScrollToElement — в случае значения true, если фокусируемый элемент находится за пределами видимой области, после фокусировки происходит подскролл к элементу, чтобы он стал видимым. в случае значения false, подскролл к элементу отключен. По умолчанию значение равно false.

Так как enableScreenKeyboard по умолчанию false, при автофокусировке на мобильных устройствах поля ввода фокусироваться не будут и экранная клавиатура не покажется. Подскролла к фокусируемому элементу так же не будет, так что при перезагрузке страницы состояние скролла страницы сохранится.

API метода UI/Focus:focus

Метод UI/Focus:focus — замена нативному фокусу. API аналогично методу wasaby-контролов activate.

Рекомендуется использовать его вместо нативного фокуса.

  • Это удобная точка отладки, чтобы отслеживать переход фокуса.
  • Метод предоставляет различные возможности
    • Возможность отключения подскролла
    • Возможность отключения показа экранной клавиатуры
    • Проверка на то, что фокус перевелся
    • Фокусировка SVG
    • Возможность исправлять другие возможные проблемы нативного фокуса

События активности

Чтобы можно было отреагировать на изменение активности контрола, предоставляется 2 события:

  • activated (контрол активирован)
  • deactivated (контрол деактивирован)

На эти события можно подписаться только у контролов, так как только контрол обладает состоянием активности.

Вычисление активности контролов вслед за изменением фокуса

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

При переводе фокуса с элемента input 1-1-1 на элемент input 1-2-1 будут вызваны события фокусов и события активности. Сначала сработают нативные события, а потом события активности.

  1. Событие потери фокуса focusout сработает на элементах:
    • input 1-1-1
    • div 1-1
    • div 1
  2. Событие получения фокуса focusin сработает на элементах:
    • input 1-2-1
    • div 1-2
    • div 1

У контрола 1 события активности deactivated и activated вызваны не будут, потому что он не деактивировался и не активировался. Он как был активирован, так и остался активирован (по 2 признаку). Активация переходила среди его дочерних элементов.

  1. Событие deactivated сработает:
    • на контроле 1-1-1
    • контроле 1-1
  2. Событие activated сработает:
    • на контроле 1-2-1
    • контроле 1-2

Событие activated

Если контрол становится активным, на нем срабатывает событие activated. Событие также будет содержать дополнительную информацию:

  • isTabPressed — значение true означает, что активность перешла переходом по Tab, значение false означает, что активность перешла по клику.
  • isShiftKey — значение true означает, что активность перешла переходом по Tab в обратном порядке (с использованием клавиши shift).

В качестве примера очистим значение поля ввода, если оно было активировано переходом по Tab.

Шаблон контрола Example:

<Controls.input:Number bind:value="{{value}}" on:activated="clearValue()"/>

Контроллер шаблона Example:

...
clearValue: function(event, config) {
   if (config.isTabPressed) {
      this.value = '';
   }
}

Событие deactivated

Если контрол становится неактивным, на нем срабатывает событие deactivated. Событие также будет содержать дополнительную информацию:

  • isTabPressed — значение true означает, что активность перешла переходом по Tab, значение false означает, что активность перешла по клику.
  • isShiftKey — значение true означает, что активность перешла переходом по Tab в обратном порядке (с использованием клавиши shift).

В качестве примера провалидируем введенное значение в поле ввода при деактивации.

Шаблон контрола Example:

<Controls.input:Number bind:value="{{value}}" on:deactivated="validate()"/>

Контроллер шаблона Example:

...
validate: function(event, config) {
   if (!this.value) {
      this.showWarning('Необходимо ввести значение');
   }
}

Особенности фокусировки по клику

Клик по области фокусирует область. Таким образом можно фокусировать области кликом и начинать управление с клавиатуры по месту клика.

Атрибут ws-no-focus не дает клику перевести фокус на элемент, оставляя фокус там, где он находится. При этом обработчик клика выполняется. Так как элемент при клике не будет сфокусирован, соответственно события activated и deactivated не будут вызваны. Управление с клавиатуры будет осуществляться в контексте элемента, который сфокусирован.

Восстановление фокуса

Потеря фокуса происходит, если сфокусированный элемент неожиданно скрывается или удаляется из DOM. В таком случае фокус перемещается в body и возможность управления с клавиатуры теряется. Чтобы предотвратить это, реализовано восстановление фокуса.

По умолчанию фокус восстанавливается ближайшему родителю, способному принять фокус. Поиск контрола осуществляется среди активных контролов, то есть с учетом опенеров. У найденного контрола фокусируется корневой элемент.

Шаблон контрола Example:

<div>
   <input name="input_2"/>
   <button on:click="close()"/>
</div>

Контроллер Example:

...
close: function() {
   this.isVisible = false;
   this._notify('isVisibleChanged', [false]);
}

Шаблон контрола Container:

<div>
   <input name="input_1"/>
   <ws:if data="{{ isVisible }}">
      <Example bind:isVisible="isVisible"/>
   </ws:if>
</div>

Если фокус находится на input_2 и пользователь кликает на кнопку закрытия, контрол Example пропадает. Механизм восстановления фокуса переводит фокуса на корневой элемент контрола Container<div>. При этом события activated и deactivated не срабатывают. Таким образом контекст фокуса сохранился и можно продолжать управление с клавиатуры.

Если такой способ восстановления не устраивает — программист, чей контрол своими действиями повлек потерю фокуса, может восстановить фокус самостоятельно туда, куда ему нужно. Для этого он может воспользоваться методом wasaby-контрола activate или нативным методом HTML-элемента focus.