@sberbusiness/triplex-next-mcp-server 1.53.0 → 1.54.0
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/README.md +7 -0
- package/bundle/components/ChipSelect.md +7 -5
- package/bundle/components/ChipSelectTarget.md +388 -0
- package/bundle/components/ChipSuggest.md +8 -15
- package/bundle/components/ChipSuggestTarget.md +506 -0
- package/bundle/components/Dropdown.md +2 -1
- package/bundle/components/MultiselectField.md +2 -57
- package/bundle/components/Portal.md +5 -2
- package/bundle/components/Puller.md +85 -90
- package/bundle/components/SelectExtendedField.md +6 -4
- package/bundle/components/SelectField.md +4 -2
- package/bundle/manifest.json +19 -2
- package/bundle/release-notes/1.53.0.mdx +1 -1
- package/bundle/release-notes/1.54.0.mdx +38 -0
- package/bundle/release-notes/1.55.0.mdx +7 -0
- package/bundle/tokens.json +1 -1
- package/dist/index.js +39 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -133,7 +133,7 @@ MultiselectField.Divider_Color
|
|
|
133
133
|
- Пока выпадающий блок открыт, фокус заперт внутри него (`FocusTrap` из `focus-trap-react`). Содержимое обёрнуто в `<div role="presentation">` — обёртка нужна `FocusTrap` как единственный контейнер и не должна попадать в дерево доступности.
|
|
134
134
|
- `clickOutsideDeactivates: true` — нажатие вне блока снимает перехват фокуса, после чего блок закрывает уже `SelectExtendedField` по своему `mousedown`-слушателю.
|
|
135
135
|
- Возврат фокуса на поле при закрытии зависит от того, как блок открыли: после клавиатуры фокус возвращается, после мыши — нет (иначе клик вне блока повторно подсвечивал бы поле). Признак сбрасывается на каждое закрытие, поэтому следующее открытие оценивается заново.
|
|
136
|
-
- `aria-haspopup="listbox"` и `aria-expanded` выставляет `MultiselectField.Target` (то есть `SelectExtendedFieldTarget`). Клавиши открытия поля — `Space`, `Enter`, `ArrowDown`, `ArrowUp
|
|
136
|
+
- `aria-haspopup="listbox"` и `aria-expanded` выставляет `MultiselectField.Target` (то есть `SelectExtendedFieldTarget`). Клавиши открытия поля — `Space`, `Enter`, `ArrowDown`, `ArrowUp`, только когда фокус на самом поле, а не на кнопке тега.
|
|
137
137
|
- `Escape` закрывает блок из любого места страницы — слушатель `SelectExtendedField` висит на `window`.
|
|
138
138
|
- Роли и клавиатурную навигацию **внутри** блока компонент не задаёт: содержимое произвольное (чекбоксы, дерево, кнопки), связь поля со списком (`aria-controls`, `aria-activedescendant`) и `aria-label` на элементах — забота потребителя.
|
|
139
139
|
- Компонент не хардкодит текстовые строки: `fieldLabel`, `placeholder`, подписи кнопок и заголовок мобильной версии передаёт потребитель — библиотека мультиязычная.
|
|
@@ -200,6 +200,7 @@ MultiselectField.Divider_Color
|
|
|
200
200
|
| 2026-09-03 | Создан документ AI-ready для `MultiselectField` (TRI-61). |
|
|
201
201
|
| 2026-09-03 | AI-рефакторинг (TRI-61): JSDoc на props, контексте и всех частях выпадающего блока; значение контекста обёрнуто в `useMemo`, обработчик `onMouseDown` — в `useCallback` (выигрыша в текущей связке нет: части приходят из рендер-функции `children` и пересоздаются каждый рендер независимо от контекста, — это страховка на случай `React.memo` у потребителя); объект-заглушка `mobileViewProps` вынесен в константу, чтобы инвариант «мобильная версия включена всегда» читался из самого выражения; unit-тесты разбиты по субкомпонентам и расширены с 1 до 45 кейсов. Публичный API и наблюдаемое поведение не изменены. |
|
|
202
202
|
| 2026-10-21 | **Breaking** (TRIPLEX-961): prop `mobileViewProps`, унаследованный от `Dropdown`, переименован в `pullerProps`. |
|
|
203
|
+
| 2026-10-29 | TRIPLEX-1028: клик и Enter / Space на кнопках тегов и других фокусируемых элементах внутри `MultiselectField.Target` больше не переключают выпадающий блок, `stopPropagation` в `onKeyDown` тегов для этого не нужен. Клавиши открытия — см. «Accessibility». `onClear` у `MultiselectField.Target` получает событие клика. |
|
|
203
204
|
|
|
204
205
|
---
|
|
205
206
|
|
|
@@ -316,11 +317,6 @@ export const DefaultExample = () => {
|
|
|
316
317
|
const handleTagFocus = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
317
318
|
const handleTagBlur = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
318
319
|
const handleTagClick = (event: React.MouseEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
319
|
-
const handleTagKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
320
|
-
if (event.code === "Enter" || event.code === "Space") {
|
|
321
|
-
event.stopPropagation();
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
320
|
|
|
325
321
|
if (!selectedOptions.length) return null;
|
|
326
322
|
if (selectedOptions.length > 3) {
|
|
@@ -331,7 +327,6 @@ export const DefaultExample = () => {
|
|
|
331
327
|
onFocus={handleTagFocus}
|
|
332
328
|
onBlur={handleTagBlur}
|
|
333
329
|
onClick={handleTagClick}
|
|
334
|
-
onKeyDown={handleTagKeyDown}
|
|
335
330
|
onRemove={handleClearAll}
|
|
336
331
|
>
|
|
337
332
|
{`Выбрано ${selectedOptions.length} значения`}
|
|
@@ -349,7 +344,6 @@ export const DefaultExample = () => {
|
|
|
349
344
|
onFocus={handleTagFocus}
|
|
350
345
|
onBlur={handleTagBlur}
|
|
351
346
|
onClick={handleTagClick}
|
|
352
|
-
onKeyDown={handleTagKeyDown}
|
|
353
347
|
onRemove={() => handleToggle(opt, false)}
|
|
354
348
|
>
|
|
355
349
|
{opt.label}
|
|
@@ -576,11 +570,6 @@ const SizeItem = ({ size }: ISizeItemProps) => {
|
|
|
576
570
|
const handleTagFocus = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
577
571
|
const handleTagBlur = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
578
572
|
const handleTagClick = (event: React.MouseEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
579
|
-
const handleTagKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
580
|
-
if (event.code === "Enter" || event.code === "Space") {
|
|
581
|
-
event.stopPropagation();
|
|
582
|
-
}
|
|
583
|
-
};
|
|
584
573
|
|
|
585
574
|
if (!selectedOptions.length) return null;
|
|
586
575
|
|
|
@@ -594,7 +583,6 @@ const SizeItem = ({ size }: ISizeItemProps) => {
|
|
|
594
583
|
onFocus={handleTagFocus}
|
|
595
584
|
onBlur={handleTagBlur}
|
|
596
585
|
onClick={handleTagClick}
|
|
597
|
-
onKeyDown={handleTagKeyDown}
|
|
598
586
|
onRemove={() => handleToggle(opt, false)}
|
|
599
587
|
>
|
|
600
588
|
{opt.label}
|
|
@@ -789,11 +777,6 @@ const StatusItem = ({ idPrefix, status }: IStatusItemProps) => {
|
|
|
789
777
|
const handleTagFocus = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
790
778
|
const handleTagBlur = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
791
779
|
const handleTagClick = (event: React.MouseEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
792
|
-
const handleTagKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
793
|
-
if (event.code === "Enter" || event.code === "Space") {
|
|
794
|
-
event.stopPropagation();
|
|
795
|
-
}
|
|
796
|
-
};
|
|
797
780
|
|
|
798
781
|
if (!selectedOptions.length) return null;
|
|
799
782
|
|
|
@@ -807,7 +790,6 @@ const StatusItem = ({ idPrefix, status }: IStatusItemProps) => {
|
|
|
807
790
|
onFocus={handleTagFocus}
|
|
808
791
|
onBlur={handleTagBlur}
|
|
809
792
|
onClick={handleTagClick}
|
|
810
|
-
onKeyDown={handleTagKeyDown}
|
|
811
793
|
onRemove={() => handleToggle(opt, false)}
|
|
812
794
|
>
|
|
813
795
|
{opt.label}
|
|
@@ -1009,11 +991,6 @@ const LoadingMultiselect = ({
|
|
|
1009
991
|
const handleTagFocus = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1010
992
|
const handleTagBlur = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1011
993
|
const handleTagClick = (event: React.MouseEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1012
|
-
const handleTagKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
1013
|
-
if (event.code === "Enter" || event.code === "Space") {
|
|
1014
|
-
event.stopPropagation();
|
|
1015
|
-
}
|
|
1016
|
-
};
|
|
1017
994
|
|
|
1018
995
|
if (!selectedOptions.length) return null;
|
|
1019
996
|
|
|
@@ -1027,7 +1004,6 @@ const LoadingMultiselect = ({
|
|
|
1027
1004
|
onFocus={handleTagFocus}
|
|
1028
1005
|
onBlur={handleTagBlur}
|
|
1029
1006
|
onClick={handleTagClick}
|
|
1030
|
-
onKeyDown={handleTagKeyDown}
|
|
1031
1007
|
onRemove={() => handleToggle(opt, false)}
|
|
1032
1008
|
>
|
|
1033
1009
|
{opt.label}
|
|
@@ -1216,11 +1192,6 @@ export const DropdownWithoutInputExample = () => {
|
|
|
1216
1192
|
const handleTagFocus = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1217
1193
|
const handleTagBlur = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1218
1194
|
const handleTagClick = (event: React.MouseEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1219
|
-
const handleTagKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
1220
|
-
if (event.code === "Enter" || event.code === "Space") {
|
|
1221
|
-
event.stopPropagation();
|
|
1222
|
-
}
|
|
1223
|
-
};
|
|
1224
1195
|
|
|
1225
1196
|
if (!selectedOptions.length) return null;
|
|
1226
1197
|
|
|
@@ -1234,7 +1205,6 @@ export const DropdownWithoutInputExample = () => {
|
|
|
1234
1205
|
onFocus={handleTagFocus}
|
|
1235
1206
|
onBlur={handleTagBlur}
|
|
1236
1207
|
onClick={handleTagClick}
|
|
1237
|
-
onKeyDown={handleTagKeyDown}
|
|
1238
1208
|
onRemove={() => handleToggle(opt, false)}
|
|
1239
1209
|
>
|
|
1240
1210
|
{opt.label}
|
|
@@ -1413,11 +1383,6 @@ export const WithClearButtonExample = () => {
|
|
|
1413
1383
|
const handleTagFocus = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1414
1384
|
const handleTagBlur = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1415
1385
|
const handleTagClick = (event: React.MouseEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1416
|
-
const handleTagKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
1417
|
-
if (event.code === "Enter" || event.code === "Space") {
|
|
1418
|
-
event.stopPropagation();
|
|
1419
|
-
}
|
|
1420
|
-
};
|
|
1421
1386
|
|
|
1422
1387
|
if (!selectedOptions.length) return null;
|
|
1423
1388
|
|
|
@@ -1431,7 +1396,6 @@ export const WithClearButtonExample = () => {
|
|
|
1431
1396
|
onFocus={handleTagFocus}
|
|
1432
1397
|
onBlur={handleTagBlur}
|
|
1433
1398
|
onClick={handleTagClick}
|
|
1434
|
-
onKeyDown={handleTagKeyDown}
|
|
1435
1399
|
onRemove={() => handleToggle(opt, false)}
|
|
1436
1400
|
>
|
|
1437
1401
|
{opt.label}
|
|
@@ -1645,11 +1609,6 @@ export const WithPrefixAndPostfixExample = () => {
|
|
|
1645
1609
|
const handleTagFocus = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1646
1610
|
const handleTagBlur = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1647
1611
|
const handleTagClick = (event: React.MouseEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1648
|
-
const handleTagKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
1649
|
-
if (event.code === "Enter" || event.code === "Space") {
|
|
1650
|
-
event.stopPropagation();
|
|
1651
|
-
}
|
|
1652
|
-
};
|
|
1653
1612
|
|
|
1654
1613
|
if (!selectedOptions.length) return null;
|
|
1655
1614
|
|
|
@@ -1663,7 +1622,6 @@ export const WithPrefixAndPostfixExample = () => {
|
|
|
1663
1622
|
onFocus={handleTagFocus}
|
|
1664
1623
|
onBlur={handleTagBlur}
|
|
1665
1624
|
onClick={handleTagClick}
|
|
1666
|
-
onKeyDown={handleTagKeyDown}
|
|
1667
1625
|
onRemove={() => handleToggle(opt, false)}
|
|
1668
1626
|
>
|
|
1669
1627
|
{opt.label}
|
|
@@ -1899,11 +1857,6 @@ const ProductionMultiselect = () => {
|
|
|
1899
1857
|
const handleTagFocus = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1900
1858
|
const handleTagBlur = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1901
1859
|
const handleTagClick = (event: React.MouseEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
1902
|
-
const handleTagKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
1903
|
-
if (event.code === "Enter" || event.code === "Space") {
|
|
1904
|
-
event.stopPropagation();
|
|
1905
|
-
}
|
|
1906
|
-
};
|
|
1907
1860
|
|
|
1908
1861
|
if (!selectedOptions.length) return null;
|
|
1909
1862
|
|
|
@@ -1917,7 +1870,6 @@ const ProductionMultiselect = () => {
|
|
|
1917
1870
|
onFocus={handleTagFocus}
|
|
1918
1871
|
onBlur={handleTagBlur}
|
|
1919
1872
|
onClick={handleTagClick}
|
|
1920
|
-
onKeyDown={handleTagKeyDown}
|
|
1921
1873
|
onRemove={() => handleToggle(opt, false)}
|
|
1922
1874
|
>
|
|
1923
1875
|
{opt.label}
|
|
@@ -2152,11 +2104,6 @@ export const WithCheckboxTreeExample = () => {
|
|
|
2152
2104
|
const handleTagFocus = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
2153
2105
|
const handleTagBlur = (event: React.FocusEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
2154
2106
|
const handleTagClick = (event: React.MouseEvent<HTMLSpanElement>) => event.stopPropagation();
|
|
2155
|
-
const handleTagKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
2156
|
-
if (event.code === "Enter" || event.code === "Space") {
|
|
2157
|
-
event.stopPropagation();
|
|
2158
|
-
}
|
|
2159
|
-
};
|
|
2160
2107
|
|
|
2161
2108
|
if (!selectedLeafNodes.length) return null;
|
|
2162
2109
|
if (selectedLeafNodes.length > 3) {
|
|
@@ -2167,7 +2114,6 @@ export const WithCheckboxTreeExample = () => {
|
|
|
2167
2114
|
onFocus={handleTagFocus}
|
|
2168
2115
|
onBlur={handleTagBlur}
|
|
2169
2116
|
onClick={handleTagClick}
|
|
2170
|
-
onKeyDown={handleTagKeyDown}
|
|
2171
2117
|
onRemove={() => setSelectedIds([])}
|
|
2172
2118
|
>
|
|
2173
2119
|
{`Выбрано ${selectedLeafNodes.length} значения`}
|
|
@@ -2184,7 +2130,6 @@ export const WithCheckboxTreeExample = () => {
|
|
|
2184
2130
|
onFocus={handleTagFocus}
|
|
2185
2131
|
onBlur={handleTagBlur}
|
|
2186
2132
|
onClick={handleTagClick}
|
|
2187
|
-
onKeyDown={handleTagKeyDown}
|
|
2188
2133
|
onRemove={() => setSelectedIds((prev) => prev.filter((id) => id !== node.id))}
|
|
2189
2134
|
>
|
|
2190
2135
|
{node.label}
|
|
@@ -86,9 +86,12 @@ version: "1.0"
|
|
|
86
86
|
## Связанные компоненты
|
|
87
87
|
|
|
88
88
|
- `Dropdown` (`src/components/Dropdown/`) — рендерит выпадающий слой через
|
|
89
|
-
Portal в `document.body
|
|
89
|
+
Portal в `document.body`; мобильная версия — через `Puller`, в
|
|
90
|
+
`pullerProps.renderContainer ?? document.body`.
|
|
90
91
|
- `Tooltip` (`src/components/Tooltip/`) — desktop- и mobile-реализации
|
|
91
|
-
рендерят тултип через Portal: mobile —
|
|
92
|
+
рендерят тултип через Portal: mobile — через `Puller`, всегда в
|
|
93
|
+
`document.body`, desktop — в `renderContainer ?? document.body`.
|
|
94
|
+
- `Puller` (`src/components/Puller/`) — рендерит шторку через Portal в
|
|
92
95
|
`renderContainer ?? document.body`.
|
|
93
96
|
|
|
94
97
|
---
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
component: Puller
|
|
3
3
|
category: Puller
|
|
4
|
-
related: [Overlay, ButtonIcon, LoaderSmall, Badge, FormField]
|
|
4
|
+
related: [Overlay, Portal, ButtonIcon, LoaderSmall, Badge, FormField]
|
|
5
5
|
tokens:
|
|
6
6
|
- Puller.Backdrop
|
|
7
7
|
- Puller.Body_Background
|
|
@@ -46,7 +46,11 @@ version: "1.0"
|
|
|
46
46
|
|
|
47
47
|
#### Опциональные props
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
50
|
+
|---|---|---|---|
|
|
51
|
+
| `renderContainer` | `Element \| DocumentFragment` | `document.body` | DOM-узел, в который рендерится шторка |
|
|
52
|
+
|
|
53
|
+
Остальные — HTML-атрибуты `<div>`.
|
|
50
54
|
|
|
51
55
|
### Субкомпоненты
|
|
52
56
|
|
|
@@ -64,7 +68,8 @@ version: "1.0"
|
|
|
64
68
|
|
|
65
69
|
### Ограничения
|
|
66
70
|
|
|
67
|
-
- `
|
|
71
|
+
- `.pullerWrapper` — `position: fixed`: `transform`, `filter`, `perspective`, `contain` или собственный stacking context у `renderContainer` и его родителей сдвинут, обрежут или перекроют шторку.
|
|
72
|
+
- Пока шторка открыта, скролл блокируется на `body`, а не на `renderContainer`.
|
|
68
73
|
- Шторка не закрывается по `Escape` и не перехватывает фокус — это добавляет потребитель (как `MultiselectField.Dropdown` и `DatePickerExtended`).
|
|
69
74
|
- Высота ограничена `50vh`: длинное содержимое кладётся в прокручиваемый `PullerBody`.
|
|
70
75
|
|
|
@@ -92,6 +97,7 @@ PullerList.Selected_Background
|
|
|
92
97
|
## Инварианты
|
|
93
98
|
|
|
94
99
|
- `forwardRef` на `Puller` и всех субкомпонентах, кроме `PullerLoader`, — не убирать. Ref `Puller` указывает на контентный `<div>` (`.puller`), а не на wrapper. На это опирается `Dropdown`: его ref в мобильном режиме ведёт сюда.
|
|
100
|
+
- `Puller` рендерится через `Portal`, по умолчанию в `document.body`. Смена контейнера по умолчанию — breaking change для потребителей, которые ищут шторку в DOM.
|
|
95
101
|
- В закрытом состоянии (`opened`, `opening` и `closing` равны `false`) `PullerInner` возвращает `null`. Анимация закрытия доигрывает до `transitionend` на подложке и только потом снимает разметку.
|
|
96
102
|
- Непрозрачность подложки задаётся альфа-каналом токена `Puller.Backdrop`; `opacity` подложки только анимирует появление (0 → 1).
|
|
97
103
|
- Анимация открытия требует сначала отрендерить шторку закрытой: `openingState` / `openedState` выставляются через `setTimeout`. Не заменять на синхронную установку, иначе переход не проиграется.
|
|
@@ -115,6 +121,7 @@ PullerList.Selected_Background
|
|
|
115
121
|
## Связанные компоненты
|
|
116
122
|
|
|
117
123
|
- `Overlay` — семейство, к которому относится `OverlayBase`: `Puller` рендерит `OverlayBase` с `direction={EOverlayDirection.BOTTOM}` и получает от него флаги `opening` / `closing` и сеттеры анимации.
|
|
124
|
+
- `Portal` — выносит шторку из дерева родителя, см. «Инварианты».
|
|
118
125
|
- `ButtonIcon` — база `PullerClose`, `IPullerCloseProps` расширяет `IButtonIconProps`.
|
|
119
126
|
- `LoaderSmall` — рендерится в `PullerLoader` (тема `BRAND`, SM) и в элементе-лоадере `PullerList` (MD). `IPullerLoaderProps` расширяет `ILoaderSmallProps`.
|
|
120
127
|
- `Badge` — `Badge.Dot` в углу `PullerListItem` при `showNotificationIcon`.
|
|
@@ -130,7 +137,7 @@ PullerList.Selected_Background
|
|
|
130
137
|
Основные истории: `stories/Puller/Puller.stories.tsx`
|
|
131
138
|
Файлы примеров: `stories/Puller/examples/`
|
|
132
139
|
|
|
133
|
-
Примеры
|
|
140
|
+
Примеры стартуют закрытыми. Шторку открывает `play`; на docs-странице (`viewMode === "docs"`) он ничего не делает.
|
|
134
141
|
|
|
135
142
|
| Story | Example file | Что демонстрирует |
|
|
136
143
|
|---|---|---|
|
|
@@ -138,7 +145,7 @@ PullerList.Selected_Background
|
|
|
138
145
|
| `Default` | `Default.tsx` | Кнопка открывает шторку: шапка с заголовком и `PullerClose`, тело, футер с кнопкой |
|
|
139
146
|
| `WithList` | `WithList.tsx` | `PullerList` с выбранным элементом, значком уведомления и `loading`; выбор закрывает шторку |
|
|
140
147
|
| `WithInput` | `WithInput.tsx` | Шапка с `PullerInput`, `PullerLoader` и `PullerClose` — раскладка мобильного suggest |
|
|
141
|
-
| `
|
|
148
|
+
| `RenderContainer` («Render container») | `RenderContainer.tsx` | Рендер шторки в заданный контейнер через `renderContainer` |
|
|
142
149
|
| `VisualTests` | `VisualTests.tsx` | Длинный заголовок, лоадер, скролл длинного списка, выбранный элемент, значок, футер с двумя кнопками |
|
|
143
150
|
|
|
144
151
|
---
|
|
@@ -148,6 +155,7 @@ PullerList.Selected_Background
|
|
|
148
155
|
| Дата | Изменение |
|
|
149
156
|
|---|---|
|
|
150
157
|
| 2026-10-21 | Создан документ (TRIPLEX-961). |
|
|
158
|
+
| 2026-10-31 | **Breaking**: `Puller` сам рендерится через `Portal`: по умолчанию в `document.body`, контейнер задаёт новый prop `renderContainer` (TRIPLEX-961). |
|
|
151
159
|
|
|
152
160
|
---
|
|
153
161
|
|
|
@@ -164,7 +172,6 @@ import {
|
|
|
164
172
|
EButtonTheme,
|
|
165
173
|
EComponentSize,
|
|
166
174
|
ETextSize,
|
|
167
|
-
Portal,
|
|
168
175
|
Puller,
|
|
169
176
|
PullerBody,
|
|
170
177
|
PullerClose,
|
|
@@ -187,31 +194,29 @@ export const Default = () => {
|
|
|
187
194
|
>
|
|
188
195
|
Open
|
|
189
196
|
</Button>
|
|
190
|
-
<
|
|
191
|
-
<
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
</Puller>
|
|
214
|
-
</Portal>
|
|
197
|
+
<Puller opened={opened} setOpened={setOpened} role="dialog" aria-labelledby="puller-default-title">
|
|
198
|
+
<PullerHeader controlButtons={<PullerClose aria-label="Close" onClick={() => setOpened(false)} />}>
|
|
199
|
+
<Text id="puller-default-title" size={ETextSize.B1}>
|
|
200
|
+
Title
|
|
201
|
+
</Text>
|
|
202
|
+
</PullerHeader>
|
|
203
|
+
<PullerBody>
|
|
204
|
+
<Text tag="div" size={ETextSize.B3} style={{ padding: "16px" }}>
|
|
205
|
+
Puller content. The puller closes via the close button, the footer button or a tap on the
|
|
206
|
+
backdrop.
|
|
207
|
+
</Text>
|
|
208
|
+
</PullerBody>
|
|
209
|
+
<PullerFooter>
|
|
210
|
+
<Button
|
|
211
|
+
theme={EButtonTheme.GENERAL}
|
|
212
|
+
size={EComponentSize.MD}
|
|
213
|
+
block
|
|
214
|
+
onClick={() => setOpened(false)}
|
|
215
|
+
>
|
|
216
|
+
Done
|
|
217
|
+
</Button>
|
|
218
|
+
</PullerFooter>
|
|
219
|
+
</Puller>
|
|
215
220
|
</div>
|
|
216
221
|
);
|
|
217
222
|
};
|
|
@@ -228,7 +233,6 @@ import {
|
|
|
228
233
|
EButtonTheme,
|
|
229
234
|
EComponentSize,
|
|
230
235
|
ETextSize,
|
|
231
|
-
Portal,
|
|
232
236
|
Puller,
|
|
233
237
|
PullerBody,
|
|
234
238
|
PullerClose,
|
|
@@ -264,30 +268,28 @@ export const WithList = () => {
|
|
|
264
268
|
>
|
|
265
269
|
Open
|
|
266
270
|
</Button>
|
|
267
|
-
<
|
|
268
|
-
<
|
|
269
|
-
<
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
<
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
</Puller>
|
|
290
|
-
</Portal>
|
|
271
|
+
<Puller opened={opened} setOpened={setOpened} role="dialog" aria-labelledby="puller-with-list-title">
|
|
272
|
+
<PullerHeader controlButtons={<PullerClose aria-label="Close" onClick={() => setOpened(false)} />}>
|
|
273
|
+
<Text id="puller-with-list-title" size={ETextSize.B1}>
|
|
274
|
+
Select a value
|
|
275
|
+
</Text>
|
|
276
|
+
</PullerHeader>
|
|
277
|
+
<PullerBody>
|
|
278
|
+
<PullerList loading aria-labelledby="puller-with-list-title">
|
|
279
|
+
{OPTIONS.map((option) => (
|
|
280
|
+
<PullerListItem
|
|
281
|
+
key={option.id}
|
|
282
|
+
id={option.id}
|
|
283
|
+
selected={option.id === selectedId}
|
|
284
|
+
showNotificationIcon={option.hasNotifications}
|
|
285
|
+
onSelect={() => handleSelect(option.id)}
|
|
286
|
+
>
|
|
287
|
+
{option.label}
|
|
288
|
+
</PullerListItem>
|
|
289
|
+
))}
|
|
290
|
+
</PullerList>
|
|
291
|
+
</PullerBody>
|
|
292
|
+
</Puller>
|
|
291
293
|
</div>
|
|
292
294
|
);
|
|
293
295
|
};
|
|
@@ -303,7 +305,6 @@ import {
|
|
|
303
305
|
Button,
|
|
304
306
|
EButtonTheme,
|
|
305
307
|
EComponentSize,
|
|
306
|
-
Portal,
|
|
307
308
|
Puller,
|
|
308
309
|
PullerBody,
|
|
309
310
|
PullerClose,
|
|
@@ -340,39 +341,33 @@ export const WithInput = () => {
|
|
|
340
341
|
>
|
|
341
342
|
Open
|
|
342
343
|
</Button>
|
|
343
|
-
<
|
|
344
|
-
<
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
<
|
|
362
|
-
|
|
363
|
-
{
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
</PullerListItem>
|
|
371
|
-
))}
|
|
372
|
-
</PullerList>
|
|
373
|
-
</PullerBody>
|
|
374
|
-
</Puller>
|
|
375
|
-
</Portal>
|
|
344
|
+
<Puller opened={opened} setOpened={setOpened} role="dialog" aria-label="City search">
|
|
345
|
+
<PullerHeader
|
|
346
|
+
controlButtons={
|
|
347
|
+
<>
|
|
348
|
+
{/* В реальном сценарии лоадер показывается на время запроса подсказок. */}
|
|
349
|
+
<PullerLoader />
|
|
350
|
+
<PullerClose aria-label="Close" onClick={() => setOpened(false)} />
|
|
351
|
+
</>
|
|
352
|
+
}
|
|
353
|
+
>
|
|
354
|
+
<PullerInput
|
|
355
|
+
value={inputValue}
|
|
356
|
+
placeholder="Enter a city"
|
|
357
|
+
aria-label="City"
|
|
358
|
+
onChange={(event) => setInputValue(event.target.value)}
|
|
359
|
+
/>
|
|
360
|
+
</PullerHeader>
|
|
361
|
+
<PullerBody>
|
|
362
|
+
<PullerList aria-label="Suggestions">
|
|
363
|
+
{OPTIONS.map((option) => (
|
|
364
|
+
<PullerListItem key={option.id} id={option.id} onSelect={() => handleSelect(option.label)}>
|
|
365
|
+
{option.label}
|
|
366
|
+
</PullerListItem>
|
|
367
|
+
))}
|
|
368
|
+
</PullerList>
|
|
369
|
+
</PullerBody>
|
|
370
|
+
</Puller>
|
|
376
371
|
</div>
|
|
377
372
|
);
|
|
378
373
|
};
|
|
@@ -75,8 +75,8 @@ version: "1.0"
|
|
|
75
75
|
| `label` | `React.ReactNode` | — | Выбранное значение |
|
|
76
76
|
| `placeholder` | `React.ReactNode` | — | Отображается, пока нет `label` |
|
|
77
77
|
| `loading` | `boolean` | `false` | Вместо каретки — лоадер; клик и клавиатура не открывают блок |
|
|
78
|
-
| `onClear` | `
|
|
79
|
-
| `prefix` / `postfix` | `React.ReactNode` | — | Слоты `FormFieldPrefix` / `FormFieldPostfix`. Постфикс идёт **после**
|
|
78
|
+
| `onClear` | `React.MouseEventHandler<HTMLButtonElement>` | — | Если передан, в постфиксе появляется кнопка очистки |
|
|
79
|
+
| `prefix` / `postfix` | `React.ReactNode` | — | Слоты `FormFieldPrefix` / `FormFieldPostfix`. Постфикс идёт **после** каретки. Обработка событий — см. «Ограничения» |
|
|
80
80
|
| `size` | `EComponentSize` | `EComponentSize.MD` | Размер поля. Определяет размер каретки и лоадера |
|
|
81
81
|
| `status` | `EFormFieldStatus` | `EFormFieldStatus.DEFAULT` | Визуальное состояние. `DISABLED` блокирует открытие |
|
|
82
82
|
|
|
@@ -94,7 +94,8 @@ version: "1.0"
|
|
|
94
94
|
- `SelectExtendedFieldDropdown` — не `forwardRef`-компонент. Ссылка передаётся через `forwardedRef`.
|
|
95
95
|
- Выпадающий блок компонент не рендерит сам и не знает, открыт ли он визуально: `opened` нужно передать в `SelectExtendedFieldDropdown` (или в свою разметку) самостоятельно.
|
|
96
96
|
- Компонент не хранит выбранное значение и не знает про опции. Значение — целиком забота потребителя.
|
|
97
|
-
- Клик по
|
|
97
|
+
- Клик по фокусируемым элементам внутри поля (`tabIndex >= 0`: кнопка очистки, `HelpBox`, кнопки тегов в значении) и по содержимому порталов (Tooltip) не переключает выпадающий блок; Enter / Space на них не открывают блок и не отменяют их нативный клик. Клик по остальному содержимому поля, включая элементы с `tabIndex < 0` (`<button tabIndex={-1}>`, `div` с `onClick` без `tabIndex`), блок переключает. Затрагивает `SelectField` и `MultiselectField.Target`.
|
|
98
|
+
- Клик по кнопке очистки (`onClear`) не всплывает: `onClick` поля и обработчики внешних элементов его не получают.
|
|
98
99
|
|
|
99
100
|
---
|
|
100
101
|
|
|
@@ -135,7 +136,7 @@ version: "1.0"
|
|
|
135
136
|
|
|
136
137
|
- `SelectExtendedFieldTarget` выставляет на корневом элементе `aria-haspopup="listbox"` и `aria-expanded`, отражающий `opened`.
|
|
137
138
|
- Роли `listbox` / `option` и клавиатурную навигацию по опциям (`ArrowUp` / `ArrowDown`, выбор по `Enter` / `Space`) даёт `DropdownList` — то есть `SelectExtendedField.Dropdown.List`. Если выпадающий блок не список, эти роли не появляются и навигацию организует потребитель.
|
|
138
|
-
- Клавиши открытия на поле выбора: `Space`, `Enter`, `ArrowDown`, `ArrowUp
|
|
139
|
+
- Клавиши открытия на поле выбора: `Space`, `Enter`, `ArrowDown`, `ArrowUp` (только когда фокус на самом поле). Обработчик вызывает `preventDefault` (чтобы страница не прокручивалась) и `stopPropagation` — иначе то же нажатие поймает документный обработчик `DropdownListItem` и список сразу выберет опцию.
|
|
139
140
|
- В состояниях `loading` и `status={EFormFieldStatus.DISABLED}` поле не реагирует ни на клик, ни на клавиатуру; `onClick` / `onKeyDown` потребителя в этих состояниях тоже не вызываются.
|
|
140
141
|
- `Escape` закрывает блок из любого места страницы — слушатель `KeyDownListener` висит на `window`.
|
|
141
142
|
- `Tab` закрывает блок только при `closeOnTab`. Фокус компонент не перехватывает и не возвращает: focus trap, если нужен, добавляет потребитель.
|
|
@@ -206,6 +207,7 @@ version: "1.0"
|
|
|
206
207
|
| 2026-08-07 | AI-рефакторинг (TRI-74): `onOpen` / `onClose` сравнивают предыдущее значение `opened` вместо флага «смонтирован» — колбэки больше не срабатывают ложно при двойном маунте в React 18 StrictMode. Удалены ссылки на несуществующие CSS-классы в `SelectExtendedFieldTarget` (в `class` попадал литерал `undefined`). Клик по кнопке очистки больше не раскрывает выпадающий блок — всплытие останавливается, как в `Chip.Select`. Слушатель `mousedown` навешивается только пока блок открыт, callback-ref стабилизирован через `useCallback`, убран мёртвый внутренний `targetRef` в `SelectExtendedFieldTarget`, дедуплицирован список опций десктопной и мобильной версий в `SelectExtendedFieldDropdownDefault`. JSDoc на всех props, unit-тесты разбиты по субкомпонентам и расширены с 42 до 69 кейсов. Stories переведены на modern pattern. Публичный API не изменён. |
|
|
207
208
|
| 2026-09-29 | Аудит симметрии `related` (TRI-156): из `related` убраны несимметричные имена (`Chip`). Пояснения к убранным именам сохранены прозой в «Связанные компоненты»; публичный API и поведение не затронуты. |
|
|
208
209
|
| 2026-10-21 | **Breaking** (TRIPLEX-961): prop `mobileViewProps`, унаследованный `SelectExtendedField.Dropdown` от `Dropdown`, переименован в `pullerProps`. |
|
|
210
|
+
| 2026-10-29 | TRIPLEX-1028: `SelectExtendedField.Target` переключает выпадающий блок только по событиям самого поля — см. «Ограничения». `onClear` получает событие клика. |
|
|
209
211
|
|
|
210
212
|
---
|
|
211
213
|
|
|
@@ -147,8 +147,9 @@ version: "1.0"
|
|
|
147
147
|
`SelectField` рендерит вокруг выпадающего блока. Без этого провайдера связка не работает.
|
|
148
148
|
- `aria-expanded` и `aria-haspopup="listbox"` выставляет `SelectExtendedFieldTarget`;
|
|
149
149
|
роли `listbox` / `option` и навигацию `ArrowUp` / `ArrowDown` — `DropdownList`.
|
|
150
|
-
- Клавиши открытия списка: `Space`, `Enter`, `ArrowDown`, `ArrowUp
|
|
151
|
-
(из любого места страницы), `Tab` (`closeOnTab`) и нажатие мышью
|
|
150
|
+
- Клавиши открытия списка: `Space`, `Enter`, `ArrowDown`, `ArrowUp` (только когда фокус на самом
|
|
151
|
+
поле). Закрытие — `Escape` (из любого места страницы), `Tab` (`closeOnTab`) и нажатие мышью
|
|
152
|
+
вне поля и списка.
|
|
152
153
|
- В состояниях `loading` и `status={EFormFieldStatus.DISABLED}` поле не реагирует ни на клик,
|
|
153
154
|
ни на клавиатуру.
|
|
154
155
|
- **Доступное имя задаёт потребитель.** Компонент не хардкодит текст (библиотека мультиязычная):
|
|
@@ -207,6 +208,7 @@ version: "1.0"
|
|
|
207
208
|
| 2026-09-03 | Создан документ (TRI-75). AI-рефакторинг: удалён мёртвый внутренний `targetRef`, внешний `ref` пробрасывается напрямую и больше не пересоздаётся на каждый рендер; идентификатор списка переведён с `useRef(uniqueId())` на `useState(() => uniqueId())`, чтобы счётчик не крутился на каждом рендере; параметры рендер-функций перестали затенять `props` компонента; импорты приведены к алиасу пакета; JSDoc на всех props и на компоненте. Unit-тесты расширены с 15 до 24 кейсов (`onChange` с аргументом, опции и выбранное значение, `aria-activedescendant` через `DropdownListContext`, стабильность `listId`, ref-target). Stories переведены на modern pattern, `States` разделена на `Statuses` и `Loading`. Публичный API не изменён |
|
|
208
209
|
| 2026-09-29 | Аудит симметрии `related` (TRI-156): из `related` убраны несимметричные имена (`Chip`); добавлены обратные ссылки (`FormField`). Пояснения к убранным именам сохранены прозой в «Связанные компоненты»; публичный API и поведение не затронуты. |
|
|
209
210
|
| 2026-10-21 | Переход на `Puller` (TRIPLEX-961): мобильный элемент списка — `PullerListItem` (бывший `DropdownMobileListItem`). |
|
|
211
|
+
| 2026-10-29 | TRIPLEX-1028: Enter / Space на кнопке очистки и других фокусируемых элементах внутри поля больше не открывают список, клик по таким элементам, кроме очистки, — не переключает. Клавиши открытия — см. «Accessibility». `targetProps.onClear` получает событие клика. |
|
|
210
212
|
|
|
211
213
|
---
|
|
212
214
|
|
package/bundle/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"triplexVersion": "1.
|
|
4
|
-
"generatedAt": "2026-
|
|
3
|
+
"triplexVersion": "1.54.0",
|
|
4
|
+
"generatedAt": "2026-11-06T17:58:45.272Z",
|
|
5
5
|
"components": [
|
|
6
6
|
{
|
|
7
7
|
"name": "AbstractTree",
|
|
@@ -267,6 +267,12 @@
|
|
|
267
267
|
"file": "components/ChipSelect.md",
|
|
268
268
|
"ai_ready": true
|
|
269
269
|
},
|
|
270
|
+
{
|
|
271
|
+
"name": "ChipSelectTarget",
|
|
272
|
+
"path": "src/components/Chip/ChipSelectTarget-ai.md",
|
|
273
|
+
"file": "components/ChipSelectTarget.md",
|
|
274
|
+
"ai_ready": true
|
|
275
|
+
},
|
|
270
276
|
{
|
|
271
277
|
"name": "ChipSort",
|
|
272
278
|
"path": "src/components/Chip/ChipSort-ai.md",
|
|
@@ -279,6 +285,12 @@
|
|
|
279
285
|
"file": "components/ChipSuggest.md",
|
|
280
286
|
"ai_ready": true
|
|
281
287
|
},
|
|
288
|
+
{
|
|
289
|
+
"name": "ChipSuggestTarget",
|
|
290
|
+
"path": "src/components/Chip/ChipSuggestTarget-ai.md",
|
|
291
|
+
"file": "components/ChipSuggestTarget.md",
|
|
292
|
+
"ai_ready": true
|
|
293
|
+
},
|
|
282
294
|
{
|
|
283
295
|
"name": "CodeText",
|
|
284
296
|
"path": "src/components/Typography/CodeText-ai.md",
|
|
@@ -1304,6 +1316,11 @@
|
|
|
1304
1316
|
"file": "release-notes/1.54.0.mdx",
|
|
1305
1317
|
"path": "stories/release-notes/v1/1.54.0.mdx"
|
|
1306
1318
|
},
|
|
1319
|
+
{
|
|
1320
|
+
"version": "1.55.0",
|
|
1321
|
+
"file": "release-notes/1.55.0.mdx",
|
|
1322
|
+
"path": "stories/release-notes/v1/1.55.0.mdx"
|
|
1323
|
+
},
|
|
1307
1324
|
{
|
|
1308
1325
|
"version": "1.6.0",
|
|
1309
1326
|
"file": "release-notes/1.6.0.mdx",
|
|
@@ -21,7 +21,7 @@ import { Meta, Title, Heading } from "@storybook/addon-docs/blocks";
|
|
|
21
21
|
|
|
22
22
|
<Heading>Ломающие изменения</Heading>
|
|
23
23
|
|
|
24
|
-
**Dropdown / MultiselectField.Dropdown / SelectExtendedField.Dropdown /
|
|
24
|
+
**ChipSuggest / Dropdown / MultiselectField.Dropdown / SelectExtendedField.Dropdown / Suggest**
|
|
25
25
|
|
|
26
26
|
- Prop `mobileViewProps` переименован в `pullerProps` (тип `Omit<IPullerProps, "opened" | "setOpened">`), поведение прежнее. Переименование распространяется на props, унаследованные от `IDropdownProps`: `IMultiselectFieldDropdownProps`, `ISelectExtendedFieldDropdownProps`, `ISuggestDropdownProps`, `dropdownProps` у `ChipSuggest`.
|
|
27
27
|
- CSS-класс блокировки скролла на `body` у десктопного меню переименован: `.dropdownDesktopOverflowHidden` → `.dropdownDesktopScrollLock`.
|