Поддержка Touch в JavaScript

    image


    Какие проблемы могут быть у frontend-программиста, если тестировщик запустит его приложение на iPad с новой трекпад-клавиатурой, Windows-планшете, с неопределенным состоянием “режима планшета” или ноутбуке с подключенным к нему телевизором c поддержкой Multi-touch?


    Это далеко не полный список допустимых конфигураций оборудования, которые мы поддерживаем при разработке системы СБИС. Сегодня СБИС — это не только знакомое многим решение для сдачи отчетности, ведения электронного документооборота и бухгалтерии, но и набор инструментов для автоматизации розницы, общепита, доставки и логистики. В этих сферах нужно уметь хорошо работать на самых разных планшетах и гаджетах с различными экранами и типами устройств ввода. И далеко не всегда проблемы могут быть связаны с экзотическим сочетанием настроек операционных систем и драйверов: если взять обычный iPad с браузером Safari, Android планшет или ноутбук-трансформер на Windows10 с последней версией Google Chrome — везде будет свой набор ошибок и особенностей обработки пользовательского ввода.


    Эта статья о том, как, а главное, зачем вводить в обычных Web приложениях режим поддержки Touch.


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


    Соответственно на уровне приложения необходимо предусмотреть:


    • возможность работы без активации CSS селекторов с псевдоклассом :hover
    • адаптировать интерфейс под условия уменьшающегося viewport из-за показа клавиатуры

    Мы разрабатываем собственную библиотеку визуальных компонентов, поэтому стараемся решать все проблемы по максимуму на уровне фреймворка.


    Определяем момент активации Touch-режима


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


    Это означает, что заведомо мы никогда не знаем в каком режиме используется устройство.
    Еще пример: в наших переговорках к ноутбукам подключены телевизоры с поддержкой Multi-touch. Формально браузер на ноутбуке считает, что его устройство поддерживает Touch-события. Именно на этой конфигурации оборудования мы обнаружили проблему в нашем первом алгоритме определения Touch-режима по наличию поддержки специализированных событий. Несмотря на то, что телевизор закреплен на стене, а на ноутбуке мы работаем в обычном режиме с подключенной мышью, приложение переключилось в режим, адаптированный для работы на планшете.


    Сегодня мы определяем Touch-режим путем анализа реакции на события touchstart и mousemove: если вызову mousemove не предшествовал touchstart, значит пользователь ведет мышкой, иначе нажал пальцем.


    Исходный код примерно такой


        touchstartHandler() {
           this._isTouch = null;
        }
        mousemoveHandler() {
           if (this._isTouch === null) {
              this.updateState(true);
           } else if (this._isTouch) {
              this.updateState(false);
           }
        }
        updateState(state) {
           this._isTouch = state;
           this._triggerEvent();
        }

    В результате генерируется событие и мы сохраняем значение режима в объекте с информацией о текущем окружении, а также на body выставляем классы is-touch или is-hover.


    Особенности поддержки :hover


    В нашем приложении мы полностью отказываемся от поддержки :hover при работе в Touch режиме. Все стили с поддержкой :hover выглядят примерно так:


       body.is-hover .my-button:hover {
       }

    Этот способ работает надежнее стандартного @media (hover: hover), который не отключает поддержку hover свойства в Chrome и Firefox при управлении пальцами.


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


    Следует обратить особое внимание на использование этого псевдокласса для управления видимостью блока.


    image


    Наши компоненты списков, таблиц и деревьев используют :hover-селекторы для показа операций над строкой при наведении (например редактировать, удалить и т.п.).


    Подобное применение :hover на iOS приведет к тому, что первый клик по строке только изменит видимость элементов управления, поэтому пользователю придется делать второй клик. А вот на Android и Windows операции над строкой никто никогда не увидит.


    При переходе в Touch-режим в списочных компонентах отключается поддержка :hover и включается поддержка управления жестами, например swipe или долгое нажатие.


    image


    Swipe-жесты мы обрабатываем самостоятельно по событию mousemove в touch-режиме при следующих условиях:


    • минимальное расстояние перемещения по горизонтали не менее 50px;
    • максимальное отклонение по вертикали не более 25px;
    • продолжительность жеста не дольше 600мс;
    • жест не регистрируется в диапазоне 25px от границы экрана, т.к. браузер обрабатывает этот событие как смену навигации.

    Особенности при работе с виртуальной клавиатурой


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


    Иногда мы сами регистрируем ошибки:
    Ссылка 1
    Ссылка 2
    Иногда находим на уже зарегистрированные:
    Ссылка 1
    Ссылка 2


    В демо-примерах проблема может выглядеть безобидной, но в реально работающем приложении все скачет в разные стороны и по несколько раз. Поэтому, при наличии возможности, мы стараемся размещать поля ввода выше предполагаемой границы виртуальной клавиатуры. Это позволяет сгладить эффект от ряда существующих ошибок и минимизировать риски при появлении новых при обновлении браузера. В идеале уложиться в 300px по высоте, чтобы обеспечить комфортную работу даже на маленьких POS-терминалах.


    image


    Самый простой пример: как не нужно размещать поля логин и пароль на форме авторизации


    image


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


    image


    На первый взгляд задача выглядит не сложной, но до недавнего времени она не имела 100% работающего решения:


    Во-первых, API браузера не предоставлял информации о том, показана виртуальная клавиатура или нет. Нам приходилось отслеживать приход и уход фокуса для каждого поля ввода. При этом клавиатуру можно просто скрыть и на состояние фокусировки это никак не повлияет.


    Во-вторых, никто не знал реальный размер клавиатуры. Мы подобрали коэффициенты в зависимости от ориентации экрана: 0.3 для портретной и 0.55 для ландшафтной.


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


    image


    К счастью, сегодня все современные браузеры получили поддержку VisualViewport. Прошло целых 12 лет с момента релиза iOS, прежде чем разработчики смогли получить информацию об изменении размера viewport при показе виртуальной клавиатуры.


    Новое API позволяет решить сразу 2 проблемы:


    • стреляет событием resize, когда изменяется размер рабочей области, в том числе при показе и скрытии клавиатуры;
    • получает правильный размер видимой области.

    День отказа от поддержки iOS12 станет нашим большим праздником, т.к. мы сможем окончательной перейти на поддержку VisualViewport, убрав из кода всю магию.


    Не злоупотребляйте ручной фокусировкой


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


    Подобные ошибки периодически встречаются во всех крупных веб-сервисах. Я лично видел проблемы на iPad в разное время и в Google при поиске картинок, когда клавиатура для поля поиска скрывалась сразу после показа и сама же открывалась обратно, и в Яндекс Маркете, когда клавиатура исчезала во время ввода текста в поле поиска.


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


    Но если часть ресурсов грузиться по требованию, или после действия пользователя необходимо совершить асинхронный запрос за данными, то проблема становится актуальной. В таком случае, например, после перехода по навигации в SPA-приложении или после открытии диалога, нельзя устанавливать фокус в поле ввода на iOS.


    Так у каждого нашего компонента есть метод activate для установки фокуса. По умолчанию активация не устанавливает фокус в поля ввода на iOS-устройствах. При этом можно передать параметр enableScreenKeyboard: true для показа клавиатуры. В этом случае программист должен обеспечить синхронный вызов метода в обработчике пользовательского ввода.


    Touch — это просто


    Таким образом, в современных браузерах с помощью нехитрого набора приемов достаточно легко можно организовать поддержку Touch-режима. Это те самые 20% усилий, которые помогут добиться 80% результата: у большинства пользователей приложение будет предсказуемо работать на их конфигурациях оборудования.


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


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


    Если эта тема будет интересна, я готов продолжить описывать их в следующей статье.

    Тензор
    Разработчик системы СБИС

    Комментарии 4

      0
      если тестировщик запустит его приложение на iPad с новой трекпад-клавиатурой, Windows-планшете, с неопределенным состоянием “режима планшета” или ноутбуке с подключенным к нему телевизором c поддержкой Multi-touch?


      Или на сенсорном 24" мониторе Dell P2418HT (210-AKBD).

        +3
        Сегодня мы определяем Touch-режим путем анализа реакции на события touchstart и mousemove:

        Современные браузеры поддерживают Pointer Events API, где event.pointerType принимает значения 'mouse', 'pen' или 'touch'. Рассматривали-ли вы такой вариант?
          0
          Отличная идея, спасибо. Судя по caniuse, в iOS появилось только в прошлом году в 13 версии. Теперь можно будет перейти на этот вариант — он проще. Дождемся только отказа от поддержки iOS12.
          0
          Немного ностальгического оффтопа

          Приятно видеть, что помимо Боровикова с его интересными, но порой слишком сложными и нерелевантными для меня материалами по PostgreSQL, на Хабр понемногу приходят и другие разработчики Тензора.
          По опыту знаю, что тензоровским фронтенд-специалистам есть чем поделиться с сообществом, так что ждём новых публикаций. :)

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое