Перейти к содержимому
Плотность
Тема

Агентный слой

Дерево

JS Ленивая подгрузка детей — слой приложения. Стрелки, раскрытие и бегущий tabindex делает кит

Иерархия: файлы проекта, структура сцены, вложенность вызовов.

Компонент
terrain
heightmap.ts
chunks.bin
Разметка
<div class="inst-tree" role="tree" aria-label="Файлы проекта">
  <div class="inst-tree-item" role="treeitem" aria-level="1" aria-expanded="true"
       tabindex="0" style="--depth:0">
    <span class="inst-tree-twist"></span>terrain
  </div>
  <div class="inst-tree-item" role="treeitem" aria-level="2"
       tabindex="-1" style="--depth:1">heightmap.ts</div>
  <div class="inst-tree-item" role="treeitem" aria-level="2" aria-selected="true"
       tabindex="-1" style="--depth:1">chunks.bin</div>
</div>

Использование

Кит рисует состояние, но не выдумывает его.

Что Почему
role="tree" на контейнере Иначе это набор <div>
role="treeitem" на узле Носитель роли
aria-level Единственный способ озвучить глубину: визуальный отступ скринридеру не виден
aria-expanded Только у узлов с детьми. На листе он соврёт, что там что-то есть
aria-selected Если выбор поддерживается
Бегущий tabindex 0 у активного, -1 у остальных

Когда использовать

Используйте Возьмите другое
Иерархия, глубина которой заранее неизвестна Плоский список объектовочередь: дерево на одном уровне это список с лишними ролями
Узлы раскрываются и сворачиваются Свойства одного объектасписок свойств
Важна структура, а не сравнение Сравнение записей по полямтаблица
Две-три вложенных группы, известных наперёдсекции: дерево здесь избыточно и требует JS

Устройство

<div class="inst-tree-item" style="--depth:2">…</div>

Отступ считается из --depth умножением на --size-indent, поэтому он реагирует на плотность контейнера. Глубина задаётся инлайном как значение — тот же разрешённый канал, что у ширины заполнения меры и границ отрезка дорожки.

Плоский список узлов с --depth, а не вложенные контейнеры: так строку можно виртуализировать, а бегущий tabindex не должен обходить дерево рекурсивно.

JS

Подключите модуль один раз на страницу — инициализировать компоненты по отдельности не нужно, кит работает делегированием и видит узлы, пришедшие позже.

<script type="module" src="src/kit.js"></script>

Что делает кит

Пример в шапке живой: и по узлам, раскрывает, сворачивает и уводит к родителю.

Контракт роли treeitem целиком: стрелки вверх и вниз по узлам, раскрывает свёрнутый и уходит внутрь раскрытого, сворачивает и уводит к родителю. Родитель ищется по aria-level, поэтому вложенность в разметке может быть плоской — для дерева на десять тысяч узлов это единственный рабочий вариант.

Раскрытие меняет aria-expanded; как оно выглядит, решает CSS.

События

inst:select — на выбранном узле, { value } в detail.

tree.addEventListener('inst:select', (e) => open(e.detail.value));

Что остаётся приложению

Ленивая подгрузка детей. Кит переключил aria-expanded — приложение решает, надо ли идти за данными.

Правила

Так Глубина — данные

Уровень вложенности приходит из данных инлайном, а не из вложенных обёрток с отступами.

Не так Отступ вложенными div

Десять уровней дают десять обёрток, и Tab начинает проходить по контейнерам, а не по узлам.

Так Роли дерева целиком

role="tree", role="treeitem", aria-expanded, aria-level. Половина контракта хуже его отсутствия.

Не так Дерево вместо навигации

У пункта навигации нет уровня и раскрытия. Дерево — про иерархию объектов.

Доступность

Клавиатура — по видимым узлам, — раскрыть или войти, — свернуть или к родителю, Home/End. Поведение целиком на приложении
Глубина Только aria-level. Отступ — визуальный признак, для скринридера его нет
aria-expanded на листе Частая ошибка: атрибут со значением false на узле без детей объявляет несуществующее свёрнутое поддерево
Треугольник Декоративен. Состояние раскрытия несёт aria-expanded, а не поворот значка
Цель нажатия Высота — --control-h-sm, то есть 26px. В плотности compact — 22px, ниже 24px по WCAG 2.5.8
Виртуализация При виртуализации обязателен aria-setsize и aria-posinset: иначе скринридер объявит «3 из 3», когда узлов три тысячи

API

ИмяЗначениеЧто делает
класс
inst-treeКонтейнер
inst-tree-itemУзел
inst-tree-twistТреугольник раскрытия. Поворачивается по aria-expanded
атрибут
aria-expandedtrue · false. Только у узлов с детьми
aria-selectedtrue · false
aria-levelцелое, с 1
переменная
--depth0
токен
--size-indent14px
--control-h-sm26px
--space-36px
--radius-sm5px
--text-sm0.8125rem
--surface-hoverlight-dark(oklch(0 0 0 / 0.035), oklch(1 0 0 / 0.045))
--surface-selectedcolor-mix(in oklab, var(--a-4) 14%, transparent)
--size-chevron10px

Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument