В этой статье рассказывается о внедрении в Chromium/Blink новой фичи. А именно — псевдокласса :focus-within из спецификации Selectors 4. Также поговорим о разных вещах, с которыми приходится сталкиваться при разработке.

Псевдокласс :focus-within


Это новый селектор, позволяющий модифицировать стиль элемента при фокусировке на этом элементе или любом из его дочерних элементов (descendant). Это аналогично действию селектора :focus, только применяется и по отношению к родительским элементам, так что работает примерно как :active и :hover.

Всё будет понятно из примера:

<style>
  form:focus-within {
    background-color: green;
  }
</style>
<form>
  <input />
</form>

Когда пользователь выбирает форму ввода, её фон становится зелёным.

Намерение поставки


Хотя спецификация всё ещё в состоянии Editor’s Draft (редакторский черновик), она уже реализована в Firefox 52 и Safari 10.1, так что она хороший кандидат и на добавление в Chromium.

Для этого вам нужно отправить письмо о намерении в blink-dev. Задача казалась простой и лёгкой, и после небольшого раздумья я отправил письмо Intent to Implement and Ship: CSS Selectors Level 4: :focus-within pseudo-class (Намерение о внедрении: CSS Selectors Level 4: псевдокласс :focus-within).

Но тут возникло первое затруднение…

Проблемы со спецификацией


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

В моём случае удалось быстро выявить проблему с текстом спецификации, связанную с использованием этого селектора (а также :active и :hover) с Shadow DOM. Старый текст спецификации гласит:

Элемент также соответствует (matches) :focus-within, если один из его дочерних элементов, включающий в себя Shadow, соответствует :focus.

Похоже, спецификация готова в отношении Shadow DOM, но в ней есть ошибка. Это может быть не так просто понять, но, если интересно, взгляните на пример:

<div id="shadowHost">
  <input />
</div>
<script>
  shadowHost.attachShadow({ mode: "open"}).innerHTML =
    "<style>" +
    "  #shadowDiv:focus-within { border: thick solid green; }" +
    "</style>" +
    "<div id='shadowDiv'>" +
    "  <slot></slot>" +
    "</div>";
</script>

Если не поняли: элемент input вставляется в тег slot (быстрое и упрощённое объяснение этого конкретного примера с Shadow DOM).

В этом примере flat tree будет выглядеть так:

<div id="shadowHost">
  #shadow-root
  <div id="shadowDiv">
    <slot>
      <input />
    </slot>
  </div>
</div>

Проблема в том, что когда фокусируешься на input, который сейчас внутри slot, то ожидаешь, что рамка вокруг shadowDiv станет зелёной. Однако input не является дочерним элементом shadowDiv, включающим в себя Shadow. Вместо этого в спецификации должно говориться о дочерних элементах flat tree.

О проблеме было сообщено в GitHub-репозитории CSS WG, в спецификации теперь говорится:

Элемент также соответствует (matches) :focus-within, если один из его дочерних элементов в flat tree (включающий неэлементные узлы (non-element nodes) вроде текстовых) соответствует условиям соответствия :focus.

Внедрение :focus-within


После решения проблемы со спецификацией намерение было одобрено. Моему внедрению дали зелёный свет.

Патч, добавляющий поддержку фичи, в основном состоит из шаблонного кода (boilerplate code), необходимого для добавления в Blink нового селектора. По большей части он делает всё то же самое, что и :focus, но затем идёт интересная часть: циклический проход по дочерним элементам с помощью flat tree:

for (ContainerNode* node = this; node;
     node = FlatTreeTraversal::Parent(*node)) {
  node->SetHasFocusWithin(received);
  node->FocusWithinStateChanged();
}

Что насчёт тестов?


Конечно, любые изменения в Blink нужно протестировать. Мне повезло, в репозитории W3C Web Platform Tests (WPT) уже было несколько тестов для этого нового селектора.

Я импортировал тесты (не без проблем) в Blink и проверил, что мой патч их проходит (включая тесты Mozilla, которые уже апстримлены). Также я прошерстил тесты в репозитории WebKit, поскольку они уже внедрили эту фичу, и апстримил один из них, проверявший некоторые интересные комбинации. Наконец, я написал ещё несколько тестов для покрытия дополнительных ситуаций (вроде описанной выше проблемы со спецификацией).

Фокус и display:none


Во время code review мне помогли найти ещё один спорный момент. Что произойдёт с выбранным элементом, когда он помечен как display: none? На первый взгляд кажется, что фокусировка должна быть снята, и это действительно так (в спецификации HTML есть правило, описывающее такую ситуацию).

Но здесь идёт речь о проблеме совместимости, потому что это правило в Blink соблюдается только движком. Применительно к остальным браузерам опубликованы отчёты о багах, то есть о проблеме, судя по всему, известно. Однако она пока никак не решена. Вот один из отчётов: Chromium bug #491828.

Если воспользоваться селектором :focus для изменения, например, цвета фона в input, то не особенно важно, что происходит, когда этот input получает display: none и исчезает. Какая разница, что делается с фоном того, что вы больше не видите. Но в случае с focus-within эта проблема более важна. Представьте, что вы изменили цвет фона в форме, когда выбрано какое-то из полей ввода. Если оно помечено как display: none, то не будет фокусировки ни на одном из полей формы, а цвет фона должен быть изменён. Но сейчас это происходит только в Chromium.

Стратегия работы с родительским элементом


Изначально патч с поддержкой :focus-within внедрили в Chrome 59, но пометили флагом как экспериментальный. Основная причина: его ещё нужно доработать, чтобы он был включён по умолчанию.

Одна из доработок была связана с повторными вычислениями стилей (style recalculations). Начальная реализация приводила к избыточному количеству вычислений.

Возьмём новый пример:

<style>
  *:focus-within {
    background-color: green;
  }
</style>
<form>
  <ul>
    <li id="li1"><input id="input1" /></li>
    <li id="li2"><input id="input2" /></li>
  </ul>
</form>

Что произойдёт, когда вместо input1 вы выберете input2?

Рассмотрим пошагово, как это работает в первоначальном патче:

  1. Сначала выбирается input1, так что этот элемент и все его дочерние элементы — в том числе input1, li1, ul и form (на самом деле даже body и html, но здесь мы их опустим) — получают флаг :focus-within (у всех появляется зелёная рамка).
  2. Потом переходим на input2. Первый выбранный элемент — input1 — теряет фокусировку. И здесь мы проходим по цепочке родительских элементов, убирая флаг :focus-within у input1, li1, ul и form.
  3. Теперь input2 действительно выбран. Снова идём по цепочке родительских элементов и добавляем флаг у input2, li2, ul и form.

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

В новой версии патча в пункт 2 внесли изменение: теперь у элементов, теряющих и получающих фокусировку, ищутся общие родительские элементы. В нашем случае при переходе с input1 к input2 это будет ul. Проходя по цепочке родительских элементов для добавления/удаления флага :focus-within, система пропускает общий родительский элемент и оставляет его (и всех его родителей) неизменённым. Так мы экономим количество вычислений.

Теперь в пункте 2 флаг будет снят только у input1 и li1, а в пункте 3 добавится только у input2 и li2. Элементы ul и form останутся нетронутыми.

И дополнительно…


Закончив работу в Chromium, я сообразил, что WebKit не соблюдает спецификацию в случае с flat tree. Поэтому я импортировал в WebKit тесты WPT и написал патч, позволяющий использовать flat tree и в WebKit.

Добавление нового селектора может выглядеть простой задачей. Но позвольте показать вам количество коммитов в разные репозитории, которые были связаны с этой работой:


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

Применение


Теперь всё готово, :focus-within будет доступен по умолчанию в Chrome 60. Можно его использовать.

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



Новый селектор важен для повышения доступности веб-приложений и сайтов, особенно при работе с клавиатурой. Например, если у вас задействован только :hover, то часть пользователей, которые привыкли к кнопочной навигации, не смогут воспользоваться вашим продуктом. А если вы добавите :focus-within — вы избежите таких проблем.

Я создал типичное меню, использующее :hover и :focus-within, взгляните, как теперь работает кнопочная навигация.



Обратите внимание, что в Firefox есть баг, из-за которого этот пример не работает.
Поделиться с друзьями
-->

Комментарии (9)


  1. vanxant
    14.05.2017 21:18

    Вот буквально недавно сталкивался с ситуацией, где focus-within реально очень помог бы.


    1. Aingis
      15.05.2017 14:14

      С одной стороны это мило, а с другой на рынке есть ещё IE11, который идёт с Windows 7, и который никогда не обновится. IE11 — это новый IE6, который уйдёт ещё очень нескоро.

      Ведь если в случае IE6 на Windows XP его в своё время заменил IE8, то IE11 на Windows 7 ничего не заменит.


      1. john_2013
        15.05.2017 18:00
        -1

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


      1. Caravus
        15.05.2017 19:43
        -1

        Если не сложно, поделитесь — почему не заменит? Сильно встроен в ОСь? Так ие6 тоже вроде как был встроен, нет?


        1. extempl
          15.05.2017 21:18

          Наверное потому что нет IE12, не на что обновляться. Есть Edge, но он позиционируется как совсем другой браузер не имеющий с IE ничего общего кроме вендора. Будет ли он обновляться на Edge — вопрос.


          1. Caravus
            15.05.2017 21:24

            Ну заменят «обновление ие11 на эдж» формулировкой «замена вашему устаревшему браузеру, купите скачайте уже сегодня, новый сияющиq spartan Edge!» было бы желание. Просто я подумал что там тех. проблемы какие с этим…


        1. Aingis
          15.05.2017 21:31

          Потому что Edge выпущен только на Windows 10. Майкрософт так рекламирует свой новый браузер, постоянно забывая, что его нет и не будет на самой популярной на текущий момент ОС.


          1. Caravus
            15.05.2017 21:34

            Ой ну это просто маркетинг. Когда впарят всем кому смогут вин10 — переведут и другие оси на Edge, чтоб съэкономить на поддержке других браузеров, как минимум. Они не для того этот Edge запилили чтоб теперь получить на свою голову ещё один ие6. Ну по крайней мере я на это надеюсь…


  1. bano-notit
    15.05.2017 21:49

    Блин, да этот селектор очень даже поможет делать всё! Теперь появится много говнокода и всё такое.
    Помнится с друзьями говорили, что было бы неплохо реализовать псевдо класс, который бы срабатывал, если внутри элемента присутствует другой селектор, и сами поняли, что тогда будет очень долго искать, почему же этот грёбаный класс сработал! Сверху же ничерта не меняется да и на него никаких классов не работает!
    Короче говоря мы тогда подумали, что делать что-то типа обратного порядка слекторов (.parent < .child.has.this.class). Типа такого. Всё же это можно сделать, но для такого нужно очень сильно и долго вкуривать, что селекторы могут работать почти так же как и ивенты в js, то есть не только спускаться, но и подниматься.