В этой статье рассказывается о внедрении в 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
?
Рассмотрим пошагово, как это работает в первоначальном патче:
- Сначала выбирается
input1
, так что этот элемент и все его дочерние элементы — в том числеinput1
,li1
,ul
иform
(на самом деле дажеbody
иhtml
, но здесь мы их опустим) — получают флаг:focus-within
(у всех появляется зелёная рамка). - Потом переходим на
input2
. Первый выбранный элемент —input1
— теряет фокусировку. И здесь мы проходим по цепочке родительских элементов, убирая флаг:focus-within
уinput1
,li1
,ul
иform
. - Теперь
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 есть баг, из-за которого этот пример не работает.
Автор: Mail.Ru Group