@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 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
- commitBuffer();
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
- next = groups.map(
983
- (g, i) => i === edit.group ? { ...g, [edit.type]: clamped } : g
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
- let next;
1007
- if (isRange) {
1008
- const [startText, endText] = text.split(RANGE_SEPARATOR);
1009
- const start = parseOne(startText ?? "");
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 outOfBounds = next.map(datePartsToDate).some((d) => d instanceof Date && !isWithinBounds(d));
1027
- if (outOfBounds) {
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,