Давайте разрабатывать UI-элементы правильно

    Не так давно у меня кончилось терпение. По началу всякие плагины, а затем различные framework'и начинают «насиловать» HTMLDocument. Что бы понять потерял элемент фокус или нет — они отслеживают событие onclick на HTMLBodyElement или на HTMLDocument. И если некоторые из них обращают внимание на нажатие Taba при потере фокуса, то большая часть вообще игнорирует данный факт.


    Focus/Blur

    Есть 4 «волшебных» DOM события:
    • focus
    • blur
    • focusin
    • focusout

    Обычные DOMElements их не генерируют, однако, если использовать волшебный атрибут tabindex, то у нас появляется такая возможность.
    При создании своего UI элемента для jQuery пришлось столкнуться с 2 проблемами, связанные с тем, что внутри моего элемента используется HTMLInputElement (тут и далее я подразумеваю так же HTMLSelectElement и HTMLButtonElement).

    Проблема номер один — отсутствие всплывания событий focus и blur. И тут нам на помощь придут focusin и focusout.Теперь мы можем оперировать событиями фокуса на уровне родительского элемента,.
    Проблема номер два — генерация событий blur и focusout в родительском элементе, когда происходит click на дочернем HTMLInputElement(атрибут tabindex влияет лишь на получение фокуса с помощью Taba). Что в свою очередь «ломает» нам всю логику работы с разрабатываемым UI-элементом. На данный момент «приличного» решения данной проблемы я так и не нашел (отслеживание события onclick на document или body, тоже не является «приличным»)
    Первое возможное решение — это трансляция событий клавиатуры «детям» при удержании фокуса на «родителе», но на данный момент ни один из браузеров не поддерживает корректную генерацию событий этого типа (Fx поддерживает свой собственный интерфейс. IE9 и Chrome придерживаются стандарта, но у них баг при передаче кода нажатой клавиши — всегда передается ноль. Опера вообще не может генерировать события клавиатуры)
    Второе — это проверка document.activeElement при событиях потери фокуса ( и тут не без неприятностей, в Fx и Chrome activeElement становится ссылкой на новый элемент получивший фокус, только после завершения события blur).
    И так как, ни один из этих методов, не работает, то пришлось прибегнуть к «грязному хаку». Инструкции события blur выносить из общего потока, другими словами, используем setTimout.
    >>>UPD
    Оказывается, мы с разработчиками jQuery пошли одинаковым путем для решения данной проблемы(autocomplete)
    <<<<UPD


    CSS System Colors

    Почему-то, мало кто помнит(знает) про системные цвета. Дорогие разработчики, давайте уважать желание пользователя и использовать те цвета, которые он предпочитает видеть в своей ОС. Ведь не все хотят читать на белом фоне и соответственно не пользуются синим для выделения текста.

    >>>UPD
    Конечно же, если вы создаете кнопочку, для вашего стилизованного приложения, то использовать системные цвета глупо. Я же упоминаю случаи, когда эмулируется системный GUI-элемент или его вариация. Например в Sencha(бывшее ExtJS) для фона элементов используется белый цвет, вместо Window. Вот пример.
    В jQuery по умолчанию во всех UI-решениях идет оранжевый цвет. Почему они решили сделать так, для меня до сих пор остается загадкой. Они бы многим облегчили жизнь, если бы использовали системные цвета по умолчанию( Признавайтесь, кто начинал мучиться со стилями jQuery UI при прикручивании календарика? :) )
    <<<<UPD


    event.stopPropagation()

    Инструкция прерывания потока события — это самое большое зло, которое я встречал на web-страницах. Если вдруг у вас не верно работает какой то элемент, перестают работать ссылки при странных стечениях обстоятельств, то знайте — это результат присутствия данной инструкции.
    Её можно аккуратно использовать лишь там, где вы точно уверены, что без нее не обойтись. Где альтернативное решение обойдется вам слишком дорого. Но как правило, если вы использовали «это», значит в вашем решении архитектурный просчет.
    Банальный пример, перехват щелчков мыши на вашем элементе с обрыванием потока сообщений. Все — теперь все кто завязаны на получения этого события (см. на все плагины, которые «закрываются» при клике вне них) будут работать не корректно.
    Или сворачивание всплывающего окна, по клавише Escape не будет работать, если ваш элемент перехватывает нажатие клавиш и находится в фокусе.


    WAI-ARIA

    Ну и наконец — стандарт о котором я узнал после выхода IE8. Он определяет подходы к содержимому сайта и/или интернет-приложения со стороны устройства(браузера). Стандарт был разработан и преподносится как механизм позволяющий людям с ограниченными возможностями полноценно использовать ваш интернет-проект.
    Но мне кажется, что данный стандарт будет активно применяться в мобильных браузерах. Т.к. использование UI-элементов поддерживающих WAI-ARIA позволит сделать их более удобным. Например вот так выглядит раскрытый HTMLSelectElement в мобильной версии IE9:


    Согласитесь, выбирать из такого списка на мобильном телефоне гораздо удобнее и приятнее, чем из небольшого, хоть и «симпатичного», сделанного с помощью JS и HTML


    Вместо послесловия

    По большому счету, данную заметку я решил написать, что бы попросить помощи у хабросообщества с решением проблемы focus/blur. Я ищу его уже не 1 месяц, и даже подумываю обратиться либо к разработчикам браузеров для стандартизации изменения activeElement либо в w3c.


    «Это интересно».

    Наиболее правильно оформленные UI-элементы реализованы корпорацией Google в Gmail. Это кнопочки над списком писем.
    Share post

    Comments 27

      +12
      Работать надо хорошо, плохо работать не надо.
        0
        jqueryUI autocomplete не пробовали анализировать?
          0
          Пробовал.
          Но там все на много проще. К HTMLInputElement цепляется всплывающий список. Это-то сделать, достаточно просто.
          Я больше наблюдаю вот за этим wiki.jqueryui.com/w/page/12138056/Selectmenu
          этим wiki.jqueryui.com/w/page/12138055/SelectComboboxAutocomplete
          и вот этим wiki.jqueryui.com/w/page/12137757/Combobox

          Попытка создать combobox где эта проблема представлена наиболее ярко ведутся уже больше года.
          Были убиты уже рабочие прототипы интересных задумок, которые, увы, работали только с мышью, т.к. не было возможности адаптировать их под работу с фокусом.
          0
          Решил еще раз посмотреть, после вашего комментария. Похоже я перепутал. Я тогда смотрел datepicker, т.к. когда я начал разработку своего плагина autocomplete еще не существовал.

          Разработчики jQuery пришли к такому же решению, что и я
          // clicks on the menu (or a button to trigger a search) will cause a blur event
          self.closing = setTimeout(function() {
          self.close( event );
          self._change( event );
          }, 150 );


          Забавно, даже интервал у нас совпал -))
          FireFox требует задержку в 150мс что бы успело инициализироваться событие click после blur
            0
            Возникло ощущение, что вы говорите о чём-то важном, но я не могу понять, о чём.
            Можете поподробнее рассказать, с какой проблемой столкнулись разработчики этого куска кода и как этот кусок кода им помог?
              0
              Давайте рассмотрим проблему на примере автокомплита.
              Есть поле ввода и есть выпадающий список (HTMLInputElement и HTMLDivElement)
              Автокоплит должен закрываться тогда, когда он теряет фокус ввода.
              Так же, мы должны позволить пользователю выбрать вариант с помощью мышки, ткнув ею в выпадающем списке.

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

              И это еще самый простой вариант UI-элемента.
                0
                агаа, теперь понятно.
                я тоже ещё недавно писал контрол свой экспериментальный, где нужно было узнать, с какого элемента и на какой ушёл фокус.
                а, как я понял, никто не регламентирует очерёдности вызова этих событий, и единственный способ узнать — вешать focusin на document.
                так я и сделал.
                и теперь знаю, с какого на какой элемент скачет фокус.
                  0
                  :)
                  И вот тут во всю проявляется проблема event.stopPropagation()
                  Так же, проблема «засирания» HTMLDocument'a множественными обработчиками(негативно сказывается на реакции станицы на действие пользователя)
                  И самое неприятное в вашем случае, это инструкция .blur() которая не будет генерить события focusin на HTMLDocument
                  0
                  я отлавливаю mousedown, выключаю обработчик blur и через миллисекунду включаю.
            0
            Ошибок 132… Вы же их не чистите? :)
              +5
              Как только мне откроют доступ к исходникам Gmail, обязательно ими займусь :)
              0
              Не вижу особых проблем. Можно оверлею (вашему выпадающему меню или календарику) назначить tabIndex = 0 и он будет получать фокус от кликов по нему. После чего ставим 2 листенера на window — на focusin и focusout, и скрывем оверлей, если фокус покидает пределы исходного инпута или оверлея.

              Минусы: надо тестировать во всех браузерах, надо немного возиться с таймаутами, и немного извращаться: например, Опера любит дополнительно вызывать focusin на document при фокусировке в инпуте, для этого надо просто приписать костыль, также минусом является то, что оверлей тоже получает фокус и приходится нажимать Tab в 2 раза чаще. чтобы пройтись по всем полям.

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

              Правда, при этой схеме, подвох все же есть: оверлей приходится вставлять в дерево сразу после input, и это несет много скрытых багов: например, элементы управления в оверлее становятся частью формы, которой принадлежит input, что очень-очень плохо. Может нарушаться вложенность тегов (если в оверлее есть тег p и мы его, оверлей, вставляем внутрь существующего тега p, IE начинает лихорадить). Если же добавлять оверлей в дети к body, нарушается последовательность перехода табом… (((

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

              Вот еще кстати интересная задача при разработке UI: при открытии на странице всплывающего окна, надо как-то удерживать фокус внутри него (чтобы табом нельзя было уйти на страницу под ним).
                0
                После чего ставим 2 листенера на window — на focusin и focusout, и скрывем оверлей, если фокус покидает пределы исходного инпута или оверлея.

                В этом главная загвоздка. В начале топика я пишу, что именно это не верное решение.

                На счет jQuery UI — соглашусь. Только вот он очень популярный, а виджет я делаю как для себя, так и для людей.
                  0
                  У вас очень плохим языком статья написана, трудно понять в чем именно проблема, но:

                  > Проблема номер два — генерация событий blur и focusout в родительском элементе, когда происходит click на дочернем HTMLInputElement(атрибут tabindex влияет лишь на получение фокуса с помощью Taba). Что в свою очередь «ломает» нам всю логику работы с разрабатываемым UI-элементом.

                  Я в упор не вижу, в чем проблема. Вы хотите, чтобы по клику на инпут внутри виджета виджет генерировал события focusin? Не понимаю, нафига это нужно. Вы хотите игнорировать смену фокуса в пределах виджета? В любом случае, всегда можно отслеживать смену фокуса листенером на window или на верхнем элементе виджета.
                    0
                    Да, хочу игнорировать смену фокуса внутри элемента.
                    Его отслеживание на window — это плохое решение т.к. event.stopPropagation() ломает работу виджета.
                    + отрицательно сказывается сказывается на отзывчивость страницы. Каждое изменение фокуса, будет проверяться X раз, где X — количество виджетов, которые работают подобным образом
                      0
                      Неверно (по моему мнению). Во-первых, если кто-то для события focusout вызывает event.stopProragation(), он сам себе злобный буратино. Так можно любую систему разломать.

                      Во-вторых, имхо, предположение про замедление работы браузера — бред. Смотрите, сколько событий смены фокуса может нагенерировать пользователь? Если он зажмет и не будет отпускать клавишу Tab, будет генерироваться около 5 событий blur, focus, focusin и focusout в секунду. Если у нас есть 1 обработчик, наверху страницы, то вызываться он будет только один раз. 20 вызовов простой функции в секунду (если вы не пишете там ничего сложного) не замедлят работу браузера.

                      И смешно говорить про замедление, когда вы используете тяжелый jQuery и криво написанный jQuery UI. Там в одних конструкторах столько кода поналяпано, что у вас тормозить страница будет и без ваших стараний.
                        0
                        Есть две составляющие:
                        — доступность расширения
                        — комплексность

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

                        Во втором случае, я должен обеспечить безглючность плагина, автономность и не влияние его на окружение. При навешивании событий на window, HTMLDocument (и т.п.) я фактически, оставляю дыры для утечек памяти и возникновения ошибок. Т.к. DOM плагина могут удалить. К тому же, delegate (навешивание события на родительскую ноду, для отслеживания события на дочерней) зарекомендовал себя, как очень «тяжелый» метод. Не раз сталкивался, что отказ от него позволял изменить впечатление о сайте с «тормознутого» до «быстрого»
                          0
                          Наверно, в вашем случае применение jQuery UI более чем оправдано.

                          Но про обработку событий на window, мне кажется, вы ошибаетесь. Навешивание на корень документа опасно, если это событие из разряда mouseover/mouseenter/mouseout/mousemove/scroll. А как перехват кликов может тормозить браузер, если юзер в это время никуда не кликает?

                          Насчет удаления DOM — проблем нет, при удалении элементов от них перестанут поступать события. А вот навешивая обработчики на сам виджет (а не на корень), вы наоборот усугубляете проблему, так как:

                          1) мы должны в начале загрузки каждой странице (в этот момент парсятся скрипты, достраивается ДОМ, CSS, процессор сильно нагружен и лучше его не беспокоить в этот момент) на сайте вызывать медленный метод $('.someClass').plugin(), который медленно ищет на полузагрузившейся странице нужные классы и медленно-медленно цепляет к ним обработчики.

                          Фаерфокс, например, создает индекс для поиска по классам при первом вызове getElementsByClassName, и первый вызов этой ф-и будет медленным. Лучше ее вообще не вызывать. IE ниже 8 версии вообще не может найти элемент по имени класса.

                          2) Мы должны при вставке нового контента в страницу вручную вызывать плагин (и все остальные плагины, работающие по тому же принципу). Не надоест искать все места в программе, где вставляется контент и вписывать туда руками вызов всех плагинов?

                          Я могу ошибаться, но интуиция мне подсказывает, что навешивание 1 обработчика быстрее беготни по дереву, сложного поиска селекторов и навешивания множества обработчиков.
                            0
                            1) На самом деле в HTMLDocument нужно помещать уже «собранный» виджет. Где все события уже подцеплены и делать это надо после того, как подгрузится весь основной DOM.
                            $( function () {
                            var el = $( '...' ).bind('click', function() { ... };
                            $(document).append(el);
                            });

                            Так что тут все просто отлично

                            2) Вставку контента нужно оборачивать в объект(функцию) который будет знать, что подгружать и какие виджиты подключать, так что тут проблем не вижу.
                            Оверхед при использовании delegate по сравнению с навешенным событием будет минимум в 10 раз(правда на 1000 итераций 8мс или 80мс не так уж и важно, но я использовал простейший счетчик внутри, и скорее всего у многих браузеров сработала оптимизация, т.к. повышение количество итераций почти не сказалось на времени выполнения. IE вешался при навешивании delegate 10000 раз :) )
                            При этом, delegate при каждом вызове функции в родителе будет проверять event.currentTarget на совпадение с правилом — что не есть очень хорошо.
                            Повторюсь, в нескольких проектах уход от delegate творило чудеса в восприятии людьми реагирование страниц на работу с пользовательским вводом.
                  0
                  Вот еще кстати интересная задача при разработке UI: при открытии на странице всплывающего окна, надо как-то удерживать фокус внутри него (чтобы табом нельзя было уйти на страницу под ним).
                  мне кажется, что тут необходимо смотреть в сторону iframe, хотя нужно проверять
                  0
                  Я правильно понял, что проблема в том чтобы для выпадающего списка(например), при клике на элемент списка, контрол не терял бы фокус и не отрабатывал событие onblur, но при общей потере фокуса контролом, отрабатывал бы его корректно?
                    0
                    да :)
                      0
                      Мне кажется, что можно как-то извратиться с элементами label, object и всеми другими элементами, которые могут содержать другие элементы и поддерживают эти события, они же отрабатывает focus, blur и т.п. А вообще я бы попробовал поковыряться в DTD и создать свой элемент control, который объединялбы свойства div и button(последняя не передает события click внутренним элементам)
                        +1
                        * сорри за кашу, перечитал DTD
                        0
                        P.S. Видимо, браузерам совершенно по барабану DTD когда разговор доходит до поведения элементов. Может что-то не так сделал. Но, как говорится, не взлетел (
                          0
                          На сколько я знаю, считывают DTD браузеры только в случае с Xml. С html поступают проще, ищут в по стандартным, если не находят, то переключаются в Quirks mode.

                    Only users with full accounts can post comments. Log in, please.