@sberbusiness/triplex-next-mcp-server 1.46.0-beta → 1.48.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 -7
- package/bundle/components/AmountField.md +222 -323
- package/bundle/components/Avatar.md +138 -146
- package/bundle/components/CardStatic.md +402 -0
- package/bundle/components/CheckboxTree.md +157 -215
- package/bundle/components/CheckboxTreeExtended.md +7 -7
- package/bundle/components/ChipGroup.md +1 -1
- package/bundle/components/ChipMonthYearPicker.md +4 -4
- package/bundle/components/ChipSuggest.md +7 -6
- package/bundle/components/CollapsibleTree.md +8 -4
- package/bundle/components/CollapsibleTreeExtended.md +8 -6
- package/bundle/components/Confirm.md +346 -281
- package/bundle/components/DateRange.md +351 -210
- package/bundle/components/FooterPage.md +1 -0
- package/bundle/components/FormField.md +11 -4
- package/bundle/components/FormGroup.md +3 -2
- package/bundle/components/LightBox.md +8 -0
- package/bundle/components/ListMaster.md +2 -2
- package/bundle/components/ListTableItem.md +6 -6
- package/bundle/components/MonthYearField.md +15 -4
- package/bundle/components/Slider.md +389 -97
- package/bundle/components/SliderRange.md +486 -105
- package/bundle/components/StatusTracker.md +386 -94
- package/bundle/components/Stepper.md +355 -102
- package/bundle/components/StepperExtended.md +633 -0
- package/bundle/components/SuggestField.md +1049 -96
- package/bundle/components/SwipeableArea.md +7 -7
- package/bundle/components/Tabs.md +265 -92
- package/bundle/components/TabsExtended.md +18 -18
- package/bundle/components/TabsLine.md +301 -127
- package/bundle/components/TagColor.md +187 -66
- package/bundle/components/TagGroup.md +184 -48
- package/bundle/components/TextareaField.md +278 -70
- package/bundle/components/TopOverlay.md +17 -1
- package/bundle/components/TreeView.md +16 -7
- package/bundle/components/TriggerClickOnKeyDownEvent.md +277 -0
- package/bundle/components/UploadZone.md +318 -0
- package/bundle/guides/codestyle.md +16 -1
- package/bundle/guides/commits.md +47 -1
- package/bundle/guides/tests.md +23 -2
- package/bundle/manifest.json +48 -21
- package/bundle/release-notes/1.47.0.mdx +115 -0
- package/bundle/release-notes/1.48.0.mdx +68 -0
- package/bundle/release-notes/1.49.0.mdx +7 -0
- package/bundle/tokens.json +3 -3
- package/dist/index.js +220 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
component: AmountField
|
|
3
3
|
category: TextFields
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
stories: stories/
|
|
4
|
+
related: [TextField, FormField, Amount, NumberField]
|
|
5
|
+
tokens:
|
|
6
|
+
- FormField.Input_Color_Default
|
|
7
|
+
- FormField.Input_Color_Disabled
|
|
8
|
+
stories: stories/AmountField/AmountField.stories.tsx
|
|
9
9
|
version: "1.0"
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -13,272 +13,175 @@ version: "1.0"
|
|
|
13
13
|
|
|
14
14
|
## Назначение
|
|
15
15
|
|
|
16
|
-
Поле ввода денежной
|
|
17
|
-
(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
Поле ввода денежной суммы. Пока пользователь печатает, значение форматируется на лету
|
|
17
|
+
(разряды разделяются пробелом, целая и дробная части — запятой: `1234.56` → `1 234,56`),
|
|
18
|
+
а наружу через `inputProps.onChange` уходит нормализованное значение с точкой и без
|
|
19
|
+
разделителей разрядов (`"1234.56"`) — его можно отправлять на бэкенд без дополнительной
|
|
20
|
+
обработки. Дробная часть всегда дописывается до `fractionDigits` знаков, а каретка
|
|
21
|
+
удерживается в осмысленной позиции при вводе, удалении и вставке.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
инкапсулирована в классе `AmountBaseInputCore`, который внутренне делит работу на
|
|
25
|
-
парсер (`AmountBaseInputParser`) и форматер (`AmountBaseInputFormatter`).
|
|
23
|
+
Композиция над семейством `FormField`: `TextFieldBase` (→ `FormGroup` → `FormField`) +
|
|
24
|
+
`FormFieldInput`, плюс собственный слой со знаком валюты.
|
|
26
25
|
|
|
27
|
-
Используй когда: пользователь вводит
|
|
28
|
-
|
|
29
|
-
Не используй когда: нужен просто числовой ввод без валюты (используй `NumberField`);
|
|
30
|
-
нужно только отобразить сумму без ввода (используй `Amount`).
|
|
26
|
+
Используй когда: пользователь вводит сумму — платёж, лимит, ставку, любой денежный
|
|
27
|
+
показатель, который нужно показывать с разрядами и хранить числом.
|
|
31
28
|
|
|
32
|
-
|
|
29
|
+
Не используй когда: нужно только показать готовую сумму без ввода (`Amount`), нужен
|
|
30
|
+
счётчик/произвольное число без денежного форматирования (`NumberField`), значение имеет
|
|
31
|
+
фиксированный формат-маску (`MaskedField`) или формат произвольный (`TextField`).
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
---
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
src/components/AmountField/
|
|
38
|
-
├── AmountField.tsx # React.forwardRef, главный компонент
|
|
39
|
-
├── AmountBaseInputCore.ts # Класс-оркестратор: parse → format, хранит caret и cache
|
|
40
|
-
├── AmountBaseInputParser.ts # Класс-парсер: нормализует value, trimLeadingZeros, splits integer/fractional
|
|
41
|
-
├── AmountBaseInputFormatter.ts # Класс-форматер: добавляет пробелы между группами, запятую
|
|
42
|
-
├── utils.ts # createPlaceholder, setFallbackCaret
|
|
43
|
-
├── index.ts # Barrel (только AmountField)
|
|
44
|
-
└── __tests__/ # Unit-тесты
|
|
45
|
-
```
|
|
35
|
+
## Варианты и props
|
|
46
36
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- **Нет `stories/AmountField/`** — stories лежат в `stories/TextFields/AmountField.stories.tsx`
|
|
51
|
-
(логически относится к группе TextFields).
|
|
52
|
-
- Core-логика разделена на 3 класса намеренно — parser, formatter, core (оркестратор).
|
|
53
|
-
Такое разделение нужно, чтобы можно было тестировать parse/format изолированно.
|
|
37
|
+
`IAmountFieldProps` расширяет `ITextFieldBaseProps` (кроме `children`), поэтому
|
|
38
|
+
`size`, `status`, `active`, `label`, `prefix`, `postfix`, `description`, `counter`,
|
|
39
|
+
`className` и div-атрибуты ведут себя так же, как у `TextField`.
|
|
54
40
|
|
|
55
|
-
|
|
41
|
+
### Обязательные props
|
|
56
42
|
|
|
57
|
-
|
|
43
|
+
| Prop | Тип | Описание |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `inputProps` | `Omit<IFormFieldInputProps, "type" \| "maxLength" \| "onChange" \| "inputMode" \| "autoComplete"> & { value, onChange, ref? }` | Свойства поля ввода. `value` — нормализованное значение (`"1234.56"`, пустая строка для пустого поля), `onChange(value)` получает такое же нормализованное значение, `ref` — ссылка на `<input>`. Остальные атрибуты (`placeholder`, `id`, `onFocus`, `onBlur`, `onKeyDown`, `onSelect`, data-атрибуты) пробрасываются в `FormFieldInput`. |
|
|
58
46
|
|
|
59
|
-
###
|
|
47
|
+
### Опциональные props
|
|
60
48
|
|
|
61
49
|
| Prop | Тип | По умолчанию | Описание |
|
|
62
50
|
|---|---|---|---|
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
|
|
|
70
|
-
|
|
71
|
-
###
|
|
72
|
-
|
|
73
|
-
- `
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
51
|
+
| `size` | `EComponentSize` | `LG` | Размер поля: SM / MD / LG. Тот же размер получает слой со знаком валюты. |
|
|
52
|
+
| `status` | `EFormFieldStatus` | `DEFAULT` | Статус: default / error / warning / disabled. `DISABLED` блокирует input и приглушает знак валюты. |
|
|
53
|
+
| `currency` | `string` | — | Наименование валюты справа от значения. Не рендерится, пока отформатированное значение пустое. |
|
|
54
|
+
| `maxIntegerDigits` | `number` | `16` | Максимальное количество знаков перед запятой. Лишние цифры не попадают в значение. |
|
|
55
|
+
| `fractionDigits` | `number` | `2` | Количество знаков после запятой. `0` — поле без дробной части. |
|
|
56
|
+
| `onClear` | `() => void` | — | Обработчик очистки. Если передан, перед пользовательским `postfix` рендерится `FormFieldClear`. Компонент сам значение не сбрасывает — это делает потребитель. |
|
|
57
|
+
| `clearProps` | `Omit<IFormFieldClearProps, "onClick">` | — | Свойства кнопки очистки. Имеют смысл только вместе с `onClear`. Сюда передаётся `aria-label` — доступное имя кнопки. `onClick` задаётся через `onClear` и не переопределяется. |
|
|
58
|
+
|
|
59
|
+
### Особенности поведения
|
|
60
|
+
|
|
61
|
+
- **Значение контролируемое и нормализованное.** В `value` хранится `"<целая>.<дробная>"`
|
|
62
|
+
без разделителей разрядов; в поле показывается форматированный вариант с запятой.
|
|
63
|
+
Значение, пришедшее снаружи, нормализуется только для отображения: `"1234,5"` покажется
|
|
64
|
+
как `1 234,50`, но `onChange` при этом не вызывается — в состоянии потребителя останется
|
|
65
|
+
`"1234,5"`, пока пользователь не отредактирует поле. Синхронизировать состояние с
|
|
66
|
+
нормализованным видом компонент не берётся: это задача потребителя.
|
|
67
|
+
- **Дробная часть дополняется нулями** до `fractionDigits`. Ввод только дробной части
|
|
68
|
+
(`,5`) восстанавливает целый ноль → `"0.50"`. Если целой части нет, а дробная нулевая
|
|
69
|
+
(`,00`) — значение считается пустым (`""`).
|
|
70
|
+
- **Ведущие нули отбрасываются** вместе со всем, что стоит перед первой значащей цифрой;
|
|
71
|
+
любые символы, кроме цифр и десятичного разделителя, игнорируются.
|
|
72
|
+
- **Разделители.** Ввод принимает и `,`, и `.` как десятичный разделитель. В отображаемом
|
|
73
|
+
значении разряды разделяются обычным пробелом (U+0020), десятичная часть — запятой.
|
|
74
|
+
- **Placeholder.** Если `inputProps.placeholder` не передан (или пустая строка), он
|
|
75
|
+
генерируется из `fractionDigits`: `0,00` при `2`, `0` при `0`.
|
|
76
|
+
- **Каретка.** После каждого ввода позиция каретки пересчитывается с учётом добавленных
|
|
77
|
+
разделителей и восстанавливается в layout-эффекте. Эффект зависит не только от
|
|
78
|
+
`inputProps.value`, но и от `maxIntegerDigits` и `fractionDigits`: смена формата
|
|
79
|
+
пересчитывает `core.caret` при неизменном значении, и без этих зависимостей каретка
|
|
80
|
+
осталась бы на индексе прежнего формата. Если значение снаружи не изменилось
|
|
81
|
+
(ввод невалиден или значение то же), каретка ставится хелпером `setFallbackCaret` —
|
|
82
|
+
он же учитывает `Backspace`/`Delete` рядом с разделителем и направление выделения.
|
|
83
|
+
- **Знак валюты** рисуется поверх поля отдельным слоем: невидимая (`color: transparent`)
|
|
84
|
+
копия значения задаёт отступ, за ней идёт сам `currency`. Копия помечена
|
|
85
|
+
`aria-hidden="true"`.
|
|
86
|
+
- **`data-test-id`** на компоненте порождает суффиксы: `{id}__input` на элементе ввода и
|
|
87
|
+
`{id}__unit` на знаке валюты.
|
|
78
88
|
|
|
79
89
|
---
|
|
80
90
|
|
|
81
|
-
##
|
|
82
|
-
|
|
83
|
-
### Разделение value и formattedValue
|
|
84
|
-
|
|
85
|
-
- **`value`** (то, что во внешнем state) — нормализованная строка: только цифры и `.`
|
|
86
|
-
как разделитель (например, `"1234.56"`).
|
|
87
|
-
- **`formattedValue`** (то, что показывается в `<input>`) — с пробелами и запятой
|
|
88
|
-
(например, `"1 234,56"`).
|
|
89
|
-
|
|
90
|
-
`AmountField` рендерит `formattedValue`, но во внешний `onChange` отдаёт `value`.
|
|
91
|
-
Это значит: внешнее состояние всегда совместимо с `big.js` / `parseFloat` /
|
|
92
|
-
backend API без дополнительной очистки.
|
|
93
|
-
|
|
94
|
-
### Ленивая инициализация core
|
|
95
|
-
|
|
96
|
-
```tsx
|
|
97
|
-
const core = useRef<AmountBaseInputCore>();
|
|
98
|
-
if (core.current === undefined) core.current = new AmountBaseInputCore(maxIntegerDigits, fractionDigits);
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Создаётся **один раз при первом рендере**. Изменение `maxIntegerDigits`/`fractionDigits`
|
|
102
|
-
применяется через `core.current.maxIntegerDigits = ...` в `getFormattedValue`, а не
|
|
103
|
-
через пересоздание инстанса.
|
|
104
|
-
|
|
105
|
-
### Синхронизация внешнего value с core
|
|
106
|
-
|
|
107
|
-
```tsx
|
|
108
|
-
if (inputProps.value != core.current.value || ...) {
|
|
109
|
-
core.current.apply(inputProps.value, inputProps.value.length);
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
На каждом рендере проверяется, изменилось ли значение извне (controlled re-render)
|
|
114
|
-
или параметры (`max/fractionDigits`). Если да — `core.apply()` пересчитывает
|
|
115
|
-
`formattedValue`. Это гарантирует, что компонент работает как controlled: внешний
|
|
116
|
-
state — единственный источник истины.
|
|
117
|
-
|
|
118
|
-
### Управление кареткой: `useLayoutEffect` + `setCaretPosition`
|
|
119
|
-
|
|
120
|
-
```tsx
|
|
121
|
-
useLayoutEffect(() => {
|
|
122
|
-
if (core.current && refInput.current == document.activeElement)
|
|
123
|
-
setCaretPosition(refInput.current, Math.max(core.current.caret, 0));
|
|
124
|
-
}, [inputProps.value]);
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
После каждого изменения value, если input в фокусе, восстанавливается позиция
|
|
128
|
-
каретки в `core.caret`. Это компенсирует то, что React при controlled input
|
|
129
|
-
сбрасывает каретку в конец после rerender.
|
|
130
|
-
|
|
131
|
-
**`useLayoutEffect`, не `useEffect`** — каретка ставится синхронно до отрисовки,
|
|
132
|
-
чтобы не было визуального "мигания".
|
|
133
|
-
|
|
134
|
-
### Fallback caret через `setFallbackCaret`
|
|
135
|
-
|
|
136
|
-
Если `onChange` **не меняет значение** (невалидный ввод, идентичное значение) —
|
|
137
|
-
React не перерендерится, и `useLayoutEffect` не сработает. Для этого случая
|
|
138
|
-
`handleChange` вызывает `setFallbackCaret(event.target, core, fractionDigits)`,
|
|
139
|
-
которая обрабатывает спецкейсы:
|
|
140
|
-
- Каретка перед разделителем `,` → сдвиг вперёд.
|
|
141
|
-
- Backspace/Delete возле разделителей → сдвиг.
|
|
142
|
-
- Обратное направление выделения → каретка в конец.
|
|
143
|
-
|
|
144
|
-
Эта утилита — единственное место, где компонент напрямую дёргает input DOM API
|
|
145
|
-
(`setSelectionRange`).
|
|
146
|
-
|
|
147
|
-
### Кеш для восстановления контекста
|
|
148
|
-
|
|
149
|
-
`core.cache` хранит: последнее `formattedValue`, `key` нажатой клавиши,
|
|
150
|
-
`selectionStart`/`End`/`Direction` на момент последнего select-event. Это даёт
|
|
151
|
-
парсеру/формате́ру/fallback-caret полную картину "что сделал пользователь", чтобы
|
|
152
|
-
правильно обработать ввод.
|
|
153
|
-
|
|
154
|
-
`handleSelect` обновляет `cache.selectionStart/End/Direction` на каждом
|
|
155
|
-
событии выделения — значит, к моменту `handleChange` эти поля актуальны.
|
|
91
|
+
## Дизайн-токены
|
|
156
92
|
|
|
157
|
-
|
|
93
|
+
Переопределяются через `ThemeProvider` (prop `tokens`) — см. `ThemeProvider-ai.md` →
|
|
94
|
+
«Как переопределять токены». Значения по умолчанию — `src/components/DesignTokens/components/FormField.ts`.
|
|
158
95
|
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (typeof ref === "function") ref(instance);
|
|
163
|
-
else if (ref) ref.current = instance;
|
|
164
|
-
};
|
|
96
|
+
```text
|
|
97
|
+
FormField.Input_Color_Default
|
|
98
|
+
FormField.Input_Color_Disabled
|
|
165
99
|
```
|
|
166
100
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
внешний ref.
|
|
170
|
-
|
|
171
|
-
### `createPlaceholder(fractionDigits)`
|
|
172
|
-
|
|
173
|
-
Если потребитель не передал `inputProps.placeholder`, генерируется автоматически:
|
|
174
|
-
- `fractionDigits=0` → `"0"`.
|
|
175
|
-
- `fractionDigits=2` → `"0,00"`.
|
|
176
|
-
- `fractionDigits=4` → `"0,0000"`.
|
|
177
|
-
|
|
178
|
-
### `parseFractionalPart` восстанавливает целый ноль
|
|
179
|
-
|
|
180
|
-
Если пользователь ввёл `.5` (без целой части) и дробная часть ненулевая, парсер
|
|
181
|
-
восстанавливает `0.5`. Если дробная часть только из нулей (`"00"`) — целая часть
|
|
182
|
-
остаётся пустой (парсинг останавливается), итоговое значение `""`.
|
|
183
|
-
|
|
184
|
-
### `trimLeadingZeros` — не агрессивен
|
|
185
|
-
|
|
186
|
-
Удаляет ведущие нули **только если следующая цифра ненулевая**. Значение `"0"` или
|
|
187
|
-
`"00"` остаётся целой частью `""` или `"0"` в зависимости от дробной. Это
|
|
188
|
-
обеспечивает корректное поведение при вводе `0,5` (пользователь пишет с нуля).
|
|
101
|
+
Собственных токенов у `AmountField` нет: знак валюты красится теми же токенами, что и текст
|
|
102
|
+
поля, остальное оформление наследуется от семейства `FormField`.
|
|
189
103
|
|
|
190
104
|
---
|
|
191
105
|
|
|
192
106
|
## Инварианты
|
|
193
107
|
|
|
194
|
-
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
-
|
|
210
|
-
|
|
211
|
-
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
-
|
|
215
|
-
|
|
216
|
-
- **`maxIntegerDigits=16` по умолчанию** — покрывает квадриллионы, но не больше
|
|
217
|
-
(bigint). Если лимит мал — увеличивать через prop, не в коде.
|
|
108
|
+
- `forwardRef` на компоненте — не убирать. Внешний `ref` указывает на корневой `<div>`
|
|
109
|
+
`FormField`, ссылка на `<input>` передаётся через `inputProps.ref`.
|
|
110
|
+
- Публичный API (`IAmountFieldProps`: `inputProps`, `currency`, `maxIntegerDigits`,
|
|
111
|
+
`fractionDigits`, `onClear`, `clearProps` + унаследованные от `ITextFieldBaseProps`) —
|
|
112
|
+
изменение имён, типов и дефолтов является breaking change.
|
|
113
|
+
- `inputProps` не принимает `type`, `maxLength`, `inputMode`, `autoComplete` и собственный
|
|
114
|
+
`onChange` с сигнатурой события: их задаёт сам компонент (`inputMode="decimal"`,
|
|
115
|
+
`autoComplete="off"`, `onChange(value: string)`).
|
|
116
|
+
- Формат нормализованного значения (точка как десятичный разделитель, отсутствие
|
|
117
|
+
разделителей разрядов) — часть контракта: на нём построены потребители, отправляющие
|
|
118
|
+
значение на бэкенд.
|
|
119
|
+
- Разделитель разрядов в отображаемом значении — обычный пробел U+0020, тогда как
|
|
120
|
+
`Amount` (отображение суммы) использует `AmountConst.GroupSeparator` — неразрывный
|
|
121
|
+
пробел U+00A0. Различие существующее и наблюдаемое (копирование значения, e2e-селекторы),
|
|
122
|
+
менять его без отдельного решения нельзя.
|
|
123
|
+
- `AmountBaseInputCore`, `AmountBaseInputParser`, `AmountBaseInputFormatter` и `utils.ts` —
|
|
124
|
+
внутренняя кухня компонента, из barrel `index.ts` не экспортируется.
|
|
125
|
+
- Экземпляр `AmountBaseInputCore` живёт в `useRef` весь срок жизни компонента: он хранит
|
|
126
|
+
позицию каретки и кэш выделения между рендерами. Пересоздавать его на каждый рендер или
|
|
127
|
+
заменять на state нельзя — потеряется каретка.
|
|
128
|
+
- CSS-класс `currencyLayout` используется в unit-тестах как селектор слоя валюты —
|
|
129
|
+
переименование потребует правки теста.
|
|
218
130
|
|
|
219
131
|
---
|
|
220
132
|
|
|
221
133
|
## Accessibility
|
|
222
134
|
|
|
223
|
-
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
FormFieldClear
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
## Дизайн-токены
|
|
242
|
-
|
|
243
|
-
Нет собственных — все цвета и размеры наследуются из `TextFieldBase`/
|
|
244
|
-
`FormFieldInput`/`Typography`. См. их документацию для списка токенов.
|
|
135
|
+
- Лейбл — настоящий `<label htmlFor>`: `FormFieldInput` генерирует `id` через `lodash uniqueId`
|
|
136
|
+
и связывает его с `FormFieldLabel`. Свой `id` можно передать в `inputProps`.
|
|
137
|
+
- Копия значения под знаком валюты помечена `aria-hidden="true"` — скринридер читает только
|
|
138
|
+
значение поля и сам знак валюты.
|
|
139
|
+
- `inputMode="decimal"` даёт числовую клавиатуру на мобильных устройствах.
|
|
140
|
+
- `status={EFormFieldStatus.DISABLED}` выставляет нативный `disabled` — поле штатно выпадает
|
|
141
|
+
из таб-обхода.
|
|
142
|
+
- Семантика ошибки (`aria-invalid`, `aria-describedby`) не хардкодится: при необходимости
|
|
143
|
+
потребитель передаёт атрибуты через `inputProps`.
|
|
144
|
+
- Кнопка очистки (`onClear`) — `FormFieldClear` с одной иконкой, без текста. Доступное имя
|
|
145
|
+
задаётся потребителем через `clearProps={{ "aria-label": "…" }}`: дефолтную строку зашить
|
|
146
|
+
нельзя, библиотека мультиязычная. Без `aria-label` кнопка остаётся безымянной — в примерах
|
|
147
|
+
и production-коде его передавать обязательно. Остальные поля семейства (`SuggestField`,
|
|
148
|
+
`DateField`, `ChipSuggest`, `MonthYearField`, `SelectExtendedField`) такого prop пока не
|
|
149
|
+
имеют и рендерят `FormFieldClear` только с `onClick`.
|
|
150
|
+
- Собственных клавиатурных сокращений нет; `onKeyDown` и `onSelect` из `inputProps`
|
|
151
|
+
вызываются после внутренних обработчиков, которые запоминают нажатую клавишу и выделение
|
|
152
|
+
для расчёта каретки.
|
|
245
153
|
|
|
246
154
|
---
|
|
247
155
|
|
|
248
156
|
## Связанные компоненты
|
|
249
157
|
|
|
250
|
-
- `TextFieldBase` (`
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
- `
|
|
257
|
-
|
|
258
|
-
- `
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
визуально совпадает, но алгоритмы различны.
|
|
262
|
-
- `NumberField` (`src/components/NumberField`) — числовой ввод без валюты.
|
|
263
|
-
- `AmountConst` (`src/consts/AmountConst`) — общие константы разделителей.
|
|
264
|
-
- Утилиты `utils/inputUtils.ts` (`setCaretPosition`), `utils/keyboard.ts` (`isKey`),
|
|
265
|
-
`utils/stringUtils.ts` (`StringUtils.isDigit`, `isDecimalSeparator`).
|
|
158
|
+
- `TextFieldBase` — базовая раскладка поля (`FormGroup`/`FormField` + слоты `prefix`,
|
|
159
|
+
`label`, `postfix`, `description`, `counter`); `IAmountFieldProps` наследует её props.
|
|
160
|
+
Живёт в семействе `TextField`.
|
|
161
|
+
- `FormField` / `FormFieldInput` — контейнер поля и сам элемент ввода, куда уходит
|
|
162
|
+
`inputProps`.
|
|
163
|
+
- `FormFieldClear` — кнопка очистки, которую компонент добавляет в постфикс при `onClear`.
|
|
164
|
+
- `Amount` — отображение уже готовой суммы (без ввода), парная компонента для read-only
|
|
165
|
+
сценариев.
|
|
166
|
+
- `NumberField` — поле для произвольных чисел без денежного форматирования.
|
|
167
|
+
- `MaskedField`, `TextField` — соседние поля той же группы `TextFields` для форматов с
|
|
168
|
+
маской и произвольного текста.
|
|
266
169
|
|
|
267
170
|
---
|
|
268
171
|
|
|
269
172
|
## Stories
|
|
270
173
|
|
|
271
|
-
`stories/
|
|
272
|
-
Файлы примеров: `stories/
|
|
174
|
+
Основные истории: `stories/AmountField/AmountField.stories.tsx`
|
|
175
|
+
Файлы примеров: `stories/AmountField/examples/`
|
|
273
176
|
|
|
274
177
|
| Story | Example file | Что демонстрирует |
|
|
275
178
|
|---|---|---|
|
|
276
|
-
| `Playground` | `
|
|
277
|
-
| `
|
|
278
|
-
| `Sizes` | `
|
|
279
|
-
| `Statuses` | `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
179
|
+
| `Playground` | `Playground.tsx` | Интерактивный контроль size / status / label / currency / maxIntegerDigits / fractionDigits и настроек: кнопка очистки, `HelpBox`, описание |
|
|
180
|
+
| `Default` | `Default.tsx` | Минимальное контролируемое поле со знаком валюты |
|
|
181
|
+
| `Sizes` | `Sizes.tsx` | Размеры SM / MD / LG |
|
|
182
|
+
| `Statuses` | `Statuses.tsx` | Статусы default / error / warning / disabled |
|
|
183
|
+
| `Production` | `Production.tsx` | Production-композиция (`Example: production`): кнопка очистки, `HelpBox` в постфиксе, description со ссылкой |
|
|
184
|
+
| `VisualTests` | `VisualTests.tsx` | Скриншот-регрессия: пустое поле в фокусе (через `play`), заполненные размеры со знаком валюты, заблокированное заполненное поле |
|
|
282
185
|
|
|
283
186
|
---
|
|
284
187
|
|
|
@@ -286,37 +189,36 @@ forwarded ref. `setRef` передаёт instance в оба. Не заменят
|
|
|
286
189
|
|
|
287
190
|
| Дата | Изменение |
|
|
288
191
|
|---|---|
|
|
289
|
-
| 2026-
|
|
192
|
+
| 2026-09-10 | Создан документ (TRI-9). AI-рефакторинг AmountField, unit-тесты на ядро, парсер, форматер и утилиты; добавлен `displayName` |
|
|
193
|
+
| 2026-09-14 | Правки по ревью (TRI-9): добавлен prop `clearProps` — доступное имя кнопки очистки задаётся через `aria-label`. Хелпер `setForwardedRef` переехал в общий внутренний модуль `src/helpers/setForwardedRef.ts` (пять копий по репозиторию свёрнуты в одну); публичный API это не затрагивает. |
|
|
194
|
+
| 2026-09-17 | Правки по ревью (TRI-9): `setForwardedRef` импортируется относительным путём вместо алиаса `@sberbusiness/triplex-next/helpers/...` — алиас в `src` зарезервирован за публичными subentry. |
|
|
195
|
+
| 2026-09-17 | Правки по ревью (TRI-9): в зависимости `useLayoutEffect`, восстанавливающего каретку, добавлены `maxIntegerDigits` и `fractionDigits`. Без них смена формата при неизменном `value` пересчитывала `core.caret`, но не переносила каретку в DOM. |
|
|
290
196
|
|
|
291
197
|
---
|
|
292
198
|
|
|
293
199
|
## Примеры использования
|
|
294
200
|
|
|
295
|
-
###
|
|
201
|
+
### Default
|
|
296
202
|
|
|
297
|
-
`stories/
|
|
203
|
+
`stories/AmountField/examples/Default.tsx`
|
|
298
204
|
|
|
299
205
|
```tsx
|
|
300
206
|
import React, { useState } from "react";
|
|
301
|
-
import { AmountField
|
|
207
|
+
import { AmountField } from "@sberbusiness/triplex-next";
|
|
302
208
|
|
|
303
|
-
export const
|
|
304
|
-
const [value, setValue] = useState("");
|
|
209
|
+
export const Default = () => {
|
|
210
|
+
const [value, setValue] = useState<string>("");
|
|
305
211
|
|
|
306
212
|
return (
|
|
307
|
-
<div style={{ maxWidth:
|
|
213
|
+
<div style={{ maxWidth: 300 }}>
|
|
308
214
|
<AmountField
|
|
215
|
+
label="Label"
|
|
309
216
|
inputProps={{
|
|
310
217
|
value,
|
|
311
|
-
placeholder: "0,00",
|
|
218
|
+
placeholder: "0,00 ₽",
|
|
312
219
|
onChange: setValue,
|
|
313
220
|
}}
|
|
314
|
-
|
|
315
|
-
size={EComponentSize.LG}
|
|
316
|
-
label="Label"
|
|
317
|
-
currency="RUB"
|
|
318
|
-
maxIntegerDigits={18}
|
|
319
|
-
fractionDigits={2}
|
|
221
|
+
currency="₽"
|
|
320
222
|
/>
|
|
321
223
|
</div>
|
|
322
224
|
);
|
|
@@ -325,136 +227,133 @@ export const BasicExample: React.FC = () => {
|
|
|
325
227
|
|
|
326
228
|
### Sizes
|
|
327
229
|
|
|
328
|
-
`stories/
|
|
230
|
+
`stories/AmountField/examples/Sizes.tsx`
|
|
329
231
|
|
|
330
232
|
```tsx
|
|
331
233
|
import React, { useState } from "react";
|
|
332
234
|
import { AmountField, EComponentSize } from "@sberbusiness/triplex-next";
|
|
333
235
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return (
|
|
338
|
-
<AmountField
|
|
339
|
-
inputProps={{
|
|
340
|
-
value,
|
|
341
|
-
placeholder: "0,00",
|
|
342
|
-
onChange: setValue,
|
|
343
|
-
}}
|
|
344
|
-
size={size}
|
|
345
|
-
label="Label"
|
|
346
|
-
/>
|
|
347
|
-
);
|
|
348
|
-
};
|
|
236
|
+
interface ISizeItemProps {
|
|
237
|
+
size: EComponentSize;
|
|
238
|
+
}
|
|
349
239
|
|
|
350
|
-
|
|
351
|
-
const
|
|
240
|
+
const SizeItem = ({ size }: ISizeItemProps) => {
|
|
241
|
+
const [value, setValue] = useState<string>("");
|
|
352
242
|
|
|
353
243
|
return (
|
|
354
|
-
<div style={{ maxWidth:
|
|
355
|
-
{
|
|
356
|
-
|
|
357
|
-
|
|
244
|
+
<div style={{ maxWidth: 300 }}>
|
|
245
|
+
<div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{size.toUpperCase()}</div>
|
|
246
|
+
<AmountField
|
|
247
|
+
size={size}
|
|
248
|
+
label="Label"
|
|
249
|
+
inputProps={{
|
|
250
|
+
value,
|
|
251
|
+
placeholder: "0,00 ₽",
|
|
252
|
+
onChange: setValue,
|
|
253
|
+
}}
|
|
254
|
+
currency="₽"
|
|
255
|
+
/>
|
|
358
256
|
</div>
|
|
359
257
|
);
|
|
360
258
|
};
|
|
259
|
+
|
|
260
|
+
const SIZES = Object.values(EComponentSize);
|
|
261
|
+
|
|
262
|
+
export const Sizes = () => (
|
|
263
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
264
|
+
{SIZES.map((size) => (
|
|
265
|
+
<SizeItem key={size} size={size} />
|
|
266
|
+
))}
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
361
269
|
```
|
|
362
270
|
|
|
363
271
|
### Statuses
|
|
364
272
|
|
|
365
|
-
`stories/
|
|
273
|
+
`stories/AmountField/examples/Statuses.tsx`
|
|
366
274
|
|
|
367
275
|
```tsx
|
|
368
276
|
import React, { useState } from "react";
|
|
369
277
|
import { AmountField, EFormFieldStatus } from "@sberbusiness/triplex-next";
|
|
370
278
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
return (
|
|
375
|
-
<AmountField
|
|
376
|
-
inputProps={{
|
|
377
|
-
value,
|
|
378
|
-
placeholder: "0,00",
|
|
379
|
-
onChange: setValue,
|
|
380
|
-
}}
|
|
381
|
-
status={status}
|
|
382
|
-
label="Label"
|
|
383
|
-
/>
|
|
384
|
-
);
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
export const StatusesExample: React.FC = () => {
|
|
388
|
-
const statuses = Object.values(EFormFieldStatus);
|
|
389
|
-
|
|
390
|
-
return (
|
|
391
|
-
<div style={{ maxWidth: "300px", display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
392
|
-
{statuses.map((status) => (
|
|
393
|
-
<AmountFieldItem key={status} status={status} />
|
|
394
|
-
))}
|
|
395
|
-
</div>
|
|
396
|
-
);
|
|
397
|
-
};
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
### WithClearButton
|
|
401
|
-
|
|
402
|
-
`stories/TextFields/examples/AmountField/WithClearButtonExample.tsx`
|
|
403
|
-
|
|
404
|
-
```tsx
|
|
405
|
-
import React, { useState } from "react";
|
|
406
|
-
import { AmountField } from "@sberbusiness/triplex-next";
|
|
279
|
+
interface IStatusItemProps {
|
|
280
|
+
status: EFormFieldStatus;
|
|
281
|
+
}
|
|
407
282
|
|
|
408
|
-
|
|
409
|
-
const [value, setValue] = useState("
|
|
283
|
+
const StatusItem = ({ status }: IStatusItemProps) => {
|
|
284
|
+
const [value, setValue] = useState<string>("");
|
|
410
285
|
|
|
411
286
|
return (
|
|
412
|
-
<div style={{ maxWidth:
|
|
287
|
+
<div style={{ maxWidth: 300 }}>
|
|
288
|
+
<div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{status.toUpperCase()}</div>
|
|
413
289
|
<AmountField
|
|
290
|
+
status={status}
|
|
291
|
+
label="Label"
|
|
414
292
|
inputProps={{
|
|
415
293
|
value,
|
|
416
|
-
placeholder: "0,00",
|
|
294
|
+
placeholder: "0,00 ₽",
|
|
417
295
|
onChange: setValue,
|
|
418
296
|
}}
|
|
419
|
-
|
|
420
|
-
onClear={() => setValue("")}
|
|
297
|
+
currency="₽"
|
|
421
298
|
/>
|
|
422
299
|
</div>
|
|
423
300
|
);
|
|
424
301
|
};
|
|
302
|
+
|
|
303
|
+
const STATUSES = Object.values(EFormFieldStatus);
|
|
304
|
+
|
|
305
|
+
export const Statuses = () => (
|
|
306
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
307
|
+
{STATUSES.map((status) => (
|
|
308
|
+
<StatusItem key={status} status={status} />
|
|
309
|
+
))}
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
425
312
|
```
|
|
426
313
|
|
|
427
|
-
###
|
|
314
|
+
### Production
|
|
428
315
|
|
|
429
|
-
`stories/
|
|
316
|
+
`stories/AmountField/examples/Production.tsx`
|
|
430
317
|
|
|
431
318
|
```tsx
|
|
432
|
-
import React, { useState } from "react";
|
|
433
|
-
import { AmountField,
|
|
319
|
+
import React, { useState, useRef } from "react";
|
|
320
|
+
import { AmountField, Text, HelpBox, Link, ETextSize, EFontType, ETooltipSize } from "@sberbusiness/triplex-next";
|
|
321
|
+
|
|
322
|
+
export const Production = () => {
|
|
323
|
+
const [value, setValue] = useState<string>("");
|
|
324
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
434
325
|
|
|
435
|
-
|
|
436
|
-
|
|
326
|
+
const handleLinkClick: React.MouseEventHandler<HTMLAnchorElement> = (event) => {
|
|
327
|
+
event.preventDefault();
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const handleClear = () => {
|
|
331
|
+
setValue("");
|
|
332
|
+
inputRef.current?.focus();
|
|
333
|
+
};
|
|
437
334
|
|
|
438
335
|
return (
|
|
439
|
-
<div style={{ maxWidth:
|
|
336
|
+
<div style={{ maxWidth: 300 }}>
|
|
440
337
|
<AmountField
|
|
441
|
-
inputProps={{
|
|
442
|
-
value,
|
|
443
|
-
placeholder: "0,00",
|
|
444
|
-
onChange: setValue,
|
|
445
|
-
}}
|
|
446
338
|
label="Label"
|
|
447
|
-
currency="RUB"
|
|
448
339
|
postfix={<HelpBox tooltipSize={ETooltipSize.SM}>Helpful details appear here</HelpBox>}
|
|
449
340
|
description={
|
|
450
341
|
<Text tag="div" size={ETextSize.B4} type={EFontType.SECONDARY}>
|
|
451
342
|
(21) Description{" "}
|
|
452
|
-
<Link href="#" onClick={
|
|
343
|
+
<Link href="#" onClick={handleLinkClick}>
|
|
453
344
|
Link text
|
|
454
345
|
</Link>
|
|
455
346
|
</Text>
|
|
456
347
|
}
|
|
457
|
-
|
|
348
|
+
inputProps={{
|
|
349
|
+
value,
|
|
350
|
+
placeholder: "0,00 ₽",
|
|
351
|
+
onChange: setValue,
|
|
352
|
+
ref: inputRef,
|
|
353
|
+
}}
|
|
354
|
+
currency="₽"
|
|
355
|
+
onClear={handleClear}
|
|
356
|
+
clearProps={{ "aria-label": "Очистить сумму" }}
|
|
458
357
|
/>
|
|
459
358
|
</div>
|
|
460
359
|
);
|