armtek-uikit-react 1.0.269 → 1.0.271

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.
@@ -9,12 +9,15 @@ import { AdornmentContainer } from "../../Adornment/Adornment.js";
9
9
  import { Popper } from "../../Popper/Popper.js";
10
10
  import "../../Popper/PopperBase.js";
11
11
  const DELIMITER = " - ";
12
+ const COMPLETE_DATE_REGEX = /^\d{2}\.\d{2}\.\d{4}$/;
13
+ const COMPLETE_PERIOD_REGEX = new RegExp(`\\d{2}\\.\\d{2}\\.\\d{4}${DELIMITER}\\d{2}\\.\\d{2}\\.\\d{4}`);
12
14
  const format = "dd.MM.yyyy";
13
15
  function getPeriodDisplay(date1, date2) {
14
16
  if (date1 && date2) return DateService(date1).format(format) + DELIMITER + DateService(date2).format(format);
15
17
  return "";
16
18
  }
17
19
  function validateDate(date, minDate, maxDate) {
20
+ if (!COMPLETE_DATE_REGEX.test(date)) return null;
18
21
  const startParts = date.split(".");
19
22
  const startTimeStamp = Date.parse(`${startParts[2]}-${startParts[1]}-${startParts[0]}`);
20
23
  if (isNaN(startTimeStamp) === false) {
@@ -25,6 +28,55 @@ function validateDate(date, minDate, maxDate) {
25
28
  }
26
29
  return null;
27
30
  }
31
+ function parseSerializedDateTuple(value) {
32
+ const boundaryIndex = value.indexOf("),");
33
+ if (boundaryIndex < 0) return null;
34
+ const leftPart = value.slice(0, boundaryIndex + 1).trim();
35
+ const rightPart = value.slice(boundaryIndex + 2).trim();
36
+ const leftDate = new Date(leftPart);
37
+ const rightDate = new Date(rightPart);
38
+ if (Number.isNaN(leftDate.getTime()) || Number.isNaN(rightDate.getTime()) || rightDate.getTime() < leftDate.getTime()) return null;
39
+ return [leftDate, rightDate];
40
+ }
41
+ function isBackspaceAtPeriodBoundary(prevValue, nextValue) {
42
+ return (prevValue.endsWith(".") || prevValue.endsWith(" ") || prevValue.endsWith("-")) && prevValue.length > nextValue.length;
43
+ }
44
+ function normalizePeriodInput(rawValue) {
45
+ return rawValue.replace(/[^\d -.]/g, "").replace(/^(\d{2})$/, "$1.").replace(/^(\d{2})(\d)$/, "$1.$2").replace(/^(\d{2})\.(\d{2})$/, "$1.$2.").replace(/^(\d{2})\.(\d{2})(\d)$/, "$1.$2.$3").replace(/^(\d{2})\.(\d{2})\.(\d{4})$/, "$1.$2.$3" + DELIMITER).replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})$`), `$1.$2.$3${DELIMITER}$4.`).replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})(\\d)$`), `$1.$2.$3${DELIMITER}$4.$5`).replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})\\.(\\d{2})$`), "$1.$2.$3 - $4.$5.").replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})\\.(\\d{2})(\\d)$`), "$1.$2.$3 - $4.$5.$6").replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})\\.(\\d{2})\\.(\\d{4})$`), "$1.$2.$3 - $4.$5.$6").replace(new RegExp(`^(\\d{2})\\.(\\d{2})\\.(\\d{4})${DELIMITER}(\\d{2})\\.(\\d{2})\\.(\\d{4})(\\d)$`), "$1.$2.$3 - $4.$5.$6");
46
+ }
47
+ function parsePeriodInputForChange(value, minDate, maxDate) {
48
+ const valueParts = value.split(DELIMITER);
49
+ let startDateValid = null;
50
+ let endDateValid = null;
51
+ if (COMPLETE_DATE_REGEX.test(valueParts[0])) {
52
+ startDateValid = validateDate(valueParts[0], minDate, maxDate);
53
+ if (!startDateValid) return {
54
+ type: "invalid-start"
55
+ };
56
+ }
57
+ if (COMPLETE_PERIOD_REGEX.test(value) && valueParts.length === 2) {
58
+ endDateValid = validateDate(valueParts[1], minDate, maxDate);
59
+ if (!endDateValid) return {
60
+ type: "invalid-end"
61
+ };
62
+ }
63
+ if (startDateValid && endDateValid && startDateValid.getTime() <= endDateValid.getTime()) return {
64
+ type: "complete",
65
+ value: [startDateValid, endDateValid]
66
+ };
67
+ return {
68
+ type: "partial"
69
+ };
70
+ }
71
+ function parseStrictPeriodValue(value, minDate, maxDate) {
72
+ const valueParts = value.split(DELIMITER);
73
+ if (valueParts.length !== 2) return null;
74
+ const startDateValid = validateDate(valueParts[0], minDate, maxDate);
75
+ const endDateValid = validateDate(valueParts[1], minDate, maxDate);
76
+ if (!startDateValid || !endDateValid) return null;
77
+ if (endDateValid.getTime() < startDateValid.getTime()) return null;
78
+ return [startDateValid, endDateValid];
79
+ }
28
80
  const Period = forwardRef((props, ref) => {
29
81
  const {
30
82
  onChange,
@@ -45,11 +97,56 @@ const Period = forwardRef((props, ref) => {
45
97
  const displayText = defaultValue ? getPeriodDisplay(defaultValue[0], defaultValue[1]) : "";
46
98
  const [textValue, setTextValue] = useState(displayText);
47
99
  const btnRef = useRef();
100
+ const inputRef = useRef(null);
101
+ const didSyncFromRefValueRef = useRef(false);
102
+ const datesEqual = (left, right) => {
103
+ var _a, _b, _c, _d;
104
+ return (((_a = left[0]) == null ? void 0 : _a.getTime()) || 0) === (((_b = right[0]) == null ? void 0 : _b.getTime()) || 0) && (((_c = left[1]) == null ? void 0 : _c.getTime()) || 0) === (((_d = right[1]) == null ? void 0 : _d.getTime()) || 0);
105
+ };
106
+ const setInputRef = (node) => {
107
+ inputRef.current = node;
108
+ if (typeof ref === "function") ref(node);
109
+ else if (ref) ref.current = node;
110
+ if (node && !Array.isArray(props.value) && !didSyncFromRefValueRef.current) {
111
+ syncFromInputValue(node.value);
112
+ didSyncFromRefValueRef.current = true;
113
+ }
114
+ };
48
115
  const [startValue, endValue] = value || [];
49
116
  useEffect(() => {
50
117
  if (startValue && endValue) setTextValue(getPeriodDisplay(startValue, endValue));
51
118
  else if (startValue === null && endValue === null) setTextValue("");
52
119
  }, [startValue, endValue]);
120
+ const syncFromInputValue = (nextInputValue) => {
121
+ if (Array.isArray(props.value)) return;
122
+ if (!nextInputValue) {
123
+ setTextValue((prev) => prev === "" ? prev : "");
124
+ setCurrentValue((prev) => datesEqual(prev, [null, null]) ? prev : [null, null]);
125
+ return;
126
+ }
127
+ const valueParts = nextInputValue.split(DELIMITER);
128
+ if (valueParts.length !== 2) {
129
+ const serializedDates = parseSerializedDateTuple(nextInputValue);
130
+ if (!serializedDates) return;
131
+ const normalizedText2 = getPeriodDisplay(serializedDates[0], serializedDates[1]);
132
+ setCurrentValue((prev) => datesEqual(prev, serializedDates) ? prev : serializedDates);
133
+ setTextValue((prev) => prev === normalizedText2 ? prev : normalizedText2);
134
+ return;
135
+ }
136
+ const startDate = validateDate(valueParts[0], minDate, maxDate);
137
+ const endDate = validateDate(valueParts[1], minDate, maxDate);
138
+ if (!startDate || !endDate || endDate.getTime() < startDate.getTime()) {
139
+ setTextValue((prev) => prev === nextInputValue ? prev : nextInputValue);
140
+ setCurrentValue((prev) => datesEqual(prev, [null, null]) ? prev : [null, null]);
141
+ return;
142
+ }
143
+ const normalizedText = getPeriodDisplay(startDate, endDate);
144
+ setCurrentValue((prev) => {
145
+ const nextPeriod = [startDate, endDate];
146
+ return datesEqual(prev, nextPeriod) ? prev : nextPeriod;
147
+ });
148
+ setTextValue((prev) => prev === normalizedText ? prev : normalizedText);
149
+ };
53
150
  const handleClick = (e) => {
54
151
  e.stopPropagation();
55
152
  setActive((prev) => !prev);
@@ -79,43 +176,26 @@ const Period = forwardRef((props, ref) => {
79
176
  };
80
177
  const handleChange = (e) => {
81
178
  if (disabled) return;
82
- let value2 = e.target.value;
83
- const bs = (textValue.endsWith(".") || textValue.endsWith(" ") || textValue.endsWith("-")) && textValue.length > value2.length;
84
- if (!bs) {
85
- value2 = value2.replace(/[^\d -.]/g, "").replace(/^(\d{2})$/, "$1.").replace(/^(\d{2})(\d)$/, "$1.$2").replace(/^(\d{2})\.(\d{2})$/, "$1.$2.").replace(/^(\d{2})\.(\d{2})(\d)$/, "$1.$2.$3").replace(/^(\d{2})\.(\d{2})\.(\d{4})$/, "$1.$2.$3" + DELIMITER).replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2})$`), `$1.$2.$3${DELIMITER}$4.`).replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2})(d)$`), `$1.$2.$3${DELIMITER}$4.$5`).replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2}).(\\d{2})$`), "$1.$2.$3 - $4.$5.").replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2}).(\\d{2})(d)$`), "$1.$2.$3 - $4.$5.$6").replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2}).(\\d{2}).(\\d{4})$`), "$1.$2.$3 - $4.$5.$6").replace(new RegExp(`^(\\d{2}).(\\d{2}).(\\d{4})${DELIMITER}(\\d{2}).(\\d{2}).(\\d{4})(\\d)$`), "$1.$2.$3 - $4.$5.$6");
86
- }
87
- let startDateValid = null;
88
- let endDateValid = null;
89
- const valueParts = value2.split(DELIMITER);
90
- if (valueParts[0].match(/^\d{2}\.\d{2}\.\d{4}$/)) {
91
- startDateValid = validateDate(valueParts[0], minDate, maxDate);
92
- if (!startDateValid) {
93
- setTextValue("");
94
- handleSelect([null, null]);
95
- return;
96
- }
97
- }
179
+ const rawValue = e.target.value;
180
+ const value2 = isBackspaceAtPeriodBoundary(textValue, rawValue) ? rawValue : normalizePeriodInput(rawValue);
98
181
  setTextValue(value2);
99
182
  setCurrentValue([null, null]);
100
- if (value2.match(new RegExp(`\\d{2}\\.\\d{2}\\.\\d{4}${DELIMITER}\\d{2}\\.\\d{2}\\.\\d{4}`))) {
101
- if (valueParts.length === 2) {
102
- endDateValid = validateDate(valueParts[1], minDate, maxDate);
103
- if (!endDateValid) {
104
- setTextValue("");
105
- }
106
- }
183
+ const parseResult = parsePeriodInputForChange(value2, minDate, maxDate);
184
+ if (parseResult.type === "invalid-start") {
185
+ setTextValue("");
186
+ handleSelect([null, null]);
187
+ return;
107
188
  }
108
- if (startDateValid && endDateValid && startDateValid.getTime() <= endDateValid.getTime()) {
109
- handleSelect([startDateValid, endDateValid]);
189
+ if (parseResult.type === "invalid-end") {
190
+ setTextValue("");
191
+ return;
110
192
  }
193
+ if (parseResult.type === "complete") handleSelect(parseResult.value);
111
194
  };
112
195
  const handleBlur = (e) => {
113
196
  setActive(false);
114
- const value2 = e.target.value;
115
- const valueParts = value2.split(DELIMITER);
116
- const startDateValid = valueParts[0] ? validateDate(valueParts[0], minDate, maxDate) : null;
117
- const endDateValid = valueParts[1] ? validateDate(valueParts[1], minDate, maxDate) : null;
118
- if (!startDateValid || !endDateValid || startDateValid && endDateValid && endDateValid.getTime() < startDateValid.getTime()) {
197
+ const parsedPeriod = parseStrictPeriodValue(e.target.value, minDate, maxDate);
198
+ if (!parsedPeriod) {
119
199
  setTextValue("");
120
200
  handleSelect([null, null]);
121
201
  }
@@ -129,7 +209,7 @@ const Period = forwardRef((props, ref) => {
129
209
  /* @__PURE__ */ jsx(ButtonIcon, { "data-calendar-btn": true, size: "medium", "data-testid": "period-btn", variant: "transparent", color: "neutral", disabled, onClick: handleClick, children: /* @__PURE__ */ jsx("span", { className: "material_icon", ref: btnRef, children: "calendar_today" }) })
130
210
  ] });
131
211
  return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "arm-period", "data-testid": "period", children: [
132
- /* @__PURE__ */ jsx(TextField, { ref, placeholder: "дд.мм.гггг - дд.мм.гггг", ...restProps, disabled, value: textValue, onBlur: handleBlur, onChange: handleChange, onFocus: () => setActive(false), autoComplete: "off", endAdornment }),
212
+ /* @__PURE__ */ jsx(TextField, { ref: setInputRef, placeholder: "дд.мм.гггг - дд.мм.гггг", ...restProps, disabled, value: textValue, onBlur: handleBlur, onChange: handleChange, onFocus: () => setActive(false), autoComplete: "off", endAdornment }),
133
213
  active && /* @__PURE__ */ jsx(Popper, { anchorEl: btnRef.current, open: active, placement: "bottom-end", children: /* @__PURE__ */ jsx(Card, { className: "arm-period__picker", "data-testid": "period_card", children: /* @__PURE__ */ jsx(DatePicker, { onClickOutside: handleClickOutside, onChange: handleSelect, selectsRange: true, selected: realValue[0], startDate: realValue[0], endDate: realValue[1], minDate: minDate || void 0, maxDate: maxDate || void 0, showMonthDropdown, showYearDropdown, ...datePickerProps }) }) })
134
214
  ] }) });
135
215
  });
@@ -1,6 +1,6 @@
1
1
  import { jsx, Fragment, jsxs } from "react/jsx-runtime";
2
2
  import { TextField } from "../TextField/TextField.js";
3
- import { forwardRef, useState, useRef, useImperativeHandle, useMemo } from "react";
3
+ import { forwardRef, useState, useRef, useImperativeHandle, useLayoutEffect, useMemo } from "react";
4
4
  import clsx from "clsx";
5
5
  import useClickOutside from "../../../lib/hooks/useClickOutside.js";
6
6
  import ButtonIcon from "../../ButtonIcon/ButtonIcon.js";
@@ -37,6 +37,8 @@ function Select(props, ref) {
37
37
  beforeList,
38
38
  query,
39
39
  optionClassName,
40
+ onBlur: inputOnBlur,
41
+ onFocus: inputOnFocus,
40
42
  ...inputProps
41
43
  } = props;
42
44
  let [active, setActive] = useState(!!defaultOpen);
@@ -49,6 +51,40 @@ function Select(props, ref) {
49
51
  useImperativeHandle(ref, () => {
50
52
  return inputRef.current;
51
53
  }, []);
54
+ useLayoutEffect(() => {
55
+ var _a2;
56
+ if (value !== void 0 || multiple || search) return;
57
+ if (!inputRef.current) return;
58
+ const inputElement = inputRef.current;
59
+ const syncSelectedValue = (nextInputValue) => {
60
+ setSelected((prevSelected) => {
61
+ if (Array.isArray(prevSelected)) return prevSelected;
62
+ if (!nextInputValue) return prevSelected === "" ? prevSelected : "";
63
+ const optionExists = options.some((item) => getOptionValue(item) === nextInputValue);
64
+ if (!optionExists || prevSelected === nextInputValue) return prevSelected;
65
+ return nextInputValue;
66
+ });
67
+ };
68
+ syncSelectedValue(inputElement.value);
69
+ const inputValueDescriptor = Object.getOwnPropertyDescriptor(inputElement, "value");
70
+ if (!(inputValueDescriptor == null ? void 0 : inputValueDescriptor.get) || !(inputValueDescriptor == null ? void 0 : inputValueDescriptor.set)) return;
71
+ Object.defineProperty(inputElement, "value", {
72
+ configurable: true,
73
+ enumerable: (_a2 = inputValueDescriptor.enumerable) != null ? _a2 : true,
74
+ get() {
75
+ var _a3;
76
+ return (_a3 = inputValueDescriptor.get) == null ? void 0 : _a3.call(this);
77
+ },
78
+ set(nextValue) {
79
+ var _a3;
80
+ (_a3 = inputValueDescriptor.set) == null ? void 0 : _a3.call(this, nextValue);
81
+ syncSelectedValue(nextValue);
82
+ }
83
+ });
84
+ return () => {
85
+ Object.defineProperty(inputElement, "value", inputValueDescriptor);
86
+ };
87
+ }, [multiple, options, search, value]);
52
88
  const handleOpen = () => {
53
89
  if (open !== void 0) return;
54
90
  if (!inputProps.disabled) {
@@ -92,12 +128,14 @@ function Select(props, ref) {
92
128
  const handleSearch = (e) => {
93
129
  setQ(e.target.value);
94
130
  };
95
- const handleFocus = search ? () => {
131
+ const handleFocus = search ? (e) => {
96
132
  setFocused(true);
97
- } : void 0;
98
- const handleBlur = !!search ? () => {
133
+ if (inputOnFocus) inputOnFocus(e);
134
+ } : inputOnFocus;
135
+ const handleBlur = !!search ? (e) => {
99
136
  setFocused(false);
100
- } : void 0;
137
+ if (inputOnBlur) inputOnBlur(e);
138
+ } : inputOnBlur;
101
139
  const handleSelectAll = (e) => {
102
140
  if (props.disabled) return;
103
141
  let value2 = options.filter((item) => !item.disabled).map((item) => getOptionValue(item));
@@ -5,6 +5,7 @@ declare const TimeField: import("react").ForwardRefExoticComponent<{
5
5
  value?: Date | string | null;
6
6
  showTime?: boolean;
7
7
  showTimeOnly?: boolean;
8
+ preserveIncompleteTimeInput?: boolean;
8
9
  inputProps?: Omit<import("../TextField").TextFieldProps, "onChange" | "value"> & import("../../../types/theme").DataAttributes;
9
10
  showMonthYearPicker?: boolean;
10
11
  showYearPicker?: boolean;
@@ -21,6 +22,7 @@ declare const TimeField: import("react").ForwardRefExoticComponent<{
21
22
  value?: Date | string | null;
22
23
  showTime?: boolean;
23
24
  showTimeOnly?: boolean;
25
+ preserveIncompleteTimeInput?: boolean;
24
26
  inputProps?: Omit<import("../TextField").TextFieldProps, "onChange" | "value"> & import("../../../types/theme").DataAttributes;
25
27
  showMonthYearPicker?: boolean;
26
28
  showYearPicker?: boolean;
@@ -2,7 +2,7 @@ import { jsx, Fragment } from "react/jsx-runtime";
2
2
  import { DateField } from "../DateField/DateField.js";
3
3
  import { forwardRef } from "react";
4
4
  const TimeField = forwardRef((props, ref) => {
5
- return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(DateField, { ...props, showTime: true, ref, showTimeOnly: true }) });
5
+ return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(DateField, { ...props, showTime: true, ref, showTimeOnly: true, preserveIncompleteTimeInput: true }) });
6
6
  });
7
7
  export {
8
8
  TimeField as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "armtek-uikit-react",
3
- "version": "1.0.269",
3
+ "version": "1.0.271",
4
4
  "description": "Armtek UIKit for React",
5
5
  "main": "lib/cjs/index.cjs",
6
6
  "module": "lib/esm/index.js",
@@ -26,7 +26,7 @@
26
26
  "scripts": {
27
27
  "start": "storybook dev -p 3001 --no-open --config-dir ./config/storybook",
28
28
  "build:storybook": "storybook build --config-dir ./config/storybook",
29
- "clean:build": "node -e \"const fs=require('fs'); fs.rmSync('lib',{recursive:true,force:true}); fs.rmSync('types',{recursive:true,force:true});\"",
29
+ "clean:build": "node -e \"const fs=require('fs'); const path=require('path'); const rmOpts={recursive:true,force:true,maxRetries:10,retryDelay:200}; const clean=(dir)=>{ if(!fs.existsSync(dir)) return; for (const entry of fs.readdirSync(dir)) fs.rmSync(path.join(dir, entry), rmOpts); }; clean('lib'); clean('types');\"",
30
30
  "build:esm": "vite build --config vite.config.ts --mode esm",
31
31
  "build:cjs": "vite build --config vite.config.ts --mode cjs",
32
32
  "build": "npm run clean:build && npm run build-types && npm run build:esm && npm run build:cjs && node config/postbuild-vite.mjs",
@@ -74,11 +74,11 @@
74
74
  "eslint-plugin-jsx-a11y": "^6.7.1",
75
75
  "eslint-plugin-react": "^7.33.1",
76
76
  "eslint-plugin-react-hooks": "^4.6.0",
77
- "eslint-plugin-storybook": "^9.1.7",
78
77
  "fs-extra": "^11.1.1",
79
78
  "identity-obj-proxy": "^3.0.0",
80
79
  "jsdom": "^26.1.0",
81
80
  "prettier": "^3.0.1",
81
+ "react-hook-form": "^7.73.1",
82
82
  "sass": "^1.84.0",
83
83
  "storybook": "^10.3.5",
84
84
  "ts-node": "^10.9.1",