CSS против коронавируса: доступное представление иерархических табличных данных
О новом опасном вирусе, наверное, уже наслышаны все. Многие из нас с тревогой следят за официальной статистикой через гуглоперевод. И я подумал, что эта ситуация — неплохой пример, как важна бывает доступность веб-контента обычным людям. Ведь от информации может зависеть здоровье, а то и жизнь, а обстоятельства, в которых мы её ищем, бывают самые разные. Скажем, у вас срочная командировка в одну из охваченных эпидемией стран, и вы строите маршрут в объезд главных очагов. А у гостиничного компьютера из-за угрозы заражения убрали мышку (как лишнюю поверхность контакта). Да еще незнакомый язык и негибкая верстка, в которую длинные переведенные названия просто не помещаются…
Не помогут ли нам новые возможности HTML и CSS сделать эту информацию доступнее и избежать опасности?
Дилемма древовидной таблицы
Итак, наша цель — удобное представление статистики по вирусу (таблицы с разбивкой по частям света, в них — по странам, а для Китая — еще по провинциям и городам). Что это именно таблица, сомнений нет: вертикальные связи важны. Но иерархия, включая возможность сворачивать и разворачивать ветви, важна не меньше.
Увы, в HTML табличность и иерархичность плохо сочетаются. Есть максимум один уровень группировки строк (thead
/tbody
/tfoot
). Обычно приходится жертвовать либо иерархичностью, эмулируя ее через классы для строк (как в jQuery-плагине treetable), либо табличностью, визуально имитируя ее фиксированным размером блоков (как на китайском сайте по ссылке в начале статьи). Оба варианта неудобные и негибкие.
К счастью, сейчас в CSS есть способ сгладить это противоречие — display: contents
.
Решение «на чистом CSS»?
Под решениями чего-либо «на чистом CSS» часто кроются хитроумные, но непрактичные хаки в разметке. Но найти такое решение часто всё равно хочется — ради упражнения и эксперимента. Нашлось оно и для нашей задачи! Даже переход Tab-ом по заголовкам раскрываемых уровней работает:
See the Pen
Tree Table with no JS using display:contents (very early proof of concept) by Ilya Streltsyn (@SelenIT)
on CodePen.
Вот его главные составляющие:
- нативные HTML-элементы для скрытия/показа контента —
details
иsummary
; - наш
display: contents
; - анонимные боксы для пропущенных уровней табличной структуры (стандартная особенность табличной модели в CSS).
И вот как это всё работает:
- Вкладываем дочерние таблицы в ячейки родительской.
- Убираем из визуальной структуры родительской таблицы
tr
иtd
, в которой лежит внутренняя. - У внутренней таблицы убираем
table
иtbody
, оставляя «голые»tr
. «Родителем» этихtr
теперь оказываетсяtbody
внешней таблицы (все промежуточные обертки пропали), т.е. внутренние и внешниеtr
оказываются на одном уровне. - У первой строки внутренней таблицы убираем
tr
и совсем убираем первуюtd
(display: none
). Оставшиеся ячейки браузер оборачивает в анонимный бокс типаtable-row
(на одном уровне со всемиtr
, между ними). - Перед внутренней таблицей вставляем
details
csummary
. - Этому
details
задаемdisplay: table-cell
. Эта «ячейка» попадает в тот же анонимныйtable-row
, на место первой ячейки первой строки вложенной таблицы. - Скрываем или показываем строки внутренней таблицы, кроме первой, в зависимости от состояния
details
(т.е. его атрибутаopen
, на который CSS не влияет).
Получилась этакая таблица-мутант, у которой вместо очередной строки внешней таблицы иногда попадается «сборная» анонимная строка с суррогатной ячейкой из details
и «отдельно взятыми» ячейками бывшей первой строки внутренней таблицы, потом идут остальные строки бывшей внутренней таблицы, а за ними опять строки внешней. Звучит сложно, признаю. Так что «поковыряйте» пример, посмотрите, что на что влияет и как эти части собираются воедино.
Идею класть скрываемый контент не внутрь details, а рядом с ним, недавно подсказала Амелия Беллами-Ройдз. Этим убиваем двух зайцев: во-первых, сохранили логичную последовательность ячеек в DOM, а во-вторых, обошли досадную проблему в Хроме. Дело в том, что details
— «особенный элемент», и display: contents
для него работает со странностями. А так он просто весь «упакуется» в одну ячейку.
Что ж, это интересная демонстрация малоизвестных возможностей CSS (новых и «хорошо забытых старых»). Но вряд ли решение: сложно, неуниверсально, да и такой финт с details
— хак (что признала и Амелия), хоть и менее грубый, чем скрытые чекбоксы. И это мы еще не заглядывали в инспектор доступности в отладчиках, не говоря о поведении в реальном скринридере. Так что пора рассмотреть…
Более практичный вариант
Для этого примера я «выдрал» исходные данные прямо из кода того китайского сайта. Чтобы проверить гибкость верстки, можно тут же перевести их Гуглом (виджет прилагается):
See the Pen
wvadgJK by Ilya Streltsyn (@SelenIT)
on CodePen.
Главная идея та же: вложенные таблицы превращаются в одну сплошную за счет убирания промежуточных оберток. Целые группы строк легко прячутся и показываются сменой display
с contents
на none
и обратно для одного-единственного элемента (никаких циклов!). Вся логика компонента хранится в ARIA-атрибутах, а CSS просто отражает ее. Скрипт нужен по сути только для обновления атрибута aria-expanded
по нажатию на строку или Enter на клавиатуре.
Самое интересное (и сложное) предсказуемо оказалось связано с ARIA-атрибутами.
Выбор роли: treegrid
против table
В спецификации WAI-ARIA нет отдельной роли для древовидной таблицы, но есть очень близкая к ней: treegrid
. Она наследует свойства одновременно от дерева (tree
) и грида с данными (grid
), который в свою очередь наследует от таблицы (table
) и отличается от нее интерактивностью ячеек (типичный пример — Excel). У рабочей группы по ARIA в W3C есть даже страничка с примером реализации такого виджета. И у строк такой интерактивной таблицы предусмотрен атрибут aria-level
, чтобы сообщать уровень вложенности. То, что надо?
Увы: в большинстве реальных браузеров/скринридеров даже сам W3C-шный пример реализации этой роли… не работает! Особенно в Windows, где, судя по тому же ишью, со всеми производными от tree
вообще путаница. После долгих безуспешных попыток «завести» табличную навигацию в NVDA с теоретически правильной ролью мне пришлось сдаться. С ролью table
скринридеры по крайней мере поддерживают ключевую для этой задачи навигацию по строкам и столбцам.
Другие досадные мелочи
Без них не обошлось. Например, в iOS 12.4 Safari на стареньком iPad mini 2 — уровни таблицы раскрывались как положено, а вот скрываться обратно почему-то не хотели, хотя атрибуты менялись как надо. Словно отрисовка где-то «застревала». К счастью, в iOS 13 проблемы уже нет.
Другая проблема возникла в Firefox: там скринридер почему-то упорно не хотел переходить между уровнями таблицы, уверяя, что таблица кончилась. Причина нашлась быстро: оказалось, что display:contents
и role="presentation"
плохо сочетаются для элемента tbody
. Без display:contents
смена роли убирает все промежуточные элементы из дерева доступности, и все строки — независимо от уровня вложенности — в дереве доступности оказываются соседями (по сути тот же эффект, что в структуре визуального отображения делает display:contents
). А вот вместе они работать не хотят. Баг в багзиллу отправлен, и (добавлено 29.12.2020) в версии 84 уже пофикшен.
Дерево доступности в Firefox для вложенных таблиц с role="presentation"
для промежуточных уровней: вверху без display:contents
, внизу с ним
Интересно, что в Хроме ситуация обратная: там нужное нам дерево доступности получается как раз с display:contents
, а без него остаются лишние промежуточные элементы. Строго говоря, это тоже баг (display
, кроме none
, на семантику и доступность влиять не должен!), но Хром, похоже, отталкивается от визуальной структуры, и в данном случае это оказалось нам на руку.
Но всё-таки, по-моему, в главном идея себя оправдала и после небольшой доработки ее можно будет применять не только для статистики по вирусам, но и везде, где нужно сочетать табличность с иерархичностью. И да, простите за кликбейтный заголовок. Хотя реализация доступности таблиц в современных браузерах, судя по всему, тоже… та еще зараза:)
P.S. Это тоже может быть интересно:
Спасибо за хорошее решение. Только переходы между строк, наверно, лучше сделать с помощью курсорных клавиш, чтобы с помощью tab можно было «проскочить» через всю таблицу.
Спасибо за отклик! Насчет курсорных клавиш – я подумал, что это менее привычно пользователям для в целом неинтерактивного (помимо раскрытия подуровней) элемента. Возможно, в этом я был неправ. В W3Cшном примере
treegrid
-а как раз навигация стрелками, но мне, как пользователю, она показалась перегруженной и не слишком интуитивной, если честно. А в скринридерах доступна своя навигация по таблицам. Но в любом случае, этот пример – больше прототип идеи, чем окончательное решение, поэтому любые улучшения приветствуются!