@sberbusiness/triplex-next-mcp-server 1.43.0-beta → 1.44.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,7 @@
1
1
  ---
2
2
  component: TreeView
3
- category: Data Display
4
- figma-node: ""
5
- figma-file: ""
6
- related: [AbstractTree]
3
+ category: TreeView
4
+ related: [TreeViewNode, CollapsibleTreeExtended, CollapsibleTree]
7
5
  tokens: []
8
6
  stories: stories/TreeView/TreeView.stories.tsx
9
7
  version: "1.0"
@@ -13,212 +11,391 @@ version: "1.0"
13
11
 
14
12
  ## Назначение
15
13
 
16
- Базовый каркас визуального дерева: семантическая разметка
17
- (`<ul role="tree">` + `<li role="treeitem">` + `<ul role="group">`) и
18
- keyboard navigation по вертикали (ArrowUp/ArrowDown). Параллельно
19
- поддерживает абстрактное дерево (`TreeViewAbstractNode`) для трекинга
20
- active/opened/tabIndex состояний нод. Контент каждой ноды — render-prop
21
- (заголовок, иконка, custom-обёртка — на потребителе).
14
+ Каркас визуального дерева: семантическая разметка (`<ul role="tree">`,
15
+ `<li role="treeitem">`, `aria-expanded`), построение абстрактного дерева нод по
16
+ JSX-разметке, управление `tabIndex` и перемещение активной ноды стрелками
17
+ вверх/вниз. Собственного оформления у компонента нет — разметку узла целиком
18
+ рисует потребитель через render-функцию `children`.
22
19
 
23
- Используй когда: нужно дерево с произвольным content'ом нод. Сам
24
- TreeView не рисует expand-кнопку и chevron — потребитель использует
25
- `hasChildNodes`/`openedNode`/`setOpenedNode` из render-prop'а.
20
+ Используй когда: нужен только каркас дерева (семантика, регистрация нод,
21
+ навигация стрелками), а весь UI узла — свой.
26
22
 
27
- ---
28
-
29
- ## Файловая структура
30
-
31
- ```text
32
- src/components/TreeView/
33
- ├── TreeView.tsx # class TreeView (Component) + ITreeViewComposition
34
- ├── TreeViewContext.tsx # ITreeViewContext + withTreeViewContext HOC
35
- ├── TreeViewAbstractNode.tsx # AbstractTreeNode + active/opened/tabIndex
36
- ├── TreeViewAbstractNodeUtils.ts # навигация по абстрактному дереву
37
- ├── components/
38
- │ ├── TreeViewNode.tsx # class TreeViewNodeWithContext (Component) + withTreeViewContext
39
- │ └── TreeViewGroup.tsx # FC, <ul role="group">
40
- └── styles/TreeView.module.less
41
- ```
42
-
43
- **Примечание:** в директории нет `index.ts` — импортировать из `./TreeView`,
44
- `./components/TreeViewNode`, `./components/TreeViewGroup` напрямую (или из
45
- точки экспорта дизайн-системы, если она настроена в `src/components/index.ts`).
23
+ Не используй когда:
24
+ - нужно типовое дерево «папка/файл» с шевроном, hover и focus-visible —
25
+ бери `CollapsibleTree`;
26
+ - нужен свой UI узла, но с анимацией раскрытия и controlled/uncontrolled
27
+ состоянием — бери `CollapsibleTreeExtended`.
46
28
 
47
29
  ---
48
30
 
49
- ## Композиция
50
-
51
- ```tsx
52
- <TreeView>
53
- <TreeView.Node id="1" prevNodeId={undefined} nextNodeId="2">
54
- {({ activeNode, openedNode, hasChildNodes, isLastNode, setOpenedNode }) => (
55
- <>
56
- <button onClick={() => setOpenedNode(!openedNode)}>
57
- {hasChildNodes ? (openedNode ? "▾" : "▸") : "•"} Node 1
58
- </button>
59
- {openedNode && (
60
- <TreeView.Group>
61
- <TreeView.Node id="1.1">{/* ... */}</TreeView.Node>
62
- </TreeView.Group>
63
- )}
64
- </>
65
- )}
66
- </TreeView.Node>
67
- <TreeView.Node id="2" prevNodeId="1">{/* ... */}</TreeView.Node>
68
- </TreeView>
69
- ```
70
-
71
- ---
72
-
73
- ## Публичный API
74
-
75
- ### `TreeView` (class component)
31
+ ## Варианты и props
76
32
 
77
- | Prop | Тип | Описание |
78
- |---|---|---|
79
- | `children` | `ReactNode` | Дерево из `TreeView.Node` / `TreeView.Group`. |
80
- | `...HTMLAttributes<HTMLUListElement>` | — | На корневой `<ul role="tree">`. |
81
-
82
- Композиция: `TreeView.Node = TreeViewNode`, `TreeView.Group = TreeViewGroup`.
33
+ ### `TreeView`
83
34
 
84
- ### `TreeView.Node` (`ITreeViewNodeProps`)
35
+ Class-компонент. Собственных props, кроме `children`, нет — остальное это
36
+ стандартные атрибуты `<ul>`.
85
37
 
86
- ```ts
87
- extends Omit<HTMLAttributes<HTMLElement>, "children">
88
- ```
89
-
90
- | Prop | Тип | Описание |
91
- |---|---|---|
92
- | `id` | `string` | **Обязателен**. Уникальный id ноды. |
93
- | `children` | `(props: ITreeViewNodeProvideProps) => JSX.Element` | Render-prop — потребитель рисует содержимое. |
94
- | `opened?` | `boolean` | Внешнее управление раскрытием. Синхронизируется через `componentDidUpdate`. |
95
- | `prevNodeId?` | `string` | Id соседа слева (для корректной вставки в абстрактное дерево). |
96
- | `nextNodeId?` | `string` | Id соседа справа. |
97
- | `...HTMLAttributes<HTMLElement>` | — | На `<li role="treeitem">`. |
38
+ | Prop | Тип | По умолчанию | Описание |
39
+ |---|---|---|---|
40
+ | `children` | `React.ReactNode` | — | Ноды дерева: `TreeView.Node` напрямую или компоненты-обёртки над ним |
41
+ | `...HTMLUListElementAttributes` | — | — | Все стандартные атрибуты `<ul>`; `className` мерджится с собственным классом |
98
42
 
99
- #### `ITreeViewNodeProvideProps` (render-prop)
43
+ Статические свойства: `TreeView.Node` (= `TreeViewNode`),
44
+ `TreeView.Group` (= `TreeViewGroup`).
100
45
 
101
- | Поле | Тип | Описание |
102
- |---|---|---|
103
- | `activeNode` | `boolean` | Нода активна для keyboard-навигации. |
104
- | `openedNode` | `boolean` | Раскрыта. |
105
- | `hasChildNodes` | `boolean` | Есть вложенные `TreeView.Node`. |
106
- | `isLastNode` | `boolean` | Последняя в полном обходе дерева. |
107
- | `setOpenedNode` | `(opened: boolean) => void` | Переключение opened. |
108
-
109
- ### `TreeView.Group` (`ITreeViewGroupProps`)
110
-
111
- ```ts
112
- extends HTMLAttributes<HTMLUListElement>
113
- ```
46
+ ### `TreeView.Node` (`TreeViewNode`)
114
47
 
115
- `<ul role="group">` — обёртка для вложенных нод.
48
+ Узел дерева со своим API: `id`, `opened`, `prevNodeId` / `nextNodeId`,
49
+ render-prop `children` с пятью provide-props, контракт по `focus` / `blur` и
50
+ `aria-expanded`. Props, инварианты, порядок регистрации нод и правила
51
+ раскрытия описаны в отдельном документе —
52
+ [`TreeViewNode-ai.md`](./TreeViewNode-ai.md).
116
53
 
117
- ### `TreeViewContext` / `withTreeViewContext` / `IWithTreeViewContextProps`
54
+ ### `TreeView.Group` (`TreeViewGroup`)
118
55
 
119
- Экспорт контекста и HOC. `withTreeViewContext<T>(Component)` оборачивает
120
- класс-компонент, передавая `treeViewContext: ITreeViewContext` как props.
56
+ `<ul role="group">` — обёртка вложенных нод. `forwardRef` на `<ul>`,
57
+ `className` мерджится, остальные атрибуты пробрасываются. Собственных props нет.
121
58
 
122
- ### `TreeViewAbstractNode` (class)
123
-
124
- Расширяет `AbstractTreeNode<TreeViewAbstractNode>`:
125
-
126
- | Метод | Назначение |
127
- |---|---|
128
- | `getId()` / id фиксирован в конструкторе | — |
129
- | `getActive()` / `setActive(boolean)` | Активность для keyboard. |
130
- | `getOpened()` / `setOpened(boolean)` | Раскрытие. |
131
- | `getTabIndex()` / `setTabIndex(n)` | Управление roving tabindex (0 для первой ноды первого уровня, -1 — для остальных). |
59
+ ---
132
60
 
133
- ### `TreeViewAbstractNodeUtils`
61
+ ## Дизайн-токены
134
62
 
135
- Утилиты обхода: `getNode`, `setActiveNode`, `setActiveNextNode`,
136
- `setActivePrevNode`, `getActiveNode`, `isLastNode`.
63
+ Собственных CSS-переменных компонент не определяет. `TreeView.module.less`
64
+ задаёт только сброс отступов и маркеров списка плюс `outline: none` на `<li>`
65
+ (focus-стиль рисует потребитель).
137
66
 
138
67
  ---
139
68
 
140
- ## Поведение
69
+ ## Инварианты
141
70
 
142
- ### Жизненный цикл ноды
71
+ - **`TreeView` и `TreeViewNodeWithContext` — class-компоненты.** Это legacy,
72
+ зафиксированный публичным API: `ref` на `TreeView` отдаёт инстанс класса.
73
+ Перевод на функциональные компоненты и добавление `forwardRef` — breaking
74
+ change поведения `ref`. Не мигрировать без отдельного решения мейнтейнера.
75
+ - **Barrel `src/components/TreeView/index.ts`** реэкспортирует модули семейства
76
+ целиком (`export * from ...`), поэтому публично всё, что они экспортируют:
77
+ `TreeView` и `ITreeViewProps`, `TreeViewGroup` и `ITreeViewGroupProps`, а от
78
+ ноды — состав, перечисленный в
79
+ [TreeViewNode-ai.md](./TreeViewNode-ai.md) (там же и класс
80
+ `TreeViewNodeWithContext`). Состав и имена менять нельзя — от них зависят
81
+ `CollapsibleTreeExtended` и `CollapsibleTree`.
82
+ - **`TreeViewContext` не экспортируется из barrel**, но `ITreeViewContext`
83
+ транзитивно виден через `ITreeViewNodePropsWithContext`. Поле `updateCount`
84
+ из контекста убирать нельзя без breaking change типа.
85
+ - **Абстрактное дерево мутируемое.** `TreeViewAbstractNode` меняется на месте,
86
+ поэтому смена контекста сигнализируется числом `updateCount` в состоянии
87
+ `TreeView`. Любая мутация дерева обязана сопровождаться его инкрементом,
88
+ иначе ноды отрендерят устаревшие `hasChildNodes` / `isLastNode`.
89
+ - **Слушатель `keydown` висит на `window`**, а не на корневом `<ul>`: активная
90
+ нода не обязана иметь DOM-фокус. Подписка живёт ровно столько, сколько в дереве
91
+ есть активная нода: появляется на `focus` узла и снимается, как только фокус
92
+ ушёл из дерева. Пока активной ноды нет, дерево не обрабатывает стрелки и не
93
+ отменяет скролл страницы.
94
+ - **`role="tree"` / `role="treeitem"` / `role="group"` и `aria-expanded`**
95
+ выставляются после `{...props}` — семантика дерева часть контракта компонента,
96
+ потребитель её не переопределяет. `tabIndex` остаётся до спреда: roving
97
+ tabindex — поведение, а не семантика, и его потребитель при желании может
98
+ задать сам.
143
99
 
144
- `TreeViewNodeWithContext` (class):
145
- - **constructor:** создаёт `TreeViewAbstractNode({id})`, вызывает `treeViewContext.setOpenedNode(...)` + `registerNode(node, parentNode, prevNode, nextNode)`.
146
- - **componentDidUpdate:** при изменении `opened` props синхронизирует с абстрактной нодой.
147
- - **componentWillUnmount:** `treeViewContext.removeNode(node)`.
100
+ ---
148
101
 
149
- ### Контекст
102
+ ## Accessibility
150
103
 
151
- Каждая нода **переопределяет** `parentNode` в `TreeViewContext.Provider`,
152
- чтобы дочерние `TreeView.Node` регистрировались в правильном родителе.
104
+ Ориентир — WAI-ARIA Authoring Practices,
105
+ [treeview-2a](https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-2/treeview-2a.html).
106
+
107
+ - Корень — `<ul role="tree">`, узел — `<li role="treeitem">` с `aria-expanded`,
108
+ вложенная группа — `<ul role="group">` (`TreeView.Group`).
109
+ - **Roving tabindex:** первая нода первого уровня получает `tabIndex={0}`,
110
+ остальные — `tabIndex={-1}`. Пересчитывается при регистрации и удалении нод,
111
+ то есть в дерево можно попасть одним Tab.
112
+ - **Клавиатура:** `ArrowDown` / `ArrowUp` (по `event.keyCode`) переносят
113
+ активную ноду на следующую/предыдущую с `preventDefault` (чтобы не скроллить
114
+ страницу). Обход циклический: после последней ноды активной становится первая.
115
+ Стрелки спускаются только в раскрытые (`opened`) ветки: `ArrowUp` с самой
116
+ первой ноды дерева переходит на последнюю **видимую** ноду — если последняя
117
+ ветка свёрнута, активной становится она сама, а не её скрытый потомок.
118
+ - **Активная нода ≠ DOM-фокус.** Активной нода становится по `focus`
119
+ (`focusin` всплывает до `<li>`, дальше `stopPropagation`), а стрелки двигают
120
+ только флаг активности в абстрактном дереве — DOM-фокус остаётся на исходном
121
+ узле. Подсветку рисует потребитель по provide-prop `activeNode`. `blur`
122
+ снимает активность.
123
+ - Раскрытие/сворачивание по клавиатуре (`ArrowRight`/`ArrowLeft`, `Home`/`End`)
124
+ компонент **не** реализует — это на потребителе.
125
+ - Текстовые подписи и `aria-label` компонент не хардкодит: библиотека
126
+ мультиязычная, всю разметку узла даёт потребитель.
153
127
 
154
- ### Keyboard navigation
128
+ ---
155
129
 
156
- `TreeView.componentDidMount` вешает `window.addEventListener("keydown")`:
157
- - ArrowDown → `setActiveNextNode` + `preventDefault` + триггер ререндера.
158
- - ArrowUp → `setActivePrevNode`.
159
- - Срабатывает только если есть активная нода (`getActiveNode != undefined`).
130
+ ## Связанные компоненты
160
131
 
161
- ### Active state и focus
132
+ - `TreeViewNode` (`TreeView.Node`) — узел дерева, единственная часть семейства
133
+ со своим API: render-prop `children`, регистрация в абстрактном дереве,
134
+ focus/blur-контракт. См. [`TreeViewNode-ai.md`](./TreeViewNode-ai.md).
135
+ - `CollapsibleTreeExtended` (`src/components/CollapsibleTreeExtended`) — обёртка
136
+ над `TreeView`, добавляющая каждой ноде accordion-логику: анимацию раскрытия
137
+ и controlled/uncontrolled состояние.
138
+ - `CollapsibleTree` (`src/components/CollapsibleTree`) — готовое дерево
139
+ «папка/файл» поверх `CollapsibleTreeExtended`. Закрывает большинство
140
+ прикладных случаев.
141
+ - `AbstractTreeNode` (`src/components/AbstractTree`) — базовая реализация
142
+ абстрактного дерева, от которой наследуется `TreeViewAbstractNode`, и функция
143
+ обхода `traverseAbstractTree`.
162
144
 
163
- - `<li>` `tabIndex` берётся из `abstractNode.getTabIndex()` (0 для первой первого уровня, -1 — остальных).
164
- - `onFocus` (event.stopPropagation, чтобы родитель не получил bubble) → `setActiveNode(node, true)`.
165
- - `onBlur` → `setActiveNode(node, false)`.
145
+ ---
166
146
 
167
- ### `updateCount` в state
147
+ ## Stories
168
148
 
169
- Числовой счётчик, увеличивается при `registerNode` / `setActiveNode` /
170
- `setOpenedNode` / keyboard навигации. Триггерит ререндер контекста, т.к.
171
- `abstractNode` — мутируемый объект (изменения не вызывают триггер
172
- самостоятельно).
149
+ Основные истории: `stories/TreeView/TreeView.stories.tsx`
150
+ Файлы примеров: `stories/TreeView/examples/`
173
151
 
174
- ⚠️ В коде `prevCount++` (post-increment) — возвращает старое значение,
175
- поэтому фактически `updateCount` не растёт. Тем не менее `setState` всё
176
- равно вызывает ререндер, поэтому работает.
152
+ | Story | Example file | Что демонстрирует |
153
+ |---|---|---|
154
+ | `Playground` | `Playground.tsx` | Интерактивный контроль `defaultOpened` и глубины вложенности |
155
+ | `Default` | `Default.tsx` | Минимальное дерево: плоский список `TreeView.Node` |
156
+ | `Nested` | `Nested.tsx` | Вложенность через `TreeView.Group`, раскрытие через prop `opened`, `hasChildNodes` |
157
+ | `Keyboard navigation` | `KeyboardNavigation.tsx` | Перемещение `activeNode` стрелками, подсветка активной ноды, `isLastNode` |
158
+ | `VisualTests` | `VisualTests.tsx` | Свёрнутое и раскрытое дерево в одном кадре, активная нода через `play` |
177
159
 
178
160
  ---
179
161
 
180
- ## Инварианты
162
+ ## История изменений
181
163
 
182
- - **`TreeView` и `TreeViewNode` — class components**, не FC и не forwardRef. Рефы на `<ul>`/`<li>` приходят через `htmlAttributes` (но не пробрасываются явно).
183
- - **`TreeView.Node`'s `id` обязателен и должен быть уникальным** во всём дереве.
184
- - **`prevNodeId`/`nextNodeId` опциональны**, но без них вставка в абстрактное дерево идёт в конец (порядок может разойтись с DOM при динамическом добавлении).
185
- - **Render-prop `children`** — обязателен (`(props) => JSX.Element`).
186
- - **`<li role="treeitem">`** + `<ul role="group">` + `<ul role="tree">` — фиксированная семантика по WAI-ARIA `treeview-2a`.
187
- - **Keyboard listener на `window`** — может конфликтовать с другими компонентами; для multiple TreeView'ов обработчики складываются (срабатывает первый с активной нодой).
188
- - **Mutable `abstractNode`** — изменения требуют ручного `setState({updateCount})` для ререндера. Не доставать ссылку из contextValue для дальнейшей мутации без `setState`.
189
- - **`event.stopPropagation` в onFocus/onBlur ноды** — обязательно, чтобы родительский `<li>` не получил bubble.
190
- - **Композиция через `TreeView.Node = TreeViewNode`** — статические свойства класса.
191
- - **Опечатка в `prevCount++` (vs `++prevCount`)** — намеренно или нет, не править без анализа поведения.
192
- - **Нет `index.ts`** в директории — импорты идут напрямую из файлов компонентов.
164
+ | Дата | Изменение |
165
+ |---|---|
166
+ | 2026-08-26 | Описание `TreeView.Node` вынесено в отдельный `TreeViewNode-ai.md`, здесь оставлена ссылка |
167
+ | 2026-08-25 | Слушатель `keydown` подписан только пока в дереве есть активная нода: после ухода фокуса из дерева стрелки снова достаются странице. `role` и `aria-expanded` перенесены после `{...props}` — потребитель больше не может их переопределить |
168
+ | 2026-08-24 | `ArrowUp` с первой ноды дерева больше не заходит внутрь свёрнутой последней ветки — активной становится последняя видимая нода (`TreeViewAbstractNodeUtils.getPrevNode`) |
169
+ | 2026-08-21 | Создан документ. AI-рефакторинг: починен счётчик `updateCount` (постфиксный инкремент не увеличивал значение), удалён мёртвый код, добавлены unit-тесты и stories. `TreeView.Group` получил `forwardRef` и перестал затирать собственный класс переданным `className`; после удаления ноды соседние ноды теперь перерисовываются (актуальные `hasChildNodes` / `isLastNode`) |
193
170
 
194
171
  ---
195
172
 
196
- ## Accessibility
173
+ ## Примеры использования
197
174
 
198
- - Реализовано по WAI-ARIA `treeview-2a` pattern: https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-2/treeview-2a.html
199
- - `role="tree"` на корне, `role="treeitem"` на `<li>`, `role="group"` на `<ul>`.
200
- - `aria-expanded={openedNode}` на каждом `<li>`.
201
- - Roving tabindex (только первая нода первого уровня в tab-стопе).
202
- - ArrowDown/ArrowUp — навигация по дереву (только при наличии активной ноды).
203
- - ArrowLeft/ArrowRight (свернуть/раскрыть) — НЕ реализованы; должен делать потребитель в render-prop'е через `setOpenedNode`.
175
+ ### Default
204
176
 
205
- ---
177
+ `stories/TreeView/examples/Default.tsx`
206
178
 
207
- ## Связанные компоненты
179
+ ```tsx
180
+ import React from "react";
181
+ import { ETextSize, Text, TreeView } from "@sberbusiness/triplex-next";
182
+ import "./Default.less";
183
+
184
+ const items = [
185
+ { id: "item-1", label: "Item text" },
186
+ { id: "item-2", label: "Item text" },
187
+ { id: "item-3", label: "Item text" },
188
+ ];
189
+
190
+ export const Default = () => (
191
+ <div className="tree-view-default-example" style={{ maxWidth: 420 }}>
192
+ <TreeView aria-label="Tree">
193
+ {items.map((item) => (
194
+ <TreeView.Node key={item.id} id={item.id}>
195
+ {() => (
196
+ <div className="tree-view-default-example-row">
197
+ <Text size={ETextSize.B1} tag="span">
198
+ {item.label}
199
+ </Text>
200
+ </div>
201
+ )}
202
+ </TreeView.Node>
203
+ ))}
204
+ </TreeView>
205
+ </div>
206
+ );
207
+ ```
208
208
 
209
- - `AbstractTree` / `AbstractTreeNode` — базовый класс для абстрактного дерева.
210
- - Любой потребитель — рисует UI ноды (заголовок, кнопка, иконка expand) через render-prop.
209
+ ### Nested
211
210
 
212
- ---
211
+ `stories/TreeView/examples/Nested.tsx`
213
212
 
214
- ## Stories
213
+ ```tsx
214
+ import React from "react";
215
+ import clsx from "clsx";
216
+ import { CaretrightStrokeSrvIcon24 } from "@sberbusiness/icons-next";
217
+ import { ETextSize, Text, TreeView } from "@sberbusiness/triplex-next";
218
+ import "./Nested.less";
219
+
220
+ interface INodeData {
221
+ id: string;
222
+ label: string;
223
+ children?: INodeData[];
224
+ }
225
+
226
+ const nodes: INodeData[] = [
227
+ {
228
+ id: "folder-1",
229
+ label: "Folder text",
230
+ children: [
231
+ { id: "file-1-1", label: "File text" },
232
+ { id: "file-1-2", label: "File text" },
233
+ ],
234
+ },
235
+ {
236
+ id: "folder-2",
237
+ label: "Folder text",
238
+ children: [
239
+ {
240
+ id: "folder-2-1",
241
+ label: "Folder text",
242
+ children: [{ id: "file-2-1-1", label: "File text" }],
243
+ },
244
+ ],
245
+ },
246
+ { id: "file-3", label: "File text" },
247
+ ];
248
+
249
+ interface ITreeNodeProps {
250
+ node: INodeData;
251
+ }
252
+
253
+ const TreeNode = ({ node }: ITreeNodeProps) => {
254
+ const [opened, setOpened] = React.useState(false);
255
+ const branch = Boolean(node.children?.length);
256
+
257
+ const toggle = () => setOpened((prevOpened) => !prevOpened);
258
+
259
+ const handleClick = (event: React.MouseEvent<HTMLElement>) => {
260
+ // Клик по вложенной ноде не должен раскрывать родительскую.
261
+ event.stopPropagation();
262
+
263
+ if (branch) {
264
+ toggle();
265
+ }
266
+ };
267
+
268
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
269
+ if (event.key !== "Enter" && event.key !== " ") {
270
+ return;
271
+ }
272
+
273
+ event.preventDefault();
274
+ event.stopPropagation();
275
+
276
+ if (branch) {
277
+ toggle();
278
+ }
279
+ };
280
+
281
+ return (
282
+ <TreeView.Node id={node.id} opened={opened} onClick={handleClick} onKeyDown={handleKeyDown}>
283
+ {({ hasChildNodes, openedNode }) => (
284
+ <>
285
+ <div className={clsx("tree-view-nested-example-row", { branch })}>
286
+ {hasChildNodes ? (
287
+ <CaretrightStrokeSrvIcon24
288
+ paletteIndex={5}
289
+ className={clsx("tree-view-nested-example-chevron", { opened: openedNode })}
290
+ aria-hidden
291
+ />
292
+ ) : (
293
+ <span className="tree-view-nested-example-chevron-placeholder" />
294
+ )}
295
+ <Text size={ETextSize.B1} tag="span">
296
+ {node.label}
297
+ </Text>
298
+ </div>
299
+ {node.children ? (
300
+ // Группа остаётся смонтированной: так вложенные ноды зарегистрированы в дереве
301
+ // и родитель знает про hasChildNodes даже в свёрнутом состоянии.
302
+ <TreeView.Group className={clsx("tree-view-nested-example-group", { collapsed: !openedNode })}>
303
+ {node.children.map((child) => (
304
+ <TreeNode key={child.id} node={child} />
305
+ ))}
306
+ </TreeView.Group>
307
+ ) : null}
308
+ </>
309
+ )}
310
+ </TreeView.Node>
311
+ );
312
+ };
313
+
314
+ export const Nested = () => (
315
+ <div className="tree-view-nested-example" style={{ maxWidth: 420 }}>
316
+ <TreeView aria-label="Tree">
317
+ {nodes.map((node) => (
318
+ <TreeNode key={node.id} node={node} />
319
+ ))}
320
+ </TreeView>
321
+ </div>
322
+ );
323
+ ```
215
324
 
216
- Основные истории: `stories/TreeView/TreeView.stories.tsx`
325
+ ### Keyboard navigation
217
326
 
218
- ---
327
+ `stories/TreeView/examples/KeyboardNavigation.tsx`
219
328
 
220
- ## История изменений
329
+ ```tsx
330
+ import React from "react";
331
+ import clsx from "clsx";
332
+ import { ECaptionSize, ETextSize, Caption, Text, TreeView } from "@sberbusiness/triplex-next";
333
+ import "./KeyboardNavigation.less";
334
+
335
+ interface INodeData {
336
+ id: string;
337
+ label: string;
338
+ children?: INodeData[];
339
+ }
340
+
341
+ const nodes: INodeData[] = [
342
+ {
343
+ id: "folder-1",
344
+ label: "Folder text",
345
+ children: [
346
+ { id: "file-1-1", label: "File text" },
347
+ { id: "file-1-2", label: "File text" },
348
+ ],
349
+ },
350
+ {
351
+ id: "folder-2",
352
+ label: "Folder text",
353
+ children: [{ id: "file-2-1", label: "File text" }],
354
+ },
355
+ ];
356
+
357
+ interface ITreeNodeProps {
358
+ node: INodeData;
359
+ }
360
+
361
+ const TreeNode = ({ node }: ITreeNodeProps) => (
362
+ // Ветки раскрыты сразу: стрелки спускаются только в раскрытые ноды.
363
+ <TreeView.Node id={node.id} opened>
364
+ {({ activeNode, isLastNode }) => (
365
+ <>
366
+ <div className={clsx("tree-view-keyboard-example-row", { active: activeNode })}>
367
+ <Text size={ETextSize.B1} tag="span">
368
+ {node.label}
369
+ </Text>
370
+ {isLastNode ? (
371
+ <Caption size={ECaptionSize.C2} tag="span">
372
+ isLastNode
373
+ </Caption>
374
+ ) : null}
375
+ </div>
376
+ {node.children ? (
377
+ <TreeView.Group>
378
+ {node.children.map((child) => (
379
+ <TreeNode key={child.id} node={child} />
380
+ ))}
381
+ </TreeView.Group>
382
+ ) : null}
383
+ </>
384
+ )}
385
+ </TreeView.Node>
386
+ );
387
+
388
+ export const KeyboardNavigation = () => (
389
+ <div className="tree-view-keyboard-example" style={{ maxWidth: 420 }}>
390
+ <Caption size={ECaptionSize.C1} tag="p">
391
+ Поставьте фокус на любой узел и используйте стрелки вверх/вниз.
392
+ </Caption>
393
+ <TreeView aria-label="Tree">
394
+ {nodes.map((node) => (
395
+ <TreeNode key={node.id} node={node} />
396
+ ))}
397
+ </TreeView>
398
+ </div>
399
+ );
400
+ ```
221
401
 
222
- | Дата | Изменение |
223
- |---|---|
224
- | 2026-04-18 | Создан документ |