@sberbusiness/triplex-next-mcp-server 1.42.0-beta → 1.43.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.
- package/bundle/components/Button.md +1 -0
- package/bundle/components/Checkbox.md +7 -1
- package/bundle/components/CheckboxTreeExtended.md +149 -248
- package/bundle/components/CheckboxXGroup.md +2 -0
- package/bundle/components/CheckboxYGroup.md +7 -2
- package/bundle/components/ChipGroup.md +195 -126
- package/bundle/components/LightBox.md +6 -0
- package/bundle/components/MediaWidth.md +7 -1
- package/bundle/components/Notification.md +21 -3
- package/bundle/components/SliderExtended.md +358 -222
- package/bundle/components/TabsExtended.md +380 -194
- package/bundle/components/UnorderedList.md +254 -0
- package/bundle/components/UnorderedListExtended.md +255 -0
- package/bundle/components/WindowResizeListener.md +249 -0
- package/bundle/guides/commits.md +26 -0
- package/bundle/guides/stories.md +1 -1
- package/bundle/manifest.json +23 -14
- package/bundle/release-notes/1.43.0.mdx +87 -0
- package/bundle/release-notes/1.44.0.mdx +7 -0
- package/package.json +1 -1
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
component: TabsExtended
|
|
3
|
-
category:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
category: Tabs
|
|
4
|
+
related: [Tabs, TabsLine, Badge, Text]
|
|
5
|
+
tokens:
|
|
6
|
+
- --triplex-next-Tabs-Type1_Background
|
|
7
|
+
- --triplex-next-Tabs-Type2_Background
|
|
8
|
+
- --triplex-next-Tabs-Type1_Tab_Background_Default
|
|
9
|
+
- --triplex-next-Tabs-Type2_Tab_Background_Default
|
|
10
|
+
- --triplex-next-Tabs-Type1_Tab_Background_Hover
|
|
11
|
+
- --triplex-next-Tabs-Type2_Tab_Background_Hover
|
|
12
|
+
- --triplex-next-Tabs-Type1_Tab_Background_Selected
|
|
13
|
+
- --triplex-next-Tabs-Type2_Tab_Background_Selected
|
|
14
|
+
- --triplex-next-Tabs-Tab_BorderColor_Default
|
|
15
|
+
- --triplex-next-Tabs-Tab_BorderColor_Focus
|
|
16
|
+
stories: stories/TabsExtended/TabsExtended.stories.tsx
|
|
9
17
|
version: "1.0"
|
|
10
18
|
---
|
|
11
19
|
|
|
@@ -13,261 +21,439 @@ version: "1.0"
|
|
|
13
21
|
|
|
14
22
|
## Назначение
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
для слотов (`Content`, `TabsWrapper`, `Tab`, `TabButton`, `DropdownWrapper`).
|
|
18
|
-
Сам не задаёт компоновку — потребитель выбирает структуру вручную.
|
|
19
|
-
`TabsWrapper` автоматически вычисляет, какие табы не помещаются в строку,
|
|
20
|
-
и передаёт их id в `DropdownWrapper` для overflow-меню.
|
|
24
|
+
Базовый компонент табов-переключателей: сам он задаёт контекст, корневой контейнер с `role="tablist"` и логику «какие табы не поместились в строку», а разметку каждого таба собирает потребитель из субкомпонентов. Табы, которые не помещаются по ширине, автоматически уезжают в выпадающий список — его рендерит потребитель через render-prop.
|
|
21
25
|
|
|
22
|
-
Используй когда: нужны табы с
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
Используй когда: нужны табы с нестандартным содержимым или нестандартным триггером выпадающего списка — то, что нельзя выразить через готовый `Tabs`.
|
|
27
|
+
Не используй когда:
|
|
28
|
+
|
|
29
|
+
- достаточно списка табов «лейбл + значок уведомления» — возьми `Tabs`: он собран поверх `TabsExtended`, добавляет клавиатурную навигацию стрелками и готовую кнопку выпадающего списка;
|
|
30
|
+
- нужны табы-подчёркивания в шапке раздела — возьми `TabsLine`;
|
|
31
|
+
- нужен просто набор переключателей без семантики табов — это не задача компонента.
|
|
25
32
|
|
|
26
33
|
---
|
|
27
34
|
|
|
28
|
-
##
|
|
35
|
+
## Состав (compound-компонент)
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
src/components/TabsExtended/
|
|
32
|
-
├── TabsExtended.tsx # FC + Composition; Provider
|
|
33
|
-
├── TabsExtendedContext.ts # state overflow-распределения
|
|
34
|
-
├── enums.ts # ETabsExtendedType
|
|
35
|
-
├── types.ts # TTabsExtendedOnSelectTab
|
|
36
|
-
├── utils.ts # SIZE→TEXT, TYPE→className maps
|
|
37
|
-
├── components/
|
|
38
|
-
│ ├── TabsExtendedContent.tsx # FC + Composition
|
|
39
|
-
│ ├── TabsExtendedTabsWrapper.tsx # measurement + overflow
|
|
40
|
-
│ ├── TabsExtendedTab.tsx # forwardRef<HTMLSpanElement>
|
|
41
|
-
│ ├── TabsExtendedTabButton.tsx # forwardRef<HTMLButtonElement>
|
|
42
|
-
│ ├── TabsExtendedDropdownWrapper.tsx # render-prop slot
|
|
43
|
-
│ └── TabsExtendedTabContext.ts # { isFakeTab }
|
|
44
|
-
├── styles/
|
|
45
|
-
└── index.ts
|
|
46
|
-
```
|
|
37
|
+
Состав задаётся вложенностью субкомпонентов, порядок важен: `TabsWrapper` должен идти раньше `DropdownWrapper`.
|
|
47
38
|
|
|
48
|
-
|
|
39
|
+
| Субкомпонент | Обязателен | Что делает |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `TabsExtended.Content` | Да | Контейнер с фоном, скруглением и внутренними отступами. Несёт размер (`size`) |
|
|
42
|
+
| `TabsExtended.Content.TabsWrapper` | Да | Рендерит табы и скрытую копию табов для замеров. По замерам решает, какие табы уезжают в Dropdown |
|
|
43
|
+
| `TabsExtended.Content.Tab` | Да | Контейнер (`<span>`) одного таба. Render-prop отдаёт `selected`, `isFirstInlineTab`, `isLastInlineTab`. Обрабатывает клик |
|
|
44
|
+
| `TabsExtended.Content.TabButton` | Практически всегда | Кнопка таба: `<button role="tab">` с текстом и опциональным значком уведомлений |
|
|
45
|
+
| `TabsExtended.Content.DropdownWrapper` | Нет | Контейнер выпадающего списка. Render-prop отдаёт id не поместившихся табов и обработчик выбора. Скрыт (`hidden`), пока все табы помещаются |
|
|
49
46
|
|
|
50
|
-
|
|
47
|
+
Без `DropdownWrapper` компонент работает, но не поместившиеся табы просто исчезают из строки — попасть в них будет нельзя.
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
<TabsExtended selectedId={id} onSelectTab={setId} type={ETabsExtendedType.TYPE_1}>
|
|
54
|
-
<TabsExtended.Content size={EComponentSize.MD}>
|
|
55
|
-
<TabsExtended.Content.TabsWrapper>
|
|
56
|
-
{tabs.map(t => (
|
|
57
|
-
<TabsExtended.Content.Tab key={t.id} id={t.id}>
|
|
58
|
-
{({ selected, isFirstInlineTab, isLastInlineTab }) => (
|
|
59
|
-
<TabsExtended.Content.TabButton selected={selected}>
|
|
60
|
-
{t.label}
|
|
61
|
-
</TabsExtended.Content.TabButton>
|
|
62
|
-
)}
|
|
63
|
-
</TabsExtended.Content.Tab>
|
|
64
|
-
))}
|
|
65
|
-
</TabsExtended.Content.TabsWrapper>
|
|
66
|
-
|
|
67
|
-
<TabsExtended.Content.DropdownWrapper>
|
|
68
|
-
{({ dropdownItemsIds, onSelectTab }) => (/* свой dropdown */)}
|
|
69
|
-
</TabsExtended.Content.DropdownWrapper>
|
|
70
|
-
</TabsExtended.Content>
|
|
71
|
-
</TabsExtended>
|
|
72
|
-
```
|
|
49
|
+
---
|
|
73
50
|
|
|
74
|
-
|
|
75
|
-
`TabsExtendedTabsWrapper`, `TabsExtendedTab`, `TabsExtendedTabButton`,
|
|
76
|
-
`TabsExtendedDropdownWrapper`.
|
|
51
|
+
## Варианты и props
|
|
77
52
|
|
|
78
|
-
|
|
53
|
+
`ITabsExtendedProps` расширяет `React.HTMLAttributes<HTMLDivElement>`. Неизвестные компоненту атрибуты уходят на корневой `<div>`.
|
|
79
54
|
|
|
80
|
-
|
|
55
|
+
### Обязательные props
|
|
81
56
|
|
|
82
|
-
|
|
57
|
+
| Prop | Тип | Описание |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `selectedId` | `string` | Id выбранного таба. Компонент полностью управляемый: своего состояния выбора у него нет |
|
|
60
|
+
| `onSelectTab` | `TTabsExtendedOnSelectTab` | Запрос на смену выбранного таба. Вызывается **только** если кликнули по табу, отличному от `selectedId` |
|
|
61
|
+
|
|
62
|
+
### Опциональные props
|
|
83
63
|
|
|
84
|
-
| Prop | Тип |
|
|
64
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
85
65
|
|---|---|---|---|
|
|
86
|
-
| `
|
|
87
|
-
| `onSelectTab` | `(id: string) => void` | — | Колбэк выбора. Не вызывается, если `id === selectedId`. |
|
|
88
|
-
| `type?` | `ETabsExtendedType` | `TYPE_1` | Стиль таба. |
|
|
89
|
-
| `...HTMLAttributes<HTMLDivElement>` | — | — | На корневой div (role=tablist). |
|
|
66
|
+
| `type` | `ETabsExtendedType` | `TYPE_1` | Тип оформления, выбирается по фону **страницы**: `TYPE_1` — серый фон контейнера, для белой страницы; `TYPE_2` — белый фон контейнера, для затенённой страницы. Значение уходит в контекст и применяется к кнопкам таба |
|
|
90
67
|
|
|
91
|
-
###
|
|
68
|
+
### Props субкомпонентов
|
|
92
69
|
|
|
93
|
-
|
|
|
70
|
+
| Субкомпонент | Prop | По умолчанию | Описание |
|
|
94
71
|
|---|---|---|---|
|
|
95
|
-
| `
|
|
96
|
-
|
|
|
72
|
+
| `Content` | `size` | `EComponentSize.MD` | Скругление и внутренние отступы контейнера |
|
|
73
|
+
| `Tab` | `id` | — | Id таба. Сопоставляется с `selectedId` и с id из `DropdownWrapper` |
|
|
74
|
+
| `TabButton` | `selected` | `false` | Выбранное состояние: класс `selected` и `aria-selected` |
|
|
75
|
+
| `TabButton` | `size` | `EComponentSize.MD` | Размер кнопки и размер текста внутри неё |
|
|
76
|
+
| `TabButton` | `showNotificationIcon` | `false` | Значок новых уведомлений (`Badge.Dot`) в правом верхнем углу кнопки |
|
|
97
77
|
|
|
98
|
-
###
|
|
78
|
+
### Ограничения
|
|
99
79
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
```
|
|
80
|
+
- `size` на `Content` и `size` на `TabButton` — **независимые props**. Контейнер не транслирует свой размер кнопкам; передавать нужно оба, иначе отступы контейнера и высота кнопок разойдутся.
|
|
81
|
+
- `Tab` и `DropdownWrapper` принимают `children` только как функцию (render-prop). Обычные узлы туда положить нельзя — тип это запрещает.
|
|
82
|
+
- Компонент не рендерит выпадающий список сам: и триггер, и список — целиком на потребителе. В библиотеке для этого обычно берут `ButtonDropdown`.
|
|
83
|
+
- **Высоту триггера потребитель обязан согласовать с высотой кнопки таба.** `DropdownWrapper` — обычный flex-элемент строки, а строка тянется по самому высокому: `ButtonDropdown` размера MD — 40px против 32px у таба того же размера, поэтому «сырой» триггер растягивает и контейнер (40px → 48px), и сами табы (32px → 40px, из-за `align-items: stretch`). Ориентир по высоте: SM — 20px, MD — 32px, LG — 40px; в библиотеке это зашито в `Tabs` классом `.tabButtonDropdown`, в примерах stories — через `buttonAttributes.style`.
|
|
84
|
+
- У `TabButton` нет собственного оформления для `disabled`: атрибут проставится, но визуально кнопка не изменится.
|
|
85
|
+
- Возможность переопределить роль зависит от порядка спреда и у каждого субкомпонента своя: на `TabsExtended` `role="tablist"` стоит до `...rest` — переопределяется; на `Tab` `role="presentation"` и `data-tab-item-id` стоят после спреда — не переопределяются (и `data-tab-item-id` трогать нельзя, по нему идут замеры); на `TabButton` спред идёт последним — переопределяется всё, включая `role="tab"`, `aria-selected` и `type`.
|
|
107
86
|
|
|
108
|
-
|
|
87
|
+
---
|
|
109
88
|
|
|
110
|
-
|
|
111
|
-
|---|---|---|
|
|
112
|
-
| `selected` | `boolean` | `selectedId === id`. |
|
|
113
|
-
| `isFirstInlineTab` | `boolean` | Первый в `inlineItemsIds`. |
|
|
114
|
-
| `isLastInlineTab` | `boolean` | Последний в `inlineItemsIds`. |
|
|
89
|
+
## Дизайн-токены
|
|
115
90
|
|
|
116
|
-
|
|
91
|
+
Компонент использует токены семейства `Tabs` — они общие с `Tabs`:
|
|
117
92
|
|
|
118
|
-
```
|
|
119
|
-
|
|
93
|
+
```text
|
|
94
|
+
--triplex-next-Tabs-Type1_Background
|
|
95
|
+
--triplex-next-Tabs-Type2_Background
|
|
96
|
+
--triplex-next-Tabs-Type1_Tab_Background_Default
|
|
97
|
+
--triplex-next-Tabs-Type2_Tab_Background_Default
|
|
98
|
+
--triplex-next-Tabs-Type1_Tab_Background_Hover
|
|
99
|
+
--triplex-next-Tabs-Type2_Tab_Background_Hover
|
|
100
|
+
--triplex-next-Tabs-Type1_Tab_Background_Selected
|
|
101
|
+
--triplex-next-Tabs-Type2_Tab_Background_Selected
|
|
102
|
+
--triplex-next-Tabs-Tab_BorderColor_Default
|
|
103
|
+
--triplex-next-Tabs-Tab_BorderColor_Focus
|
|
120
104
|
```
|
|
121
105
|
|
|
122
|
-
|
|
123
|
-
|---|---|---|---|
|
|
124
|
-
| `selected?` | `boolean` | — | Подсветка + `aria-selected`. |
|
|
125
|
-
| `size?` | `EComponentSize` | `MD` | Размер. |
|
|
126
|
-
| `showNotificationIcon?` | `boolean` | — | Рендерит `Badge.Dot`. |
|
|
106
|
+
Собственного префикса `--triplex-next-TabsExtended-*` у компонента нет: переименование токенов `Tabs` ломает оба компонента сразу.
|
|
127
107
|
|
|
128
|
-
|
|
108
|
+
---
|
|
129
109
|
|
|
130
|
-
|
|
110
|
+
## Инварианты
|
|
131
111
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
112
|
+
- `forwardRef` есть у `TabsExtended` и всех пяти публичных субкомпонентов — не убирать. Ref указывает на тот же элемент, который получает `className` и `...rest`: у `TabsWrapper` это **отображаемый** контейнер (`tabsReal`), а не скрытый контейнер замеров.
|
|
113
|
+
- `TabsExtended` — `forwardRef`-компонент со статическим свойством `Content`, у `Content` — статические `TabsWrapper`, `Tab`, `TabButton`, `DropdownWrapper`. Набор статических свойств — часть публичного API.
|
|
114
|
+
- Интерфейс props кнопки называется `ITabsExtendedButtonProps` (без `Tab` в середине) — рассинхрон с именем компонента `TabsExtendedTabButton`. Переименование — breaking change, требует отдельного согласования.
|
|
115
|
+
- Атрибут `data-tab-item-id` на `Tab` — контракт замеров: по нему `TabsWrapper` находит табы среди детей скрытого контейнера. Не переименовывать и не снимать.
|
|
116
|
+
- Скрытый контейнер `tabsFake` обязан идти в разметке **раньше** отображаемого `tabsReal`: на этом построено сравнение позиций.
|
|
117
|
+
- `TabsWrapper` копирует `children` в скрытый контейнер, вырезая `data-`атрибуты потребителя (`stripDataAttributes`). Это нужно, чтобы селекторы по `data-`атрибутам не находили две копии одного таба. Копия создаётся через `React.createElement(child.type, props)` — компонент-обёртка вокруг `Tab` в копию не попадёт корректно, вкладывать `Tab` в собственные обёртки не стоит.
|
|
118
|
+
- У таба, уехавшего в Dropdown, и у любого таба из скрытой копии атрибут `id` снимается — иначе в DOM оказались бы два элемента с одинаковым `id`. `data-tab-item-id` при этом остаётся.
|
|
119
|
+
- Контекст `TabsExtendedContext` и `TabsExtendedTabContext` через barrel `index.ts` не экспортируются (`TabsExtendedTabContext` реэкспортируется из `components/index.ts` — это исторический факт, а не приглашение им пользоваться). Их форма — внутренняя деталь.
|
|
120
|
+
- `Tabs` построен поверх `TabsExtended` и опирается на `isFirstInlineTab` / `isLastInlineTab` в клавиатурной навигации. Любое изменение семантики этих флагов ломает `Tabs`.
|
|
121
|
+
- Значение контекста мемоизировано (`useMemo`), а `onSelectTab` обёрнут в `useCallback`. Возврат к пересозданию объекта на каждый рендер вернёт лишние перерисовки всех табов.
|
|
135
122
|
|
|
136
|
-
|
|
123
|
+
---
|
|
137
124
|
|
|
138
|
-
|
|
125
|
+
## Accessibility
|
|
139
126
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
127
|
+
- Корневой элемент — `role="tablist"`, кнопка таба — `role="tab"` с `aria-selected`. Контейнер таба (`<span>`) помечен `role="presentation"`, чтобы не добавлять лишний уровень в дерево доступности.
|
|
128
|
+
- `aria-controls` и связку с панелью контента компонент **не выставляет** — если табы переключают панели, `id` панели и `aria-controls` проставляет потребитель.
|
|
129
|
+
- Клавиатурной навигации стрелками у `TabsExtended` нет: обработка `ArrowLeft` / `ArrowRight` и roving `tabIndex` живут в `Tabs`. Если строишь свой набор табов на `TabsExtended`, клавиатуру нужно реализовать самому — флаги `isFirstInlineTab` / `isLastInlineTab` из render-prop `Tab` для этого и предназначены.
|
|
130
|
+
- Выбор таба срабатывает по клику на контейнере `Tab`; нативная активация `<button>` по `Enter` / `Space` работает за счёт того, что `TabButton` рендерит настоящий `<button>`. Если подставляешь свой компонент кнопки, он тоже должен рендерить `<button>`, иначе выбор с клавиатуры сломается.
|
|
131
|
+
- Фокус подсвечивается через `:focus-visible` (токен `Tab_BorderColor_Focus`) — только при клавиатурной навигации.
|
|
132
|
+
- Таб, уехавший в Dropdown, скрыт через `display: none` и из порядка обхода клавиатурой выпадает; попасть в него можно только через выпадающий список.
|
|
133
|
+
- Компонент не хардкодит текст: `aria-label` кнопки выпадающего списка задаёт потребитель (библиотека мультиязычная).
|
|
146
134
|
|
|
147
|
-
|
|
135
|
+
---
|
|
148
136
|
|
|
149
|
-
|
|
150
|
-
|---|---|---|
|
|
151
|
-
| `dropdownItemsIds` | `string[]` | Id табов, не поместившихся inline. |
|
|
152
|
-
| `onSelectTab` | `(id: string) => void` | Колбэк выбора (тот же, что в Context). |
|
|
137
|
+
## Связанные компоненты
|
|
153
138
|
|
|
154
|
-
|
|
139
|
+
Отдельного AI.md нет ни у одного субкомпонента семейства — все они описаны здесь. Это область задачи TRI-91 («Scope: только TabsExtended»): в таблице `docs/ai/ROADMAP.md` у `TabsExtended` одна строка.
|
|
155
140
|
|
|
156
|
-
|
|
141
|
+
**Части составного компонента (`components/`):**
|
|
157
142
|
|
|
158
|
-
|
|
143
|
+
- `TabsExtendedContent` — контейнер с фоном и размером; хост статических субкомпонентов.
|
|
144
|
+
- `TabsExtendedTabsWrapper` — замеры и распределение табов между строкой и Dropdown; единственное место, где живёт `useResizeDetector`.
|
|
145
|
+
- `TabsExtendedTab` — контейнер таба, обработка клика, render-prop с состоянием.
|
|
146
|
+
- `TabsExtendedTabButton` — кнопка таба: два слоя текста (обычный и hover-состояние) и значок уведомлений.
|
|
147
|
+
- `TabsExtendedDropdownWrapper` — контейнер выпадающего списка; отдаёт наружу id не поместившихся табов.
|
|
159
148
|
|
|
160
|
-
|
|
149
|
+
**Инфраструктура (через barrel не экспортируется):**
|
|
161
150
|
|
|
162
|
-
|
|
151
|
+
- `TabsExtendedContext` — выбранный таб, распределение табов, ref на Dropdown, тип оформления.
|
|
152
|
+
- `TabsExtendedTabContext` — флаг `isFakeTab`, отличающий скрытую копию таба от отображаемой.
|
|
153
|
+
- `utils.ts` — маппинги «размер → размер текста» и «тип → CSS-класс».
|
|
163
154
|
|
|
164
|
-
|
|
165
|
-
{
|
|
166
|
-
dropdownItemsIds: string[]; // id табов в overflow
|
|
167
|
-
inlineItemsIds: string[]; // id видимых inline
|
|
168
|
-
setDropdownItemsIds(ids): void;
|
|
169
|
-
setInlineItemsIds(ids): void;
|
|
170
|
-
selectedId: string;
|
|
171
|
-
onSelectTab(id): void; // wrapper, не вызывает onSelect повторно
|
|
172
|
-
type: ETabsExtendedType;
|
|
173
|
-
dropdownRef: RefObject<HTMLDivElement>; // на DropdownWrapper, для замера ширины
|
|
174
|
-
}
|
|
175
|
-
```
|
|
155
|
+
**Соседние компоненты:**
|
|
176
156
|
|
|
177
|
-
|
|
157
|
+
- `Tabs` — готовые табы поверх `TabsExtended`: принимает массив `tabs`, сам рендерит `ButtonDropdown` и добавляет навигацию стрелками. Первый выбор для типовой задачи.
|
|
158
|
+
- `TabsLine` — альтернативный визуальный паттерн табов (подчёркивание вместо залитой кнопки), собственной логики переполнения не имеет.
|
|
159
|
+
- `Badge` — `TabButton` рендерит `Badge.Dot` как значок новых уведомлений.
|
|
160
|
+
- `Text` — текст внутри `TabButton`; размер выбирается по `size` через `TABS_EXTENDED_SIZE_TO_TEXT_SIZE_MAP`.
|
|
161
|
+
- `ButtonDropdown` — компонент, которым в примерах и в `Tabs` заполняется `DropdownWrapper`. Зависимости от него у `TabsExtended` нет.
|
|
178
162
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Stories
|
|
166
|
+
|
|
167
|
+
Основные истории: `stories/TabsExtended/TabsExtended.stories.tsx`
|
|
168
|
+
Файлы примеров: `stories/TabsExtended/examples/`
|
|
182
169
|
|
|
183
|
-
|
|
170
|
+
| Story | Example file | Что демонстрирует |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `Playground` | `Playground.tsx` | Интерактивный контроль `type`, размера, количества табов и ширины контейнера |
|
|
173
|
+
| `Default` | `Default.tsx` | Минимальный состав: три таба без выпадающего списка |
|
|
174
|
+
| `Types` | `Types.tsx` | Типы оформления `TYPE_1` и `TYPE_2` |
|
|
175
|
+
| `Sizes` | `Sizes.tsx` | Размеры SM / MD / LG на `Content` и `TabButton` |
|
|
176
|
+
| `WithNotificationIcon` | `WithNotificationIcon.tsx` | Значок новых уведомлений на табах |
|
|
177
|
+
| `WithDropdown` | `WithDropdown.tsx` | Узкий контейнер: часть табов уезжает в `ButtonDropdown` |
|
|
178
|
+
| `VisualTests` | `VisualTests.tsx` | Типы × размеры, значок уведомлений, раскрытый выпадающий список, `:focus-visible` на табе (через `play`) |
|
|
184
179
|
|
|
185
180
|
---
|
|
186
181
|
|
|
187
|
-
##
|
|
182
|
+
## История изменений
|
|
188
183
|
|
|
189
|
-
|
|
184
|
+
| Дата | Изменение |
|
|
185
|
+
|---|---|
|
|
186
|
+
| 2026-08-14 | Создан документ AI-ready для `TabsExtended` (TRI-91). |
|
|
187
|
+
| 2026-08-14 | Правки по ревью (документация и stories, кода компонента не касаются): в «Ограничения» добавлено требование согласовывать высоту триггера Dropdown с высотой кнопки таба — «сырой» `ButtonDropdown` растягивает контейнер и табы; в примерах stories это согласование сделано через `buttonAttributes.style` (проверено в браузере: контейнер 48px → 40px, таб 40px → 32px). Описание `ETabsExtendedType` в JSDoc и здесь приведено к одной формулировке — тип выбирается по фону страницы, а не контейнера. |
|
|
188
|
+
| 2026-08-14 | AI-рефакторинг: `TabsExtended`, `TabsExtendedContent`, `TabsExtendedTabsWrapper` и `TabsExtendedDropdownWrapper` переведены с `React.FC` на `forwardRef` (у `DropdownWrapper` ref потребителя объединён с внутренним ref замеров), у всех субкомпонентов появился `displayName`. Значение контекста мемоизировано. В `TabsWrapper` убраны non-null assertion'ы, чтение ref во время рендера заменено на состояние, зависимости `useLayoutEffect` приведены в порядок, константы и `stripDataAttributes` вынесены на module scope. JSDoc проставлен на всех публичных props, enum и типах. Публичный API (имена props, значения enum, barrel-экспорты) не изменён. Добавлены unit-тесты на все субкомпоненты и stories по modern pattern. |
|
|
190
189
|
|
|
191
|
-
|
|
192
|
-
2. **`tabsReal`** (видимый) — настоящие табы.
|
|
190
|
+
---
|
|
193
191
|
|
|
194
|
-
|
|
195
|
-
- Идёт по детям `tabsFake`, читает `getBoundingClientRect`.
|
|
196
|
-
- Если `right > containerRight - 4 - dropdownWidth` → таб уходит в `dropdownIds`.
|
|
197
|
-
- `containerRight - 4` — учёт padding (2px) + gap (2px).
|
|
198
|
-
- Если **хотя бы один** таб реально вылез за `containerRight` (`overflow = true`),
|
|
199
|
-
выставляется `setDropdownItemsIds(dropdownIds)`. Иначе — `[]` (всё помещается).
|
|
200
|
-
- Inline-табы — те, у которых `data-tab-item-id` есть и right укладывается.
|
|
192
|
+
## Примеры использования
|
|
201
193
|
|
|
202
|
-
|
|
203
|
-
`useLayoutEffect` каждый рендер; при изменении — повторный замер.
|
|
194
|
+
### Default
|
|
204
195
|
|
|
205
|
-
|
|
196
|
+
`stories/TabsExtended/examples/Default.tsx`
|
|
206
197
|
|
|
207
|
-
|
|
198
|
+
```tsx
|
|
199
|
+
import React, { useState } from "react";
|
|
200
|
+
import { TabsExtended } from "@sberbusiness/triplex-next";
|
|
201
|
+
|
|
202
|
+
const TABS = [
|
|
203
|
+
{ id: "tab-1", label: "Обзор" },
|
|
204
|
+
{ id: "tab-2", label: "Операции" },
|
|
205
|
+
{ id: "tab-3", label: "Документы" },
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
export const Default = () => {
|
|
209
|
+
const [selectedId, setSelectedId] = useState("tab-1");
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<TabsExtended selectedId={selectedId} onSelectTab={setSelectedId}>
|
|
213
|
+
<TabsExtended.Content>
|
|
214
|
+
<TabsExtended.Content.TabsWrapper>
|
|
215
|
+
{TABS.map(({ id, label }) => (
|
|
216
|
+
<TabsExtended.Content.Tab key={id} id={id}>
|
|
217
|
+
{({ selected }) => (
|
|
218
|
+
<TabsExtended.Content.TabButton selected={selected}>
|
|
219
|
+
{label}
|
|
220
|
+
</TabsExtended.Content.TabButton>
|
|
221
|
+
)}
|
|
222
|
+
</TabsExtended.Content.Tab>
|
|
223
|
+
))}
|
|
224
|
+
</TabsExtended.Content.TabsWrapper>
|
|
225
|
+
</TabsExtended.Content>
|
|
226
|
+
</TabsExtended>
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
```
|
|
208
230
|
|
|
209
|
-
|
|
210
|
-
- `id` атрибут НЕ ставится, если `isFakeTab` или таб в `dropdownItemsIds` (избегаем дублирования id в DOM).
|
|
211
|
-
- `onClick` блокируется в fake-режиме.
|
|
212
|
-
- Видимость: при попадании в overflow — добавляется `styles.hidden` (display:none), но таб остаётся в DOM (для пересчёта inline-кадра).
|
|
231
|
+
### Types
|
|
213
232
|
|
|
214
|
-
|
|
233
|
+
`stories/TabsExtended/examples/Types.tsx`
|
|
215
234
|
|
|
216
|
-
|
|
235
|
+
```tsx
|
|
236
|
+
import React, { useState } from "react";
|
|
237
|
+
import { ETabsExtendedType, TabsExtended } from "@sberbusiness/triplex-next";
|
|
217
238
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
-
|
|
221
|
-
|
|
239
|
+
const TABS = [
|
|
240
|
+
{ id: "tab-1", label: "Обзор" },
|
|
241
|
+
{ id: "tab-2", label: "Операции" },
|
|
242
|
+
{ id: "tab-3", label: "Документы" },
|
|
243
|
+
];
|
|
222
244
|
|
|
223
|
-
|
|
245
|
+
interface ITypeItemProps {
|
|
246
|
+
type: ETabsExtendedType;
|
|
247
|
+
}
|
|
224
248
|
|
|
225
|
-
|
|
249
|
+
const TypeItem = ({ type }: ITypeItemProps) => {
|
|
250
|
+
const [selectedId, setSelectedId] = useState("tab-1");
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<div>
|
|
254
|
+
<div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>{type}</div>
|
|
255
|
+
<TabsExtended type={type} selectedId={selectedId} onSelectTab={setSelectedId}>
|
|
256
|
+
<TabsExtended.Content>
|
|
257
|
+
<TabsExtended.Content.TabsWrapper>
|
|
258
|
+
{TABS.map(({ id, label }) => (
|
|
259
|
+
<TabsExtended.Content.Tab key={id} id={id}>
|
|
260
|
+
{({ selected }) => (
|
|
261
|
+
<TabsExtended.Content.TabButton selected={selected}>
|
|
262
|
+
{label}
|
|
263
|
+
</TabsExtended.Content.TabButton>
|
|
264
|
+
)}
|
|
265
|
+
</TabsExtended.Content.Tab>
|
|
266
|
+
))}
|
|
267
|
+
</TabsExtended.Content.TabsWrapper>
|
|
268
|
+
</TabsExtended.Content>
|
|
269
|
+
</TabsExtended>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const TYPES = Object.values(ETabsExtendedType);
|
|
275
|
+
|
|
276
|
+
export const Types = () => (
|
|
277
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
|
|
278
|
+
{TYPES.map((type) => (
|
|
279
|
+
<TypeItem key={type} type={type} />
|
|
280
|
+
))}
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
```
|
|
226
284
|
|
|
227
|
-
|
|
228
|
-
- **`TabsExtended.Content` — `FC` + composition** (`TabsWrapper`/`Tab`/`TabButton`/`DropdownWrapper`).
|
|
229
|
-
- **`TabsExtendedTab` и `TabsExtendedTabButton` — `forwardRef`** (Span/Button). Не убирать.
|
|
230
|
-
- **`Tab.children` — render-prop**: `(state) => ReactNode`. Не accept'ить плоские дети.
|
|
231
|
-
- **`DropdownWrapper.children` — render-prop**: получает `dropdownItemsIds` + `onSelectTab`.
|
|
232
|
-
- **`TabsWrapper` рендерит две копии children** (fake + real). Любой таб должен корректно работать как fake (без `id`, без onClick).
|
|
233
|
-
- **`stripDataAttributes` чистит только `data-*` пропсы у прямых children** (на 1 уровень). Глубже — потребитель ответственен.
|
|
234
|
-
- **`onSelectTab` в Context — wrapper**, который НЕ вызывает props.onSelectTab при `id === selectedId`. Дедупликация выбора.
|
|
235
|
-
- **`dropdownRef` — на `DropdownWrapper`** (не на самой кнопке dropdown'а потребителя). Используется для замера зарезервированной ширины.
|
|
236
|
-
- **`role="tablist"` / `role="tab"` / `aria-selected`** — обязательная семантика; не переопределять без причины.
|
|
237
|
-
- **`ETabsExtendedType` экспортируется** — часть публичного API.
|
|
285
|
+
### Sizes
|
|
238
286
|
|
|
239
|
-
|
|
287
|
+
`stories/TabsExtended/examples/Sizes.tsx`
|
|
240
288
|
|
|
241
|
-
|
|
289
|
+
```tsx
|
|
290
|
+
import React, { useState } from "react";
|
|
291
|
+
import { EComponentSize, TabsExtended } from "@sberbusiness/triplex-next";
|
|
242
292
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
- Таб в overflow не имеет id и убран из таб-ордера через `display:none` —
|
|
249
|
-
попадает в кастомный dropdown потребителя (там ARIA — на потребителе).
|
|
293
|
+
const TABS = [
|
|
294
|
+
{ id: "tab-1", label: "Обзор" },
|
|
295
|
+
{ id: "tab-2", label: "Операции" },
|
|
296
|
+
{ id: "tab-3", label: "Документы" },
|
|
297
|
+
];
|
|
250
298
|
|
|
251
|
-
|
|
299
|
+
interface ISizeItemProps {
|
|
300
|
+
size: EComponentSize;
|
|
301
|
+
}
|
|
252
302
|
|
|
253
|
-
|
|
303
|
+
const SizeItem = ({ size }: ISizeItemProps) => {
|
|
304
|
+
const [selectedId, setSelectedId] = useState("tab-1");
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div>
|
|
308
|
+
<div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>{size.toUpperCase()}</div>
|
|
309
|
+
<TabsExtended selectedId={selectedId} onSelectTab={setSelectedId}>
|
|
310
|
+
{/* size задаётся отдельно: на Content — отступы и скругление контейнера, на TabButton — высота кнопки. */}
|
|
311
|
+
<TabsExtended.Content size={size}>
|
|
312
|
+
<TabsExtended.Content.TabsWrapper>
|
|
313
|
+
{TABS.map(({ id, label }) => (
|
|
314
|
+
<TabsExtended.Content.Tab key={id} id={id}>
|
|
315
|
+
{({ selected }) => (
|
|
316
|
+
<TabsExtended.Content.TabButton selected={selected} size={size}>
|
|
317
|
+
{label}
|
|
318
|
+
</TabsExtended.Content.TabButton>
|
|
319
|
+
)}
|
|
320
|
+
</TabsExtended.Content.Tab>
|
|
321
|
+
))}
|
|
322
|
+
</TabsExtended.Content.TabsWrapper>
|
|
323
|
+
</TabsExtended.Content>
|
|
324
|
+
</TabsExtended>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const SIZES = Object.values(EComponentSize);
|
|
330
|
+
|
|
331
|
+
export const Sizes = () => (
|
|
332
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
|
|
333
|
+
{SIZES.map((size) => (
|
|
334
|
+
<SizeItem key={size} size={size} />
|
|
335
|
+
))}
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
```
|
|
254
339
|
|
|
255
|
-
|
|
256
|
-
- `TabsLine` — горизонтальные линейные табы (без overflow, своя реализация).
|
|
257
|
-
- `Badge.Dot` — индикатор в `TabButton`.
|
|
258
|
-
- `Typography.Text` — текст лейбла.
|
|
340
|
+
### WithNotificationIcon
|
|
259
341
|
|
|
260
|
-
|
|
342
|
+
`stories/TabsExtended/examples/WithNotificationIcon.tsx`
|
|
261
343
|
|
|
262
|
-
|
|
344
|
+
```tsx
|
|
345
|
+
import React, { useState } from "react";
|
|
346
|
+
import { TabsExtended } from "@sberbusiness/triplex-next";
|
|
347
|
+
|
|
348
|
+
const TABS = [
|
|
349
|
+
{ id: "tab-1", label: "Обзор", showNotificationIcon: false },
|
|
350
|
+
{ id: "tab-2", label: "Операции", showNotificationIcon: true },
|
|
351
|
+
{ id: "tab-3", label: "Документы", showNotificationIcon: true },
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
export const WithNotificationIcon = () => {
|
|
355
|
+
const [selectedId, setSelectedId] = useState("tab-1");
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<TabsExtended selectedId={selectedId} onSelectTab={setSelectedId}>
|
|
359
|
+
<TabsExtended.Content>
|
|
360
|
+
<TabsExtended.Content.TabsWrapper>
|
|
361
|
+
{TABS.map(({ id, label, showNotificationIcon }) => (
|
|
362
|
+
<TabsExtended.Content.Tab key={id} id={id}>
|
|
363
|
+
{({ selected }) => (
|
|
364
|
+
<TabsExtended.Content.TabButton
|
|
365
|
+
selected={selected}
|
|
366
|
+
showNotificationIcon={showNotificationIcon}
|
|
367
|
+
>
|
|
368
|
+
{label}
|
|
369
|
+
</TabsExtended.Content.TabButton>
|
|
370
|
+
)}
|
|
371
|
+
</TabsExtended.Content.Tab>
|
|
372
|
+
))}
|
|
373
|
+
</TabsExtended.Content.TabsWrapper>
|
|
374
|
+
</TabsExtended.Content>
|
|
375
|
+
</TabsExtended>
|
|
376
|
+
);
|
|
377
|
+
};
|
|
378
|
+
```
|
|
263
379
|
|
|
264
|
-
|
|
265
|
-
в `src/components/TabsExtended/components/index.ts` (`Type1Example`, `Type2Example`, `PlaygroundExample`).
|
|
380
|
+
### WithDropdown
|
|
266
381
|
|
|
267
|
-
|
|
382
|
+
`stories/TabsExtended/examples/WithDropdown.tsx`
|
|
268
383
|
|
|
269
|
-
|
|
384
|
+
```tsx
|
|
385
|
+
import React, { useState } from "react";
|
|
386
|
+
import {
|
|
387
|
+
ButtonDropdown,
|
|
388
|
+
EButtonDotsTheme,
|
|
389
|
+
EComponentSize,
|
|
390
|
+
ITabsExtendedDropdownWrapperProvideProps,
|
|
391
|
+
TabsExtended,
|
|
392
|
+
} from "@sberbusiness/triplex-next";
|
|
393
|
+
|
|
394
|
+
const TABS = [
|
|
395
|
+
{ id: "tab-1", label: "Обзор" },
|
|
396
|
+
{ id: "tab-2", label: "Операции" },
|
|
397
|
+
{ id: "tab-3", label: "Документы" },
|
|
398
|
+
{ id: "tab-4", label: "Реквизиты" },
|
|
399
|
+
{ id: "tab-5", label: "Выписка" },
|
|
400
|
+
{ id: "tab-6", label: "Настройки" },
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Высоту триггера нужно согласовать с высотой кнопки таба: по умолчанию кнопка размера MD
|
|
405
|
+
* выше таба (40px против 32px) и растягивает по себе и контейнер, и сами табы.
|
|
406
|
+
*/
|
|
407
|
+
const DROPDOWN_TRIGGER_STYLE: React.CSSProperties = {
|
|
408
|
+
minWidth: "32px",
|
|
409
|
+
minHeight: "32px",
|
|
410
|
+
height: "32px",
|
|
411
|
+
padding: 0,
|
|
412
|
+
justifyContent: "center",
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
export const WithDropdown = () => {
|
|
416
|
+
const [selectedId, setSelectedId] = useState("tab-1");
|
|
417
|
+
|
|
418
|
+
/** Табы, не поместившиеся в строку, превращаются в опции выпадающего списка. */
|
|
419
|
+
const getDropdownOptions = ({ dropdownItemsIds, onSelectTab }: ITabsExtendedDropdownWrapperProvideProps) =>
|
|
420
|
+
TABS.filter(({ id }) => dropdownItemsIds.includes(id)).map((tab) => ({
|
|
421
|
+
...tab,
|
|
422
|
+
onSelect: () => onSelectTab(tab.id),
|
|
423
|
+
}));
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
// Ширина контейнера ограничена, чтобы часть табов не поместилась в строку.
|
|
427
|
+
<div style={{ maxWidth: "420px" }}>
|
|
428
|
+
<TabsExtended selectedId={selectedId} onSelectTab={setSelectedId}>
|
|
429
|
+
<TabsExtended.Content>
|
|
430
|
+
<TabsExtended.Content.TabsWrapper>
|
|
431
|
+
{TABS.map(({ id, label }) => (
|
|
432
|
+
<TabsExtended.Content.Tab key={id} id={id}>
|
|
433
|
+
{({ selected }) => (
|
|
434
|
+
<TabsExtended.Content.TabButton selected={selected}>
|
|
435
|
+
{label}
|
|
436
|
+
</TabsExtended.Content.TabButton>
|
|
437
|
+
)}
|
|
438
|
+
</TabsExtended.Content.Tab>
|
|
439
|
+
))}
|
|
440
|
+
</TabsExtended.Content.TabsWrapper>
|
|
441
|
+
|
|
442
|
+
<TabsExtended.Content.DropdownWrapper>
|
|
443
|
+
{(provideProps) => (
|
|
444
|
+
<ButtonDropdown
|
|
445
|
+
theme={EButtonDotsTheme.DOTS_SECONDARY}
|
|
446
|
+
size={EComponentSize.MD}
|
|
447
|
+
options={getDropdownOptions(provideProps)}
|
|
448
|
+
selected={TABS.find(({ id }) => id === selectedId)}
|
|
449
|
+
buttonAttributes={{ "aria-label": "Ещё табы", style: DROPDOWN_TRIGGER_STYLE }}
|
|
450
|
+
/>
|
|
451
|
+
)}
|
|
452
|
+
</TabsExtended.Content.DropdownWrapper>
|
|
453
|
+
</TabsExtended.Content>
|
|
454
|
+
</TabsExtended>
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
};
|
|
458
|
+
```
|
|
270
459
|
|
|
271
|
-
| Дата | Изменение |
|
|
272
|
-
|---|---|
|
|
273
|
-
| 2026-04-18 | Создан документ |
|