@xsolla/xui-date-picker 0.206.0 → 0.206.2

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/web/index.mjs CHANGED
@@ -4,7 +4,8 @@ import {
4
4
  DualCalendar as DualCalendar2,
5
5
  CalendarHeader,
6
6
  CalendarGrid,
7
- CalendarChips
7
+ CalendarChips,
8
+ formatDate
8
9
  } from "@xsolla/xui-calendar";
9
10
 
10
11
  // src/DatePicker.tsx
@@ -288,15 +289,6 @@ import { Calendar, DualCalendar } from "@xsolla/xui-calendar";
288
289
  import { Calendar as CalendarIcon } from "@xsolla/xui-icons-base";
289
290
 
290
291
  // src/utils.ts
291
- import { format } from "date-fns";
292
- import * as locales from "date-fns/locale";
293
- var defaultLocale = "enUS";
294
- function formatDate(date, formatStr, locale = defaultLocale) {
295
- const localeObj = locales[locale] || locales[defaultLocale];
296
- return format(date, formatStr, {
297
- locale: localeObj
298
- });
299
- }
300
292
  var SEGMENT_WIDTH = {
301
293
  day: 2,
302
294
  month: 2,
@@ -307,21 +299,109 @@ var SEGMENT_TOKEN = {
307
299
  month: "mm",
308
300
  year: "yyyy"
309
301
  };
310
- function parseDateFormat(format2) {
302
+ var DEFAULT_DATE_FORMAT = "MM/dd/yyyy";
303
+ var SEGMENT_LETTER = {
304
+ d: "day",
305
+ M: "month",
306
+ y: "year"
307
+ };
308
+ var CASING_REPAIRS = { D: "d", Y: "y", m: "M" };
309
+ var SUPPORTED_TOKENS_HINT = "supported tokens are `d`/`dd` (day), `M`/`MM` (month) and `y`/`yyyy` (year); any other character is a literal separator. Textual and localised tokens (`MMM`, `MMMM`, `P`, `PP`) are not supported \u2014 the field renders numeric segments only.";
310
+ var ALL_SEGMENTS = ["day", "month", "year"];
311
+ var isLetter = (c) => /[A-Za-z]/.test(c);
312
+ var isSupportedRun = (letter, length) => {
313
+ if (letter === "y") return true;
314
+ if (letter === "d" || letter === "M") return length <= 2;
315
+ return false;
316
+ };
317
+ function analyzeDateFormat(format, fallback = DEFAULT_DATE_FORMAT) {
318
+ const issues = [];
319
+ const used = /* @__PURE__ */ new Set();
320
+ for (let i2 = 0; i2 < format.length; i2++) {
321
+ if (isLetter(format[i2])) used.add(format[i2]);
322
+ }
323
+ let repaired = "";
324
+ let hasUnsupported = false;
325
+ let i = 0;
326
+ while (i < format.length) {
327
+ const c = format[i];
328
+ if (!isLetter(c)) {
329
+ repaired += c;
330
+ i++;
331
+ continue;
332
+ }
333
+ let end = i;
334
+ while (end < format.length && format[end] === c) end++;
335
+ const run = format.slice(i, end);
336
+ i = end;
337
+ if (isSupportedRun(c, run.length)) {
338
+ repaired += run;
339
+ continue;
340
+ }
341
+ const repair = CASING_REPAIRS[c];
342
+ if (repair && !used.has(repair) && isSupportedRun(repair, run.length)) {
343
+ const fixed = repair.repeat(run.length);
344
+ issues.push({ token: run, kind: "casing", suggestion: fixed });
345
+ repaired += fixed;
346
+ continue;
347
+ }
348
+ issues.push({ token: run, kind: "unsupported" });
349
+ hasUnsupported = true;
350
+ repaired += run;
351
+ }
352
+ const present = new Set(
353
+ parseDateFormat(repaired).filter(
354
+ (t) => t.kind === "segment"
355
+ ).map((t) => t.type)
356
+ );
357
+ const missing = ALL_SEGMENTS.filter((type) => !present.has(type));
358
+ for (const type of missing) issues.push({ token: type, kind: "missing" });
359
+ const usable = !hasUnsupported && missing.length === 0;
360
+ return {
361
+ format: usable ? repaired : fallback,
362
+ usedFallback: !usable,
363
+ issues
364
+ };
365
+ }
366
+ function describeDateFormatIssues(format, analysis) {
367
+ const clauses = analysis.issues.map((issue) => {
368
+ if (issue.kind === "missing") {
369
+ return `it has no ${issue.token} segment, so the field can never be completed`;
370
+ }
371
+ if (issue.kind === "casing") {
372
+ return `\`${issue.token}\` is not a \`date-fns\` date token \u2014 use \`${issue.suggestion}\``;
373
+ }
374
+ return `\`${issue.token}\` is not a supported token`;
375
+ });
376
+ const resolution = analysis.usedFallback ? `Falling back to \`${analysis.format}\`.` : `Reading it as \`${analysis.format}\`.`;
377
+ return `[DatePicker] Invalid \`dateFormat\` \`${format}\`: ${clauses.join("; ")}. ${resolution} Note ${SUPPORTED_TOKENS_HINT}`;
378
+ }
379
+ var warnedFormats = /* @__PURE__ */ new Set();
380
+ function resolveDateFormat(format, fallback = DEFAULT_DATE_FORMAT) {
381
+ const analysis = analyzeDateFormat(format, fallback);
382
+ if (analysis.issues.length > 0) {
383
+ const key = `${format}|${fallback}`;
384
+ const isProduction = globalThis.process?.env?.NODE_ENV === "production";
385
+ if (!isProduction && !warnedFormats.has(key)) {
386
+ warnedFormats.add(key);
387
+ console.warn(describeDateFormatIssues(format, analysis));
388
+ }
389
+ }
390
+ return analysis.format;
391
+ }
392
+ function parseDateFormat(format) {
311
393
  const tokens = [];
312
394
  let i = 0;
313
- while (i < format2.length) {
314
- const c = format2[i];
315
- if (c === "d" || c === "M" || c === "y") {
316
- while (i < format2.length && format2[i] === c) i++;
317
- tokens.push({
318
- kind: "segment",
319
- type: c === "d" ? "day" : c === "M" ? "month" : "year"
320
- });
395
+ while (i < format.length) {
396
+ const c = format[i];
397
+ const type = SEGMENT_LETTER[c];
398
+ if (type) {
399
+ while (i < format.length && format[i] === c) i++;
400
+ tokens.push({ kind: "segment", type });
321
401
  } else {
322
402
  let text = "";
323
- while (i < format2.length && !"dMy".includes(format2[i])) {
324
- text += format2[i];
403
+ while (i < format.length && !SEGMENT_LETTER[format[i]]) {
404
+ text += format[i];
325
405
  i++;
326
406
  }
327
407
  tokens.push({ kind: "literal", text });
@@ -365,8 +445,8 @@ function stepDatePart(type, current, delta, parts) {
365
445
  const dim = daysInMonth(year, monthIndex);
366
446
  return ((current - 1 + delta) % dim + dim) % dim + 1;
367
447
  }
368
- function buildSegmentedValue(format2, parts, active) {
369
- const tokens = parseDateFormat(format2);
448
+ function buildSegmentedValue(format, parts, active) {
449
+ const tokens = parseDateFormat(format);
370
450
  let text = "";
371
451
  const ranges = [];
372
452
  for (const token of tokens) {
@@ -407,14 +487,14 @@ function isSegmentComplete(type, buffer) {
407
487
  function daysInMonth(year, monthIndex) {
408
488
  return new Date(year, monthIndex + 1, 0).getDate();
409
489
  }
410
- function getDateSegments(format2, value) {
490
+ function getDateSegments(format, value) {
411
491
  const segments = [];
412
492
  let fi = 0;
413
493
  let vi = 0;
414
- while (fi < format2.length && vi < value.length) {
415
- const fc = format2[fi];
494
+ while (fi < format.length && vi < value.length) {
495
+ const fc = format[fi];
416
496
  if (fc === "d" || fc === "M" || fc === "y") {
417
- while (fi < format2.length && format2[fi] === fc) fi++;
497
+ while (fi < format.length && format[fi] === fc) fi++;
418
498
  const start = vi;
419
499
  while (vi < value.length && value[vi] >= "0" && value[vi] <= "9") vi++;
420
500
  if (vi > start) {
@@ -459,7 +539,6 @@ function stepDateSegment(date, type, delta) {
459
539
  // src/DatePicker.tsx
460
540
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
461
541
  var PLACEMENT_OFFSET = 4;
462
- var DEFAULT_DATE_FORMAT = "MM/dd/yyyy";
463
542
  var RANGE_SEPARATOR = /\s*[-–—]\s*/;
464
543
  var RANGE_DISPLAY_SEP = " - ";
465
544
  var CalendarFieldIcon = () => /* @__PURE__ */ jsx2(CalendarIcon, { size: "100%" });
@@ -561,6 +640,10 @@ var DatePicker = forwardRef(
561
640
  const editable = !readOnly && !rest.disabled;
562
641
  const isDual = variant === "dual";
563
642
  const isRange = isDual || selectsRange;
643
+ const resolvedFormat = useMemo(
644
+ () => resolveDateFormat(dateFormat),
645
+ [dateFormat]
646
+ );
564
647
  const propGroups = useCallback(
565
648
  () => isRange ? [datePartsFromDate(startDate), datePartsFromDate(endDate)] : [datePartsFromDate(selectedDate)],
566
649
  [isRange, startDate, endDate, selectedDate]
@@ -608,7 +691,7 @@ var DatePicker = forwardRef(
608
691
  };
609
692
  const composeField = () => {
610
693
  const build = (group) => buildSegmentedValue(
611
- dateFormat,
694
+ resolvedFormat,
612
695
  groups[group] ?? { day: null, month: null, year: null },
613
696
  edit && edit.group === group ? { type: edit.type, buffer: edit.buffer } : void 0
614
697
  );
@@ -855,7 +938,7 @@ var DatePicker = forwardRef(
855
938
  const parseOne = (token) => {
856
939
  const trimmed = token.trim();
857
940
  if (!trimmed) return null;
858
- const parsed = parse(trimmed, dateFormat, /* @__PURE__ */ new Date());
941
+ const parsed = parse(trimmed, resolvedFormat, /* @__PURE__ */ new Date());
859
942
  return isValid(parsed) ? parsed : null;
860
943
  };
861
944
  const handlePaste = (e) => {
@@ -872,7 +955,7 @@ var DatePicker = forwardRef(
872
955
  const end = parseOne(endText ?? "");
873
956
  if (!start) {
874
957
  setParseError(
875
- `Enter a valid date range (${dateFormat} - ${dateFormat})`
958
+ `Enter a valid date range (${resolvedFormat} - ${resolvedFormat})`
876
959
  );
877
960
  return;
878
961
  }
@@ -880,7 +963,7 @@ var DatePicker = forwardRef(
880
963
  } else {
881
964
  const date = parseOne(text);
882
965
  if (!date) {
883
- setParseError(`Enter a valid date (${dateFormat})`);
966
+ setParseError(`Enter a valid date (${resolvedFormat})`);
884
967
  return;
885
968
  }
886
969
  next = [datePartsFromDate(date)];
@@ -1076,7 +1159,7 @@ var DatePicker = forwardRef(
1076
1159
  ...rest,
1077
1160
  ref: inputRef,
1078
1161
  size,
1079
- placeholder: placeholder || (isRange ? "Enter a date range" : "Choose a date"),
1162
+ placeholder: placeholder || (isRange ? "Choose a date range" : "Choose a date"),
1080
1163
  value: inputValue,
1081
1164
  readOnly,
1082
1165
  onFocus: handleFieldFocus,
@@ -1123,18 +1206,22 @@ export {
1123
1206
  CalendarChips,
1124
1207
  CalendarGrid,
1125
1208
  CalendarHeader,
1209
+ DEFAULT_DATE_FORMAT,
1126
1210
  DatePicker,
1127
1211
  DualCalendar2 as DualCalendar,
1212
+ analyzeDateFormat,
1128
1213
  buildSegmentedValue,
1129
1214
  clampDatePart,
1130
1215
  datePartsFromDate,
1131
1216
  datePartsToDate,
1132
1217
  daysInMonth,
1218
+ describeDateFormatIssues,
1133
1219
  formatDate,
1134
1220
  getDateSegments,
1135
1221
  isDatePartsComplete,
1136
1222
  isSegmentComplete,
1137
1223
  parseDateFormat,
1224
+ resolveDateFormat,
1138
1225
  stepDatePart,
1139
1226
  stepDateSegment
1140
1227
  };