@sberbusiness/triplex-next-mcp-server 1.43.0-beta → 1.45.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/README.md +7 -6
- package/bundle/components/AlertContext.md +11 -8
- package/bundle/components/AlertProcess.md +16 -12
- package/bundle/components/Badge.md +6 -3
- package/bundle/components/BodyPage.md +72 -4
- package/bundle/components/Button.md +46 -43
- package/bundle/components/ButtonDropdownExtended.md +3 -2
- package/bundle/components/ButtonIcon.md +7 -4
- package/bundle/components/Calendar.md +62 -59
- package/bundle/components/Caption.md +26 -23
- package/bundle/components/Checkbox.md +32 -29
- package/bundle/components/CheckboxTreeExtended.md +9 -5
- package/bundle/components/Chip.md +44 -41
- package/bundle/components/CodeText.md +26 -23
- package/bundle/components/Col.md +1 -2
- package/bundle/components/CollapsibleTree.md +8 -5
- package/bundle/components/CollapsibleTreeExtended.md +6 -1
- package/bundle/components/DatePickerExtended.md +409 -262
- package/bundle/components/Dropdown.md +33 -28
- package/bundle/components/EmptyView.md +12 -5
- package/bundle/components/ExpandAnimation.md +263 -218
- package/bundle/components/Footer.md +235 -278
- package/bundle/components/FooterPage.md +6 -2
- package/bundle/components/FormField.md +44 -41
- package/bundle/components/FormGroup.md +192 -52
- package/bundle/components/Header.md +491 -211
- package/bundle/components/HeaderPage.md +6 -2
- package/bundle/components/ImageGallery.md +33 -34
- package/bundle/components/ImageGalleryExtended.md +33 -34
- package/bundle/components/Island.md +52 -14
- package/bundle/components/LightBox.md +25 -18
- package/bundle/components/Link.md +211 -96
- package/bundle/components/ListActionItem.md +7 -4
- package/bundle/components/ListItem.md +11 -8
- package/bundle/components/ListItemControlsButton.md +16 -13
- package/bundle/components/ListItemControlsButtonDropdown.md +9 -6
- package/bundle/components/ListItemSelectable.md +7 -4
- package/bundle/components/ListMasterFooter.md +8 -5
- package/bundle/components/ListMasterHeader.md +8 -5
- package/bundle/components/ListSortableItem.md +11 -8
- package/bundle/components/ListTableItem.md +7 -4
- package/bundle/components/LoaderMiddle.md +7 -4
- package/bundle/components/LoaderScreen.md +11 -8
- package/bundle/components/LoaderSmall.md +7 -4
- package/bundle/components/Marker.md +184 -69
- package/bundle/components/MarkerStatus.md +206 -88
- package/bundle/components/MaskedField.md +2 -4
- package/bundle/components/MasterTable.md +602 -0
- package/bundle/components/ModalWindow.md +7 -4
- package/bundle/components/MultiselectField.md +2183 -132
- package/bundle/components/Notification.md +12 -9
- package/bundle/components/Overlay.md +33 -5
- package/bundle/components/Page.md +2 -2
- package/bundle/components/Pagination.md +27 -21
- package/bundle/components/Row.md +1 -1
- package/bundle/components/SelectExtendedField.md +3 -3
- package/bundle/components/SelectField.md +466 -148
- package/bundle/components/SliderExtended.md +39 -36
- package/bundle/components/SmallInput.md +194 -51
- package/bundle/components/Suggest.md +535 -99
- package/bundle/components/SwipeableArea.md +428 -79
- package/bundle/components/TableBasic.md +2302 -0
- package/bundle/components/TabsExtended.md +24 -21
- package/bundle/components/Tag.md +263 -78
- package/bundle/components/Text.md +28 -25
- package/bundle/components/TextField.md +3 -4
- package/bundle/components/ThemeProvider.md +50 -1
- package/bundle/components/Title.md +26 -23
- package/bundle/components/Tooltip.md +24 -21
- package/bundle/components/TopOverlay.md +368 -89
- package/bundle/components/TreeView.md +343 -166
- package/bundle/components/TreeViewNode.md +436 -0
- package/bundle/guides/codestyle.md +3 -0
- package/bundle/guides/commits.md +106 -26
- package/bundle/guides/context.md +84 -12
- package/bundle/guides/guidelines.md +321 -0
- package/bundle/guides/template.md +22 -12
- package/bundle/manifest.json +54 -20
- package/bundle/release-notes/1.44.0.mdx +113 -0
- package/bundle/release-notes/1.45.0.mdx +124 -0
- package/bundle/release-notes/1.46.0.mdx +7 -0
- package/bundle/tokens.json +6362 -0
- package/dist/index.js +177 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
component: Suggest
|
|
3
|
-
category:
|
|
4
|
-
|
|
5
|
-
figma-file: ""
|
|
6
|
-
related: [SuggestField, SelectField, Dropdown]
|
|
3
|
+
category: Suggest
|
|
4
|
+
related: [SuggestField, Chip, Dropdown]
|
|
7
5
|
tokens: []
|
|
8
|
-
stories:
|
|
6
|
+
stories: stories/Suggest/Suggest.stories.tsx
|
|
9
7
|
version: "1.0"
|
|
10
8
|
---
|
|
11
9
|
|
|
@@ -13,143 +11,171 @@ version: "1.0"
|
|
|
13
11
|
|
|
14
12
|
## Назначение
|
|
15
13
|
|
|
16
|
-
Headless
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
Headless-основа выпадающего списка с фильтрацией по введённому значению. Собственной
|
|
15
|
+
разметки не добавляет: рендерит `<div>`-обёртку и раздаёт потомкам через `SuggestContext`
|
|
16
|
+
значение поля ввода, видимость выпадающего списка и обработчики выбора, фильтрации и
|
|
17
|
+
клавиатуры. Управляющий элемент (поле ввода, чипс, кнопка) и сам выпадающий список пишет
|
|
18
|
+
потребитель — хук `useSuggestContext` отдаёт всё нужное состояние.
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
Внутри библиотеки на этой основе построен `ChipSuggest`. `SuggestField` решает ту же
|
|
21
|
+
задачу, но собственной реализацией состояния — на `Suggest` он пока не переведён
|
|
22
|
+
(в `SuggestField.tsx` стоит `TODO: Переписать через useSuggest`).
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Используй когда: нужен suggest с нестандартным управляющим элементом или нестандартным
|
|
25
|
+
выпадающим списком, а готовые `SuggestField` / `ChipSuggest` не подходят по разметке.
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
├── SuggestContext.ts
|
|
32
|
-
├── types.ts # ISuggestProps, ISuggestOption, ISuggestTargetProps...
|
|
33
|
-
├── useSuggest.ts # весь state + колбэки
|
|
34
|
-
├── SuggestMobileDropdownContent.tsx # mobile-вариант списка
|
|
35
|
-
├── index.ts
|
|
36
|
-
└── styles/
|
|
37
|
-
```
|
|
27
|
+
Не используй когда:
|
|
28
|
+
- Нужно обычное поле-suggest с лейблом, статусами и кнопкой очистки — возьми
|
|
29
|
+
`SuggestField`, он уже собран.
|
|
30
|
+
- Нужен suggest-фильтр в ряду чипсов — возьми `ChipSuggest`.
|
|
31
|
+
- Список не фильтруется вводом — возьми `SelectField` или `Dropdown` напрямую.
|
|
38
32
|
|
|
39
33
|
---
|
|
40
34
|
|
|
41
|
-
##
|
|
35
|
+
## Варианты и props
|
|
42
36
|
|
|
43
|
-
###
|
|
44
|
-
|
|
45
|
-
Generic по типу опции. Корневой `<div>` с `onKeyDown` и ref.
|
|
37
|
+
### Обязательные props
|
|
46
38
|
|
|
47
39
|
| Prop | Тип | Описание |
|
|
48
40
|
|---|---|---|
|
|
49
|
-
| `value` | `T \| undefined` | Выбранная опция. |
|
|
50
|
-
| `options` | `T[]` | Список опций. |
|
|
51
|
-
| `size` | `EComponentSize` |
|
|
52
|
-
| `onSelect` | `(value: T \| undefined) => void` |
|
|
53
|
-
| `onFilter` | `(value: string) => void` |
|
|
54
|
-
| `onScrollEnd?` | `() => void` | Вызов при скролле до конца списка (пагинация). |
|
|
55
|
-
| `placeholder?` | `string` | Передаётся в Context. |
|
|
56
|
-
| `noOptionsText?` | `string` | Текст «ничего не найдено». |
|
|
57
|
-
| `loading?` | `boolean` | Флаг загрузки корневого уровня. |
|
|
58
|
-
| `dropdownListLoading?` | `boolean` | Флаг загрузки списка (пагинация). |
|
|
59
|
-
| `tooltipOpen?` | `boolean` | Контролируемое открытие Tooltip (в потребителях). |
|
|
60
|
-
| `clearInputOnFocus?` | `boolean` | Очищать input при focus. |
|
|
61
|
-
| `children` | `ReactNode` | Слои UI (Target/Dropdown потребителя). |
|
|
62
|
-
| `...HTMLAttributes<HTMLDivElement>` | — | Прокидываются на корневой div (кроме `onSelect`). |
|
|
63
|
-
|
|
64
|
-
### `ISuggestOption`
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
{ id: string; label: string; content?: ReactNode; showNotificationIcon?: boolean }
|
|
68
|
-
```
|
|
41
|
+
| `value` | `T \| undefined` | Выбранная опция. Контролируемое значение: при его смене поле ввода перезаписывается на `value.label` |
|
|
42
|
+
| `options` | `T[]` | Список опций. Фильтрацию делает потребитель в `onFilter` — компонент список не фильтрует |
|
|
43
|
+
| `size` | `EComponentSize` | Размер. Suggest его **не отображает и не кладёт в контекст** — свойство существует, чтобы не попасть в DOM атрибутом; раздавай размер управляющему элементу и списку сам |
|
|
44
|
+
| `onSelect` | `(value: T \| undefined) => void` | Вызывается после выбора опции. Перед вызовом список закрывается, а в поле ввода подставляется `label` выбранной опции |
|
|
45
|
+
| `onFilter` | `(value: string) => void` | Вызывается на каждое изменение поля ввода. Ответ — обновлённый `options` |
|
|
69
46
|
|
|
70
|
-
|
|
47
|
+
### Опциональные props
|
|
71
48
|
|
|
72
|
-
|
|
49
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
50
|
+
|---|---|---|---|
|
|
51
|
+
| `placeholder` | `string` | — | Текст подсказки. Только раздаётся через контекст, компонент его не рендерит |
|
|
52
|
+
| `noOptionsText` | `string` | — | Текст при пустом `options`. Рендерится в мобильном наполнении, в десктопном — потребителем |
|
|
53
|
+
| `loading` | `boolean` | — | Флаг загрузки. В мобильном наполнении рисует лоадер в шапке, в десктопном — на усмотрение потребителя |
|
|
54
|
+
| `dropdownListLoading` | `boolean` | — | Флаг догрузки списка: лоадер под уже загруженными опциями |
|
|
55
|
+
| `tooltipOpen` | `boolean` | — | Управление видимостью Tooltip у потребителя. Suggest только раздаёт значение через контекст |
|
|
56
|
+
| `clearInputOnFocus` | `boolean` | — | Очищать поле ввода при получении фокуса. Обработчик `onFocus` пишет потребитель — Suggest ничего не слушает |
|
|
57
|
+
| `onScrollEnd` | `() => void` | — | Доступные опции закончились при скролле списка. В мобильном наполнении вызывается автоматически |
|
|
58
|
+
| `onKeyDown` | `React.KeyboardEventHandler` | — | Обработчик клавиатуры на корневом `<div>`. Вызывается после внутренней обработки `Escape` |
|
|
73
59
|
|
|
74
|
-
|
|
60
|
+
Все остальные `React.HTMLAttributes<HTMLDivElement>` (кроме `onSelect`) попадают на
|
|
61
|
+
корневой `<div>`: `className`, `id`, `data-*`, `aria-*`, `style`.
|
|
75
62
|
|
|
76
|
-
|
|
77
|
-
- `inputValue`, `setInputValue`
|
|
78
|
-
- `activeDescendant`, `setActiveDescendant`
|
|
79
|
-
- `dropdownOpen`, `setDropdownOpen`
|
|
80
|
-
- `dropdownListId` (уникальный `dropdown-list-<n>`)
|
|
81
|
-
- `closeDropdown(newInputValue?)` — закрытие с восстановлением значения из `latestValueRef`
|
|
82
|
-
- `onSelect` — вызывает `closeDropdown(label)` + `props.onSelect`
|
|
83
|
-
- `onFilter` — вызывает `setInputValue` + `props.onFilter`
|
|
84
|
-
- `onKeyDown` — Escape закрывает dropdown (с `stopPropagation`), остальные — прокидывает в props.onKeyDown.
|
|
63
|
+
### Что даёт `useSuggestContext`
|
|
85
64
|
|
|
86
|
-
|
|
65
|
+
Помимо перечисленных выше props потребитель получает состояние, которого нет во внешнем API:
|
|
87
66
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
67
|
+
| Поле | Описание |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `inputValue` / `setInputValue` | Текущий текст поля ввода. `setInputValue` не вызывает `onFilter` |
|
|
70
|
+
| `dropdownOpen` / `setDropdownOpen` | Видимость списка. `setDropdownOpen(false)` не сбрасывает поле ввода |
|
|
71
|
+
| `closeDropdown(newInputValue?)` | Закрывает список и возвращает в поле `newInputValue` либо `label` текущего `value` |
|
|
72
|
+
| `activeDescendant` / `setActiveDescendant` | Идентификатор элемента, выделенного клавиатурой, для `aria-activedescendant`. Библиотека его не выставляет — клавиатурную навигацию по списку реализует потребитель. `setActiveDescendant` приходит в контекст из `useSuggest` в рантайме, но в типе значения контекста не объявлен — типизация поля меняет публичный тип `SuggestContext`, решение за мейнтейнером |
|
|
73
|
+
| `dropdownListId` | Сгенерированный `dropdown-list-{n}` для `id` списка и `aria-controls` на управляющем элементе |
|
|
74
|
+
| `suggestRef` | Ссылка на корневой `<div>`. Годится как `targetRef` для `Dropdown` |
|
|
75
|
+
| `dropdownRef` | Ссылку **обязан заполнить потребитель**, повесив её на выпадающий список. По ней Suggest отличает клик внутри списка от клика вне компонента |
|
|
76
|
+
| `onSelect` / `onFilter` | Обёртки над одноимёнными props: дополнительно обновляют `inputValue` и закрывают список |
|
|
94
77
|
|
|
95
|
-
|
|
78
|
+
### Опция (`ISuggestOption`)
|
|
96
79
|
|
|
97
|
-
|
|
80
|
+
`id` (уникальный, используется как ключ и для сравнения с `value`), `label` (текст в поле
|
|
81
|
+
ввода и в списке), `content` (кастомная разметка опции в списке вместо `label`),
|
|
82
|
+
`showNotificationIcon` (значок новых уведомлений).
|
|
98
83
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
при восстановлении `inputValue`.
|
|
84
|
+
Тип опции расширяется дженериком: `<Suggest<IMyOption> ...>` протаскивает `IMyOption`
|
|
85
|
+
в `value`, `options` и `onSelect`.
|
|
102
86
|
|
|
103
87
|
---
|
|
104
88
|
|
|
105
|
-
##
|
|
89
|
+
## Дизайн-токены
|
|
106
90
|
|
|
107
|
-
|
|
108
|
-
`
|
|
109
|
-
|
|
91
|
+
Собственных токенов нет: единственный стиль компонента —
|
|
92
|
+
`styles/SuggestDropdown.module.less` — задаёт высоту и отступы мобильного наполнения
|
|
93
|
+
фиксированными значениями. Внешний вид определяют компоненты, которыми потребитель
|
|
94
|
+
собирает управляющий элемент и список (`TextField`, `Chip`, `Dropdown`), — их токены
|
|
95
|
+
переопределяются через `ThemeProvider` в соответствующих AI.md.
|
|
110
96
|
|
|
111
97
|
---
|
|
112
98
|
|
|
113
|
-
##
|
|
114
|
-
|
|
115
|
-
`useEffect` на `dropdownOpen`: вешает `mousedown`-listener на `document`,
|
|
116
|
-
закрывает dropdown, если клик не попал ни в `suggestRef`, ни в `dropdownRef`.
|
|
99
|
+
## Инварианты
|
|
117
100
|
|
|
118
|
-
`
|
|
101
|
+
- `forwardRef` на `Suggest` — не убирать. Ref указывает на корневой `<div>`, тот же элемент
|
|
102
|
+
попадает в `suggestRef` контекста.
|
|
103
|
+
- Корневой элемент — `<div>`. На него же вешается внутренний `onKeyDown`, поэтому событие
|
|
104
|
+
клавиатуры от любого потомка проходит через обработчик `Escape`.
|
|
105
|
+
- `size`, `onSelect`, `onFilter` и `onKeyDown` намеренно исключаются из `...restProps` —
|
|
106
|
+
иначе React отрендерит невалидные атрибуты на `<div>` и потеряет внутреннюю обработку.
|
|
107
|
+
- Закрытие по клику вне работает только если потребитель заполнил `dropdownRef`. Список
|
|
108
|
+
обычно живёт в портале и корневым `<div>` не покрывается.
|
|
109
|
+
- `Escape` останавливает всплытие **только когда список открыт** — на этом строится
|
|
110
|
+
вложенность suggest'а в модальные окна: закрытым suggest'ом `Escape` пробрасывается выше.
|
|
111
|
+
- Имена полей контекста — публичный API: `SuggestContext` и `useSuggestContext`
|
|
112
|
+
экспортируются из barrel и используются вне папки компонента (`ChipSuggest`).
|
|
113
|
+
- `SuggestMobileDropdownContent` из barrel **не экспортируется**, но импортируется
|
|
114
|
+
`ChipSuggest` по прямому пути. Переименование файла ломает `ChipSuggest`.
|
|
119
115
|
|
|
120
116
|
---
|
|
121
117
|
|
|
122
|
-
##
|
|
118
|
+
## Accessibility
|
|
123
119
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
- **ESC закрывает dropdown с `stopPropagation()`** — чтобы не всплывало в `Overlay`/`Modal`.
|
|
127
|
-
- **Синхронизация `value` → `inputValue`** через render-сравнение (`value !== prevValue`). Не переписывать на `useEffect` без обсуждения — появятся «мерцания».
|
|
128
|
-
- **`closeDropdown` использует `latestValueRef`, не `value`** — чтобы closure в listener'ах отдавал актуальное значение.
|
|
129
|
-
- **`dropdownListId` — один на инстанс** (через `uniqueId()` в `useState` initializer).
|
|
130
|
-
- **`onSelect(undefined)`** — допустимо (сбрасывает выбор). Потребитель должен это учесть.
|
|
120
|
+
Собственных ARIA-атрибутов Suggest не выставляет — разметку управляющего элемента и
|
|
121
|
+
списка пишет потребитель. Компонент даёт для этого:
|
|
131
122
|
|
|
132
|
-
|
|
123
|
+
- `dropdownListId` — общий идентификатор для `id` списка и `aria-controls` на управляющем
|
|
124
|
+
элементе;
|
|
125
|
+
- `dropdownOpen` — значение для `aria-expanded`;
|
|
126
|
+
- `activeDescendant` / `setActiveDescendant` — для `aria-activedescendant` при клавиатурной
|
|
127
|
+
навигации по списку.
|
|
133
128
|
|
|
134
|
-
|
|
129
|
+
Клавиатура: обработан только `Escape` — при открытом списке он закрывает список,
|
|
130
|
+
возвращает в поле ввода `label` текущего `value` и останавливает всплытие события.
|
|
131
|
+
Стрелки и `Enter` обрабатывает список потребителя (`DropdownList` / `DropdownListItem`
|
|
132
|
+
делают это сами). Возврат фокуса на управляющий элемент после закрытия Suggest не делает —
|
|
133
|
+
в `ChipSuggest` это решается `FocusTrap` вокруг содержимого дропдауна.
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- Escape-handling реализован на корневом div — потребитель не должен дублировать его на input.
|
|
135
|
+
Мобильное наполнение (`SuggestMobileDropdownContent`) рендерит список с `role="listbox"`
|
|
136
|
+
и опциями `role="option"` с `aria-selected`, поле ввода получает автофокус при открытии.
|
|
139
137
|
|
|
140
138
|
---
|
|
141
139
|
|
|
142
140
|
## Связанные компоненты
|
|
143
141
|
|
|
144
|
-
- `SuggestField` —
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
- `SuggestField` — готовое поле-suggest с лейблом, статусами, префиксом/постфиксом и
|
|
143
|
+
очисткой. Решает ту же задачу «из коробки»; на `Suggest` не построен и дублирует его
|
|
144
|
+
состояние своей реализацией.
|
|
145
|
+
- `Chip` — родитель `ChipSuggest`, единственного компонента библиотеки, собранного на
|
|
146
|
+
`Suggest`. `ChipSuggest` опирается не только на barrel, но и на внутренний
|
|
147
|
+
`SuggestMobileDropdownContent`.
|
|
148
|
+
- `Dropdown` — мобильное наполнение Suggest рендерится из его частей
|
|
149
|
+
(`DropdownMobileHeader`, `DropdownMobileInput`, `DropdownMobileList`, …); в десктопной
|
|
150
|
+
композиции `Dropdown` обычно и служит выпадающим списком.
|
|
151
|
+
- `SuggestMobileDropdownContent` — внутреннее наполнение полноэкранного мобильного
|
|
152
|
+
дропдауна: поле ввода, список опций, лоадер и текст при пустом списке. Данные берёт из
|
|
153
|
+
`SuggestContext`, поэтому рендерится только внутри `Suggest`. Отдельного AI.md не имеет,
|
|
154
|
+
из barrel не экспортируется.
|
|
147
155
|
|
|
148
156
|
---
|
|
149
157
|
|
|
150
158
|
## Stories
|
|
151
159
|
|
|
152
|
-
|
|
160
|
+
Основные истории: `stories/Suggest/Suggest.stories.tsx`
|
|
161
|
+
Файлы примеров: `stories/Suggest/examples/`
|
|
162
|
+
|
|
163
|
+
| Story | Example file | Что демонстрирует |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| `Playground` | `Playground.tsx` | Интерактивный контроль `size`, `placeholder`, `noOptionsText`, `clearInputOnFocus`, `dropdownListLoading` |
|
|
166
|
+
| `Default` | `Default.tsx` | Минимальная композиция: Suggest + управляющий элемент на `TextField` + список на `Dropdown` |
|
|
167
|
+
| `Sizes` | `Sizes.tsx` | Размеры SM / MD / LG — размер раздаётся потребителем |
|
|
168
|
+
| `DropdownListLoading` | `DropdownListLoading.tsx` | Догрузка опций: лоадер под уже загруженным списком |
|
|
169
|
+
| `VisualTests` | `VisualTests.tsx` | Скриншот-регрессия: размеры, заполненное значение, открытый дропдаун. Пустой список в кадр не попадает — открыт может быть только один дропдаун |
|
|
170
|
+
| `VisualTestsDropdownListLoading` | `VisualTestsDropdownListLoading.tsx` | Скриншот-регрессия: открытый дропдаун с лоадером догрузки. Отдельная стори — два Suggest'а не могут быть открыты одновременно |
|
|
171
|
+
|
|
172
|
+
Скриншот-тестами покрыты только `VisualTests` и `VisualTestsDropdownListLoading`:
|
|
173
|
+
набор исключён из скриншотов на уровне `meta` (`testRunner: { skip: true }`), а эти две
|
|
174
|
+
стори включены обратно через `testRunner: { skip: false }` — как в `ModalWindow`.
|
|
175
|
+
Suggest headless: собственной разметки он не рендерит, поэтому в кадре закрытых стори
|
|
176
|
+
только обвязка примеров (`TextField`, `Dropdown`), у которых своё визуальное покрытие,
|
|
177
|
+
а baseline `Default` и `DropdownListLoading` совпадали побайтово. Ценность есть лишь
|
|
178
|
+
у кадров с открытым списком, которые ставит `play`-функция.
|
|
153
179
|
|
|
154
180
|
---
|
|
155
181
|
|
|
@@ -157,4 +183,414 @@ if (value !== prevValue) {
|
|
|
157
183
|
|
|
158
184
|
| Дата | Изменение |
|
|
159
185
|
|---|---|
|
|
160
|
-
| 2026-
|
|
186
|
+
| 2026-09-02 | Создан документ |
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Примеры использования
|
|
191
|
+
|
|
192
|
+
### Default
|
|
193
|
+
|
|
194
|
+
`stories/Suggest/examples/Default.tsx`
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import React, { useState } from "react";
|
|
198
|
+
import {
|
|
199
|
+
Dropdown,
|
|
200
|
+
DropdownList,
|
|
201
|
+
DropdownListItem,
|
|
202
|
+
EComponentSize,
|
|
203
|
+
EDropdownWidth,
|
|
204
|
+
Suggest,
|
|
205
|
+
TextField,
|
|
206
|
+
useSuggestContext,
|
|
207
|
+
type ISuggestOption,
|
|
208
|
+
} from "@sberbusiness/triplex-next";
|
|
209
|
+
|
|
210
|
+
const FRUITS: ISuggestOption[] = [
|
|
211
|
+
{ id: "1", label: "Apple" },
|
|
212
|
+
{ id: "2", label: "Banana" },
|
|
213
|
+
{ id: "3", label: "Cherry" },
|
|
214
|
+
{ id: "4", label: "Grapes" },
|
|
215
|
+
{ id: "5", label: "Melon" },
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const SIZE = EComponentSize.LG;
|
|
219
|
+
|
|
220
|
+
/** Управляющий элемент. Значение поля ввода и состояние списка берутся из SuggestContext. */
|
|
221
|
+
const SuggestTarget = () => {
|
|
222
|
+
const { inputValue, dropdownOpen, dropdownListId, setDropdownOpen, onFilter } = useSuggestContext();
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<TextField
|
|
226
|
+
label="Fruit"
|
|
227
|
+
size={SIZE}
|
|
228
|
+
inputProps={{
|
|
229
|
+
value: inputValue,
|
|
230
|
+
placeholder: "Type to proceed",
|
|
231
|
+
role: "combobox",
|
|
232
|
+
"aria-expanded": dropdownOpen,
|
|
233
|
+
"aria-controls": dropdownListId,
|
|
234
|
+
onChange: (event) => {
|
|
235
|
+
onFilter(event.target.value);
|
|
236
|
+
setDropdownOpen(true);
|
|
237
|
+
},
|
|
238
|
+
onClick: () => setDropdownOpen(true),
|
|
239
|
+
}}
|
|
240
|
+
/>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/** Выпадающий список. dropdownRef обязателен: по нему Suggest отличает клик внутри списка от клика вне компонента. */
|
|
245
|
+
const SuggestDropdown = () => {
|
|
246
|
+
const {
|
|
247
|
+
value,
|
|
248
|
+
options,
|
|
249
|
+
noOptionsText,
|
|
250
|
+
dropdownOpen,
|
|
251
|
+
dropdownListId,
|
|
252
|
+
suggestRef,
|
|
253
|
+
dropdownRef,
|
|
254
|
+
setDropdownOpen,
|
|
255
|
+
closeDropdown,
|
|
256
|
+
onSelect,
|
|
257
|
+
} = useSuggestContext();
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<Dropdown
|
|
261
|
+
ref={dropdownRef}
|
|
262
|
+
size={SIZE}
|
|
263
|
+
targetRef={suggestRef}
|
|
264
|
+
width={EDropdownWidth.TARGET}
|
|
265
|
+
opened={dropdownOpen}
|
|
266
|
+
setOpened={(opened) => (opened ? setDropdownOpen(true) : closeDropdown())}
|
|
267
|
+
>
|
|
268
|
+
{options.length === 0 ? (
|
|
269
|
+
// id остаётся на контейнере и в пустом состоянии — иначе aria-controls
|
|
270
|
+
// управляющего элемента ссылается на несуществующий элемент.
|
|
271
|
+
<div id={dropdownListId} style={{ padding: "12px 16px" }}>
|
|
272
|
+
{noOptionsText}
|
|
273
|
+
</div>
|
|
274
|
+
) : (
|
|
275
|
+
<DropdownList id={dropdownListId} size={SIZE} dropdownOpened={dropdownOpen}>
|
|
276
|
+
{options.map((option) => (
|
|
277
|
+
<DropdownListItem
|
|
278
|
+
key={option.id}
|
|
279
|
+
id={option.id}
|
|
280
|
+
selected={option.id === value?.id}
|
|
281
|
+
onSelect={() => onSelect(option)}
|
|
282
|
+
>
|
|
283
|
+
{option.label}
|
|
284
|
+
</DropdownListItem>
|
|
285
|
+
))}
|
|
286
|
+
</DropdownList>
|
|
287
|
+
)}
|
|
288
|
+
</Dropdown>
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export const Default = () => {
|
|
293
|
+
const [value, setValue] = useState<ISuggestOption>();
|
|
294
|
+
const [options, setOptions] = useState<ISuggestOption[]>(FRUITS);
|
|
295
|
+
|
|
296
|
+
const handleFilter = (inputValue: string) =>
|
|
297
|
+
setOptions(FRUITS.filter(({ label }) => label.toLowerCase().includes(inputValue.toLowerCase())));
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div style={{ maxWidth: "300px" }}>
|
|
301
|
+
<Suggest
|
|
302
|
+
value={value}
|
|
303
|
+
options={options}
|
|
304
|
+
size={SIZE}
|
|
305
|
+
noOptionsText="No matches found."
|
|
306
|
+
onSelect={setValue}
|
|
307
|
+
onFilter={handleFilter}
|
|
308
|
+
>
|
|
309
|
+
<SuggestTarget />
|
|
310
|
+
<SuggestDropdown />
|
|
311
|
+
</Suggest>
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Sizes
|
|
318
|
+
|
|
319
|
+
`stories/Suggest/examples/Sizes.tsx`
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
import React, { useState } from "react";
|
|
323
|
+
import {
|
|
324
|
+
Dropdown,
|
|
325
|
+
DropdownList,
|
|
326
|
+
DropdownListItem,
|
|
327
|
+
EComponentSize,
|
|
328
|
+
EDropdownWidth,
|
|
329
|
+
Suggest,
|
|
330
|
+
TextField,
|
|
331
|
+
useSuggestContext,
|
|
332
|
+
type ISuggestOption,
|
|
333
|
+
} from "@sberbusiness/triplex-next";
|
|
334
|
+
|
|
335
|
+
const FRUITS: ISuggestOption[] = [
|
|
336
|
+
{ id: "1", label: "Apple" },
|
|
337
|
+
{ id: "2", label: "Banana" },
|
|
338
|
+
{ id: "3", label: "Cherry" },
|
|
339
|
+
{ id: "4", label: "Grapes" },
|
|
340
|
+
{ id: "5", label: "Melon" },
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
const SIZES = Object.values(EComponentSize);
|
|
344
|
+
|
|
345
|
+
interface ISuggestPartProps {
|
|
346
|
+
size: EComponentSize;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Управляющий элемент. Suggest сам размер не отображает — прокидывайте его в поле ввода и список. */
|
|
350
|
+
const SuggestTarget = ({ size }: ISuggestPartProps) => {
|
|
351
|
+
const { inputValue, dropdownOpen, dropdownListId, setDropdownOpen, onFilter } = useSuggestContext();
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<TextField
|
|
355
|
+
label="Fruit"
|
|
356
|
+
size={size}
|
|
357
|
+
inputProps={{
|
|
358
|
+
value: inputValue,
|
|
359
|
+
placeholder: "Type to proceed",
|
|
360
|
+
role: "combobox",
|
|
361
|
+
"aria-expanded": dropdownOpen,
|
|
362
|
+
"aria-controls": dropdownListId,
|
|
363
|
+
onChange: (event) => {
|
|
364
|
+
onFilter(event.target.value);
|
|
365
|
+
setDropdownOpen(true);
|
|
366
|
+
},
|
|
367
|
+
onClick: () => setDropdownOpen(true),
|
|
368
|
+
}}
|
|
369
|
+
/>
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/** Выпадающий список. dropdownRef обязателен: по нему Suggest отличает клик внутри списка от клика вне компонента. */
|
|
374
|
+
const SuggestDropdown = ({ size }: ISuggestPartProps) => {
|
|
375
|
+
const {
|
|
376
|
+
value,
|
|
377
|
+
options,
|
|
378
|
+
noOptionsText,
|
|
379
|
+
dropdownOpen,
|
|
380
|
+
dropdownListId,
|
|
381
|
+
suggestRef,
|
|
382
|
+
dropdownRef,
|
|
383
|
+
setDropdownOpen,
|
|
384
|
+
closeDropdown,
|
|
385
|
+
onSelect,
|
|
386
|
+
} = useSuggestContext();
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<Dropdown
|
|
390
|
+
ref={dropdownRef}
|
|
391
|
+
size={size}
|
|
392
|
+
targetRef={suggestRef}
|
|
393
|
+
width={EDropdownWidth.TARGET}
|
|
394
|
+
opened={dropdownOpen}
|
|
395
|
+
setOpened={(opened) => (opened ? setDropdownOpen(true) : closeDropdown())}
|
|
396
|
+
>
|
|
397
|
+
{options.length === 0 ? (
|
|
398
|
+
// id остаётся на контейнере и в пустом состоянии — иначе aria-controls
|
|
399
|
+
// управляющего элемента ссылается на несуществующий элемент.
|
|
400
|
+
<div id={dropdownListId} style={{ padding: "12px 16px" }}>
|
|
401
|
+
{noOptionsText}
|
|
402
|
+
</div>
|
|
403
|
+
) : (
|
|
404
|
+
<DropdownList id={dropdownListId} size={size} dropdownOpened={dropdownOpen}>
|
|
405
|
+
{options.map((option) => (
|
|
406
|
+
<DropdownListItem
|
|
407
|
+
key={option.id}
|
|
408
|
+
id={option.id}
|
|
409
|
+
selected={option.id === value?.id}
|
|
410
|
+
onSelect={() => onSelect(option)}
|
|
411
|
+
>
|
|
412
|
+
{option.label}
|
|
413
|
+
</DropdownListItem>
|
|
414
|
+
))}
|
|
415
|
+
</DropdownList>
|
|
416
|
+
)}
|
|
417
|
+
</Dropdown>
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const SizeItem = ({ size }: ISuggestPartProps) => {
|
|
422
|
+
const [value, setValue] = useState<ISuggestOption>();
|
|
423
|
+
const [options, setOptions] = useState<ISuggestOption[]>(FRUITS);
|
|
424
|
+
|
|
425
|
+
const handleFilter = (inputValue: string) =>
|
|
426
|
+
setOptions(FRUITS.filter(({ label }) => label.toLowerCase().includes(inputValue.toLowerCase())));
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<div>
|
|
430
|
+
<div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>{size.toUpperCase()}</div>
|
|
431
|
+
<Suggest
|
|
432
|
+
value={value}
|
|
433
|
+
options={options}
|
|
434
|
+
size={size}
|
|
435
|
+
noOptionsText="No matches found."
|
|
436
|
+
onSelect={setValue}
|
|
437
|
+
onFilter={handleFilter}
|
|
438
|
+
>
|
|
439
|
+
<SuggestTarget size={size} />
|
|
440
|
+
<SuggestDropdown size={size} />
|
|
441
|
+
</Suggest>
|
|
442
|
+
</div>
|
|
443
|
+
);
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
export const Sizes = () => (
|
|
447
|
+
<div style={{ maxWidth: "300px", display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
448
|
+
{SIZES.map((size) => (
|
|
449
|
+
<SizeItem key={size} size={size} />
|
|
450
|
+
))}
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### DropdownListLoading
|
|
456
|
+
|
|
457
|
+
`stories/Suggest/examples/DropdownListLoading.tsx`
|
|
458
|
+
|
|
459
|
+
```tsx
|
|
460
|
+
import React, { useRef, useState } from "react";
|
|
461
|
+
import {
|
|
462
|
+
Dropdown,
|
|
463
|
+
DropdownList,
|
|
464
|
+
DropdownListItem,
|
|
465
|
+
EComponentSize,
|
|
466
|
+
EDropdownWidth,
|
|
467
|
+
Suggest,
|
|
468
|
+
TextField,
|
|
469
|
+
useSuggestContext,
|
|
470
|
+
type ISuggestOption,
|
|
471
|
+
} from "@sberbusiness/triplex-next";
|
|
472
|
+
|
|
473
|
+
const FRUITS: ISuggestOption[] = [
|
|
474
|
+
{ id: "1", label: "Apple" },
|
|
475
|
+
{ id: "2", label: "Banana" },
|
|
476
|
+
{ id: "3", label: "Cherry" },
|
|
477
|
+
{ id: "4", label: "Grapes" },
|
|
478
|
+
{ id: "5", label: "Melon" },
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
const SIZE = EComponentSize.LG;
|
|
482
|
+
const REQUEST_DELAY = 700;
|
|
483
|
+
|
|
484
|
+
/** Управляющий элемент. Значение поля ввода и состояние списка берутся из SuggestContext. */
|
|
485
|
+
const SuggestTarget = () => {
|
|
486
|
+
const { inputValue, dropdownOpen, dropdownListId, setDropdownOpen, onFilter } = useSuggestContext();
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
<TextField
|
|
490
|
+
label="Fruit"
|
|
491
|
+
size={SIZE}
|
|
492
|
+
inputProps={{
|
|
493
|
+
value: inputValue,
|
|
494
|
+
placeholder: "Type to proceed",
|
|
495
|
+
role: "combobox",
|
|
496
|
+
"aria-expanded": dropdownOpen,
|
|
497
|
+
"aria-controls": dropdownListId,
|
|
498
|
+
onChange: (event) => {
|
|
499
|
+
onFilter(event.target.value);
|
|
500
|
+
setDropdownOpen(true);
|
|
501
|
+
},
|
|
502
|
+
onClick: () => setDropdownOpen(true),
|
|
503
|
+
}}
|
|
504
|
+
/>
|
|
505
|
+
);
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
/** Выпадающий список. dropdownListLoading дорисовывает лоадер под уже загруженными опциями. */
|
|
509
|
+
const SuggestDropdown = () => {
|
|
510
|
+
const {
|
|
511
|
+
value,
|
|
512
|
+
options,
|
|
513
|
+
noOptionsText,
|
|
514
|
+
dropdownOpen,
|
|
515
|
+
dropdownListId,
|
|
516
|
+
dropdownListLoading,
|
|
517
|
+
suggestRef,
|
|
518
|
+
dropdownRef,
|
|
519
|
+
setDropdownOpen,
|
|
520
|
+
closeDropdown,
|
|
521
|
+
onSelect,
|
|
522
|
+
} = useSuggestContext();
|
|
523
|
+
|
|
524
|
+
return (
|
|
525
|
+
<Dropdown
|
|
526
|
+
ref={dropdownRef}
|
|
527
|
+
size={SIZE}
|
|
528
|
+
targetRef={suggestRef}
|
|
529
|
+
width={EDropdownWidth.TARGET}
|
|
530
|
+
opened={dropdownOpen}
|
|
531
|
+
setOpened={(opened) => (opened ? setDropdownOpen(true) : closeDropdown())}
|
|
532
|
+
>
|
|
533
|
+
{options.length === 0 && !dropdownListLoading ? (
|
|
534
|
+
// id остаётся на контейнере и в пустом состоянии — иначе aria-controls
|
|
535
|
+
// управляющего элемента ссылается на несуществующий элемент.
|
|
536
|
+
<div id={dropdownListId} style={{ padding: "12px 16px" }}>
|
|
537
|
+
{noOptionsText}
|
|
538
|
+
</div>
|
|
539
|
+
) : (
|
|
540
|
+
<DropdownList
|
|
541
|
+
id={dropdownListId}
|
|
542
|
+
size={SIZE}
|
|
543
|
+
dropdownOpened={dropdownOpen}
|
|
544
|
+
loading={dropdownListLoading}
|
|
545
|
+
>
|
|
546
|
+
{options.map((option) => (
|
|
547
|
+
<DropdownListItem
|
|
548
|
+
key={option.id}
|
|
549
|
+
id={option.id}
|
|
550
|
+
selected={option.id === value?.id}
|
|
551
|
+
onSelect={() => onSelect(option)}
|
|
552
|
+
>
|
|
553
|
+
{option.label}
|
|
554
|
+
</DropdownListItem>
|
|
555
|
+
))}
|
|
556
|
+
</DropdownList>
|
|
557
|
+
)}
|
|
558
|
+
</Dropdown>
|
|
559
|
+
);
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
export const DropdownListLoading = () => {
|
|
563
|
+
const [value, setValue] = useState<ISuggestOption>();
|
|
564
|
+
const [options, setOptions] = useState<ISuggestOption[]>(FRUITS);
|
|
565
|
+
const [loading, setLoading] = useState(false);
|
|
566
|
+
const requestRef = useRef<ReturnType<typeof setTimeout>>();
|
|
567
|
+
|
|
568
|
+
const handleFilter = (inputValue: string) => {
|
|
569
|
+
clearTimeout(requestRef.current);
|
|
570
|
+
setLoading(true);
|
|
571
|
+
|
|
572
|
+
requestRef.current = setTimeout(() => {
|
|
573
|
+
setOptions(FRUITS.filter(({ label }) => label.toLowerCase().includes(inputValue.toLowerCase())));
|
|
574
|
+
setLoading(false);
|
|
575
|
+
}, REQUEST_DELAY);
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<div style={{ maxWidth: "300px" }}>
|
|
580
|
+
<Suggest
|
|
581
|
+
value={value}
|
|
582
|
+
options={options}
|
|
583
|
+
size={SIZE}
|
|
584
|
+
noOptionsText="No matches found."
|
|
585
|
+
dropdownListLoading={loading}
|
|
586
|
+
onSelect={setValue}
|
|
587
|
+
onFilter={handleFilter}
|
|
588
|
+
>
|
|
589
|
+
<SuggestTarget />
|
|
590
|
+
<SuggestDropdown />
|
|
591
|
+
</Suggest>
|
|
592
|
+
</div>
|
|
593
|
+
);
|
|
594
|
+
};
|
|
595
|
+
```
|
|
596
|
+
|