@sberbusiness/triplex-next-mcp-server 1.40.0-beta → 1.41.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/Calendar.md +183 -368
- package/bundle/components/Chip.md +310 -314
- package/bundle/components/FormField.md +468 -232
- package/bundle/components/IconWrapper.md +142 -89
- package/bundle/components/Island.md +221 -129
- package/bundle/components/KeyDownListener.md +215 -102
- package/bundle/components/LoaderMiddle.md +265 -0
- package/bundle/components/LoaderScreen.md +178 -81
- package/bundle/components/ThemeProvider.md +305 -117
- package/bundle/components/Tooltip.md +421 -181
- package/bundle/guides/commits.md +14 -6
- package/bundle/manifest.json +22 -11
- package/bundle/release-notes/1.41.0.mdx +76 -0
- package/bundle/release-notes/1.42.0.mdx +7 -0
- package/package.json +1 -1
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
component: Tooltip
|
|
3
|
-
category:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
category: Tooltip
|
|
4
|
+
related: [HelpBox, SMSField, Dropdown, DropdownMobile, Portal, MobileView, ThemeProvider, ButtonIcon]
|
|
5
|
+
tokens:
|
|
6
|
+
- --triplex-next-Tooltip-Background
|
|
7
|
+
- --triplex-next-Tooltip-Color
|
|
8
|
+
- --triplex-next-Tooltip-Link_Desktop_Color_Default
|
|
9
|
+
- --triplex-next-Tooltip-Link_Desktop_Color_Hover
|
|
10
|
+
- --triplex-next-Tooltip-Link_Desktop_Color_Active
|
|
11
|
+
- --triplex-next-Tooltip-Link_Desktop_Shadow_Focus
|
|
12
|
+
- --triplex-next-Tooltip-Link_Mobile_Color_Default
|
|
13
|
+
- --triplex-next-Tooltip-Link_Mobile_Color_Hover
|
|
14
|
+
- --triplex-next-Tooltip-Link_Mobile_Color_Active
|
|
15
|
+
- --triplex-next-Tooltip-Link_Mobile_Shadow_Focus
|
|
8
16
|
stories: stories/Tooltip/Tooltip.stories.tsx
|
|
9
17
|
version: "1.0"
|
|
10
18
|
---
|
|
@@ -13,242 +21,474 @@ version: "1.0"
|
|
|
13
21
|
|
|
14
22
|
## Назначение
|
|
15
23
|
|
|
16
|
-
Всплывающая
|
|
17
|
-
(`above`/`below`/`right`/`left`) и адаптивным переключением desktop/mobile.
|
|
18
|
-
Контент компонуется через слоты: `Target` (триггер) + `Body` (контент) +
|
|
19
|
-
опционально `Link`, `XButton` (закрыть), `MobileHeader`. Управление —
|
|
20
|
-
hover или click.
|
|
24
|
+
Всплывающая подсказка, привязанная к целевому элементу. Рендерится через `Portal` — по умолчанию в `document.body`, либо в контейнер из `renderContainer` — и позиционируется относительно `targetRef`. На мобильной ширине экрана (<768px) вместо всплывающей подсказки открывается полноэкранный оверлей снизу на базе `DropdownMobile`.
|
|
21
25
|
|
|
22
|
-
Используй когда:
|
|
23
|
-
|
|
26
|
+
Используй когда: нужно пояснить элемент интерфейса коротким текстом, при необходимости со ссылкой на подробное описание.
|
|
27
|
+
Не используй когда:
|
|
28
|
+
|
|
29
|
+
- нужна готовая иконка-вопрос с подсказкой и мобильным заголовком — используй `HelpBox` (обёртка над `Tooltip`);
|
|
30
|
+
- нужно выпадающее меню или произвольный интерактивный блок — используй `Dropdown` / `ButtonDropdownExtended`;
|
|
31
|
+
- содержимое подсказки требует фокусируемых элементов кроме `Tooltip.Link`: компонент не перехватывает фокус и не строит focus trap.
|
|
24
32
|
|
|
25
33
|
---
|
|
26
34
|
|
|
27
|
-
##
|
|
35
|
+
## Состав (compound-компонент)
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
│ │ ├── TooltipBody.tsx # forwardRef<HTMLDivElement>; MobileView для Text-обёртки
|
|
39
|
-
│ │ ├── TooltipLink.tsx
|
|
40
|
-
│ │ └── TooltipXButton.tsx # children: never
|
|
41
|
-
│ ├── desktop/
|
|
42
|
-
│ │ ├── TooltipDesktop.tsx
|
|
43
|
-
│ │ └── components/ # позиционирование, портал
|
|
44
|
-
│ └── mobile/
|
|
45
|
-
│ ├── TooltipMobile.tsx
|
|
46
|
-
│ └── components/TooltipMobileHeader.tsx
|
|
47
|
-
├── utils/
|
|
48
|
-
│ ├── Positioning.ts # геометрия для desktop размещения
|
|
49
|
-
│ └── useTooltipTheme.ts # инжект темы в renderContainer
|
|
50
|
-
├── styles/
|
|
51
|
-
└── index.ts
|
|
52
|
-
```
|
|
37
|
+
Состав задаётся субкомпонентами внутри `children`. Компонент разбирает `children` **по типу элемента**, поэтому порядок в разметке не важен, а произвольные узлы (текст, `<div>`, фрагменты) просто игнорируются.
|
|
38
|
+
|
|
39
|
+
| Субкомпонент | Обязателен | Что делает |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `Tooltip.Target` | Да | Клонирует единственный дочерний элемент, навешивая обработчики открытия. Собственной разметки не рендерит |
|
|
42
|
+
| `Tooltip.Body` | Практически всегда | Текст подсказки. На десктопе — `<div>`, в мобильной версии — `Text` (`ETextSize.B3`) |
|
|
43
|
+
| `Tooltip.Link` | Нет | Гиперссылка под текстом. При `target="_blank"` сам подставляет `rel="noopener"` |
|
|
44
|
+
| `Tooltip.XButton` | Нет | Кнопка закрытия. Её наличие добавляет корню класс `closable` (отступ справа у контента). В адаптивном режиме кнопка закрытия есть всегда, независимо от `Tooltip.XButton`, — он лишь передаёт ей свои props (в том числе `aria-label`) |
|
|
45
|
+
| `Tooltip.MobileHeader` | Нет | Заголовок мобильной версии. На десктопе не рендерится |
|
|
53
46
|
|
|
54
47
|
---
|
|
55
48
|
|
|
56
|
-
##
|
|
49
|
+
## Варианты и props
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
<Tooltip
|
|
60
|
-
targetRef={btnRef}
|
|
61
|
-
size={ETooltipSize.LG}
|
|
62
|
-
toggleType="hover"
|
|
63
|
-
preferPlace={ETooltipPreferPlace.ABOVE}
|
|
64
|
-
isOpen={open}
|
|
65
|
-
toggle={setOpen}
|
|
66
|
-
>
|
|
67
|
-
<Tooltip.Target>
|
|
68
|
-
<button ref={btnRef}>?</button>
|
|
69
|
-
</Tooltip.Target>
|
|
70
|
-
<Tooltip.Body>Подсказка</Tooltip.Body>
|
|
71
|
-
<Tooltip.Link href="/help">Подробнее</Tooltip.Link>
|
|
72
|
-
<Tooltip.XButton aria-label="Закрыть" />
|
|
73
|
-
<Tooltip.MobileHeader>Заголовок</Tooltip.MobileHeader>
|
|
74
|
-
</Tooltip>
|
|
75
|
-
```
|
|
51
|
+
`ITooltipProps` расширяет `React.HTMLAttributes<HTMLDivElement>`. Неизвестные компоненту атрибуты уходят на корневой `<div>` десктопной подсказки (и на контейнер мобильного оверлея в адаптивном режиме). Исключение — `onKeyDown`: см. «Ограничения».
|
|
76
52
|
|
|
77
|
-
|
|
78
|
-
типу через `React.Children.map` (см. `getChildrenElements`); порядок
|
|
79
|
-
расположения в JSX неважен.
|
|
53
|
+
### Обязательные props
|
|
80
54
|
|
|
81
|
-
|
|
55
|
+
| Prop | Тип | Описание |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `size` | `ETooltipSize` | Ширина тела подсказки в десктопной версии: `SM` — 192px, `LG` — 384px. В мобильной версии не влияет ни на что |
|
|
58
|
+
| `targetRef` | `React.MutableRefObject<HTMLElement \| null>` | Ссылка на целевой элемент. От его `getBoundingClientRect()` считается положение подсказки и на него вешаются hover-обработчики. Тот же ref нужно передать и в сам элемент внутри `Tooltip.Target` |
|
|
82
59
|
|
|
83
|
-
|
|
60
|
+
### Опциональные props
|
|
84
61
|
|
|
85
|
-
|
|
62
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `toggleType` | `"hover" \| "click"` | — | Способ открытия. Без значения подсказка сама не открывается — состоянием управляет потребитель через `isOpen` |
|
|
65
|
+
| `preferPlace` | `ETooltipPreferPlace` | — | Предпочитаемое место. Если подсказка туда не помещается, выбирается зона, где помещается. Без значения зона выбирается автоматически |
|
|
66
|
+
| `alignTip` | `ETooltipAlign` | — | Принудительное выравнивание указателя (стрелочки): `START` / `END`. Без значения указатель центрируется по целевому элементу |
|
|
67
|
+
| `isOpen` | `boolean` | — | Управляемый режим. Пока prop задан (в том числе `false`), внутреннее состояние открытости игнорируется |
|
|
68
|
+
| `toggle` | `(open: boolean) => void` | — | Запрос на смену состояния. Вызывается всегда — и в управляемом, и в неуправляемом режиме |
|
|
69
|
+
| `onShow` | `(node: HTMLDivElement) => void` | — | Вызывается при открытии, аргумент — DOM-нода подсказки (десктоп) или контентная нода оверлея (мобильная версия) |
|
|
70
|
+
| `renderContainer` | `Element` | `document.body` | Контейнер портала для десктопной версии. Мобильная версия всегда рендерится в `document.body` |
|
|
71
|
+
| `disableAdaptiveMode` | `boolean` | `false` | Отключает мобильную версию: подсказка остаётся всплывающей на любой ширине экрана |
|
|
72
|
+
| `className` | `string` | — | Дополнительный класс на корневом элементе подсказки |
|
|
73
|
+
|
|
74
|
+
### Ограничения
|
|
75
|
+
|
|
76
|
+
- **`Tooltip` — не `forwardRef`-компонент.** Это провайдер контекста без собственной разметки: ссылка на подсказку отдаётся через `onShow`, ссылка на целевой элемент — это `targetRef` потребителя. `forwardRef` есть у `Tooltip.Body`, `Tooltip.Link` и внутреннего `TooltipDesktopTip`.
|
|
77
|
+
- `Tooltip.Target` принимает **ровно один** React-элемент, который умеет принимать `onClick` / `onKeyDown`.
|
|
78
|
+
- `Tooltip.XButton` не принимает `children` (`children?: never`) — иконка крестика внутри фиксированная.
|
|
79
|
+
- `size`, `preferPlace`, `alignTip` и `renderContainer` действуют только в десктопной версии.
|
|
80
|
+
- `onKeyDown`, переданный в `Tooltip`, на десктопе до корневого элемента подсказки не доходит: `TooltipDesktopBase` вырезает его из props и никуда не прокидывает. В адаптивном режиме он, наоборот, попадает на контейнер оверлея — поведение одного и того же prop в двух режимах различается. Клавиатурный обработчик вешай на элемент внутри `Tooltip.Target`.
|
|
81
|
+
- `Tooltip.MobileHeader` подставляет кнопку закрытия сам, но слот `controlButtons` при этом остаётся открытым: переданное потребителем значение спредится **после** значения по умолчанию и молча заменяет кнопку закрытия. Сузить тип нельзя без breaking change — на это рассчитывать не стоит.
|
|
82
|
+
- Компонент не хардкодит текст: `aria-label` для целевого элемента и для `Tooltip.XButton` передаёт потребитель (библиотека мультиязычная).
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
extends HTMLAttributes<HTMLDivElement>
|
|
89
|
-
```
|
|
84
|
+
---
|
|
90
85
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
### Слоты
|
|
106
|
-
|
|
107
|
-
#### `Tooltip.Target`
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
{ children: ReactElement } // ровно один React-элемент
|
|
86
|
+
## Дизайн-токены
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
--triplex-next-Tooltip-Background
|
|
90
|
+
--triplex-next-Tooltip-Color
|
|
91
|
+
--triplex-next-Tooltip-Link_Desktop_Color_Default
|
|
92
|
+
--triplex-next-Tooltip-Link_Desktop_Color_Hover
|
|
93
|
+
--triplex-next-Tooltip-Link_Desktop_Color_Active
|
|
94
|
+
--triplex-next-Tooltip-Link_Desktop_Shadow_Focus
|
|
95
|
+
--triplex-next-Tooltip-Link_Mobile_Color_Default
|
|
96
|
+
--triplex-next-Tooltip-Link_Mobile_Color_Hover
|
|
97
|
+
--triplex-next-Tooltip-Link_Mobile_Color_Active
|
|
98
|
+
--triplex-next-Tooltip-Link_Mobile_Shadow_Focus
|
|
111
99
|
```
|
|
112
100
|
|
|
113
|
-
|
|
114
|
-
- `onClick` (если `toggleType === "click"` или `hover` + не висим в hovered-состоянии): `setTooltipOpen(!tooltipOpen)`.
|
|
115
|
-
- `onKeyDown TAB` при открытом — закрывает.
|
|
116
|
-
- Существующие хендлеры на child'е сохраняются (вызываются после).
|
|
101
|
+
Мобильная версия дополнительно наследует токены `DropdownMobile` (`--triplex-next-DropdownMobile-*`), так как построена на его субкомпонентах.
|
|
117
102
|
|
|
118
|
-
|
|
103
|
+
---
|
|
119
104
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
105
|
+
## Инварианты
|
|
106
|
+
|
|
107
|
+
- Публичный barrel `src/components/Tooltip/index.ts` реэкспортирует `Tooltip`, `enums` и `types` целиком. Компонент используется внутри `HelpBox`, `SMSField.Tooltip` и косвенно (через типы) в `Dropdown`, поэтому удаление любого экспорта ломает библиотеку изнутри.
|
|
108
|
+
- `IDropdownMobileHeaderProps` объявлен **в `src/components/Tooltip/types.ts`**, но используется `DropdownMobileHeader` из семейства `Dropdown`. Переносить тип нельзя без синхронной правки `Dropdown` (и это breaking change для deep-импортов).
|
|
109
|
+
- Файл контекста называется `TootlipContext.ts` — опечатка в имени зафиксирована в публикуемых путях подпакета; переименование ломает deep-импорты потребителей.
|
|
110
|
+
- `Tooltip` — `React.FC` со статическими субкомпонентами (`Tooltip.Target` и т.д.). Набор статических свойств — часть публичного API.
|
|
111
|
+
- Разбор `children` по типу элемента (`child.type === TooltipTarget`) означает, что обёртка субкомпонента в собственный компонент-прокси **не сработает** — узел просто не попадёт ни в один слот. Это осознанное ограничение, на нём построен весь состав.
|
|
112
|
+
- Десктопная версия отслеживает положение целевого элемента опросом с интервалом 200мс (`REFRESH_INTERVAL_MS`) плюс слушатели `scroll` / `resize`. Отписка в `componentWillUnmount` и при закрытии обязательна.
|
|
113
|
+
- Анимации появления/исчезновения длятся 500мс (`ENTER_EXIT_TRANSITION_DURATION_MS`). На это же время завязано снятие класса темы в `useTooltipTheme` — значения нужно менять синхронно.
|
|
114
|
+
- `useTooltipTheme` вешает класс темы на контейнер рендера и считает количество открытых подсказок в data-атрибуте `data-tooltip-theme-{scopeClassName}-counter`. Счётчик нужен, чтобы одна закрывшаяся подсказка не сняла тему у остальных.
|
|
115
|
+
- `TooltipDesktop` и `TooltipDesktopBase` — классовые компоненты. Это исключение из правила «только функциональные компоненты» в `docs/ai/codestyle.md`: перевод на хуки затрагивает всю логику позиционирования и анимаций и требует отдельной задачи с визуальной регрессией.
|
|
116
|
+
- Внутренние утилиты (`utils/Positioning.ts`, `utils/useTooltipTheme.ts`) через barrel не экспортируются.
|
|
123
117
|
|
|
124
|
-
|
|
118
|
+
---
|
|
125
119
|
|
|
126
|
-
|
|
120
|
+
## Accessibility
|
|
127
121
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
- Собственных ARIA-ролей и атрибутов компонент **не выставляет** — ни `role="tooltip"`, ни `aria-describedby`. Связать целевой элемент с текстом подсказки должен потребитель (пример такой связки — `SMSField.Tooltip`, который прокидывает `id` подсказки в поле).
|
|
123
|
+
- Целевой элемент отвечает за собственную доступность: он должен быть фокусируемым (`ButtonIcon` в примерах) и иметь `aria-label`, так как иконка-триггер не содержит текста.
|
|
124
|
+
- Клавиатура: `Escape` закрывает подсказку (обработчик на `document`, пока она открыта), `Tab` с целевого элемента закрывает открытую подсказку. Собственных обработчиков `Enter` / `Space` у `Tooltip.Target` нет — открытие с клавиатуры работает через нативный клик кнопки.
|
|
125
|
+
- Закрытие также срабатывает по `mousedown` вне подсказки и вне целевого элемента.
|
|
126
|
+
- Открытие по наведению: подсказка закрывается не сразу после ухода курсора, а через 500мс — за это время курсор можно довести до самой подсказки (её нода тоже слушает `mouseenter`/`mouseleave`).
|
|
127
|
+
- Фокус компонент не перехватывает и не возвращает: при открытии фокус остаётся на целевом элементе. Поэтому интерактивное содержимое сложнее `Tooltip.Link` в подсказку класть не стоит.
|
|
128
|
+
- Мобильная версия — оверлей `DropdownMobile`; кнопке закрытия `aria-label` задаёт потребитель через props `Tooltip.XButton`. Кнопка там рендерится всегда, поэтому без `Tooltip.XButton` она остаётся без доступного имени — в адаптивном сценарии `Tooltip.XButton` с `aria-label` стоит передавать, даже если на десктопе крестик не нужен.
|
|
131
129
|
|
|
132
|
-
|
|
130
|
+
---
|
|
133
131
|
|
|
134
|
-
|
|
132
|
+
## Связанные компоненты
|
|
135
133
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
{
|
|
139
|
-
|
|
134
|
+
Отдельного AI.md нет ни у одного субкомпонента семейства — они описаны здесь.
|
|
135
|
+
Это область задачи TRI-98 («Scope: только Tooltip»), а не вывод по критериям
|
|
136
|
+
`docs/ai/CONTEXT.md` → «Когда создавать `{ComponentName}-ai.md`». По этим критериям
|
|
137
|
+
собственного AI.md заслуживают разве что `HelpBox` (отдельная строка в ROADMAP),
|
|
138
|
+
остальные части семейства — внутренние и через barrel не экспортируются.
|
|
140
139
|
|
|
141
|
-
|
|
140
|
+
**Части составного компонента (`components/common/`, `components/mobile/`):**
|
|
142
141
|
|
|
143
|
-
|
|
142
|
+
- `TooltipTarget` — целевой элемент: `cloneElement` с обработчиками клика и `Tab`.
|
|
143
|
+
- `TooltipBody` — тело подсказки; на мобильной ширине рендерится через `Text`.
|
|
144
|
+
- `TooltipLink` — ссылка; защита от reverse tabnabbing через `getSafeRel`.
|
|
145
|
+
- `TooltipXButton` — кнопка закрытия десктопной версии (обёртка над `ButtonIcon`).
|
|
146
|
+
- `TooltipMobileHeader` — шапка мобильной версии; кнопку закрытия подставляет сам, наследуя props у `Tooltip.XButton`.
|
|
147
|
+
- `TooltipMobileCloseButton` — кнопка закрытия мобильной версии (обёртка над `DropdownMobileClose`).
|
|
144
148
|
|
|
145
|
-
|
|
146
|
-
extends Omit<IDropdownMobileHeaderProps, "closeButton">
|
|
147
|
-
{ controlButtons?: ReactNode }
|
|
148
|
-
```
|
|
149
|
+
**Десктопная версия (`components/desktop/`):**
|
|
149
150
|
|
|
150
|
-
|
|
151
|
+
- `TooltipDesktop` — подписки на события документа и целевого элемента (клик вне, Escape, hover).
|
|
152
|
+
- `TooltipDesktopBase` — портал, расчёт положения, анимации.
|
|
153
|
+
- `TooltipDesktopTip` — указатель (стрелочка) подсказки.
|
|
151
154
|
|
|
152
|
-
|
|
155
|
+
**Инфраструктура:**
|
|
153
156
|
|
|
154
|
-
- `
|
|
155
|
-
- `
|
|
156
|
-
- `
|
|
157
|
-
- `
|
|
157
|
+
- `TooltipContext` (`TootlipContext.ts`) — разобранные слоты, состояние открытости и сеттер между частями.
|
|
158
|
+
- `utils/Positioning.ts` — чистые функции выбора зоны и расчёта координат.
|
|
159
|
+
- `utils/useTooltipTheme.ts` — класс темы на контейнере портала со счётчиком использования.
|
|
160
|
+
- `Portal`, `MobileView`, `DropdownMobile` — инфраструктура рендера.
|
|
158
161
|
|
|
159
|
-
|
|
162
|
+
**Потребители внутри библиотеки:**
|
|
163
|
+
|
|
164
|
+
- `HelpBox` — готовая иконка-подсказка поверх `Tooltip`.
|
|
165
|
+
- `SMSField.Tooltip` — подсказка об ошибке у поля СМС-кода.
|
|
160
166
|
|
|
161
167
|
---
|
|
162
168
|
|
|
163
|
-
##
|
|
169
|
+
## Stories
|
|
164
170
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
Основные истории: `stories/Tooltip/Tooltip.stories.tsx`
|
|
172
|
+
Файлы примеров: `stories/Tooltip/examples/`
|
|
173
|
+
|
|
174
|
+
| Story | Example file | Что демонстрирует |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| `Playground` | `Playground.tsx` | Интерактивный контроль `size`, `toggleType`, `preferPlace`, `alignTip`, адаптивного режима и состава |
|
|
177
|
+
| `Default` | `Default.tsx` | Минимальный состав: целевой элемент и текст, открытие по наведению |
|
|
178
|
+
| `Sizes` | `Sizes.tsx` | Размеры SM (192px) и LG (384px) |
|
|
179
|
+
| `DifferentPlaces` | `DifferentPlaces.tsx` | Все варианты `preferPlace` |
|
|
180
|
+
| `WithLink` | `WithLink.tsx` | Ссылка `Tooltip.Link` под текстом |
|
|
181
|
+
| `WithCloseButton` | `WithCloseButton.tsx` | Кнопка закрытия `Tooltip.XButton` |
|
|
182
|
+
| `MobileHeader` | `MobileHeader.tsx` | Заголовок адаптивной версии (виден на узком экране) |
|
|
183
|
+
| `RenderContainer` | `RenderContainer.tsx` | Рендер подсказки в заданный DOM-элемент |
|
|
184
|
+
| `VisualTests` | `VisualTests.tsx` | `alignTip: start`, `alignTip: end`, широкая подсказка с длинным текстом, ссылкой и кнопкой закрытия |
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## История изменений
|
|
174
189
|
|
|
175
|
-
|
|
176
|
-
|
|
190
|
+
| Дата | Изменение |
|
|
191
|
+
|---|---|
|
|
192
|
+
| 2026-08-03 | Создан документ AI-ready для `Tooltip` (TRI-98). |
|
|
193
|
+
| 2026-08-03 | AI-рефакторинг: разбор `children` вынесен в чистую функцию и мемоизирован, значение контекста стабилизировано, дедуплицирована разметка `TooltipLink`, `switch` в `TooltipDesktopTip` заменён на маппинг, из `calcRelPos` убрано двойное приведение типов, в `useTooltipTheme` вынесен хелпер имени счётчика и добавлен guard на unmount, `displayName` `TooltipDesktopBase` приведён к имени класса, JSDoc на props и приватных методах. Исправлено: подсказка, смонтированная сразу открытой (`isOpen` в управляемом режиме), не закрывалась по действию мышью вне неё — при монтировании вешался слушатель `click`, а снимался `mousedown`. Публичный API не изменён. Добавлены unit-тесты и stories по modern pattern. |
|
|
194
|
+
| 2026-08-03 | Правки по ревью PR #496. Исправлено: класс темы не снимается с контейнера, если в течение задержки закрытия подсказку открыли снова или контейнер занял другой `Tooltip` (в `useTooltipTheme` появились учёт собственного инкремента и перепроверка счётчика). Исправлено: в адаптивном режиме `size` и `alignTip` больше не уходят в DOM (React предупреждал о нераспознанном атрибуте `alignTip`). Исправлено: при размонтировании открытой подсказки с `toggleType="hover"` обработчики наведения снимаются и с самой подсказки. В `collectTooltipElements` приведения через `as` заменены на typed-guard'ы. Публичный API не изменён. |
|
|
195
|
+
| 2026-08-03 | Правки по второму ревью PR #496 (документация и stories, кода не касаются): в «Ограничения» добавлено, что `onKeyDown` на десктопе не доходит до корневого элемента подсказки, а в адаптивном режиме доходит; уточнено, что в адаптивном режиме кнопка закрытия рендерится всегда и без `Tooltip.XButton` остаётся без `aria-label`; в `VisualTests` добавлен блок с `alignTip: end` — раньше это значение enum не было покрыто ни примером, ни тестом. |
|
|
196
|
+
| 2026-08-03 | Правки по третьему ревью PR #496. Исправлено: при смене `toggleType` на лету hover-обработчики синхронизируются и на самой подсказке, а не только на целевом элементе (`setTooltipRef` повторно не вызывается, поэтому раньше на ноде подсказки оставались устаревшие слушатели). Тесты: `Tooltip.Link` в десктопном хелпере вынесен в прямые дети `Tooltip` — раньше слот `elements.link` не исполнялся ни в одном десктопном кейсе; добавлены кейсы на ветку `disableAdaptiveMode`. Публичный API не изменён. |
|
|
177
197
|
|
|
178
198
|
---
|
|
179
199
|
|
|
180
|
-
##
|
|
200
|
+
## Примеры использования
|
|
181
201
|
|
|
182
|
-
###
|
|
202
|
+
### Default
|
|
183
203
|
|
|
184
|
-
|
|
185
|
-
- `disableAdaptiveMode === true` → всегда desktop.
|
|
186
|
-
- Иначе — `<MobileView fallback={Desktop}>{Mobile}</MobileView>`.
|
|
204
|
+
`stories/Tooltip/examples/Default.tsx`
|
|
187
205
|
|
|
188
|
-
|
|
206
|
+
```tsx
|
|
207
|
+
import React, { useRef } from "react";
|
|
208
|
+
import { QuestioncircleFilledSrvIcon16 } from "@sberbusiness/icons-next";
|
|
209
|
+
import { ButtonIcon, ETooltipPreferPlace, ETooltipSize, Tooltip } from "@sberbusiness/triplex-next";
|
|
210
|
+
|
|
211
|
+
export const Default = () => {
|
|
212
|
+
const targetRef = useRef<HTMLButtonElement | null>(null);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div style={{ display: "flex", justifyContent: "center", padding: "80px 0" }}>
|
|
216
|
+
<Tooltip
|
|
217
|
+
size={ETooltipSize.SM}
|
|
218
|
+
toggleType="hover"
|
|
219
|
+
preferPlace={ETooltipPreferPlace.BELOW}
|
|
220
|
+
targetRef={targetRef}
|
|
221
|
+
>
|
|
222
|
+
<Tooltip.Target>
|
|
223
|
+
<ButtonIcon aria-label="Показать подсказку" ref={targetRef}>
|
|
224
|
+
<QuestioncircleFilledSrvIcon16 paletteIndex={5} />
|
|
225
|
+
</ButtonIcon>
|
|
226
|
+
</Tooltip.Target>
|
|
227
|
+
<Tooltip.Body>Текст подсказки</Tooltip.Body>
|
|
228
|
+
</Tooltip>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
```
|
|
189
233
|
|
|
190
|
-
|
|
191
|
-
- `isOpen` не передан → внутренний `openState`.
|
|
192
|
-
- Сброс `targetHoveredRef.current = false` при `openProp === false` (через `useEffect`) — чтобы повторный hover после программного закрытия снова открывал tooltip.
|
|
234
|
+
### Sizes
|
|
193
235
|
|
|
194
|
-
|
|
236
|
+
`stories/Tooltip/examples/Sizes.tsx`
|
|
195
237
|
|
|
196
|
-
|
|
197
|
-
|
|
238
|
+
```tsx
|
|
239
|
+
import React, { useRef } from "react";
|
|
240
|
+
import { QuestioncircleFilledSrvIcon16 } from "@sberbusiness/icons-next";
|
|
241
|
+
import { ButtonIcon, ETooltipPreferPlace, ETooltipSize, Tooltip } from "@sberbusiness/triplex-next";
|
|
198
242
|
|
|
199
|
-
|
|
243
|
+
const TEXT_BY_SIZE: Record<ETooltipSize, string> = {
|
|
244
|
+
[ETooltipSize.SM]: "Узкая подсказка шириной 192 пикселя.",
|
|
245
|
+
[ETooltipSize.LG]: "Широкая подсказка шириной 384 пикселя. Подходит для текста в несколько строк.",
|
|
246
|
+
};
|
|
200
247
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
248
|
+
interface ISizeItemProps {
|
|
249
|
+
size: ETooltipSize;
|
|
250
|
+
}
|
|
204
251
|
|
|
205
|
-
|
|
252
|
+
const SizeItem = ({ size }: ISizeItemProps) => {
|
|
253
|
+
const targetRef = useRef<HTMLButtonElement | null>(null);
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<div style={{ display: "flex", alignItems: "center", gap: "16px", padding: "64px 0" }}>
|
|
257
|
+
<div style={{ fontSize: "16px", fontWeight: "700", width: "40px" }}>{size.toUpperCase()}</div>
|
|
258
|
+
<Tooltip
|
|
259
|
+
size={size}
|
|
260
|
+
toggleType="hover"
|
|
261
|
+
preferPlace={ETooltipPreferPlace.BELOW}
|
|
262
|
+
// Подсказка открыта постоянно, чтобы в примере были видны оба размера сразу.
|
|
263
|
+
isOpen
|
|
264
|
+
// Размер влияет только на десктопную версию: в адаптиве подсказка занимает всю ширину экрана.
|
|
265
|
+
disableAdaptiveMode
|
|
266
|
+
targetRef={targetRef}
|
|
267
|
+
>
|
|
268
|
+
<Tooltip.Target>
|
|
269
|
+
<ButtonIcon aria-label={`Подсказка размера ${size}`} ref={targetRef}>
|
|
270
|
+
<QuestioncircleFilledSrvIcon16 paletteIndex={5} />
|
|
271
|
+
</ButtonIcon>
|
|
272
|
+
</Tooltip.Target>
|
|
273
|
+
<Tooltip.Body>{TEXT_BY_SIZE[size]}</Tooltip.Body>
|
|
274
|
+
</Tooltip>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const SIZES = Object.values(ETooltipSize);
|
|
280
|
+
|
|
281
|
+
export const Sizes = () => (
|
|
282
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
|
283
|
+
{SIZES.map((size) => (
|
|
284
|
+
<SizeItem key={size} size={size} />
|
|
285
|
+
))}
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
```
|
|
206
289
|
|
|
207
|
-
|
|
290
|
+
### DifferentPlaces
|
|
208
291
|
|
|
209
|
-
|
|
210
|
-
- **`TooltipTarget` — `FC`**, ожидает **ровно одного** child'а (`React.Children.only`); должен быть `ReactElement` (не string/number).
|
|
211
|
-
- **`TooltipBody` — `forwardRef<HTMLDivElement>`** — не убирать.
|
|
212
|
-
- **Слоты распознаются по типу** (`child.type === TooltipTarget`) — нельзя оборачивать слоты в Fragment'ы или HOC'и (потеряется тип).
|
|
213
|
-
- **`size` и `targetRef` обязательны** — проверять при использовании.
|
|
214
|
-
- **Файл контекста назван `TootlipContext.ts`** (опечатка) — менять имя считается breaking change для импортов.
|
|
215
|
-
- **`renderContainer` игнорируется в mobile + adaptiveMode** — всегда `document.body` (для полноэкранного листа).
|
|
216
|
-
- **`toggleType === "hover"` использует `targetHoveredRef`**, чтобы не дёргать toggle при движении внутри target'а; при `isOpen=false` извне ref сбрасывается.
|
|
217
|
-
- **На desktop body — `<div>`, на mobile — `<Text tag="div" size=B3>`** (через MobileView). Стили совпадают по className, но обёртка разная.
|
|
218
|
-
- **`XButton.children: never`** — иконка X встроена, не передавать content.
|
|
219
|
-
- **Все enum'ы экспортируются** — часть публичного API.
|
|
292
|
+
`stories/Tooltip/examples/DifferentPlaces.tsx`
|
|
220
293
|
|
|
221
|
-
|
|
294
|
+
```tsx
|
|
295
|
+
import React, { useRef } from "react";
|
|
296
|
+
import { QuestioncircleFilledSrvIcon16 } from "@sberbusiness/icons-next";
|
|
297
|
+
import { ButtonIcon, ETooltipPreferPlace, ETooltipSize, Tooltip } from "@sberbusiness/triplex-next";
|
|
222
298
|
|
|
223
|
-
|
|
299
|
+
interface IPlaceItemProps {
|
|
300
|
+
place: ETooltipPreferPlace;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const PlaceItem = ({ place }: IPlaceItemProps) => {
|
|
304
|
+
const targetRef = useRef<HTMLButtonElement | null>(null);
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div style={{ display: "flex", alignItems: "center", gap: "16px", padding: "64px 0" }}>
|
|
308
|
+
<div style={{ fontSize: "16px", fontWeight: "700", width: "56px" }}>{place}</div>
|
|
309
|
+
<Tooltip
|
|
310
|
+
size={ETooltipSize.SM}
|
|
311
|
+
toggleType="hover"
|
|
312
|
+
preferPlace={place}
|
|
313
|
+
// Подсказки открыты постоянно, чтобы в примере были видны все варианты расположения сразу.
|
|
314
|
+
isOpen
|
|
315
|
+
// preferPlace влияет только на десктопную версию: в адаптиве подсказка открывается снизу экрана.
|
|
316
|
+
disableAdaptiveMode
|
|
317
|
+
targetRef={targetRef}
|
|
318
|
+
>
|
|
319
|
+
<Tooltip.Target>
|
|
320
|
+
<ButtonIcon aria-label={`Подсказка ${place}`} ref={targetRef}>
|
|
321
|
+
<QuestioncircleFilledSrvIcon16 paletteIndex={5} />
|
|
322
|
+
</ButtonIcon>
|
|
323
|
+
</Tooltip.Target>
|
|
324
|
+
<Tooltip.Body>Расположение {place}</Tooltip.Body>
|
|
325
|
+
</Tooltip>
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const PLACES = Object.values(ETooltipPreferPlace);
|
|
331
|
+
|
|
332
|
+
export const DifferentPlaces = () => (
|
|
333
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
|
334
|
+
{PLACES.map((place) => (
|
|
335
|
+
<PlaceItem key={place} place={place} />
|
|
336
|
+
))}
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
```
|
|
224
340
|
|
|
225
|
-
|
|
226
|
-
- TAB на target при открытом tooltip — закрывает (clean-up для tab-навигации).
|
|
227
|
-
- `XButton` — `<button>`, `aria-label` ставит потребитель (через `aria-label="Закрыть"`).
|
|
228
|
-
- Mobile-вариант — полноэкранный лист (управляется фокусом через `MobileHeader`/`closeButton`).
|
|
229
|
-
- На hover-tooltip'ах screen-reader пользователи не видят содержимое — для критичной информации использовать `role="status"` или дублировать в `aria-describedby`.
|
|
341
|
+
### WithLink
|
|
230
342
|
|
|
231
|
-
|
|
343
|
+
`stories/Tooltip/examples/WithLink.tsx`
|
|
232
344
|
|
|
233
|
-
|
|
345
|
+
```tsx
|
|
346
|
+
import React, { useRef } from "react";
|
|
347
|
+
import { QuestioncircleFilledSrvIcon16 } from "@sberbusiness/icons-next";
|
|
348
|
+
import { ButtonIcon, ETooltipPreferPlace, ETooltipSize, Tooltip } from "@sberbusiness/triplex-next";
|
|
349
|
+
|
|
350
|
+
export const WithLink = () => {
|
|
351
|
+
const targetRef = useRef<HTMLButtonElement | null>(null);
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<div style={{ display: "flex", justifyContent: "center", paddingBottom: "160px" }}>
|
|
355
|
+
<Tooltip
|
|
356
|
+
size={ETooltipSize.LG}
|
|
357
|
+
toggleType="click"
|
|
358
|
+
preferPlace={ETooltipPreferPlace.BELOW}
|
|
359
|
+
// Подсказка открыта постоянно, чтобы в примере была видна ссылка.
|
|
360
|
+
isOpen
|
|
361
|
+
disableAdaptiveMode
|
|
362
|
+
targetRef={targetRef}
|
|
363
|
+
>
|
|
364
|
+
<Tooltip.Target>
|
|
365
|
+
<ButtonIcon aria-label="Показать подсказку" ref={targetRef}>
|
|
366
|
+
<QuestioncircleFilledSrvIcon16 paletteIndex={5} />
|
|
367
|
+
</ButtonIcon>
|
|
368
|
+
</Tooltip.Target>
|
|
369
|
+
<Tooltip.Body>Ссылка ведёт на страницу с подробным описанием.</Tooltip.Body>
|
|
370
|
+
<Tooltip.Link href="https://triplex-design.ru" target="_blank">
|
|
371
|
+
Подробнее
|
|
372
|
+
</Tooltip.Link>
|
|
373
|
+
</Tooltip>
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
};
|
|
377
|
+
```
|
|
234
378
|
|
|
235
|
-
|
|
236
|
-
- `Dropdown` — для меню/списков (другая UX-семантика).
|
|
237
|
-
- `Typography.Text` — обёртка body на mobile.
|
|
238
|
-
- `ThemeProvider` (через `useTooltipTheme`) — наследование темы в портал.
|
|
239
|
-
- `IDropdownMobileHeaderProps` — родитель для `ITooltipMobileHeaderProps`.
|
|
379
|
+
### WithCloseButton
|
|
240
380
|
|
|
241
|
-
|
|
381
|
+
`stories/Tooltip/examples/WithCloseButton.tsx`
|
|
242
382
|
|
|
243
|
-
|
|
383
|
+
```tsx
|
|
384
|
+
import React, { useRef, useState } from "react";
|
|
385
|
+
import { QuestioncircleFilledSrvIcon16 } from "@sberbusiness/icons-next";
|
|
386
|
+
import { ButtonIcon, ETooltipPreferPlace, ETooltipSize, Tooltip } from "@sberbusiness/triplex-next";
|
|
387
|
+
|
|
388
|
+
export const WithCloseButton = () => {
|
|
389
|
+
const targetRef = useRef<HTMLButtonElement | null>(null);
|
|
390
|
+
const [opened, setOpened] = useState(true);
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<div style={{ display: "flex", justifyContent: "center", paddingBottom: "160px" }}>
|
|
394
|
+
<Tooltip
|
|
395
|
+
size={ETooltipSize.LG}
|
|
396
|
+
toggleType="click"
|
|
397
|
+
preferPlace={ETooltipPreferPlace.BELOW}
|
|
398
|
+
isOpen={opened}
|
|
399
|
+
toggle={setOpened}
|
|
400
|
+
disableAdaptiveMode
|
|
401
|
+
targetRef={targetRef}
|
|
402
|
+
>
|
|
403
|
+
<Tooltip.Target>
|
|
404
|
+
<ButtonIcon aria-label="Показать подсказку" ref={targetRef}>
|
|
405
|
+
<QuestioncircleFilledSrvIcon16 paletteIndex={5} />
|
|
406
|
+
</ButtonIcon>
|
|
407
|
+
</Tooltip.Target>
|
|
408
|
+
<Tooltip.Body>
|
|
409
|
+
Подсказку с кнопкой закрытия пользователь закрывает сам — по клику на крестик, по Escape или по
|
|
410
|
+
клику вне подсказки.
|
|
411
|
+
</Tooltip.Body>
|
|
412
|
+
<Tooltip.XButton aria-label="Закрыть подсказку" />
|
|
413
|
+
</Tooltip>
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
};
|
|
417
|
+
```
|
|
244
418
|
|
|
245
|
-
|
|
246
|
-
Примеры: `stories/Tooltip/examples/`
|
|
419
|
+
### MobileHeader
|
|
247
420
|
|
|
248
|
-
|
|
421
|
+
`stories/Tooltip/examples/MobileHeader.tsx`
|
|
249
422
|
|
|
250
|
-
|
|
423
|
+
```tsx
|
|
424
|
+
import React, { useRef } from "react";
|
|
425
|
+
import { QuestioncircleFilledSrvIcon16 } from "@sberbusiness/icons-next";
|
|
426
|
+
import { ButtonIcon, ETooltipPreferPlace, ETooltipSize, Tooltip } from "@sberbusiness/triplex-next";
|
|
427
|
+
|
|
428
|
+
export const MobileHeader = () => {
|
|
429
|
+
const targetRef = useRef<HTMLButtonElement | null>(null);
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<div style={{ display: "flex", justifyContent: "center", padding: "80px 0" }}>
|
|
433
|
+
<Tooltip
|
|
434
|
+
size={ETooltipSize.SM}
|
|
435
|
+
toggleType="hover"
|
|
436
|
+
preferPlace={ETooltipPreferPlace.BELOW}
|
|
437
|
+
targetRef={targetRef}
|
|
438
|
+
>
|
|
439
|
+
<Tooltip.Target>
|
|
440
|
+
<ButtonIcon aria-label="Показать подсказку" ref={targetRef}>
|
|
441
|
+
<QuestioncircleFilledSrvIcon16 paletteIndex={5} />
|
|
442
|
+
</ButtonIcon>
|
|
443
|
+
</Tooltip.Target>
|
|
444
|
+
<Tooltip.Body>Текст подсказки</Tooltip.Body>
|
|
445
|
+
<Tooltip.XButton aria-label="Закрыть подсказку" />
|
|
446
|
+
<Tooltip.MobileHeader>Заголовок для адаптивного режима</Tooltip.MobileHeader>
|
|
447
|
+
</Tooltip>
|
|
448
|
+
</div>
|
|
449
|
+
);
|
|
450
|
+
};
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### RenderContainer
|
|
454
|
+
|
|
455
|
+
`stories/Tooltip/examples/RenderContainer.tsx`
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
import React, { useRef, useState } from "react";
|
|
459
|
+
import { QuestioncircleFilledSrvIcon16 } from "@sberbusiness/icons-next";
|
|
460
|
+
import { ButtonIcon, ETooltipPreferPlace, ETooltipSize, Tooltip } from "@sberbusiness/triplex-next";
|
|
461
|
+
|
|
462
|
+
export const RenderContainer = () => {
|
|
463
|
+
const targetRef = useRef<HTMLButtonElement | null>(null);
|
|
464
|
+
// Callback-ref вместо useEffect: контейнер попадает в state сразу после монтирования.
|
|
465
|
+
const [container, setContainer] = useState<HTMLElement | null>(null);
|
|
466
|
+
|
|
467
|
+
return (
|
|
468
|
+
<div style={{ display: "flex", justifyContent: "center", paddingBottom: "160px" }}>
|
|
469
|
+
<div ref={setContainer} />
|
|
470
|
+
|
|
471
|
+
{container ? (
|
|
472
|
+
<Tooltip
|
|
473
|
+
size={ETooltipSize.SM}
|
|
474
|
+
toggleType="click"
|
|
475
|
+
preferPlace={ETooltipPreferPlace.BELOW}
|
|
476
|
+
renderContainer={container}
|
|
477
|
+
// Подсказка открыта постоянно, чтобы её было видно в примере.
|
|
478
|
+
isOpen
|
|
479
|
+
disableAdaptiveMode
|
|
480
|
+
targetRef={targetRef}
|
|
481
|
+
>
|
|
482
|
+
<Tooltip.Target>
|
|
483
|
+
<ButtonIcon aria-label="Показать подсказку" ref={targetRef}>
|
|
484
|
+
<QuestioncircleFilledSrvIcon16 paletteIndex={5} />
|
|
485
|
+
</ButtonIcon>
|
|
486
|
+
</Tooltip.Target>
|
|
487
|
+
<Tooltip.Body>Подсказка отрендерена в заданный контейнер.</Tooltip.Body>
|
|
488
|
+
</Tooltip>
|
|
489
|
+
) : null}
|
|
490
|
+
</div>
|
|
491
|
+
);
|
|
492
|
+
};
|
|
493
|
+
```
|
|
251
494
|
|
|
252
|
-
| Дата | Изменение |
|
|
253
|
-
|---|---|
|
|
254
|
-
| 2026-04-18 | Создан документ |
|