@xsolla/xui-date-picker 0.206.0 → 0.206.1

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/native/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
@@ -269,15 +270,6 @@ import { Calendar, DualCalendar } from "@xsolla/xui-calendar";
269
270
  import { Calendar as CalendarIcon } from "@xsolla/xui-icons-base";
270
271
 
271
272
  // src/utils.ts
272
- import { format } from "date-fns";
273
- import * as locales from "date-fns/locale";
274
- var defaultLocale = "enUS";
275
- function formatDate(date, formatStr, locale = defaultLocale) {
276
- const localeObj = locales[locale] || locales[defaultLocale];
277
- return format(date, formatStr, {
278
- locale: localeObj
279
- });
280
- }
281
273
  var SEGMENT_WIDTH = {
282
274
  day: 2,
283
275
  month: 2,
@@ -288,21 +280,109 @@ var SEGMENT_TOKEN = {
288
280
  month: "mm",
289
281
  year: "yyyy"
290
282
  };
291
- function parseDateFormat(format2) {
283
+ var DEFAULT_DATE_FORMAT = "MM/dd/yyyy";
284
+ var SEGMENT_LETTER = {
285
+ d: "day",
286
+ M: "month",
287
+ y: "year"
288
+ };
289
+ var CASING_REPAIRS = { D: "d", Y: "y", m: "M" };
290
+ 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.";
291
+ var ALL_SEGMENTS = ["day", "month", "year"];
292
+ var isLetter = (c) => /[A-Za-z]/.test(c);
293
+ var isSupportedRun = (letter, length) => {
294
+ if (letter === "y") return true;
295
+ if (letter === "d" || letter === "M") return length <= 2;
296
+ return false;
297
+ };
298
+ function analyzeDateFormat(format, fallback = DEFAULT_DATE_FORMAT) {
299
+ const issues = [];
300
+ const used = /* @__PURE__ */ new Set();
301
+ for (let i2 = 0; i2 < format.length; i2++) {
302
+ if (isLetter(format[i2])) used.add(format[i2]);
303
+ }
304
+ let repaired = "";
305
+ let hasUnsupported = false;
306
+ let i = 0;
307
+ while (i < format.length) {
308
+ const c = format[i];
309
+ if (!isLetter(c)) {
310
+ repaired += c;
311
+ i++;
312
+ continue;
313
+ }
314
+ let end = i;
315
+ while (end < format.length && format[end] === c) end++;
316
+ const run = format.slice(i, end);
317
+ i = end;
318
+ if (isSupportedRun(c, run.length)) {
319
+ repaired += run;
320
+ continue;
321
+ }
322
+ const repair = CASING_REPAIRS[c];
323
+ if (repair && !used.has(repair) && isSupportedRun(repair, run.length)) {
324
+ const fixed = repair.repeat(run.length);
325
+ issues.push({ token: run, kind: "casing", suggestion: fixed });
326
+ repaired += fixed;
327
+ continue;
328
+ }
329
+ issues.push({ token: run, kind: "unsupported" });
330
+ hasUnsupported = true;
331
+ repaired += run;
332
+ }
333
+ const present = new Set(
334
+ parseDateFormat(repaired).filter(
335
+ (t) => t.kind === "segment"
336
+ ).map((t) => t.type)
337
+ );
338
+ const missing = ALL_SEGMENTS.filter((type) => !present.has(type));
339
+ for (const type of missing) issues.push({ token: type, kind: "missing" });
340
+ const usable = !hasUnsupported && missing.length === 0;
341
+ return {
342
+ format: usable ? repaired : fallback,
343
+ usedFallback: !usable,
344
+ issues
345
+ };
346
+ }
347
+ function describeDateFormatIssues(format, analysis) {
348
+ const clauses = analysis.issues.map((issue) => {
349
+ if (issue.kind === "missing") {
350
+ return `it has no ${issue.token} segment, so the field can never be completed`;
351
+ }
352
+ if (issue.kind === "casing") {
353
+ return `\`${issue.token}\` is not a \`date-fns\` date token \u2014 use \`${issue.suggestion}\``;
354
+ }
355
+ return `\`${issue.token}\` is not a supported token`;
356
+ });
357
+ const resolution = analysis.usedFallback ? `Falling back to \`${analysis.format}\`.` : `Reading it as \`${analysis.format}\`.`;
358
+ return `[DatePicker] Invalid \`dateFormat\` \`${format}\`: ${clauses.join("; ")}. ${resolution} Note ${SUPPORTED_TOKENS_HINT}`;
359
+ }
360
+ var warnedFormats = /* @__PURE__ */ new Set();
361
+ function resolveDateFormat(format, fallback = DEFAULT_DATE_FORMAT) {
362
+ const analysis = analyzeDateFormat(format, fallback);
363
+ if (analysis.issues.length > 0) {
364
+ const key = `${format}|${fallback}`;
365
+ const isProduction = globalThis.process?.env?.NODE_ENV === "production";
366
+ if (!isProduction && !warnedFormats.has(key)) {
367
+ warnedFormats.add(key);
368
+ console.warn(describeDateFormatIssues(format, analysis));
369
+ }
370
+ }
371
+ return analysis.format;
372
+ }
373
+ function parseDateFormat(format) {
292
374
  const tokens = [];
293
375
  let i = 0;
294
- while (i < format2.length) {
295
- const c = format2[i];
296
- if (c === "d" || c === "M" || c === "y") {
297
- while (i < format2.length && format2[i] === c) i++;
298
- tokens.push({
299
- kind: "segment",
300
- type: c === "d" ? "day" : c === "M" ? "month" : "year"
301
- });
376
+ while (i < format.length) {
377
+ const c = format[i];
378
+ const type = SEGMENT_LETTER[c];
379
+ if (type) {
380
+ while (i < format.length && format[i] === c) i++;
381
+ tokens.push({ kind: "segment", type });
302
382
  } else {
303
383
  let text = "";
304
- while (i < format2.length && !"dMy".includes(format2[i])) {
305
- text += format2[i];
384
+ while (i < format.length && !SEGMENT_LETTER[format[i]]) {
385
+ text += format[i];
306
386
  i++;
307
387
  }
308
388
  tokens.push({ kind: "literal", text });
@@ -346,8 +426,8 @@ function stepDatePart(type, current, delta, parts) {
346
426
  const dim = daysInMonth(year, monthIndex);
347
427
  return ((current - 1 + delta) % dim + dim) % dim + 1;
348
428
  }
349
- function buildSegmentedValue(format2, parts, active) {
350
- const tokens = parseDateFormat(format2);
429
+ function buildSegmentedValue(format, parts, active) {
430
+ const tokens = parseDateFormat(format);
351
431
  let text = "";
352
432
  const ranges = [];
353
433
  for (const token of tokens) {
@@ -388,14 +468,14 @@ function isSegmentComplete(type, buffer) {
388
468
  function daysInMonth(year, monthIndex) {
389
469
  return new Date(year, monthIndex + 1, 0).getDate();
390
470
  }
391
- function getDateSegments(format2, value) {
471
+ function getDateSegments(format, value) {
392
472
  const segments = [];
393
473
  let fi = 0;
394
474
  let vi = 0;
395
- while (fi < format2.length && vi < value.length) {
396
- const fc = format2[fi];
475
+ while (fi < format.length && vi < value.length) {
476
+ const fc = format[fi];
397
477
  if (fc === "d" || fc === "M" || fc === "y") {
398
- while (fi < format2.length && format2[fi] === fc) fi++;
478
+ while (fi < format.length && format[fi] === fc) fi++;
399
479
  const start = vi;
400
480
  while (vi < value.length && value[vi] >= "0" && value[vi] <= "9") vi++;
401
481
  if (vi > start) {
@@ -440,7 +520,6 @@ function stepDateSegment(date, type, delta) {
440
520
  // src/DatePicker.tsx
441
521
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
442
522
  var PLACEMENT_OFFSET = 4;
443
- var DEFAULT_DATE_FORMAT = "MM/dd/yyyy";
444
523
  var RANGE_SEPARATOR = /\s*[-–—]\s*/;
445
524
  var RANGE_DISPLAY_SEP = " - ";
446
525
  var CalendarFieldIcon = () => /* @__PURE__ */ jsx2(CalendarIcon, { size: "100%" });
@@ -542,6 +621,10 @@ var DatePicker = forwardRef(
542
621
  const editable = !readOnly && !rest.disabled;
543
622
  const isDual = variant === "dual";
544
623
  const isRange = isDual || selectsRange;
624
+ const resolvedFormat = useMemo(
625
+ () => resolveDateFormat(dateFormat),
626
+ [dateFormat]
627
+ );
545
628
  const propGroups = useCallback(
546
629
  () => isRange ? [datePartsFromDate(startDate), datePartsFromDate(endDate)] : [datePartsFromDate(selectedDate)],
547
630
  [isRange, startDate, endDate, selectedDate]
@@ -589,7 +672,7 @@ var DatePicker = forwardRef(
589
672
  };
590
673
  const composeField = () => {
591
674
  const build = (group) => buildSegmentedValue(
592
- dateFormat,
675
+ resolvedFormat,
593
676
  groups[group] ?? { day: null, month: null, year: null },
594
677
  edit && edit.group === group ? { type: edit.type, buffer: edit.buffer } : void 0
595
678
  );
@@ -836,7 +919,7 @@ var DatePicker = forwardRef(
836
919
  const parseOne = (token) => {
837
920
  const trimmed = token.trim();
838
921
  if (!trimmed) return null;
839
- const parsed = parse(trimmed, dateFormat, /* @__PURE__ */ new Date());
922
+ const parsed = parse(trimmed, resolvedFormat, /* @__PURE__ */ new Date());
840
923
  return isValid(parsed) ? parsed : null;
841
924
  };
842
925
  const handlePaste = (e) => {
@@ -853,7 +936,7 @@ var DatePicker = forwardRef(
853
936
  const end = parseOne(endText ?? "");
854
937
  if (!start) {
855
938
  setParseError(
856
- `Enter a valid date range (${dateFormat} - ${dateFormat})`
939
+ `Enter a valid date range (${resolvedFormat} - ${resolvedFormat})`
857
940
  );
858
941
  return;
859
942
  }
@@ -861,7 +944,7 @@ var DatePicker = forwardRef(
861
944
  } else {
862
945
  const date = parseOne(text);
863
946
  if (!date) {
864
- setParseError(`Enter a valid date (${dateFormat})`);
947
+ setParseError(`Enter a valid date (${resolvedFormat})`);
865
948
  return;
866
949
  }
867
950
  next = [datePartsFromDate(date)];
@@ -1057,7 +1140,7 @@ var DatePicker = forwardRef(
1057
1140
  ...rest,
1058
1141
  ref: inputRef,
1059
1142
  size,
1060
- placeholder: placeholder || (isRange ? "Enter a date range" : "Choose a date"),
1143
+ placeholder: placeholder || (isRange ? "Choose a date range" : "Choose a date"),
1061
1144
  value: inputValue,
1062
1145
  readOnly,
1063
1146
  onFocus: handleFieldFocus,
@@ -1104,18 +1187,22 @@ export {
1104
1187
  CalendarChips,
1105
1188
  CalendarGrid,
1106
1189
  CalendarHeader,
1190
+ DEFAULT_DATE_FORMAT,
1107
1191
  DatePicker,
1108
1192
  DualCalendar2 as DualCalendar,
1193
+ analyzeDateFormat,
1109
1194
  buildSegmentedValue,
1110
1195
  clampDatePart,
1111
1196
  datePartsFromDate,
1112
1197
  datePartsToDate,
1113
1198
  daysInMonth,
1199
+ describeDateFormatIssues,
1114
1200
  formatDate,
1115
1201
  getDateSegments,
1116
1202
  isDatePartsComplete,
1117
1203
  isSegmentComplete,
1118
1204
  parseDateFormat,
1205
+ resolveDateFormat,
1119
1206
  stepDatePart,
1120
1207
  stepDateSegment
1121
1208
  };