@cloud-ru/ds-fields 2.2.3-preview-7425cb27.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/dist/cjs/segments/useSegmentedMask.d.ts +1 -3
- package/dist/cjs/segments/useSegmentedMask.js +9 -18
- package/dist/esm/segments/useSegmentedMask.d.ts +1 -3
- package/dist/esm/segments/useSegmentedMask.js +9 -18
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/segments/useSegmentedMask.ts +9 -18
|
@@ -29,9 +29,7 @@ type UseSegmentedMaskParams = {
|
|
|
29
29
|
* Сегментный ввод даты/времени (порт `useDateField` из @snack-uikit/fields). Каретка ходит по
|
|
30
30
|
* сегментам (`setSelectionRange` подсвечивает текущий), цифры заполняют сегмент с авто-переходом,
|
|
31
31
|
* стрелки двигают по сегментам, Backspace очищает сегмент до плейсхолдера.
|
|
32
|
-
*
|
|
33
|
-
* Навигация идёт через `handleKeyDown`, правка значения — через нативные `beforeinput`/`input`:
|
|
34
|
-
* `nativeInputRef` нужно передать в `ref` того же input'а.
|
|
32
|
+
* `nativeInputRef` передаётся в `ref` того же input'а: значение правится через нативные `beforeinput`/`input`.
|
|
35
33
|
*/
|
|
36
34
|
export declare function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue, onMaskedChange, onArrowDown, onArrowUp, onEscape, onEdit, }: UseSegmentedMaskParams): {
|
|
37
35
|
handleKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void;
|
|
@@ -18,9 +18,7 @@ const RESET_TYPED_KEYS = new Set([
|
|
|
18
18
|
* Сегментный ввод даты/времени (порт `useDateField` из @snack-uikit/fields). Каретка ходит по
|
|
19
19
|
* сегментам (`setSelectionRange` подсвечивает текущий), цифры заполняют сегмент с авто-переходом,
|
|
20
20
|
* стрелки двигают по сегментам, Backspace очищает сегмент до плейсхолдера.
|
|
21
|
-
*
|
|
22
|
-
* Навигация идёт через `handleKeyDown`, правка значения — через нативные `beforeinput`/`input`:
|
|
23
|
-
* `nativeInputRef` нужно передать в `ref` того же input'а.
|
|
21
|
+
* `nativeInputRef` передаётся в `ref` того же input'а: значение правится через нативные `beforeinput`/`input`.
|
|
24
22
|
*/
|
|
25
23
|
function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue, onMaskedChange, onArrowDown, onArrowUp, onEscape, onEdit, }) {
|
|
26
24
|
const firstSlot = slots[0];
|
|
@@ -125,8 +123,7 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
125
123
|
}
|
|
126
124
|
selectSlot(nextSlot(slot));
|
|
127
125
|
}, [clearSlot, isValidDate, lastSlot, nextSlot, selectSlot, slots, tryToComplete]);
|
|
128
|
-
//
|
|
129
|
-
// набрали `1` или `0` + `1`, а от этого зависит, ждать ли вторую цифру.
|
|
126
|
+
// Цифры сегмента храним отдельно от текста: по `01` в поле не отличить `1` от `0` + `1`.
|
|
130
127
|
const typedRef = (0, react_1.useRef)(null);
|
|
131
128
|
const resetTyped = (0, react_1.useCallback)(() => {
|
|
132
129
|
typedRef.current = null;
|
|
@@ -135,12 +132,11 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
135
132
|
const maxLength = slot.max.toString().length;
|
|
136
133
|
const prev = typedRef.current?.key === slot.key ? typedRef.current.digits : '';
|
|
137
134
|
const appended = prev + key;
|
|
138
|
-
//
|
|
135
|
+
// Как в React Aria и MUI X: `13` в месяце → `03`.
|
|
139
136
|
const overflow = Number(appended) > slot.max;
|
|
140
137
|
const digits = overflow ? key : appended;
|
|
141
138
|
const value = Number(digits);
|
|
142
139
|
const complete = overflow || digits.length >= maxLength || value * 10 > slot.max;
|
|
143
|
-
// Значение ниже min (`0` в дне) не показываем, но помним цифру до следующей.
|
|
144
140
|
const accepted = value >= slot.min;
|
|
145
141
|
if (accepted)
|
|
146
142
|
updateSlot(slot, value);
|
|
@@ -180,7 +176,6 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
180
176
|
const input = inputRef.current;
|
|
181
177
|
if (!input || readonly || disabled)
|
|
182
178
|
return;
|
|
183
|
-
// Навигация и правка начинают набор сегмента заново. Цифры, `Unidentified` (Android) и модификаторы — нет.
|
|
184
179
|
if (RESET_TYPED_KEYS.has(event.key))
|
|
185
180
|
resetTyped();
|
|
186
181
|
// Tab уводит фокус из поля — не перехватываем.
|
|
@@ -197,11 +192,11 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
197
192
|
onArrowUp?.();
|
|
198
193
|
return;
|
|
199
194
|
}
|
|
200
|
-
//
|
|
195
|
+
// Копирование и вставку не блокируем: правку поля перехватит beforeinput.
|
|
201
196
|
if (event.ctrlKey || event.metaKey || event.altKey)
|
|
202
197
|
return;
|
|
203
198
|
onEdit?.();
|
|
204
|
-
//
|
|
199
|
+
// Экранные клавиатуры присылают `Unidentified` вместо цифры, её данные есть только в beforeinput.
|
|
205
200
|
if (/^\d$/.test(event.key) || event.key === 'Unidentified')
|
|
206
201
|
return;
|
|
207
202
|
if (event.key === 'Escape') {
|
|
@@ -258,7 +253,6 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
258
253
|
slotFromIndex,
|
|
259
254
|
tryToComplete,
|
|
260
255
|
]);
|
|
261
|
-
// Раскладывает цифры по сегментам слева направо полными кусками — для вставки и автозаполнения.
|
|
262
256
|
const fillFromDigits = (0, react_1.useCallback)((digits) => {
|
|
263
257
|
resetTyped();
|
|
264
258
|
ensureMask();
|
|
@@ -280,8 +274,6 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
280
274
|
selectSlot(nextSlot(lastFilled));
|
|
281
275
|
}
|
|
282
276
|
}, [ensureMask, lastSlot, nextSlot, resetTyped, selectSlot, slots, tryToComplete, updateSlot]);
|
|
283
|
-
// Вставка (Ctrl+V) полной строки даты/времени. Нативный onChange у поля игнорируется (движок
|
|
284
|
-
// controlled), поэтому без этого обработчика вставка не работала бы вовсе.
|
|
285
277
|
const handlePaste = (0, react_1.useCallback)((event) => {
|
|
286
278
|
if (readonly || disabled)
|
|
287
279
|
return;
|
|
@@ -306,7 +298,7 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
306
298
|
const digits = text.replace(/\D/g, '');
|
|
307
299
|
if (!inputType.startsWith('insert') || !digits)
|
|
308
300
|
return;
|
|
309
|
-
//
|
|
301
|
+
// Автозаполнение, `fill` и drop присылают строку целиком.
|
|
310
302
|
if (text.length > 1) {
|
|
311
303
|
fillFromDigits(digits);
|
|
312
304
|
}
|
|
@@ -315,7 +307,7 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
315
307
|
}
|
|
316
308
|
emit();
|
|
317
309
|
}, [clearSlot, emit, fillFromDigits, inputRef, onEdit, resetTyped, slotFromIndex, typeDigit]);
|
|
318
|
-
//
|
|
310
|
+
// Композицию IME браузер не даёт отменить — откатываем её в input.
|
|
319
311
|
const pendingEditRef = (0, react_1.useRef)(null);
|
|
320
312
|
const handleBeforeInput = (0, react_1.useCallback)((event) => {
|
|
321
313
|
const input = inputRef.current;
|
|
@@ -336,18 +328,17 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
336
328
|
const pending = pendingEditRef.current;
|
|
337
329
|
pendingEditRef.current = null;
|
|
338
330
|
if (pending) {
|
|
339
|
-
// Браузер уже изменил значение: вырезаем вставленное, возвращаем прежнее и применяем как обычный ввод.
|
|
340
331
|
const inputType = event instanceof InputEvent ? event.inputType : '';
|
|
341
332
|
const inserted = input.value.slice(pending.start, input.value.length - (pending.value.length - pending.end));
|
|
342
333
|
input.value = pending.value;
|
|
343
334
|
setValue(pending.value);
|
|
344
335
|
input.setSelectionRange(pending.start, pending.end);
|
|
345
|
-
// Цифру
|
|
336
|
+
// Цифру композиции вводит её фиксация (insertText), иначе цифра запишется дважды.
|
|
346
337
|
if (!inputType.includes('Composition'))
|
|
347
338
|
applyEdit(inputType, inserted);
|
|
348
339
|
return;
|
|
349
340
|
}
|
|
350
|
-
//
|
|
341
|
+
// Автозаполнение меняет значение без beforeinput.
|
|
351
342
|
const digits = input.value.replace(/\D/g, '');
|
|
352
343
|
input.value = mask;
|
|
353
344
|
setValue(mask);
|
|
@@ -29,9 +29,7 @@ type UseSegmentedMaskParams = {
|
|
|
29
29
|
* Сегментный ввод даты/времени (порт `useDateField` из @snack-uikit/fields). Каретка ходит по
|
|
30
30
|
* сегментам (`setSelectionRange` подсвечивает текущий), цифры заполняют сегмент с авто-переходом,
|
|
31
31
|
* стрелки двигают по сегментам, Backspace очищает сегмент до плейсхолдера.
|
|
32
|
-
*
|
|
33
|
-
* Навигация идёт через `handleKeyDown`, правка значения — через нативные `beforeinput`/`input`:
|
|
34
|
-
* `nativeInputRef` нужно передать в `ref` того же input'а.
|
|
32
|
+
* `nativeInputRef` передаётся в `ref` того же input'а: значение правится через нативные `beforeinput`/`input`.
|
|
35
33
|
*/
|
|
36
34
|
export declare function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue, onMaskedChange, onArrowDown, onArrowUp, onEscape, onEdit, }: UseSegmentedMaskParams): {
|
|
37
35
|
handleKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void;
|
|
@@ -15,9 +15,7 @@ const RESET_TYPED_KEYS = new Set([
|
|
|
15
15
|
* Сегментный ввод даты/времени (порт `useDateField` из @snack-uikit/fields). Каретка ходит по
|
|
16
16
|
* сегментам (`setSelectionRange` подсвечивает текущий), цифры заполняют сегмент с авто-переходом,
|
|
17
17
|
* стрелки двигают по сегментам, Backspace очищает сегмент до плейсхолдера.
|
|
18
|
-
*
|
|
19
|
-
* Навигация идёт через `handleKeyDown`, правка значения — через нативные `beforeinput`/`input`:
|
|
20
|
-
* `nativeInputRef` нужно передать в `ref` того же input'а.
|
|
18
|
+
* `nativeInputRef` передаётся в `ref` того же input'а: значение правится через нативные `beforeinput`/`input`.
|
|
21
19
|
*/
|
|
22
20
|
export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue, onMaskedChange, onArrowDown, onArrowUp, onEscape, onEdit, }) {
|
|
23
21
|
const firstSlot = slots[0];
|
|
@@ -122,8 +120,7 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
122
120
|
}
|
|
123
121
|
selectSlot(nextSlot(slot));
|
|
124
122
|
}, [clearSlot, isValidDate, lastSlot, nextSlot, selectSlot, slots, tryToComplete]);
|
|
125
|
-
//
|
|
126
|
-
// набрали `1` или `0` + `1`, а от этого зависит, ждать ли вторую цифру.
|
|
123
|
+
// Цифры сегмента храним отдельно от текста: по `01` в поле не отличить `1` от `0` + `1`.
|
|
127
124
|
const typedRef = useRef(null);
|
|
128
125
|
const resetTyped = useCallback(() => {
|
|
129
126
|
typedRef.current = null;
|
|
@@ -132,12 +129,11 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
132
129
|
const maxLength = slot.max.toString().length;
|
|
133
130
|
const prev = typedRef.current?.key === slot.key ? typedRef.current.digits : '';
|
|
134
131
|
const appended = prev + key;
|
|
135
|
-
//
|
|
132
|
+
// Как в React Aria и MUI X: `13` в месяце → `03`.
|
|
136
133
|
const overflow = Number(appended) > slot.max;
|
|
137
134
|
const digits = overflow ? key : appended;
|
|
138
135
|
const value = Number(digits);
|
|
139
136
|
const complete = overflow || digits.length >= maxLength || value * 10 > slot.max;
|
|
140
|
-
// Значение ниже min (`0` в дне) не показываем, но помним цифру до следующей.
|
|
141
137
|
const accepted = value >= slot.min;
|
|
142
138
|
if (accepted)
|
|
143
139
|
updateSlot(slot, value);
|
|
@@ -177,7 +173,6 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
177
173
|
const input = inputRef.current;
|
|
178
174
|
if (!input || readonly || disabled)
|
|
179
175
|
return;
|
|
180
|
-
// Навигация и правка начинают набор сегмента заново. Цифры, `Unidentified` (Android) и модификаторы — нет.
|
|
181
176
|
if (RESET_TYPED_KEYS.has(event.key))
|
|
182
177
|
resetTyped();
|
|
183
178
|
// Tab уводит фокус из поля — не перехватываем.
|
|
@@ -194,11 +189,11 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
194
189
|
onArrowUp?.();
|
|
195
190
|
return;
|
|
196
191
|
}
|
|
197
|
-
//
|
|
192
|
+
// Копирование и вставку не блокируем: правку поля перехватит beforeinput.
|
|
198
193
|
if (event.ctrlKey || event.metaKey || event.altKey)
|
|
199
194
|
return;
|
|
200
195
|
onEdit?.();
|
|
201
|
-
//
|
|
196
|
+
// Экранные клавиатуры присылают `Unidentified` вместо цифры, её данные есть только в beforeinput.
|
|
202
197
|
if (/^\d$/.test(event.key) || event.key === 'Unidentified')
|
|
203
198
|
return;
|
|
204
199
|
if (event.key === 'Escape') {
|
|
@@ -255,7 +250,6 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
255
250
|
slotFromIndex,
|
|
256
251
|
tryToComplete,
|
|
257
252
|
]);
|
|
258
|
-
// Раскладывает цифры по сегментам слева направо полными кусками — для вставки и автозаполнения.
|
|
259
253
|
const fillFromDigits = useCallback((digits) => {
|
|
260
254
|
resetTyped();
|
|
261
255
|
ensureMask();
|
|
@@ -277,8 +271,6 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
277
271
|
selectSlot(nextSlot(lastFilled));
|
|
278
272
|
}
|
|
279
273
|
}, [ensureMask, lastSlot, nextSlot, resetTyped, selectSlot, slots, tryToComplete, updateSlot]);
|
|
280
|
-
// Вставка (Ctrl+V) полной строки даты/времени. Нативный onChange у поля игнорируется (движок
|
|
281
|
-
// controlled), поэтому без этого обработчика вставка не работала бы вовсе.
|
|
282
274
|
const handlePaste = useCallback((event) => {
|
|
283
275
|
if (readonly || disabled)
|
|
284
276
|
return;
|
|
@@ -303,7 +295,7 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
303
295
|
const digits = text.replace(/\D/g, '');
|
|
304
296
|
if (!inputType.startsWith('insert') || !digits)
|
|
305
297
|
return;
|
|
306
|
-
//
|
|
298
|
+
// Автозаполнение, `fill` и drop присылают строку целиком.
|
|
307
299
|
if (text.length > 1) {
|
|
308
300
|
fillFromDigits(digits);
|
|
309
301
|
}
|
|
@@ -312,7 +304,7 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
312
304
|
}
|
|
313
305
|
emit();
|
|
314
306
|
}, [clearSlot, emit, fillFromDigits, inputRef, onEdit, resetTyped, slotFromIndex, typeDigit]);
|
|
315
|
-
//
|
|
307
|
+
// Композицию IME браузер не даёт отменить — откатываем её в input.
|
|
316
308
|
const pendingEditRef = useRef(null);
|
|
317
309
|
const handleBeforeInput = useCallback((event) => {
|
|
318
310
|
const input = inputRef.current;
|
|
@@ -333,18 +325,17 @@ export function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, se
|
|
|
333
325
|
const pending = pendingEditRef.current;
|
|
334
326
|
pendingEditRef.current = null;
|
|
335
327
|
if (pending) {
|
|
336
|
-
// Браузер уже изменил значение: вырезаем вставленное, возвращаем прежнее и применяем как обычный ввод.
|
|
337
328
|
const inputType = event instanceof InputEvent ? event.inputType : '';
|
|
338
329
|
const inserted = input.value.slice(pending.start, input.value.length - (pending.value.length - pending.end));
|
|
339
330
|
input.value = pending.value;
|
|
340
331
|
setValue(pending.value);
|
|
341
332
|
input.setSelectionRange(pending.start, pending.end);
|
|
342
|
-
// Цифру
|
|
333
|
+
// Цифру композиции вводит её фиксация (insertText), иначе цифра запишется дважды.
|
|
343
334
|
if (!inputType.includes('Composition'))
|
|
344
335
|
applyEdit(inputType, inserted);
|
|
345
336
|
return;
|
|
346
337
|
}
|
|
347
|
-
//
|
|
338
|
+
// Автозаполнение меняет значение без beforeinput.
|
|
348
339
|
const digits = input.value.replace(/\D/g, '');
|
|
349
340
|
input.value = mask;
|
|
350
341
|
setValue(mask);
|