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).

И вот как это всё работает:

  1. Вкладываем дочерние таблицы в ячейки родительской.
  2. Убираем из визуальной структуры родительской таблицы tr и td, в которой лежит внутренняя.
  3. У внутренней таблицы убираем table и tbody, оставляя «голые» tr. «Родителем» этих tr теперь оказывается tbody внешней таблицы (все промежуточные обертки пропали), т.е. внутренние и внешние tr оказываются на одном уровне.
  4. У первой строки внутренней таблицы убираем tr и совсем убираем первую td (display: none). Оставшиеся ячейки браузер оборачивает в анонимный бокс типа table-row (на одном уровне со всеми tr, между ними).
  5. Перед внутренней таблицей вставляем details c summary.
  6. Бокс самого details убираем, а summary задаем display: table-cell. Эта «ячейка» попадает в тот же анонимный table-row, на место первой ячейки первой строки вложенной таблицы.
  7. Скрываем или показываем строки внутренней таблицы, кроме первой, в зависимости от состояния details (т.е. его атрибута open, на который CSS не влияет).

Получилась этакая таблица-мутант, у которой вместо очередной строки внешней таблицы иногда попадается «сборная» анонимная строка с суррогатной ячейкой из summary и «отдельно взятыми» ячейками бывшей первой строки внутренней таблицы, потом идут остальные строки бывшей внутренней таблицы, а за ними опять строки внешней. Звучит сложно, признаю. Так что «поковыряйте» пример, посмотрите, что на что влияет и как эти части собираются воедино.

Идею класть скрываемый контент не внутрь details, а рядом с ним, недавно подсказала Амелия Беллами-Ройдз. Этим убиваем двух зайцев: во-первых, сохранили логичную последовательность ячеек в DOM, а во-вторых, обошли досадную проблему в Хроме. Дело в том, что details — «особенный элемент», и убрать его бокс Хрому не запросто. А так в худшем случае он просто весь обернется в одну анонимную ячейку.

Что ж, это интересная демонстрация малоизвестных возможностей 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). А вот вместе они работать не хотят. Баг в багзиллу отправлен, ждем ответа (и, надеюсь, фикса).

Без display:contents дерево доступности правильное, а с ним баг
Дерево доступности в Firefox для вложенных таблиц с role="presentation" для промежуточных уровней: вверху без display:contents, внизу с ним

Интересно, что в Хроме ситуация обратная: там нужное нам дерево доступности получается как раз с display:contents, а без него остаются лишние промежуточные элементы. Строго говоря, это тоже баг (display, кроме none, на семантику и доступность влиять не должен!), но Хром, похоже, отталкивается от визуальной структуры, и в данном случае это оказалось нам на руку.

Но всё-таки, по-моему, в главном идея себя оправдала и после небольшой доработки ее можно будет применять не только для статистики по вирусам, но и везде, где нужно сочетать табличность с иерархичностью. И да, простите за кликбейтный заголовок. Хотя реализация доступности таблиц в современных браузерах, судя по всему, тоже… та еще зараза:)

P.S. Это тоже может быть интересно: