Добавляем в Chromium селектор :focus-within

в 17:04, , рубрики: Blink, chrome, chromium, css, Google Chrome, Анализ и проектирование систем, Блог компании Mail.Ru Group, никто не читает теги, Программирование, псевдоклассы

Добавляем в Chromium селектор :focus-within - 1

В этой статье рассказывается о внедрении в 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. Можно его использовать.

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

Добавляем в Chromium селектор :focus-within - 2

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

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

Добавляем в Chromium селектор :focus-within - 3

Обратите внимание, что в Firefox есть баг, из-за которого этот пример не работает.

Автор: Mail.Ru Group

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js