@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.
@@ -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 — всегда в `document.body`, desktop — в
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
- Нет, кроме HTML-атрибутов `<div>`.
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
- - `Puller` не выносит себя в `Portal`: предок с `transform`, `filter`, `perspective`, `contain` или собственным stacking context сдвинет, обрежет или перекроет шторку (`.pullerWrapper` — `position: fixed`). При прямом использовании оберни `Puller` в `<Portal container={document.body}>`; `pullerProps` у `Dropdown` оборачивать не нужно.
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
- Примеры рендерят `Puller` в `<Portal container={document.body}>` и стартуют закрытыми. Шторку открывает `play`; на docs-странице (`viewMode === "docs"`) он ничего не делает.
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
- | `SMSPuller` («Example: SMSPuller») | `SMSPuller.tsx` | Подтверждение операции СМС-кодом: `SMSField` с обратным отсчётом в `PullerBody` |
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
- <Portal container={document.body}>
191
- <Puller opened={opened} setOpened={setOpened} role="dialog" aria-labelledby="puller-default-title">
192
- <PullerHeader controlButtons={<PullerClose aria-label="Close" onClick={() => setOpened(false)} />}>
193
- <Text id="puller-default-title" size={ETextSize.B1}>
194
- Title
195
- </Text>
196
- </PullerHeader>
197
- <PullerBody>
198
- <Text tag="div" size={ETextSize.B3} style={{ padding: "16px" }}>
199
- Puller content. The puller closes via the close button, the footer button or a tap on the
200
- backdrop.
201
- </Text>
202
- </PullerBody>
203
- <PullerFooter>
204
- <Button
205
- theme={EButtonTheme.GENERAL}
206
- size={EComponentSize.MD}
207
- block
208
- onClick={() => setOpened(false)}
209
- >
210
- Done
211
- </Button>
212
- </PullerFooter>
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
- <Portal container={document.body}>
268
- <Puller opened={opened} setOpened={setOpened} role="dialog" aria-labelledby="puller-with-list-title">
269
- <PullerHeader controlButtons={<PullerClose aria-label="Close" onClick={() => setOpened(false)} />}>
270
- <Text id="puller-with-list-title" size={ETextSize.B1}>
271
- Select a value
272
- </Text>
273
- </PullerHeader>
274
- <PullerBody>
275
- <PullerList loading aria-labelledby="puller-with-list-title">
276
- {OPTIONS.map((option) => (
277
- <PullerListItem
278
- key={option.id}
279
- id={option.id}
280
- selected={option.id === selectedId}
281
- showNotificationIcon={option.hasNotifications}
282
- onSelect={() => handleSelect(option.id)}
283
- >
284
- {option.label}
285
- </PullerListItem>
286
- ))}
287
- </PullerList>
288
- </PullerBody>
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
- <Portal container={document.body}>
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
365
- key={option.id}
366
- id={option.id}
367
- onSelect={() => handleSelect(option.label)}
368
- >
369
- {option.label}
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` | `() => void` | — | Если передан, в постфиксе появляется кнопка очистки |
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
- - Клик по кнопке очистки (`onClear`) не раскрывает выпадающий блок: `SelectExtendedFieldTarget` останавливает всплытие в обработчике `FormFieldClear`. Без этого событие дошло бы до `onClick` на `FormField` и переключило `opened` — так же гасят всплытие `Chip.Select` и `Chip.DatePicker`. Не убирать: затрагивает `SelectField` и `MultiselectField.Target`, которые рендерят тот же `Target`.
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`. Обработчик вызывает `preventDefault` (чтобы страница не прокручивалась) и `stopPropagation` — иначе то же нажатие поймает документный обработчик `DropdownListItem` и список сразу выберет опцию.
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`. Закрытие — `Escape`
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
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "triplexVersion": "1.53.0",
4
- "generatedAt": "2026-10-30T17:09:54.308Z",
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 / ChipSuggest / Suggest**
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`.