@xsolla/xui-date-picker 0.225.0 → 0.227.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 +6 -0
- package/native/index.js +83 -33
- package/native/index.js.map +1 -1
- package/native/index.mjs +83 -33
- package/native/index.mjs.map +1 -1
- package/package.json +6 -6
- package/web/index.js +83 -33
- package/web/index.js.map +1 -1
- package/web/index.mjs +83 -33
- package/web/index.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -197,6 +197,12 @@ While the field is focused or partly filled it shows a mask built from the forma
|
|
|
197
197
|
|
|
198
198
|
**Not supported:** textual and localised tokens (`MMM`, `MMMM`, `P`, `PP`), time tokens (`HH:mm`), escaped literals (`'de'`), and formats missing one of the three segments (`MM/yyyy` — the field could never be completed, so `onChange` would never fire). These warn in development and fall back to `MM/dd/yyyy`, so the field stays completable and never renders raw token text as though it were a value. To display a date as `Mar 15, 2026`, format it yourself with `formatDate` outside the picker.
|
|
199
199
|
|
|
200
|
+
### Typed text
|
|
201
|
+
|
|
202
|
+
A typed date reaches `onChange` when the field loses focus or the user presses Enter, whichever way the text arrived. On web, key presses edit one segment at a time. Text can also arrive through the input's change event: React Native typing, mobile virtual keyboards, browser autofill, dictation, or `fill()` in end-to-end tests. That text is parsed as a whole in `dateFormat` (range mode: `start - end`).
|
|
203
|
+
|
|
204
|
+
A typed date outside `minDate`/`maxDate`, or text that does not parse, is **not** sent to `onChange`. The field keeps the text and shows an error (*"Date must be within the allowed range"* or *"Enter a valid date (MM/dd/yyyy)"*), so it never shows a value your form did not receive. Text emptied before blur falls back to the current value.
|
|
205
|
+
|
|
200
206
|
### `<Calendar>`
|
|
201
207
|
|
|
202
208
|
Re-exported from `@xsolla/xui-calendar`. Standalone single-month calendar.
|
package/native/index.js
CHANGED
|
@@ -615,6 +615,7 @@ var getFixedPlacementStyle = (side, align, container, panel, viewport, offset) =
|
|
|
615
615
|
const vertical = align === "center" ? { top: (container.top + container.bottom) / 2 - panel.height / 2 } : align === "end" ? { bottom: viewport.height - container.bottom } : { top: container.top };
|
|
616
616
|
return { position: "fixed", ...horizontal, ...vertical };
|
|
617
617
|
};
|
|
618
|
+
var isSegmentKey = (key) => /^[0-9]$/.test(key) || key === "Backspace" || key === "ArrowUp" || key === "ArrowDown" || key === "ArrowLeft" || key === "ArrowRight";
|
|
618
619
|
var DatePicker = (0, import_react.forwardRef)(
|
|
619
620
|
({
|
|
620
621
|
onChange,
|
|
@@ -679,6 +680,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
679
680
|
const [activeSegment, setActiveSegment] = (0, import_react.useState)(null);
|
|
680
681
|
const [internalChip, setInternalChip] = (0, import_react.useState)(null);
|
|
681
682
|
const [parseError, setParseError] = (0, import_react.useState)(null);
|
|
683
|
+
const [rawText, setRawText] = (0, import_react.useState)(null);
|
|
682
684
|
const [fixedPosition, setFixedPosition] = (0, import_react.useState)(
|
|
683
685
|
null
|
|
684
686
|
);
|
|
@@ -755,7 +757,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
755
757
|
(g) => g.day != null || g.month != null || g.year != null
|
|
756
758
|
);
|
|
757
759
|
const showMask = hasAnyFilled || focused && isWeb;
|
|
758
|
-
const inputValue = chipLabel ?? (showMask ? composed.text : "");
|
|
760
|
+
const inputValue = rawText ?? chipLabel ?? (showMask ? composed.text : "");
|
|
759
761
|
const showClear = clearable && editable && (hasAnyFilled || !!chipLabel);
|
|
760
762
|
const orderedSegments = () => composeField().segments;
|
|
761
763
|
const firstSegmentKey = () => {
|
|
@@ -824,6 +826,60 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
824
826
|
commitPart(edit.group, edit.type, Number(edit.buffer));
|
|
825
827
|
setEdit(null);
|
|
826
828
|
};
|
|
829
|
+
const parseOne = (token) => {
|
|
830
|
+
const trimmed = token.trim();
|
|
831
|
+
if (!trimmed) return null;
|
|
832
|
+
const parsed = (0, import_date_fns.parse)(trimmed, resolvedFormat, /* @__PURE__ */ new Date());
|
|
833
|
+
return (0, import_date_fns.isValid)(parsed) ? parsed : null;
|
|
834
|
+
};
|
|
835
|
+
const parseFieldText = (text) => {
|
|
836
|
+
if (isRange) {
|
|
837
|
+
const [startText, endText] = text.split(RANGE_SEPARATOR);
|
|
838
|
+
const start = parseOne(startText ?? "");
|
|
839
|
+
const end = parseOne(endText ?? "");
|
|
840
|
+
if (!start || endText?.trim() && !end) {
|
|
841
|
+
return {
|
|
842
|
+
error: `Enter a valid date range (${resolvedFormat} - ${resolvedFormat})`
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
return { parts: [datePartsFromDate(start), datePartsFromDate(end)] };
|
|
846
|
+
}
|
|
847
|
+
const date = parseOne(text);
|
|
848
|
+
if (!date) return { error: `Enter a valid date (${resolvedFormat})` };
|
|
849
|
+
return { parts: [datePartsFromDate(date)] };
|
|
850
|
+
};
|
|
851
|
+
const isOutOfBounds = (parts) => parts.map(datePartsToDate).some((d) => d instanceof Date && !isWithinBounds(d));
|
|
852
|
+
const commitRawText = (text) => {
|
|
853
|
+
setEdit(null);
|
|
854
|
+
if (!text.trim()) {
|
|
855
|
+
setRawText(null);
|
|
856
|
+
setParseError(null);
|
|
857
|
+
setGroups(propGroups());
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const result = parseFieldText(text);
|
|
861
|
+
if ("error" in result) {
|
|
862
|
+
setParseError(result.error);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (isOutOfBounds(result.parts)) {
|
|
866
|
+
setParseError("Date must be within the allowed range");
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
setRawText(null);
|
|
870
|
+
setParseError(null);
|
|
871
|
+
setGroups(result.parts);
|
|
872
|
+
emit(result.parts);
|
|
873
|
+
};
|
|
874
|
+
const handleChangeText = (text) => {
|
|
875
|
+
rest.onChangeText?.(text);
|
|
876
|
+
if (!editable) return;
|
|
877
|
+
markDirty();
|
|
878
|
+
setEdit(null);
|
|
879
|
+
setParseError(null);
|
|
880
|
+
setActiveSegment(null);
|
|
881
|
+
setRawText(text);
|
|
882
|
+
};
|
|
827
883
|
const defaultPart = (type) => {
|
|
828
884
|
const now = /* @__PURE__ */ new Date();
|
|
829
885
|
if (type === "day") return now.getDate();
|
|
@@ -916,10 +972,12 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
916
972
|
}
|
|
917
973
|
if (!isWeb || !editable) return;
|
|
918
974
|
if (e.key === "Enter") {
|
|
919
|
-
|
|
975
|
+
if (rawText !== null) commitRawText(rawText);
|
|
976
|
+
else commitBuffer();
|
|
920
977
|
setOpen(false);
|
|
921
978
|
return;
|
|
922
979
|
}
|
|
980
|
+
if (rawText !== null && isSegmentKey(e.key)) setRawText(null);
|
|
923
981
|
if (e.key === "ArrowUp") return handleArrowStep(e, 1);
|
|
924
982
|
if (e.key === "ArrowDown") return handleArrowStep(e, -1);
|
|
925
983
|
if (e.key === "ArrowLeft") return handleArrowMove(e, -1);
|
|
@@ -972,30 +1030,33 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
972
1030
|
setParseError(null);
|
|
973
1031
|
return;
|
|
974
1032
|
}
|
|
1033
|
+
if (rawText !== null) {
|
|
1034
|
+
commitRawText(rawText);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
975
1037
|
let next = groups;
|
|
1038
|
+
let rejected = false;
|
|
976
1039
|
if (edit) {
|
|
977
1040
|
const clamped = clampDatePart(
|
|
978
1041
|
edit.type,
|
|
979
1042
|
Number(edit.buffer),
|
|
980
1043
|
groups[edit.group]
|
|
981
1044
|
);
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
)
|
|
1045
|
+
const edited = { ...groups[edit.group], [edit.type]: clamped };
|
|
1046
|
+
const assembled = datePartsToDate(edited);
|
|
1047
|
+
if (assembled && !isWithinBounds(assembled)) {
|
|
1048
|
+
rejected = true;
|
|
1049
|
+
} else {
|
|
1050
|
+
next = groups.map((g, i) => i === edit.group ? edited : g);
|
|
1051
|
+
}
|
|
985
1052
|
}
|
|
986
1053
|
const props = propGroups();
|
|
987
1054
|
next = next.map((g, i) => isDatePartsComplete(g) ? g : props[i]);
|
|
988
1055
|
setEdit(null);
|
|
989
|
-
setParseError(null);
|
|
1056
|
+
setParseError(rejected ? "Date must be within the allowed range" : null);
|
|
990
1057
|
setGroups(next);
|
|
991
1058
|
emit(next);
|
|
992
1059
|
};
|
|
993
|
-
const parseOne = (token) => {
|
|
994
|
-
const trimmed = token.trim();
|
|
995
|
-
if (!trimmed) return null;
|
|
996
|
-
const parsed = (0, import_date_fns.parse)(trimmed, resolvedFormat, /* @__PURE__ */ new Date());
|
|
997
|
-
return (0, import_date_fns.isValid)(parsed) ? parsed : null;
|
|
998
|
-
};
|
|
999
1060
|
const handlePaste = (e) => {
|
|
1000
1061
|
onPasteProp?.(e);
|
|
1001
1062
|
if (e.defaultPrevented) return;
|
|
@@ -1003,34 +1064,20 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
1003
1064
|
const text = e.clipboardData?.getData("text") ?? "";
|
|
1004
1065
|
if (!text.trim()) return;
|
|
1005
1066
|
e.preventDefault();
|
|
1006
|
-
|
|
1007
|
-
if (
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
const end = parseOne(endText ?? "");
|
|
1011
|
-
if (!start) {
|
|
1012
|
-
setParseError(
|
|
1013
|
-
`Enter a valid date range (${resolvedFormat} - ${resolvedFormat})`
|
|
1014
|
-
);
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1017
|
-
next = [datePartsFromDate(start), datePartsFromDate(end)];
|
|
1018
|
-
} else {
|
|
1019
|
-
const date = parseOne(text);
|
|
1020
|
-
if (!date) {
|
|
1021
|
-
setParseError(`Enter a valid date (${resolvedFormat})`);
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
next = [datePartsFromDate(date)];
|
|
1067
|
+
const result = parseFieldText(text);
|
|
1068
|
+
if ("error" in result) {
|
|
1069
|
+
setParseError(result.error);
|
|
1070
|
+
return;
|
|
1025
1071
|
}
|
|
1026
|
-
const
|
|
1027
|
-
if (
|
|
1072
|
+
const next = result.parts;
|
|
1073
|
+
if (isOutOfBounds(next)) {
|
|
1028
1074
|
setParseError("Date must be within the allowed range");
|
|
1029
1075
|
return;
|
|
1030
1076
|
}
|
|
1031
1077
|
markDirty();
|
|
1032
1078
|
setParseError(null);
|
|
1033
1079
|
setEdit(null);
|
|
1080
|
+
setRawText(null);
|
|
1034
1081
|
setGroups(next);
|
|
1035
1082
|
emit(next);
|
|
1036
1083
|
};
|
|
@@ -1045,6 +1092,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
1045
1092
|
sessionRef.current.cleared = true;
|
|
1046
1093
|
setGroups(next);
|
|
1047
1094
|
setEdit(null);
|
|
1095
|
+
setRawText(null);
|
|
1048
1096
|
setParseError(null);
|
|
1049
1097
|
setInternalChip(null);
|
|
1050
1098
|
onChipSelect?.(null);
|
|
@@ -1064,6 +1112,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
1064
1112
|
sessionRef.current = { dirty: false, cleared: false, propKey };
|
|
1065
1113
|
setGroups(propGroups());
|
|
1066
1114
|
setEdit(null);
|
|
1115
|
+
setRawText(null);
|
|
1067
1116
|
setParseError(null);
|
|
1068
1117
|
}, [propKey]);
|
|
1069
1118
|
(0, import_react.useEffect)(() => {
|
|
@@ -1261,6 +1310,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
1261
1310
|
onKeyDown: handleKeyDown,
|
|
1262
1311
|
onClick: handleFieldClick,
|
|
1263
1312
|
onPaste: handlePaste,
|
|
1313
|
+
onChangeText: handleChangeText,
|
|
1264
1314
|
iconRight,
|
|
1265
1315
|
iconLeft,
|
|
1266
1316
|
extraClear: showClear,
|