Агентный слой
Дерево
Иерархия: файлы проекта, структура сцены, вложенность вызовов.
Разметка
<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-expanded | — | true · false. Только у узлов с детьми |
aria-selected | — | true · false |
aria-level | — | целое, с 1 |
| переменная | ||
--depth | 0 | |
| токен | ||
--size-indent | 14px | |
--control-h-sm | 26px | |
--space-3 | 6px | |
--radius-sm | 5px | |
--text-sm | 0.8125rem | |
--surface-hover | light-dark(oklch(0 0 0 / 0.035), oklch(1 0 0 / 0.045)) | |
--surface-selected | color-mix(in oklab, var(--a-4) 14%, transparent) | |
--size-chevron | 10px | |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument