@cloud-ru/ds-fields 2.2.3-preview-8025692f.0 → 2.2.3-preview-a0540d7f.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 +4 -2
- package/dist/cjs/components/FieldDate/FieldDate.js +3 -3
- package/dist/cjs/components/FieldDate/mask.d.ts +2 -3
- package/dist/cjs/components/FieldDate/mask.js +3 -4
- package/dist/cjs/components/FieldSelect/FieldSelect.js +98 -43
- package/dist/cjs/components/FieldSelect/constants.d.ts +11 -0
- package/dist/cjs/components/FieldSelect/constants.js +20 -1
- package/dist/cjs/components/FieldSelect/hooks.d.ts +33 -0
- package/dist/cjs/components/FieldSelect/hooks.js +95 -0
- package/dist/cjs/components/FieldSelect/types.d.ts +26 -3
- package/dist/cjs/components/FieldSelect/utils/customOption.d.ts +21 -0
- package/dist/cjs/components/FieldSelect/utils/customOption.js +55 -0
- package/dist/cjs/components/FieldSelect/utils/extract.d.ts +11 -0
- package/dist/cjs/components/FieldSelect/utils/extract.js +39 -0
- package/dist/cjs/components/FieldSelect/utils/filter.d.ts +3 -0
- package/dist/cjs/components/FieldSelect/utils/filter.js +40 -0
- package/dist/cjs/components/FieldSelect/utils/index.d.ts +6 -0
- package/dist/cjs/components/FieldSelect/utils/index.js +22 -0
- package/dist/cjs/components/FieldSelect/utils/items.d.ts +9 -0
- package/dist/cjs/components/FieldSelect/utils/items.js +47 -0
- package/dist/cjs/components/FieldSelect/utils/selection.d.ts +11 -0
- package/dist/cjs/components/FieldSelect/utils/selection.js +18 -0
- package/dist/cjs/components/FieldSelect/utils/tagSize.d.ts +4 -0
- package/dist/cjs/components/FieldSelect/utils/tagSize.js +5 -0
- package/dist/cjs/components/FieldTime/FieldTime.js +2 -2
- package/dist/cjs/segments/useSegmentedMask.d.ts +0 -2
- package/dist/cjs/segments/useSegmentedMask.js +56 -146
- package/dist/esm/components/FieldDate/FieldDate.js +3 -3
- package/dist/esm/components/FieldDate/mask.d.ts +2 -3
- package/dist/esm/components/FieldDate/mask.js +3 -4
- package/dist/esm/components/FieldSelect/FieldSelect.js +100 -45
- package/dist/esm/components/FieldSelect/constants.d.ts +11 -0
- package/dist/esm/components/FieldSelect/constants.js +19 -0
- package/dist/esm/components/FieldSelect/hooks.d.ts +33 -0
- package/dist/esm/components/FieldSelect/hooks.js +92 -0
- package/dist/esm/components/FieldSelect/types.d.ts +26 -3
- package/dist/esm/components/FieldSelect/utils/customOption.d.ts +21 -0
- package/dist/esm/components/FieldSelect/utils/customOption.js +48 -0
- package/dist/esm/components/FieldSelect/utils/extract.d.ts +11 -0
- package/dist/esm/components/FieldSelect/utils/extract.js +34 -0
- package/dist/esm/components/FieldSelect/utils/filter.d.ts +3 -0
- package/dist/esm/components/FieldSelect/utils/filter.js +36 -0
- package/dist/esm/components/FieldSelect/utils/index.d.ts +6 -0
- package/dist/esm/components/FieldSelect/utils/index.js +6 -0
- package/dist/esm/components/FieldSelect/utils/items.d.ts +9 -0
- package/dist/esm/components/FieldSelect/utils/items.js +41 -0
- package/dist/esm/components/FieldSelect/utils/selection.d.ts +11 -0
- package/dist/esm/components/FieldSelect/utils/selection.js +14 -0
- package/dist/esm/components/FieldSelect/utils/tagSize.d.ts +4 -0
- package/dist/esm/components/FieldSelect/utils/tagSize.js +2 -0
- package/dist/esm/components/FieldTime/FieldTime.js +2 -2
- package/dist/esm/segments/useSegmentedMask.d.ts +0 -2
- package/dist/esm/segments/useSegmentedMask.js +56 -146
- 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 +3 -4
- package/src/components/FieldSelect/FieldSelect.tsx +151 -63
- package/src/components/FieldSelect/constants.ts +22 -0
- package/src/components/FieldSelect/hooks.ts +177 -0
- package/src/components/FieldSelect/types.ts +82 -48
- package/src/components/FieldSelect/utils/customOption.ts +77 -0
- package/src/components/FieldSelect/utils/extract.ts +49 -0
- package/src/components/FieldSelect/utils/filter.ts +51 -0
- package/src/components/FieldSelect/utils/index.ts +6 -0
- package/src/components/FieldSelect/utils/items.ts +58 -0
- package/src/components/FieldSelect/utils/selection.ts +25 -0
- package/src/components/FieldSelect/utils/tagSize.ts +5 -0
- package/src/components/FieldTime/FieldTime.tsx +2 -2
- package/src/segments/useSegmentedMask.ts +52 -172
- package/dist/cjs/components/FieldSelect/utils.d.ts +0 -19
- package/dist/cjs/components/FieldSelect/utils.js +0 -103
- package/dist/esm/components/FieldSelect/utils.d.ts +0 -19
- package/dist/esm/components/FieldSelect/utils.js +0 -92
- package/src/components/FieldSelect/utils.ts +0 -132
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCustomOptionTriggerByCode = getCustomOptionTriggerByCode;
|
|
4
|
+
exports.shouldHandleCustomOptionTrigger = shouldHandleCustomOptionTrigger;
|
|
5
|
+
exports.resolveAddCustomOptionTriggers = resolveAddCustomOptionTriggers;
|
|
6
|
+
exports.isCustomOptionBlurInsideField = isCustomOptionBlurInsideField;
|
|
7
|
+
exports.shouldCommitCustomOptionOnClose = shouldCommitCustomOptionOnClose;
|
|
8
|
+
const constants_1 = require("../constants");
|
|
9
|
+
/**
|
|
10
|
+
* Сопоставляет клавишу с триггером кастомной опции (`enter` / `space` / `comma`).
|
|
11
|
+
* Сначала `KeyboardEvent.code`, затем `key` — если `code` пустой или не из словаря (`','` без `Comma`).
|
|
12
|
+
*/
|
|
13
|
+
function getCustomOptionTriggerByCode(code, key) {
|
|
14
|
+
switch (code) {
|
|
15
|
+
case 'Enter':
|
|
16
|
+
return constants_1.ADD_CUSTOM_OPTION_TRIGGER.Enter;
|
|
17
|
+
case 'Space':
|
|
18
|
+
return constants_1.ADD_CUSTOM_OPTION_TRIGGER.Space;
|
|
19
|
+
case 'Comma':
|
|
20
|
+
return constants_1.ADD_CUSTOM_OPTION_TRIGGER.Comma;
|
|
21
|
+
default:
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
switch (key) {
|
|
25
|
+
case 'Enter':
|
|
26
|
+
return constants_1.ADD_CUSTOM_OPTION_TRIGGER.Enter;
|
|
27
|
+
case ' ':
|
|
28
|
+
return constants_1.ADD_CUSTOM_OPTION_TRIGGER.Space;
|
|
29
|
+
case ',':
|
|
30
|
+
return constants_1.ADD_CUSTOM_OPTION_TRIGGER.Comma;
|
|
31
|
+
default:
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function shouldHandleCustomOptionTrigger(trigger, availableTriggers) {
|
|
36
|
+
return trigger !== undefined && availableTriggers.includes(trigger);
|
|
37
|
+
}
|
|
38
|
+
function resolveAddCustomOptionTriggers(params) {
|
|
39
|
+
if (params.allowCustomOption) {
|
|
40
|
+
const source = params.addCustomOptionTriggers !== undefined ? params.addCustomOptionTriggers : params.defaultTriggers;
|
|
41
|
+
return source.filter(trigger => params.defaultTriggers.includes(trigger));
|
|
42
|
+
}
|
|
43
|
+
if (params.addOptionByEnter) {
|
|
44
|
+
return [constants_1.ADD_CUSTOM_OPTION_TRIGGER.Enter];
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
/** Фокус ушёл на дроплист, футер или postfix — кастомную опцию на blur фиксировать не нужно. */
|
|
49
|
+
function isCustomOptionBlurInsideField(relatedTarget, nodes) {
|
|
50
|
+
return relatedTarget instanceof Node && nodes.some(node => Boolean(node?.contains(relatedTarget)));
|
|
51
|
+
}
|
|
52
|
+
/** Клик снаружи закрывает список, не снимая фокус с input — blur не придёт. */
|
|
53
|
+
function shouldCommitCustomOptionOnClose(params) {
|
|
54
|
+
return !params.skip && params.activeElement !== null && params.activeElement === params.inputElement;
|
|
55
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ItemId } from '@cloud-ru/ds-list';
|
|
2
|
+
import { Appearance as TagAppearance } from '@cloud-ru/ds-tag';
|
|
3
|
+
export type WithIdContent = {
|
|
4
|
+
id?: ItemId;
|
|
5
|
+
content?: unknown;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
appearance?: TagAppearance;
|
|
8
|
+
};
|
|
9
|
+
export declare function extractLabel(item: WithIdContent): string;
|
|
10
|
+
export declare function extractSearchText(item: WithIdContent): string;
|
|
11
|
+
export declare function extractAppearance(item?: WithIdContent): TagAppearance | undefined;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractLabel = extractLabel;
|
|
4
|
+
exports.extractSearchText = extractSearchText;
|
|
5
|
+
exports.extractAppearance = extractAppearance;
|
|
6
|
+
function extractLabel(item) {
|
|
7
|
+
const { content, id } = item;
|
|
8
|
+
if (!content) {
|
|
9
|
+
return String(id ?? '');
|
|
10
|
+
}
|
|
11
|
+
if (typeof content === 'string' || typeof content === 'number') {
|
|
12
|
+
return String(content);
|
|
13
|
+
}
|
|
14
|
+
if (typeof content === 'object' && content !== null && 'label' in content) {
|
|
15
|
+
return String(content.label);
|
|
16
|
+
}
|
|
17
|
+
return String(id ?? '');
|
|
18
|
+
}
|
|
19
|
+
// Текст для поиска: лейбл (option) + caption + description — паритет с легаси `useSearch`,
|
|
20
|
+
// который матчил запрос по всем трём полям контента, а не только по лейблу.
|
|
21
|
+
function extractSearchText(item) {
|
|
22
|
+
const parts = [extractLabel(item)];
|
|
23
|
+
const { content } = item;
|
|
24
|
+
if (content && typeof content === 'object') {
|
|
25
|
+
const c = content;
|
|
26
|
+
if (typeof c.caption === 'string') {
|
|
27
|
+
parts.push(c.caption);
|
|
28
|
+
}
|
|
29
|
+
if (typeof c.description === 'string') {
|
|
30
|
+
parts.push(c.description);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return parts.join(' ');
|
|
34
|
+
}
|
|
35
|
+
// Цвет тега выбранного значения (multiple) — паритет с легаси `option.appearance`.
|
|
36
|
+
// Проверка типа нужна и при типизированном поле: items часто приходят из ответа бэкенда.
|
|
37
|
+
function extractAppearance(item) {
|
|
38
|
+
return typeof item?.appearance === 'string' ? item.appearance : undefined;
|
|
39
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isFuzzyMatch = isFuzzyMatch;
|
|
4
|
+
exports.filterItems = filterItems;
|
|
5
|
+
const extract_1 = require("./extract");
|
|
6
|
+
// Subsequence-fuzzy: символы запроса должны встречаться в строке в исходном порядке.
|
|
7
|
+
// Пример: query=`lge` matches `Large` (L → r → arg → e).
|
|
8
|
+
function isFuzzyMatch(haystack, needle) {
|
|
9
|
+
if (!needle) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
let i = 0;
|
|
13
|
+
for (const ch of haystack) {
|
|
14
|
+
if (ch === needle[i]) {
|
|
15
|
+
i += 1;
|
|
16
|
+
if (i === needle.length) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
function filterItems(items, query, fuzzy) {
|
|
24
|
+
if (!query) {
|
|
25
|
+
return items;
|
|
26
|
+
}
|
|
27
|
+
const q = query.toLowerCase();
|
|
28
|
+
const matches = (item) => {
|
|
29
|
+
const text = (0, extract_1.extractSearchText)(item).toLowerCase();
|
|
30
|
+
return fuzzy ? isFuzzyMatch(text, q) : text.includes(q);
|
|
31
|
+
};
|
|
32
|
+
const walk = (list) => list.flatMap(item => {
|
|
33
|
+
if ('items' in item && Array.isArray(item.items)) {
|
|
34
|
+
const filteredChildren = walk(item.items);
|
|
35
|
+
return filteredChildren.length > 0 ? [{ ...item, items: filteredChildren }] : [];
|
|
36
|
+
}
|
|
37
|
+
return matches(item) ? [item] : [];
|
|
38
|
+
});
|
|
39
|
+
return walk(items);
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./customOption"), exports);
|
|
18
|
+
__exportStar(require("./extract"), exports);
|
|
19
|
+
__exportStar(require("./filter"), exports);
|
|
20
|
+
__exportStar(require("./items"), exports);
|
|
21
|
+
__exportStar(require("./selection"), exports);
|
|
22
|
+
__exportStar(require("./tagSize"), exports);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ItemId } from '@cloud-ru/ds-list';
|
|
2
|
+
import { FieldSelectItem } from '../types';
|
|
3
|
+
import { WithIdContent } from './extract';
|
|
4
|
+
export declare function flatten(items: FieldSelectItem[]): WithIdContent[];
|
|
5
|
+
export declare function findItem(items: FieldSelectItem[], id: ItemId): WithIdContent | undefined;
|
|
6
|
+
/** Айтем для выбранного значения, которого нет в `items` (кастомная опция, ленивая подгрузка). */
|
|
7
|
+
export declare function createPlaceholderItem(id: ItemId): FieldSelectItem;
|
|
8
|
+
/** Выбранные id, которых нет в дереве айтемов — в дроплист их нужно добавить отдельно. */
|
|
9
|
+
export declare function getMissingSelectedItems(selectedIds: ItemId[], allItems: FieldSelectItem[]): FieldSelectItem[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.flatten = flatten;
|
|
4
|
+
exports.findItem = findItem;
|
|
5
|
+
exports.createPlaceholderItem = createPlaceholderItem;
|
|
6
|
+
exports.getMissingSelectedItems = getMissingSelectedItems;
|
|
7
|
+
function flatten(items) {
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const item of items) {
|
|
10
|
+
out.push(item);
|
|
11
|
+
if ('items' in item && Array.isArray(item.items)) {
|
|
12
|
+
out.push(...flatten(item.items));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
function findItem(items, id) {
|
|
18
|
+
for (const item of flatten(items)) {
|
|
19
|
+
if (item.id === id) {
|
|
20
|
+
return item;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
/** Айтем для выбранного значения, которого нет в `items` (кастомная опция, ленивая подгрузка). */
|
|
26
|
+
function createPlaceholderItem(id) {
|
|
27
|
+
return { id, content: { label: String(id) } };
|
|
28
|
+
}
|
|
29
|
+
/** Выбранные id, которых нет в дереве айтемов — в дроплист их нужно добавить отдельно. */
|
|
30
|
+
function getMissingSelectedItems(selectedIds, allItems) {
|
|
31
|
+
if (selectedIds.length === 0) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const existingIds = new Set();
|
|
35
|
+
for (const item of flatten(allItems)) {
|
|
36
|
+
if (item.id !== undefined) {
|
|
37
|
+
existingIds.add(item.id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const missing = [];
|
|
41
|
+
for (const id of selectedIds) {
|
|
42
|
+
if (id !== '' && !existingIds.has(id)) {
|
|
43
|
+
missing.push(createPlaceholderItem(id));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return missing;
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ItemId } from '@cloud-ru/ds-list';
|
|
2
|
+
import { FieldSelectMultipleProps, Selection } from '../types';
|
|
3
|
+
export declare function isMultiple(props: {
|
|
4
|
+
selection?: Selection;
|
|
5
|
+
}): props is FieldSelectMultipleProps;
|
|
6
|
+
/** Выбранные id в нормализованном виде: массив и для `single`, и для `multiple`. */
|
|
7
|
+
export declare function getSelectedIds(params: {
|
|
8
|
+
multiple: boolean;
|
|
9
|
+
multipleValue: ItemId[];
|
|
10
|
+
singleValue: ItemId | undefined;
|
|
11
|
+
}): ItemId[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isMultiple = isMultiple;
|
|
4
|
+
exports.getSelectedIds = getSelectedIds;
|
|
5
|
+
const constants_1 = require("../constants");
|
|
6
|
+
function isMultiple(props) {
|
|
7
|
+
return props.selection === constants_1.SELECTION_MODE.Multiple;
|
|
8
|
+
}
|
|
9
|
+
/** Выбранные id в нормализованном виде: массив и для `single`, и для `multiple`. */
|
|
10
|
+
function getSelectedIds(params) {
|
|
11
|
+
if (params.multiple) {
|
|
12
|
+
return params.multipleValue;
|
|
13
|
+
}
|
|
14
|
+
if (params.singleValue === undefined) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
return [params.singleValue];
|
|
18
|
+
}
|
|
@@ -137,7 +137,7 @@ exports.FieldTime = (0, react_1.forwardRef)(function FieldTime({ label = '', lab
|
|
|
137
137
|
// clear/copy недостижимыми со стрелок — движок всегда делает preventDefault на ArrowRight.
|
|
138
138
|
onInputKeyDown(event);
|
|
139
139
|
}, [onInputKeyDown, timeSeg]);
|
|
140
|
-
// value ведёт сегментный движок (
|
|
140
|
+
// value ведёт сегментный движок (keydown); нативный onChange input'а игнорируем.
|
|
141
141
|
const handleInputChange = (0, react_1.useCallback)(() => undefined, []);
|
|
142
142
|
const handleInputFocus = (0, react_1.useCallback)((event) => {
|
|
143
143
|
setFocusVisible(event.target.matches(':focus-visible'));
|
|
@@ -155,7 +155,7 @@ exports.FieldTime = (0, react_1.forwardRef)(function FieldTime({ label = '', lab
|
|
|
155
155
|
readonly: readOnly,
|
|
156
156
|
hover,
|
|
157
157
|
focusVisible: focusVisible || open,
|
|
158
|
-
}), children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.acrylicBg, "aria-hidden": true }) }), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.borderStateLayer, "data-state": 'borderOnBackground' }), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.focusLayer })] }), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.fieldContainer, children: (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.contentWrapper, children: [(0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.inputLine, children: (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.inputArea, children: [(0, jsx_runtime_1.jsx)(input_private_1.InputPrivate, { ref: (0, merge_refs_1.default)(ref, localRef
|
|
158
|
+
}), children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.acrylicBg, "aria-hidden": true }) }), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.borderStateLayer, "data-state": 'borderOnBackground' }), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.focusLayer })] }), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.fieldContainer, children: (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.contentWrapper, children: [(0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.inputLine, children: (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.inputArea, children: [(0, jsx_runtime_1.jsx)(input_private_1.InputPrivate, { ref: (0, merge_refs_1.default)(ref, localRef), className: styles_module_scss_1.default.fieldInput, value: inputValue, onChange: handleInputChange, disabled: disabled, readonly: readOnly, placeholder: placeholderMask, inputMode: input_private_1.INPUT_MODE.Numeric, tabIndex: inputTabIndex, onClick: timeSeg.handleClick, onPaste: timeSeg.handlePaste, onFocus: handleInputFocus, onBlur: handleInputBlur, onKeyDown: handleInputKeyDown, id: id, name: name, autoFocus: resolvedAutoFocus, "aria-haspopup": 'dialog', "aria-expanded": open, ...(0, utils_1.extractSupportProps)(rest), "data-test-id": constants_1.TEST_IDS.fieldTimeInput }), postfixButtons && (0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_1.default.postfixButtonsSlot, children: postfixButtons })] }) }), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.iconSlot, "data-test-id": constants_1.TEST_IDS.fieldTimeIcon, "aria-hidden": true, children: (0, jsx_runtime_1.jsx)(system_1.WatchSVG, {}) })] }) })] }));
|
|
159
159
|
return ((0, jsx_runtime_1.jsx)(field_decorator_1.FieldDecorator, { className: className, label: label, labelTooltip: labelTooltip, caption: caption, hint: hint, error: error, validationState: validationState, showHintIcon: showHintIcon, length: length, required: required, labelFor: id, disabled: disabled, readonly: readOnly, size: size, children: (0, jsx_runtime_1.jsx)(calendar_1.TimePickerDropdown
|
|
160
160
|
// Явный data-test-id на дропдауне → детерминированный id портального контента:
|
|
161
161
|
// `getTestIdBuilder` строит `content-${id}`, так контент адресуем для open-picker-снимка
|
|
@@ -29,7 +29,6 @@ type UseSegmentedMaskParams = {
|
|
|
29
29
|
* Сегментный ввод даты/времени (порт `useDateField` из @snack-uikit/fields). Каретка ходит по
|
|
30
30
|
* сегментам (`setSelectionRange` подсвечивает текущий), цифры заполняют сегмент с авто-переходом,
|
|
31
31
|
* стрелки двигают по сегментам, Backspace очищает сегмент до плейсхолдера.
|
|
32
|
-
* `nativeInputRef` передаётся в `ref` того же input'а: значение правится через нативные `beforeinput`/`input`.
|
|
33
32
|
*/
|
|
34
33
|
export declare function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue, onMaskedChange, onArrowDown, onArrowUp, onEscape, onEdit, }: UseSegmentedMaskParams): {
|
|
35
34
|
handleKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void;
|
|
@@ -37,7 +36,6 @@ export declare function useSegmentedMask({ inputRef, mask, slots, readonly, disa
|
|
|
37
36
|
handleFocus: () => void;
|
|
38
37
|
handleBlur: () => void;
|
|
39
38
|
handlePaste: (event: ClipboardEvent<HTMLInputElement>) => void;
|
|
40
|
-
nativeInputRef: (node: HTMLInputElement | null) => void;
|
|
41
39
|
selectSlot: (slot: SlotMeta) => void;
|
|
42
40
|
firstSlot: SlotMeta;
|
|
43
41
|
lastSlot: SlotMeta;
|
|
@@ -1,24 +1,11 @@
|
|
|
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");
|
|
5
4
|
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
|
-
]);
|
|
17
5
|
/**
|
|
18
6
|
* Сегментный ввод даты/времени (порт `useDateField` из @snack-uikit/fields). Каретка ходит по
|
|
19
7
|
* сегментам (`setSelectionRange` подсвечивает текущий), цифры заполняют сегмент с авто-переходом,
|
|
20
8
|
* стрелки двигают по сегментам, Backspace очищает сегмент до плейсхолдера.
|
|
21
|
-
* `nativeInputRef` передаётся в `ref` того же input'а: значение правится через нативные `beforeinput`/`input`.
|
|
22
9
|
*/
|
|
23
10
|
function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue, onMaskedChange, onArrowDown, onArrowUp, onEscape, onEdit, }) {
|
|
24
11
|
const firstSlot = slots[0];
|
|
@@ -123,61 +110,70 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
123
110
|
}
|
|
124
111
|
selectSlot(nextSlot(slot));
|
|
125
112
|
}, [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
|
-
}, []);
|
|
131
113
|
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;
|
|
132
119
|
const maxLength = slot.max.toString().length;
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
120
|
+
const isTheLastInput = /^0+$/.test(text) && maxLength === 2 && digit === 0;
|
|
121
|
+
if (valueLength < maxLength) {
|
|
122
|
+
if (slotValue || slotValue >= slot.min) {
|
|
123
|
+
updateSlot(slot, slotValue);
|
|
124
|
+
if (isTheLastInput)
|
|
125
|
+
checkInputAndGoNext(slot);
|
|
126
|
+
}
|
|
127
|
+
if (slotValue * 10 > slot.max) {
|
|
128
|
+
checkInputAndGoNext(slot);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (valueLength > maxLength) {
|
|
132
|
+
if (digit * 10 > slot.max) {
|
|
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);
|
|
146
143
|
}
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
else if (digit * 10 > slot.max) {
|
|
145
|
+
updateSlot(slot, key);
|
|
149
146
|
checkInputAndGoNext(slot);
|
|
150
|
-
|
|
147
|
+
}
|
|
148
|
+
else if (digit || digit >= slot.min) {
|
|
149
|
+
updateSlot(slot, key);
|
|
150
|
+
}
|
|
151
|
+
}, [checkInputAndGoNext, getSlotText, updateSlot]);
|
|
151
152
|
const handleFocus = (0, react_1.useCallback)(() => {
|
|
152
153
|
if (readonly || disabled)
|
|
153
154
|
return;
|
|
154
|
-
resetTyped();
|
|
155
155
|
ensureMask();
|
|
156
156
|
// На фокус всегда выбираем ПЕРВЫЙ сегмент (паритет с легаси). При клике мышью `handleClick`
|
|
157
157
|
// отработает следом и перевыберет сегмент под кареткой; при Tab остаётся первый.
|
|
158
158
|
selectSlot(firstSlot);
|
|
159
|
-
}, [disabled, ensureMask, firstSlot, readonly,
|
|
159
|
+
}, [disabled, ensureMask, firstSlot, readonly, selectSlot]);
|
|
160
160
|
const handleClick = (0, react_1.useCallback)((event) => {
|
|
161
161
|
if (readonly || disabled)
|
|
162
162
|
return;
|
|
163
|
-
resetTyped();
|
|
164
163
|
ensureMask();
|
|
165
164
|
selectSlot(slotFromIndex(event.currentTarget.selectionStart));
|
|
166
|
-
}, [disabled, ensureMask, readonly,
|
|
165
|
+
}, [disabled, ensureMask, readonly, selectSlot, slotFromIndex]);
|
|
167
166
|
const handleBlur = (0, react_1.useCallback)(() => {
|
|
168
|
-
resetTyped();
|
|
169
167
|
const input = inputRef.current;
|
|
170
168
|
if (input && input.value === mask) {
|
|
171
169
|
input.value = '';
|
|
172
170
|
setValue('');
|
|
173
171
|
}
|
|
174
|
-
}, [inputRef, mask,
|
|
172
|
+
}, [inputRef, mask, setValue]);
|
|
175
173
|
const handleKeyDown = (0, react_1.useCallback)((event) => {
|
|
176
174
|
const input = inputRef.current;
|
|
177
175
|
if (!input || readonly || disabled)
|
|
178
176
|
return;
|
|
179
|
-
if (RESET_TYPED_KEYS.has(event.key))
|
|
180
|
-
resetTyped();
|
|
181
177
|
// Tab уводит фокус из поля — не перехватываем.
|
|
182
178
|
if (event.key === 'Tab')
|
|
183
179
|
return;
|
|
@@ -192,13 +188,7 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
192
188
|
onArrowUp?.();
|
|
193
189
|
return;
|
|
194
190
|
}
|
|
195
|
-
// Копирование и вставку не блокируем: правку поля перехватит beforeinput.
|
|
196
|
-
if (event.ctrlKey || event.metaKey || event.altKey)
|
|
197
|
-
return;
|
|
198
191
|
onEdit?.();
|
|
199
|
-
// Экранные клавиатуры присылают `Unidentified` вместо цифры, её данные есть только в beforeinput.
|
|
200
|
-
if (/^\d$/.test(event.key) || event.key === 'Unidentified')
|
|
201
|
-
return;
|
|
202
192
|
if (event.key === 'Escape') {
|
|
203
193
|
event.preventDefault();
|
|
204
194
|
onEscape?.();
|
|
@@ -233,6 +223,11 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
233
223
|
if (event.key === 'Enter') {
|
|
234
224
|
tryToComplete();
|
|
235
225
|
emit();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (/^\d$/.test(event.key)) {
|
|
229
|
+
typeDigit(slot, event.key);
|
|
230
|
+
emit();
|
|
236
231
|
}
|
|
237
232
|
}, [
|
|
238
233
|
clearSlot,
|
|
@@ -248,13 +243,21 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
248
243
|
onEscape,
|
|
249
244
|
prevSlot,
|
|
250
245
|
readonly,
|
|
251
|
-
resetTyped,
|
|
252
246
|
selectSlot,
|
|
253
247
|
slotFromIndex,
|
|
254
248
|
tryToComplete,
|
|
249
|
+
typeDigit,
|
|
255
250
|
]);
|
|
256
|
-
|
|
257
|
-
|
|
251
|
+
// Вставка (Ctrl+V) полной строки даты/времени: вытаскиваем цифры и заполняем сегменты
|
|
252
|
+
// слева направо. Нативный onChange у поля игнорируется (движок controlled), поэтому без
|
|
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;
|
|
258
261
|
ensureMask();
|
|
259
262
|
let cursor = 0;
|
|
260
263
|
let lastFilled;
|
|
@@ -273,100 +276,7 @@ function useSegmentedMask({ inputRef, mask, slots, readonly, disabled, setValue,
|
|
|
273
276
|
else if (lastFilled) {
|
|
274
277
|
selectSlot(nextSlot(lastFilled));
|
|
275
278
|
}
|
|
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);
|
|
346
279
|
emit();
|
|
347
|
-
}, [
|
|
348
|
-
|
|
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
|
-
};
|
|
280
|
+
}, [disabled, emit, ensureMask, lastSlot, nextSlot, readonly, selectSlot, slots, tryToComplete, updateSlot]);
|
|
281
|
+
return { handleKeyDown, handleClick, handleFocus, handleBlur, handlePaste, selectSlot, firstSlot, lastSlot };
|
|
372
282
|
}
|
|
@@ -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
|
|
325
|
-
// Single/date-time ведёт сегментный движок (useSegmentedMask), нативный
|
|
326
|
-
// игнорируется. Range — обычный ввод-по-маске (formatMask в handleRangeInput).
|
|
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 ведёт сегментный движок (keydown, useSegmentedMask), нативный
|
|
326
|
+
// onChange игнорируется. 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,9 +10,8 @@ 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
|
-
*
|
|
15
|
-
* Пример: `39` для day → `09`, `13` для month → `03`.
|
|
13
|
+
* 2. **Clamp полной двойки**: если набрано две цифры и значение превышает `max` сегмента —
|
|
14
|
+
* значение прижимается к верхней границе. Пример: `55` для day → `31`, `99` для month → `12`.
|
|
16
15
|
*/
|
|
17
16
|
export declare function formatMask(input: string, mode: MaskMode, showSeconds?: boolean): string;
|
|
18
17
|
/** Парсит `DD.MM.YYYY` или `DD.MM.YYYY, HH:mm[:ss]` в `Date`. Возвращает `undefined` при невалидной/неполной строке. */
|