@cloud-ru/ds-fields 2.2.3-preview-0c4119af.0 → 2.2.3-preview-a42433ce.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -3
- package/dist/cjs/components/FieldDate/FieldDate.js +3 -3
- package/dist/cjs/components/FieldDate/mask.d.ts +3 -2
- package/dist/cjs/components/FieldDate/mask.js +4 -3
- package/dist/cjs/components/FieldSelect/FieldSelect.js +41 -81
- package/dist/cjs/components/FieldSelect/constants.d.ts +0 -11
- package/dist/cjs/components/FieldSelect/constants.js +1 -20
- package/dist/cjs/components/FieldSelect/types.d.ts +3 -26
- package/dist/cjs/components/FieldSelect/utils.d.ts +19 -0
- package/dist/cjs/components/FieldSelect/utils.js +103 -0
- package/dist/cjs/components/FieldTime/FieldTime.js +2 -2
- package/dist/cjs/segments/useSegmentedMask.d.ts +2 -0
- package/dist/cjs/segments/useSegmentedMask.js +146 -56
- package/dist/esm/components/FieldDate/FieldDate.js +3 -3
- package/dist/esm/components/FieldDate/mask.d.ts +3 -2
- package/dist/esm/components/FieldDate/mask.js +4 -3
- package/dist/esm/components/FieldSelect/FieldSelect.js +42 -82
- package/dist/esm/components/FieldSelect/constants.d.ts +0 -11
- package/dist/esm/components/FieldSelect/constants.js +0 -19
- package/dist/esm/components/FieldSelect/types.d.ts +3 -26
- package/dist/esm/components/FieldSelect/utils.d.ts +19 -0
- package/dist/esm/components/FieldSelect/utils.js +92 -0
- package/dist/esm/components/FieldTime/FieldTime.js +2 -2
- package/dist/esm/segments/useSegmentedMask.d.ts +2 -0
- package/dist/esm/segments/useSegmentedMask.js +146 -56
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/components/FieldDate/FieldDate.tsx +3 -3
- package/src/components/FieldDate/mask.ts +4 -3
- package/src/components/FieldSelect/FieldSelect.tsx +61 -129
- package/src/components/FieldSelect/constants.ts +0 -22
- package/src/components/FieldSelect/types.ts +48 -82
- package/src/components/FieldSelect/utils.ts +132 -0
- package/src/components/FieldTime/FieldTime.tsx +2 -2
- package/src/segments/useSegmentedMask.ts +172 -52
- package/dist/cjs/components/FieldSelect/hooks.d.ts +0 -32
- package/dist/cjs/components/FieldSelect/hooks.js +0 -83
- package/dist/cjs/components/FieldSelect/utils/customOption.d.ts +0 -21
- package/dist/cjs/components/FieldSelect/utils/customOption.js +0 -55
- package/dist/cjs/components/FieldSelect/utils/extract.d.ts +0 -11
- package/dist/cjs/components/FieldSelect/utils/extract.js +0 -39
- package/dist/cjs/components/FieldSelect/utils/filter.d.ts +0 -3
- package/dist/cjs/components/FieldSelect/utils/filter.js +0 -40
- package/dist/cjs/components/FieldSelect/utils/index.d.ts +0 -6
- package/dist/cjs/components/FieldSelect/utils/index.js +0 -22
- package/dist/cjs/components/FieldSelect/utils/items.d.ts +0 -9
- package/dist/cjs/components/FieldSelect/utils/items.js +0 -47
- package/dist/cjs/components/FieldSelect/utils/selection.d.ts +0 -11
- package/dist/cjs/components/FieldSelect/utils/selection.js +0 -18
- package/dist/cjs/components/FieldSelect/utils/tagSize.d.ts +0 -4
- package/dist/cjs/components/FieldSelect/utils/tagSize.js +0 -5
- package/dist/esm/components/FieldSelect/hooks.d.ts +0 -32
- package/dist/esm/components/FieldSelect/hooks.js +0 -80
- package/dist/esm/components/FieldSelect/utils/customOption.d.ts +0 -21
- package/dist/esm/components/FieldSelect/utils/customOption.js +0 -48
- package/dist/esm/components/FieldSelect/utils/extract.d.ts +0 -11
- package/dist/esm/components/FieldSelect/utils/extract.js +0 -34
- package/dist/esm/components/FieldSelect/utils/filter.d.ts +0 -3
- package/dist/esm/components/FieldSelect/utils/filter.js +0 -36
- package/dist/esm/components/FieldSelect/utils/index.d.ts +0 -6
- package/dist/esm/components/FieldSelect/utils/index.js +0 -6
- package/dist/esm/components/FieldSelect/utils/items.d.ts +0 -9
- package/dist/esm/components/FieldSelect/utils/items.js +0 -41
- package/dist/esm/components/FieldSelect/utils/selection.d.ts +0 -11
- package/dist/esm/components/FieldSelect/utils/selection.js +0 -14
- package/dist/esm/components/FieldSelect/utils/tagSize.d.ts +0 -4
- package/dist/esm/components/FieldSelect/utils/tagSize.js +0 -2
- package/src/components/FieldSelect/hooks.ts +0 -162
- package/src/components/FieldSelect/utils/customOption.ts +0 -77
- package/src/components/FieldSelect/utils/extract.ts +0 -49
- package/src/components/FieldSelect/utils/filter.ts +0 -51
- package/src/components/FieldSelect/utils/index.ts +0 -6
- package/src/components/FieldSelect/utils/items.ts +0 -58
- package/src/components/FieldSelect/utils/selection.ts +0 -25
- package/src/components/FieldSelect/utils/tagSize.ts +0 -5
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useSegmentedMask = useSegmentedMask;
|
|
4
|
+
const utils_1 = require("@cloud-ru/ds-utils");
|
|
4
5
|
const react_1 = require("react");
|
|
6
|
+
const RESET_TYPED_KEYS = new Set([
|
|
7
|
+
'ArrowLeft',
|
|
8
|
+
'ArrowRight',
|
|
9
|
+
'ArrowUp',
|
|
10
|
+
'ArrowDown',
|
|
11
|
+
'Backspace',
|
|
12
|
+
'Delete',
|
|
13
|
+
'Enter',
|
|
14
|
+
'Tab',
|
|
15
|
+
'Escape',
|
|
16
|
+
]);
|
|
5
17
|
/**
|
|
6
18
|
* Сегментный ввод даты/времени (порт `useDateField` из @snack-uikit/fields). Каретка ходит по
|
|
7
19
|
* сегментам (`setSelectionRange` подсвечивает текущий), цифры заполняют сегмент с авто-переходом,
|
|
8
20
|
* стрелки двигают по сегментам, Backspace очищает сегмент до плейсхолдера.
|
|
21
|
+
* `nativeInputRef` передаётся в `ref` того же input'а: значение правится через нативные `beforeinput`/`input`.
|
|
9
22
|
*/
|
|
10
23
|
function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue, onMaskedChange, onArrowDown, onArrowUp, onEscape, onEdit, }) {
|
|
11
24
|
const firstSlot = slots[0];
|
|
@@ -110,70 +123,61 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
110
123
|
}
|
|
111
124
|
selectSlot(nextSlot(slot));
|
|
112
125
|
}, [clearSlot, isValidDate, lastSlot, nextSlot, selectSlot, slots, tryToComplete]);
|
|
126
|
+
// Цифры сегмента храним отдельно от текста: по `01` в поле не отличить `1` от `0` + `1`.
|
|
127
|
+
const typedRef = (0, react_1.useRef)(null);
|
|
128
|
+
const resetTyped = (0, react_1.useCallback)(() => {
|
|
129
|
+
typedRef.current = null;
|
|
130
|
+
}, []);
|
|
113
131
|
const typeDigit = (0, react_1.useCallback)((slot, key) => {
|
|
114
|
-
const text = getSlotText(slot);
|
|
115
|
-
const numberValue = Number(text) || 0;
|
|
116
|
-
const digit = Number(key);
|
|
117
|
-
const slotValue = parseInt(numberValue.toString() + key, 10) || 0;
|
|
118
|
-
const valueLength = slotValue.toString().length;
|
|
119
132
|
const maxLength = slot.max.toString().length;
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
updateSlot(slot, key);
|
|
134
|
-
checkInputAndGoNext(slot);
|
|
135
|
-
}
|
|
136
|
-
else if (digit || digit >= slot.min) {
|
|
137
|
-
updateSlot(slot, key);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
else if (slotValue <= slot.max) {
|
|
141
|
-
updateSlot(slot, slotValue);
|
|
142
|
-
checkInputAndGoNext(slot);
|
|
133
|
+
const prev = typedRef.current?.key === slot.key ? typedRef.current.digits : '';
|
|
134
|
+
const appended = prev + key;
|
|
135
|
+
// Как в React Aria и MUI X: `13` в месяце → `03`.
|
|
136
|
+
const overflow = Number(appended) > slot.max;
|
|
137
|
+
const digits = overflow ? key : appended;
|
|
138
|
+
const value = Number(digits);
|
|
139
|
+
const complete = overflow || digits.length >= maxLength || value * 10 > slot.max;
|
|
140
|
+
const accepted = value >= slot.min;
|
|
141
|
+
if (accepted)
|
|
142
|
+
updateSlot(slot, value);
|
|
143
|
+
if (!complete) {
|
|
144
|
+
typedRef.current = { key: slot.key, digits };
|
|
145
|
+
return;
|
|
143
146
|
}
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
typedRef.current = null;
|
|
148
|
+
if (accepted)
|
|
146
149
|
checkInputAndGoNext(slot);
|
|
147
|
-
|
|
148
|
-
else if (digit || digit >= slot.min) {
|
|
149
|
-
updateSlot(slot, key);
|
|
150
|
-
}
|
|
151
|
-
}, [checkInputAndGoNext, getSlotText, updateSlot]);
|
|
150
|
+
}, [checkInputAndGoNext, updateSlot]);
|
|
152
151
|
const handleFocus = (0, react_1.useCallback)(() => {
|
|
153
152
|
if (readonly || disabled)
|
|
154
153
|
return;
|
|
154
|
+
resetTyped();
|
|
155
155
|
ensureMask();
|
|
156
156
|
// На фокус всегда выбираем ПЕРВЫЙ сегмент (паритет с легаси). При клике мышью `handleClick`
|
|
157
157
|
// отработает следом и перевыберет сегмент под кареткой; при Tab остаётся первый.
|
|
158
158
|
selectSlot(firstSlot);
|
|
159
|
-
}, [disabled, ensureMask, firstSlot, readonly, selectSlot]);
|
|
159
|
+
}, [disabled, ensureMask, firstSlot, readonly, resetTyped, selectSlot]);
|
|
160
160
|
const handleClick = (0, react_1.useCallback)((event) => {
|
|
161
161
|
if (readonly || disabled)
|
|
162
162
|
return;
|
|
163
|
+
resetTyped();
|
|
163
164
|
ensureMask();
|
|
164
165
|
selectSlot(slotFromIndex(event.currentTarget.selectionStart));
|
|
165
|
-
}, [disabled, ensureMask, readonly, selectSlot, slotFromIndex]);
|
|
166
|
+
}, [disabled, ensureMask, readonly, resetTyped, selectSlot, slotFromIndex]);
|
|
166
167
|
const handleBlur = (0, react_1.useCallback)(() => {
|
|
168
|
+
resetTyped();
|
|
167
169
|
const input = inputRef.current;
|
|
168
170
|
if (input && input.value === mask) {
|
|
169
171
|
input.value = '';
|
|
170
172
|
setValue('');
|
|
171
173
|
}
|
|
172
|
-
}, [inputRef, mask, setValue]);
|
|
174
|
+
}, [inputRef, mask, resetTyped, setValue]);
|
|
173
175
|
const handleKeyDown = (0, react_1.useCallback)((event) => {
|
|
174
176
|
const input = inputRef.current;
|
|
175
177
|
if (!input || readonly || disabled)
|
|
176
178
|
return;
|
|
179
|
+
if (RESET_TYPED_KEYS.has(event.key))
|
|
180
|
+
resetTyped();
|
|
177
181
|
// Tab уводит фокус из поля — не перехватываем.
|
|
178
182
|
if (event.key === 'Tab')
|
|
179
183
|
return;
|
|
@@ -188,7 +192,13 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
188
192
|
onArrowUp?.();
|
|
189
193
|
return;
|
|
190
194
|
}
|
|
195
|
+
// Копирование и вставку не блокируем: правку поля перехватит beforeinput.
|
|
196
|
+
if (event.ctrlKey || event.metaKey || event.altKey)
|
|
197
|
+
return;
|
|
191
198
|
onEdit?.();
|
|
199
|
+
// Экранные клавиатуры присылают `Unidentified` вместо цифры, её данные есть только в beforeinput.
|
|
200
|
+
if (/^\d$/.test(event.key) || event.key === 'Unidentified')
|
|
201
|
+
return;
|
|
192
202
|
if (event.key === 'Escape') {
|
|
193
203
|
event.preventDefault();
|
|
194
204
|
onEscape?.();
|
|
@@ -223,11 +233,6 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
223
233
|
if (event.key === 'Enter') {
|
|
224
234
|
tryToComplete();
|
|
225
235
|
emit();
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
if (/^\d$/.test(event.key)) {
|
|
229
|
-
typeDigit(slot, event.key);
|
|
230
|
-
emit();
|
|
231
236
|
}
|
|
232
237
|
}, [
|
|
233
238
|
clearSlot,
|
|
@@ -243,21 +248,13 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
243
248
|
onEscape,
|
|
244
249
|
prevSlot,
|
|
245
250
|
readonly,
|
|
251
|
+
resetTyped,
|
|
246
252
|
selectSlot,
|
|
247
253
|
slotFromIndex,
|
|
248
254
|
tryToComplete,
|
|
249
|
-
typeDigit,
|
|
250
255
|
]);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// этого обработчика вставка не работала бы вовсе.
|
|
254
|
-
const handlePaste = (0, react_1.useCallback)((event) => {
|
|
255
|
-
if (readonly || disabled)
|
|
256
|
-
return;
|
|
257
|
-
event.preventDefault();
|
|
258
|
-
const digits = event.clipboardData.getData('text').replace(/\D/g, '');
|
|
259
|
-
if (!digits)
|
|
260
|
-
return;
|
|
256
|
+
const fillFromDigits = (0, react_1.useCallback)((digits) => {
|
|
257
|
+
resetTyped();
|
|
261
258
|
ensureMask();
|
|
262
259
|
let cursor = 0;
|
|
263
260
|
let lastFilled;
|
|
@@ -276,7 +273,100 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
276
273
|
else if (lastFilled) {
|
|
277
274
|
selectSlot(nextSlot(lastFilled));
|
|
278
275
|
}
|
|
276
|
+
}, [ensureMask, lastSlot, nextSlot, resetTyped, selectSlot, slots, tryToComplete, updateSlot]);
|
|
277
|
+
const handlePaste = (0, react_1.useCallback)((event) => {
|
|
278
|
+
if (readonly || disabled)
|
|
279
|
+
return;
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
const digits = event.clipboardData.getData('text').replace(/\D/g, '');
|
|
282
|
+
if (!digits)
|
|
283
|
+
return;
|
|
284
|
+
fillFromDigits(digits);
|
|
285
|
+
emit();
|
|
286
|
+
}, [disabled, emit, fillFromDigits, readonly]);
|
|
287
|
+
const applyEdit = (0, react_1.useCallback)((inputType, text) => {
|
|
288
|
+
const input = inputRef.current;
|
|
289
|
+
if (!input)
|
|
290
|
+
return;
|
|
291
|
+
onEdit?.();
|
|
292
|
+
if (inputType.startsWith('delete')) {
|
|
293
|
+
resetTyped();
|
|
294
|
+
clearSlot(slotFromIndex(input.selectionStart));
|
|
295
|
+
emit();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const digits = text.replace(/\D/g, '');
|
|
299
|
+
if (!inputType.startsWith('insert') || !digits)
|
|
300
|
+
return;
|
|
301
|
+
// Автозаполнение, `fill` и drop присылают строку целиком.
|
|
302
|
+
if (text.length > 1) {
|
|
303
|
+
fillFromDigits(digits);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
typeDigit(slotFromIndex(input.selectionStart), digits);
|
|
307
|
+
}
|
|
308
|
+
emit();
|
|
309
|
+
}, [clearSlot, emit, fillFromDigits, inputRef, onEdit, resetTyped, slotFromIndex, typeDigit]);
|
|
310
|
+
// Композицию IME браузер не даёт отменить — откатываем её в input.
|
|
311
|
+
const pendingEditRef = (0, react_1.useRef)(null);
|
|
312
|
+
const handleBeforeInput = (0, react_1.useCallback)((event) => {
|
|
313
|
+
const input = inputRef.current;
|
|
314
|
+
if (!input || readonly || disabled)
|
|
315
|
+
return;
|
|
316
|
+
ensureMask();
|
|
317
|
+
if (!event.cancelable) {
|
|
318
|
+
pendingEditRef.current = { value: input.value, start: input.selectionStart ?? 0, end: input.selectionEnd ?? 0 };
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
applyEdit(event.inputType, event.data ?? event.dataTransfer?.getData('text') ?? '');
|
|
323
|
+
}, [applyEdit, disabled, ensureMask, inputRef, readonly]);
|
|
324
|
+
const handleInput = (0, react_1.useCallback)((event) => {
|
|
325
|
+
const input = inputRef.current;
|
|
326
|
+
if (!input || readonly || disabled)
|
|
327
|
+
return;
|
|
328
|
+
const pending = pendingEditRef.current;
|
|
329
|
+
pendingEditRef.current = null;
|
|
330
|
+
if (pending) {
|
|
331
|
+
const inputType = event instanceof InputEvent ? event.inputType : '';
|
|
332
|
+
const inserted = input.value.slice(pending.start, input.value.length - (pending.value.length - pending.end));
|
|
333
|
+
input.value = pending.value;
|
|
334
|
+
setValue(pending.value);
|
|
335
|
+
input.setSelectionRange(pending.start, pending.end);
|
|
336
|
+
// Цифру композиции вводит её фиксация (insertText), иначе цифра запишется дважды.
|
|
337
|
+
if (!inputType.includes('Composition'))
|
|
338
|
+
applyEdit(inputType, inserted);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Автозаполнение меняет значение без beforeinput.
|
|
342
|
+
const digits = input.value.replace(/\D/g, '');
|
|
343
|
+
input.value = mask;
|
|
344
|
+
setValue(mask);
|
|
345
|
+
fillFromDigits(digits);
|
|
279
346
|
emit();
|
|
280
|
-
}, [disabled, emit,
|
|
281
|
-
|
|
347
|
+
}, [applyEdit, disabled, emit, fillFromDigits, inputRef, mask, readonly, setValue]);
|
|
348
|
+
// React 18 не отдаёт `inputType` в `onBeforeInput`, поэтому слушаем нативные события.
|
|
349
|
+
const onNativeBeforeInput = (0, utils_1.useEventHandler)(handleBeforeInput);
|
|
350
|
+
const onNativeInput = (0, utils_1.useEventHandler)(handleInput);
|
|
351
|
+
const attachedInputRef = (0, react_1.useRef)(null);
|
|
352
|
+
const nativeInputRef = (0, react_1.useCallback)((node) => {
|
|
353
|
+
/* eslint-disable @cloud-ru/ssr-safe-react/domApi -- callback-ref вызывается только в браузере */
|
|
354
|
+
attachedInputRef.current?.removeEventListener('beforeinput', onNativeBeforeInput);
|
|
355
|
+
attachedInputRef.current?.removeEventListener('input', onNativeInput);
|
|
356
|
+
attachedInputRef.current = node;
|
|
357
|
+
node?.addEventListener('beforeinput', onNativeBeforeInput);
|
|
358
|
+
node?.addEventListener('input', onNativeInput);
|
|
359
|
+
/* eslint-enable @cloud-ru/ssr-safe-react/domApi */
|
|
360
|
+
}, [onNativeBeforeInput, onNativeInput]);
|
|
361
|
+
return {
|
|
362
|
+
handleKeyDown,
|
|
363
|
+
handleClick,
|
|
364
|
+
handleFocus,
|
|
365
|
+
handleBlur,
|
|
366
|
+
handlePaste,
|
|
367
|
+
nativeInputRef,
|
|
368
|
+
selectSlot,
|
|
369
|
+
firstSlot,
|
|
370
|
+
lastSlot,
|
|
371
|
+
};
|
|
282
372
|
}
|
|
@@ -321,9 +321,9 @@ export const FieldDate = forwardRef(function FieldDate(props, ref) {
|
|
|
321
321
|
_jsxs("span", { className: styles.rangeInputs, style: { '--field-date-mask-ch': placeholderMask.length }, children: [_jsx(InputPrivate, { ...inputCommon, ref: mergeRefs(ref, fromInputRef), className: cn(fieldStyles.fieldInput, styles.rangeInputFrom), value: rangeInputValue[0], placeholder: placeholderMask, onChange: handleRangeInput(0), id: id, name: name, autoFocus: resolvedAutoFocus, "aria-label": labelFrom, "data-test-id": TEST_IDS.fieldDateInputFrom }), _jsx("span", { className: styles.rangeSeparator, "aria-hidden": true, children: "\u2013" }), _jsx(InputPrivate, { ...inputCommon, ref: toInputRef, className: fieldStyles.fieldInput, value: rangeInputValue[1], placeholder: placeholderMask, onChange: handleRangeInput(1),
|
|
322
322
|
// «to» — последний инпут перед кнопками: на нём живёт roving-композит
|
|
323
323
|
// (ArrowRight в конце → clear/copy), keydown-цепочка с nav-handler'ом.
|
|
324
|
-
onKeyDown: handleToInputKeyDown, tabIndex: inputTabIndex, "aria-label": labelTo, "data-test-id": TEST_IDS.fieldDateInputTo })] })) : (_jsx(InputPrivate, { ref: mergeRefs(ref, fromInputRef), className: fieldStyles.fieldInput, value: singleInputValue, placeholder: placeholderMask,
|
|
325
|
-
// Single/date-time ведёт сегментный движок (
|
|
326
|
-
//
|
|
324
|
+
onKeyDown: handleToInputKeyDown, tabIndex: inputTabIndex, "aria-label": labelTo, "data-test-id": TEST_IDS.fieldDateInputTo })] })) : (_jsx(InputPrivate, { ref: mergeRefs(ref, fromInputRef, singleSeg.nativeInputRef), className: fieldStyles.fieldInput, value: singleInputValue, placeholder: placeholderMask,
|
|
325
|
+
// Single/date-time ведёт сегментный движок (useSegmentedMask), нативный onChange
|
|
326
|
+
// игнорируется. Range — обычный ввод-по-маске (formatMask в handleRangeInput).
|
|
327
327
|
onChange: () => { }, onKeyDown: handleSingleKeyDown, onClick: singleSeg.handleClick, onPaste: singleSeg.handlePaste, onFocus: handleSingleFocus, onBlur: handleSingleBlur, disabled: disabled, readonly: readOnly, inputMode: 'numeric', tabIndex: inputTabIndex, id: id, name: name, autoFocus: resolvedAutoFocus, "aria-haspopup": 'dialog', "aria-expanded": open, "data-test-id": TEST_IDS.fieldDateInput })) }), postfixButtons && (
|
|
328
328
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
329
329
|
_jsx("span", { className: fieldStyles.postfixButtonsSlot, onMouseDown: preventSlotMouseDown, onClick: stopSlotClickPropagation, children: postfixButtons }))] }), _jsx("div", { className: fieldStyles.iconSlot, "data-test-id": TEST_IDS.fieldDateCalendar, "aria-hidden": true, children: _jsx(CalendarSVG, {}) })] }) })] }));
|
|
@@ -10,8 +10,9 @@ export declare function getMaskString(mode: MaskMode, showSeconds?: boolean): st
|
|
|
10
10
|
* «десятку» (`day` → > 3, `month` → > 1, `hour` → > 2, `minute`/`second` → > 5), цифра трактуется
|
|
11
11
|
* как «единицы» и в начало сегмента добавляется ноль. Пример: `5` для day → `05.`,
|
|
12
12
|
* фокус автоматически уезжает на следующий сегмент.
|
|
13
|
-
* 2.
|
|
14
|
-
*
|
|
13
|
+
* 2. **Переполнение полной двойки**: если набрано две цифры и значение превышает `max` сегмента —
|
|
14
|
+
* сегмент начинается заново с последней цифры (как посегментный ввод в `useSegmentedMask`).
|
|
15
|
+
* Пример: `39` для day → `09`, `13` для month → `03`.
|
|
15
16
|
*/
|
|
16
17
|
export declare function formatMask(input: string, mode: MaskMode, showSeconds?: boolean): string;
|
|
17
18
|
/** Парсит `DD.MM.YYYY` или `DD.MM.YYYY, HH:mm[:ss]` в `Date`. Возвращает `undefined` при невалидной/неполной строке. */
|
|
@@ -42,8 +42,9 @@ function buildSegments(mode, showSeconds) {
|
|
|
42
42
|
* «десятку» (`day` → > 3, `month` → > 1, `hour` → > 2, `minute`/`second` → > 5), цифра трактуется
|
|
43
43
|
* как «единицы» и в начало сегмента добавляется ноль. Пример: `5` для day → `05.`,
|
|
44
44
|
* фокус автоматически уезжает на следующий сегмент.
|
|
45
|
-
* 2.
|
|
46
|
-
*
|
|
45
|
+
* 2. **Переполнение полной двойки**: если набрано две цифры и значение превышает `max` сегмента —
|
|
46
|
+
* сегмент начинается заново с последней цифры (как посегментный ввод в `useSegmentedMask`).
|
|
47
|
+
* Пример: `39` для day → `09`, `13` для month → `03`.
|
|
47
48
|
*/
|
|
48
49
|
export function formatMask(input, mode, showSeconds = true) {
|
|
49
50
|
const maxLen = totalLen(mode, showSeconds);
|
|
@@ -69,7 +70,7 @@ export function formatMask(input, mode, showSeconds = true) {
|
|
|
69
70
|
stream = stream.slice(seg.len);
|
|
70
71
|
if (raw.length === seg.len) {
|
|
71
72
|
const n = Number(raw);
|
|
72
|
-
out += n > seg.max ?
|
|
73
|
+
out += n > seg.max ? raw.slice(-1).padStart(seg.len, '0') : raw;
|
|
73
74
|
}
|
|
74
75
|
else {
|
|
75
76
|
out += raw;
|
|
@@ -14,17 +14,14 @@ import { TEST_IDS } from '../../constants.js';
|
|
|
14
14
|
import { useAdaptiveAutoFocus } from '../../hooks/index.js';
|
|
15
15
|
import { copyTextToClipboard, getAcrylicProps, preventSlotMouseDown, stopSlotClickPropagation, toInputSize, useCopyButton, } from '../shared/index.js';
|
|
16
16
|
import fieldStyles from '../shared/styles.module.css';
|
|
17
|
-
import { useCustomOption } from './hooks.js';
|
|
18
17
|
import styles from './styles.module.css';
|
|
19
|
-
import { extractAppearance, extractLabel, filterItems, findItem, flatten,
|
|
18
|
+
import { extractAppearance, extractLabel, filterItems, findItem, flatten, isMultiple, TAG_SIZE_MAP, } from './utils.js';
|
|
20
19
|
export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
21
20
|
const { label = '', labelTooltip, caption, hint, error, validationState = VALIDATION_STATE.Default, showHintIcon, length, required, size = SIZE.M, className, fieldClassName, items, pinTop, pinBottom, placeholder = '', iconBefore, prefix, postfix, disabled, readonly: readOnly, background = true, open: openProp, onOpenChange, placement, widthStrategy = 'eq', searchable = true, search, autocomplete = false, addOptionByEnter = false, resetSearchOnOptionSelection = true, showClearButton = true, showCopyButton = true, onCopyButtonClick, id, name, autoFocus, layoutPresets, onFocus: onFocusProp, onBlur: onBlurProp, enableFuzzySearch = true, selectedOptionFormatter, footer, footerActiveElementsRefs, loading, noDataState, noResultsState, errorDataState, dataError, dataFiltered, virtualized, scrollToSelectedItem, limitedScrollHeight, untouchableScrollbars, closeOnPopstate, onKeyDown: onKeyDownProp, 'data-test-id': dataTestId = TEST_IDS.fieldSelect, } = props;
|
|
22
21
|
const multiple = isMultiple(props);
|
|
23
22
|
const chips = multiple ? (props.chips ?? true) : false;
|
|
24
23
|
const removeByBackspace = multiple ? (props.removeByBackspace ?? true) : false;
|
|
25
24
|
const closeDroplistOnItemClick = props.closeDroplistOnItemClick ?? !multiple;
|
|
26
|
-
const allowCustomOption = props.allowCustomOption === true;
|
|
27
|
-
const addCustomOptionTriggers = allowCustomOption ? props.addCustomOptionTriggers : undefined;
|
|
28
25
|
const resolvedAutoFocus = useAdaptiveAutoFocus(autoFocus, layoutPresets);
|
|
29
26
|
const inputRef = useRef(null);
|
|
30
27
|
// Отдельный ref на реальный <input>: `inputRef.current` перезаписывается span-обёрткой PopoverPrivate.
|
|
@@ -70,20 +67,16 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
70
67
|
}, [multiple, props.value, multipleLocal]);
|
|
71
68
|
const effectiveValidationState = useMemo(() => (error ? VALIDATION_STATE.Error : validationState), [error, validationState]);
|
|
72
69
|
const allItems = useMemo(() => [...(pinTop ?? []), ...items, ...(pinBottom ?? [])], [items, pinTop, pinBottom]);
|
|
73
|
-
const selectedIds = useMemo(() => getSelectedIds({ multiple, multipleValue, singleValue }), [multiple, multipleValue, singleValue]);
|
|
74
|
-
// Выбранное, которого нет в `items` (кастом по allowCustomOption, ленивая подгрузка) — в список, не только в чип.
|
|
75
|
-
const missingSelectedItems = useMemo(() => getMissingSelectedItems(selectedIds, allItems), [selectedIds, allItems]);
|
|
76
|
-
const itemsWithPlaceholders = useMemo(() => (missingSelectedItems.length > 0 ? [...missingSelectedItems, ...items] : items), [missingSelectedItems, items]);
|
|
77
70
|
// Выбранное значение переживает смену `items` (серверный поиск, ленивая подгрузка).
|
|
78
71
|
const seenItems = useRef(new Map());
|
|
79
72
|
const resolveItem = useMemo(() => {
|
|
80
|
-
for (const item of flatten(
|
|
73
|
+
for (const item of flatten(allItems)) {
|
|
81
74
|
if (item.id !== undefined) {
|
|
82
75
|
seenItems.current.set(item.id, item);
|
|
83
76
|
}
|
|
84
77
|
}
|
|
85
|
-
return (id) => findItem(allItems, id) ??
|
|
86
|
-
}, [allItems
|
|
78
|
+
return (id) => findItem(allItems, id) ?? seenItems.current.get(id);
|
|
79
|
+
}, [allItems]);
|
|
87
80
|
const selectedPairs = useMemo(() => {
|
|
88
81
|
if (!multiple || multipleValue.length === 0) {
|
|
89
82
|
return [];
|
|
@@ -138,13 +131,13 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
138
131
|
syncedLabelRef.current = selectedLabel;
|
|
139
132
|
setInputValue(selectedLabel);
|
|
140
133
|
}, [selectedLabel, typing, resetSearchOnOptionSelection, setInputValue]);
|
|
141
|
-
// При autocomplete фильтрует сервер — items берутся как
|
|
134
|
+
// При autocomplete фильтрует сервер — items берутся как есть.
|
|
142
135
|
const filteredItems = useMemo(() => {
|
|
143
136
|
if (!searchable || autocomplete || !typing) {
|
|
144
|
-
return
|
|
137
|
+
return items;
|
|
145
138
|
}
|
|
146
|
-
return filterItems(
|
|
147
|
-
}, [searchable, autocomplete, typing,
|
|
139
|
+
return filterItems(items, inputValue, enableFuzzySearch);
|
|
140
|
+
}, [searchable, autocomplete, typing, items, inputValue, enableFuzzySearch]);
|
|
148
141
|
const filteredPinTop = useMemo(() => {
|
|
149
142
|
if (!searchable || autocomplete || !typing || !pinTop) {
|
|
150
143
|
return pinTop;
|
|
@@ -157,7 +150,22 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
157
150
|
}
|
|
158
151
|
return filterItems(pinBottom, inputValue, enableFuzzySearch);
|
|
159
152
|
}, [searchable, autocomplete, typing, pinBottom, inputValue, enableFuzzySearch]);
|
|
160
|
-
const
|
|
153
|
+
const handleOpenChange = useCallback((next) => {
|
|
154
|
+
// readonly/disabled поле открывать нельзя, закрывать — можно.
|
|
155
|
+
if (next && (disabled || readOnly))
|
|
156
|
+
return;
|
|
157
|
+
if (openProp === undefined) {
|
|
158
|
+
setOpenLocal(next);
|
|
159
|
+
}
|
|
160
|
+
onOpenChange?.(next);
|
|
161
|
+
if (!next) {
|
|
162
|
+
setTyping(false);
|
|
163
|
+
if (resetSearchOnOptionSelection) {
|
|
164
|
+
setInputValue(selectedLabel);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}, [openProp, onOpenChange, disabled, readOnly, resetSearchOnOptionSelection, setInputValue, selectedLabel]);
|
|
168
|
+
const handleSingleChange = (next) => {
|
|
161
169
|
if (multiple) {
|
|
162
170
|
return;
|
|
163
171
|
}
|
|
@@ -166,8 +174,8 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
166
174
|
}
|
|
167
175
|
props.onChange?.(next);
|
|
168
176
|
setTyping(false);
|
|
169
|
-
}
|
|
170
|
-
const handleMultipleChange =
|
|
177
|
+
};
|
|
178
|
+
const handleMultipleChange = (next) => {
|
|
171
179
|
if (!multiple) {
|
|
172
180
|
return;
|
|
173
181
|
}
|
|
@@ -181,56 +189,8 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
181
189
|
setInputValue('');
|
|
182
190
|
}
|
|
183
191
|
}
|
|
184
|
-
}
|
|
185
|
-
const resetCustomOptionSearch = useCallback(() => {
|
|
186
|
-
setTyping(false);
|
|
187
|
-
if (resetSearchOnOptionSelection) {
|
|
188
|
-
setInputValue(multiple ? '' : selectedLabel);
|
|
189
|
-
}
|
|
190
|
-
}, [resetSearchOnOptionSelection, setInputValue, multiple, selectedLabel]);
|
|
191
|
-
const { listRef, suppressCloseCommit, enableCloseCommit, tryCommitOnClose, tryCommitOnBlur, tryCommitFromKeyboard, handleListSingleChange, handleListMultipleChange, } = useCustomOption({
|
|
192
|
-
allowCustomOption,
|
|
193
|
-
addCustomOptionTriggers,
|
|
194
|
-
addOptionByEnter,
|
|
195
|
-
multiple,
|
|
196
|
-
inputValue,
|
|
197
|
-
multipleValue,
|
|
198
|
-
commitSingle: handleSingleChange,
|
|
199
|
-
commitMultiple: handleMultipleChange,
|
|
200
|
-
resetSearch: resetCustomOptionSearch,
|
|
201
|
-
inputElementRef,
|
|
202
|
-
clearButtonRef,
|
|
203
|
-
copyButtonRef,
|
|
204
|
-
footerActiveElementsRefs,
|
|
205
|
-
});
|
|
206
|
-
const handleOpenChange = useCallback((next) => {
|
|
207
|
-
// readonly/disabled поле открывать нельзя, закрывать — можно.
|
|
208
|
-
if (next && (disabled || readOnly))
|
|
209
|
-
return;
|
|
210
|
-
if (openProp === undefined) {
|
|
211
|
-
setOpenLocal(next);
|
|
212
|
-
}
|
|
213
|
-
onOpenChange?.(next);
|
|
214
|
-
if (!next) {
|
|
215
|
-
setTyping(false);
|
|
216
|
-
const committed = tryCommitOnClose();
|
|
217
|
-
// Кастомная опция не зафиксирована — вернуть лейбл выбранного значения.
|
|
218
|
-
if (resetSearchOnOptionSelection && !committed) {
|
|
219
|
-
setInputValue(selectedLabel);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}, [
|
|
223
|
-
openProp,
|
|
224
|
-
onOpenChange,
|
|
225
|
-
disabled,
|
|
226
|
-
readOnly,
|
|
227
|
-
resetSearchOnOptionSelection,
|
|
228
|
-
setInputValue,
|
|
229
|
-
selectedLabel,
|
|
230
|
-
tryCommitOnClose,
|
|
231
|
-
]);
|
|
192
|
+
};
|
|
232
193
|
const handleInputChange = (next) => {
|
|
233
|
-
enableCloseCommit();
|
|
234
194
|
setInputValue(next);
|
|
235
195
|
setTyping(true);
|
|
236
196
|
if (!open) {
|
|
@@ -329,12 +289,19 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
329
289
|
}
|
|
330
290
|
if (event.key === 'Escape' && open) {
|
|
331
291
|
event.preventDefault();
|
|
332
|
-
suppressCloseCommit();
|
|
333
292
|
handleOpenChange(false);
|
|
334
293
|
}
|
|
335
|
-
if (
|
|
294
|
+
if (addOptionByEnter && event.key === 'Enter' && inputValue !== '') {
|
|
295
|
+
// Введённый текст становится новым значением (создание опции «на лету»).
|
|
336
296
|
event.preventDefault();
|
|
337
|
-
|
|
297
|
+
if (multiple) {
|
|
298
|
+
if (!multipleValue.includes(inputValue)) {
|
|
299
|
+
handleMultipleChange([...multipleValue, inputValue]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
handleSingleChange(inputValue);
|
|
304
|
+
}
|
|
338
305
|
return;
|
|
339
306
|
}
|
|
340
307
|
if (event.key === 'Enter' && !open) {
|
|
@@ -371,20 +338,13 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
371
338
|
}
|
|
372
339
|
onFocusProp?.(event);
|
|
373
340
|
}, [searchable, onFocusProp]);
|
|
374
|
-
const handleInputBlur = (event) => {
|
|
341
|
+
const handleInputBlur = useCallback((event) => {
|
|
375
342
|
setFocusVisible(false);
|
|
376
|
-
const blurResult = tryCommitOnBlur(event);
|
|
377
|
-
// Закрытый список не получит onOpenChange — вернуть лейбл выбранного значения.
|
|
378
|
-
// Клик по айтему открытого списка даёт relatedTarget === null, восстановление делает закрытие.
|
|
379
|
-
if (resetSearchOnOptionSelection && blurResult === 'skipped' && (!open || event.relatedTarget !== null)) {
|
|
380
|
-
setTyping(false);
|
|
381
|
-
setInputValue(selectedLabel);
|
|
382
|
-
}
|
|
383
343
|
onBlurProp?.(event);
|
|
384
|
-
};
|
|
344
|
+
}, [onBlurProp]);
|
|
385
345
|
const droplistSelection = multiple
|
|
386
|
-
? { mode: 'multiple', value: multipleValue, onChange:
|
|
387
|
-
: { mode: 'single', value: singleValue, onChange:
|
|
346
|
+
? { mode: 'multiple', value: multipleValue, onChange: handleMultipleChange }
|
|
347
|
+
: { mode: 'single', value: singleValue, onChange: handleSingleChange };
|
|
388
348
|
const hasChips = chips && selectedPairs.length > 0;
|
|
389
349
|
// minWidth поискового input'а = ширина введённого текста (`.inputPlug`), но не больше строки чипов.
|
|
390
350
|
// Замер до paint, иначе input мигает при вводе.
|
|
@@ -411,5 +371,5 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
411
371
|
}), children: _jsx("div", { className: fieldStyles.acrylicBg, "aria-hidden": true }) }), _jsx("div", { className: fieldStyles.borderStateLayer, "data-state": 'borderOnBackground' }), _jsx("div", { className: fieldStyles.focusLayer })] }), _jsx("div", { className: fieldStyles.fieldContainer, children: _jsxs("div", { className: fieldStyles.contentWrapper, children: [iconBefore && _jsx("div", { className: fieldStyles.iconSlot, children: iconBefore }), _jsxs("div", { className: fieldStyles.inputLine, children: [prefix && _jsx("span", { className: fieldStyles.prefix, children: prefix }), hasChips ? (_jsxs("div", { className: styles.chipsRow, ref: contentRef, "data-test-id": TEST_IDS.fieldSelectChips, children: [selectedPairs.map(pair => (_jsx(Tag, { label: formatPair(pair), size: TAG_SIZE_MAP[size], appearance: pair.appearance ?? 'neutral', onDelete: disabled || readOnly || pair.disabled ? undefined : handleRemoveChip(pair.id) }, String(pair.id)))), searchInput, _jsx("span", { ref: inputPlugRef, className: styles.inputPlug, "aria-hidden": true, children: inputValue })] })) : (searchInput), postfixButtons && (
|
|
412
372
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
413
373
|
_jsx("span", { className: fieldStyles.postfixButtonsSlot, onMouseDown: preventSlotMouseDown, onClick: stopSlotClickPropagation, children: postfixButtons })), postfix && _jsx("span", { className: fieldStyles.postfix, children: postfix })] }), _jsx("div", { className: fieldStyles.iconSlot, children: _jsx("span", { className: styles.chevron, "data-open": open || undefined, "aria-hidden": true, children: _jsx(ChevronDownSVG, {}) }) })] }) })] }));
|
|
414
|
-
return (_jsx(FieldDecorator, { className: className, label: label, labelTooltip: labelTooltip, caption: caption, hint: hint, error: error, size: size, validationState: validationState, showHintIcon: showHintIcon, length: length, required: required, labelFor: id, disabled: disabled, readonly: readOnly, children: _jsx(Droplist, { items: filteredItems, label: label, pinTop: filteredPinTop, pinBottom: filteredPinBottom, trigger: 'click', placement: placement, widthStrategy: widthStrategy, triggerElemRef: inputRef,
|
|
374
|
+
return (_jsx(FieldDecorator, { className: className, label: label, labelTooltip: labelTooltip, caption: caption, hint: hint, error: error, size: size, validationState: validationState, showHintIcon: showHintIcon, length: length, required: required, labelFor: id, disabled: disabled, readonly: readOnly, children: _jsx(Droplist, { items: filteredItems, label: label, pinTop: filteredPinTop, pinBottom: filteredPinBottom, trigger: 'click', placement: placement, widthStrategy: widthStrategy, triggerElemRef: inputRef, size: size, open: open, onOpenChange: handleOpenChange, selection: droplistSelection, closeDroplistOnItemClick: closeDroplistOnItemClick, footer: footer, footerActiveElementsRefs: footerActiveElementsRefs, loading: loading, noDataState: noDataState, noResultsState: noResultsState, errorDataState: errorDataState, dataError: dataError, dataFiltered: dataFiltered ?? (searchable && typing && inputValue !== ''), virtualized: virtualized, scroll: true, scrollToSelectedItem: scrollToSelectedItem, limitedScrollHeight: limitedScrollHeight, untouchableScrollbars: untouchableScrollbars, closeOnPopstate: closeOnPopstate, children: trigger }) }));
|
|
415
375
|
});
|
|
@@ -2,14 +2,3 @@ export declare const SELECTION_MODE: {
|
|
|
2
2
|
readonly Single: "single";
|
|
3
3
|
readonly Multiple: "multiple";
|
|
4
4
|
};
|
|
5
|
-
/** События, по которым произвольный ввод фиксируется как выбранная опция. */
|
|
6
|
-
export declare const ADD_CUSTOM_OPTION_TRIGGER: {
|
|
7
|
-
readonly Enter: "enter";
|
|
8
|
-
readonly Blur: "blur";
|
|
9
|
-
readonly Space: "space";
|
|
10
|
-
readonly Comma: "comma";
|
|
11
|
-
};
|
|
12
|
-
/** Триггеры по умолчанию для `selection='single'` при `allowCustomOption`. */
|
|
13
|
-
export declare const ADD_CUSTOM_OPTION_TRIGGERS_SINGLE: readonly ["enter", "blur"];
|
|
14
|
-
/** Триггеры по умолчанию для `selection='multiple'` при `allowCustomOption`. */
|
|
15
|
-
export declare const ADD_CUSTOM_OPTION_TRIGGERS_MULTIPLE: readonly ["enter", "blur", "space", "comma"];
|
|
@@ -2,22 +2,3 @@ export const SELECTION_MODE = {
|
|
|
2
2
|
Single: 'single',
|
|
3
3
|
Multiple: 'multiple',
|
|
4
4
|
};
|
|
5
|
-
/** События, по которым произвольный ввод фиксируется как выбранная опция. */
|
|
6
|
-
export const ADD_CUSTOM_OPTION_TRIGGER = {
|
|
7
|
-
Enter: 'enter',
|
|
8
|
-
Blur: 'blur',
|
|
9
|
-
Space: 'space',
|
|
10
|
-
Comma: 'comma',
|
|
11
|
-
};
|
|
12
|
-
/** Триггеры по умолчанию для `selection='single'` при `allowCustomOption`. */
|
|
13
|
-
export const ADD_CUSTOM_OPTION_TRIGGERS_SINGLE = [
|
|
14
|
-
ADD_CUSTOM_OPTION_TRIGGER.Enter,
|
|
15
|
-
ADD_CUSTOM_OPTION_TRIGGER.Blur,
|
|
16
|
-
];
|
|
17
|
-
/** Триггеры по умолчанию для `selection='multiple'` при `allowCustomOption`. */
|
|
18
|
-
export const ADD_CUSTOM_OPTION_TRIGGERS_MULTIPLE = [
|
|
19
|
-
ADD_CUSTOM_OPTION_TRIGGER.Enter,
|
|
20
|
-
ADD_CUSTOM_OPTION_TRIGGER.Blur,
|
|
21
|
-
ADD_CUSTOM_OPTION_TRIGGER.Space,
|
|
22
|
-
ADD_CUSTOM_OPTION_TRIGGER.Comma,
|
|
23
|
-
];
|
|
@@ -4,30 +4,9 @@ import { Appearance as TagAppearance } from '@cloud-ru/ds-tag';
|
|
|
4
4
|
import { ValueOf } from '@cloud-ru/ds-utils';
|
|
5
5
|
import { FocusEvent, KeyboardEvent, ReactNode } from 'react';
|
|
6
6
|
import { FieldLayoutPresets } from '../../hooks/index.js';
|
|
7
|
-
import {
|
|
7
|
+
import { SELECTION_MODE } from './constants.js';
|
|
8
8
|
/** Режим выбора FieldSelect: одно значение или несколько. */
|
|
9
9
|
export type Selection = ValueOf<typeof SELECTION_MODE>;
|
|
10
|
-
/** Событие, по которому произвольный ввод фиксируется как выбранная опция. */
|
|
11
|
-
export type FieldSelectAddCustomOptionTrigger = ValueOf<typeof ADD_CUSTOM_OPTION_TRIGGER>;
|
|
12
|
-
/** Триггеры кастомной опции в режиме `single`. */
|
|
13
|
-
export type FieldSelectSingleAddCustomOptionTrigger = (typeof ADD_CUSTOM_OPTION_TRIGGERS_SINGLE)[number];
|
|
14
|
-
/** Триггеры кастомной опции в режиме `multiple`. */
|
|
15
|
-
export type FieldSelectMultipleAddCustomOptionTrigger = (typeof ADD_CUSTOM_OPTION_TRIGGERS_MULTIPLE)[number];
|
|
16
|
-
type CustomOptionProps<TTrigger extends FieldSelectAddCustomOptionTrigger> = {
|
|
17
|
-
/**
|
|
18
|
-
* Разрешить фиксировать произвольный ввод из строки поиска как выбранную опцию
|
|
19
|
-
* (значение, которого нет в `items`).
|
|
20
|
-
* @default false
|
|
21
|
-
*/
|
|
22
|
-
allowCustomOption?: boolean;
|
|
23
|
-
/**
|
|
24
|
-
* События, по которым произвольный ввод фиксируется как выбранная опция.
|
|
25
|
-
* Учитывается только при `allowCustomOption={true}`.
|
|
26
|
-
* Если не задан — используются все триггеры режима (`single`: enter, blur; `multiple`: enter, blur, space, comma).
|
|
27
|
-
* Триггеры, недоступные в текущем `selection`, игнорируются.
|
|
28
|
-
*/
|
|
29
|
-
addCustomOptionTriggers?: TTrigger[];
|
|
30
|
-
};
|
|
31
10
|
type WithTagAppearance<T> = T extends {
|
|
32
11
|
items: ItemProps[];
|
|
33
12
|
} ? Omit<T, 'items'> & {
|
|
@@ -119,8 +98,6 @@ type CommonSelectProps = FieldSelectDecoratorProps & DroplistPassthrough & {
|
|
|
119
98
|
autocomplete?: boolean;
|
|
120
99
|
/**
|
|
121
100
|
* Зафиксировать введённый текст как новый выбор по `Enter` (создание опции «на лету»).
|
|
122
|
-
*
|
|
123
|
-
* @deprecated Используйте `allowCustomOption`. `true` эквивалентен `allowCustomOption` с триггером `enter`.
|
|
124
101
|
* @default false
|
|
125
102
|
*/
|
|
126
103
|
addOptionByEnter?: boolean;
|
|
@@ -177,7 +154,7 @@ type CommonSelectProps = FieldSelectDecoratorProps & DroplistPassthrough & {
|
|
|
177
154
|
/** Тестовый id корня */
|
|
178
155
|
'data-test-id'?: string;
|
|
179
156
|
};
|
|
180
|
-
export type FieldSelectSingleProps = CommonSelectProps &
|
|
157
|
+
export type FieldSelectSingleProps = CommonSelectProps & {
|
|
181
158
|
/** Режим выбора. По умолчанию `'single'`. */
|
|
182
159
|
selection?: typeof SELECTION_MODE.Single;
|
|
183
160
|
/** Управляемое значение. Пустая строка трактуется как «значение не выбрано». */
|
|
@@ -192,7 +169,7 @@ export type FieldSelectSingleProps = CommonSelectProps & CustomOptionProps<Field
|
|
|
192
169
|
*/
|
|
193
170
|
closeDroplistOnItemClick?: boolean;
|
|
194
171
|
};
|
|
195
|
-
export type FieldSelectMultipleProps = CommonSelectProps &
|
|
172
|
+
export type FieldSelectMultipleProps = CommonSelectProps & {
|
|
196
173
|
/** Режим выбора */
|
|
197
174
|
selection: typeof SELECTION_MODE.Multiple;
|
|
198
175
|
/** Управляемые значения */
|