@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/README.md +26 -3
- package/native/index.d.mts +26 -9
- package/native/index.d.ts +26 -9
- package/native/index.js +125 -45
- package/native/index.js.map +1 -1
- package/native/index.mjs +120 -33
- package/native/index.mjs.map +1 -1
- package/package.json +6 -6
- package/web/index.d.mts +26 -9
- package/web/index.d.ts +26 -9
- package/web/index.js +125 -35
- package/web/index.js.map +1 -1
- package/web/index.mjs +120 -33
- package/web/index.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ It’s a form field that lets users select a single date or date range. It is al
|
|
|
13
13
|
- When the date is derived automatically and not user-selected
|
|
14
14
|
|
|
15
15
|
### Content guidelines
|
|
16
|
-
- Placeholder text: use *"Choose a date"* for single mode and *"
|
|
16
|
+
- Placeholder text: use *"Choose a date"* for single mode and *"Choose a date range"* for range mode — these are the defaults.
|
|
17
17
|
- Labels (via Field Group): keep them short and clear — *"Date"*, *"Start date"*, *"Report period"*.
|
|
18
18
|
- Error messages: explain what went wrong — *"Please select a date"* or *"End date must be after start date"*.
|
|
19
19
|
- Use standard labels — Today, Yesterday, Last 7 days, Last 30 days, This month, Last month. Chip labels are shown directly in the DatePicker field when a chip is selected, so keep them short and self-explanatory — the label must make sense both in the chip row and as standalone field content.
|
|
@@ -96,7 +96,8 @@ const [date, setDate] = useState<Date | null>(null);
|
|
|
96
96
|
| `endDate` | `Date \| null` | — | Range end date. |
|
|
97
97
|
| `selectsRange` | `boolean` | `false` | Enable range selection. |
|
|
98
98
|
| `onChange` | `(date: Date \| DateRangeType) => void` | — | Fired when the date or range changes. |
|
|
99
|
-
| `placeholder` | `string` | `'
|
|
99
|
+
| `placeholder` | `string` | `'Choose a date'` / `'Choose a date range'` | Placeholder for the input, shown while it is empty. Not derived from `dateFormat`. |
|
|
100
|
+
| `dateFormat` | `string` | `'MM/dd/yyyy'` | `date-fns` format used to render and parse the field. Numeric tokens only — see [Date formats](#date-formats). |
|
|
100
101
|
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Input size. |
|
|
101
102
|
| `disabled` | `boolean` | `false` | Disable the input. |
|
|
102
103
|
| `backgroundColor` | `string` | — | Custom background colour for the input. |
|
|
@@ -122,6 +123,28 @@ const [date, setDate] = useState<Date | null>(null);
|
|
|
122
123
|
|
|
123
124
|
Inherits `ThemeOverrideProps` (`themeMode`, `themeProductContext`).
|
|
124
125
|
|
|
126
|
+
### Date formats
|
|
127
|
+
|
|
128
|
+
The field is edited segment by segment (type digits, arrow-step a segment, tab between them), so `dateFormat` supports **numeric tokens only** — exactly three, in any order, with anything between them treated as a literal separator:
|
|
129
|
+
|
|
130
|
+
| Token | Segment | Rendered as |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| `d`, `dd` | Day | Always two digits (`05`) |
|
|
133
|
+
| `M`, `MM` | Month | Always two digits (`03`) |
|
|
134
|
+
| `y`, `yyyy` | Year | Always four digits (`2026`) |
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
<DatePicker dateFormat="dd/MM/yyyy" /> // 15/03/2026
|
|
138
|
+
<DatePicker dateFormat="yyyy-MM-dd" /> // 2026-03-15
|
|
139
|
+
<DatePicker dateFormat="dd.MM.yyyy" /> // 15.03.2026
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
While the field is focused or partly filled it shows a mask built from the format (`dd/mm/yyyy`), which is separate from `placeholder` — the placeholder shows only while the field is empty and does not track `dateFormat`.
|
|
143
|
+
|
|
144
|
+
**Mind the casing.** `date-fns` tokens are not Moment.js/Day.js tokens: month is `MM` (`mm` is *minutes*), day is `dd` (`DD` is day-of-year) and year is `yyyy` (`YYYY` is week-numbering year). Passing `dd/mm/yyyy`, `DD/MM/YYYY`, `dd/MM/YYYY` or `DD.MM.YYYY` is read as the intended `dd/MM/yyyy` and logs a `console.warn` in development naming the token and its replacement. Fix the format string rather than relying on the repair.
|
|
145
|
+
|
|
146
|
+
**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.
|
|
147
|
+
|
|
125
148
|
### `<Calendar>`
|
|
126
149
|
|
|
127
150
|
Re-exported from `@xsolla/xui-calendar`. Standalone single-month calendar.
|
|
@@ -146,7 +169,7 @@ Re-exported from `@xsolla/xui-calendar`. Preset chips bar (e.g. "Last 7 days", "
|
|
|
146
169
|
|
|
147
170
|
| Export | Type | Description |
|
|
148
171
|
| --- | --- | --- |
|
|
149
|
-
| `formatDate` | `(date: Date, formatStr: string, locale?: CalendarLocaleType) => string` | Format a date with a `date-fns` locale identifier. |
|
|
172
|
+
| `formatDate` | `(date: Date, formatStr: string, locale?: CalendarLocaleType) => string` | Format a date with a `date-fns` locale identifier. Re-exported from `@xsolla/xui-calendar`; the full `date-fns` token set applies, so use it for display text the picker's own field cannot render (`MMM d, yyyy`). |
|
|
150
173
|
|
|
151
174
|
### Types
|
|
152
175
|
|
package/native/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CalendarProps
|
|
2
|
-
export { Calendar, CalendarChipOption, CalendarChips, CalendarChipsProps, CalendarGrid, CalendarGridProps, CalendarHeader, CalendarHeaderProps, CalendarLocaleType, CalendarProps, DualCalendar, DualCalendarProps } from '@xsolla/xui-calendar';
|
|
1
|
+
import { CalendarProps } from '@xsolla/xui-calendar';
|
|
2
|
+
export { Calendar, CalendarChipOption, CalendarChips, CalendarChipsProps, CalendarGrid, CalendarGridProps, CalendarHeader, CalendarHeaderProps, CalendarLocaleType, CalendarProps, DualCalendar, DualCalendarProps, formatDate } from '@xsolla/xui-calendar';
|
|
3
3
|
import * as react from 'react';
|
|
4
4
|
import { ReactNode, FocusEvent, MouseEvent, KeyboardEvent, ClipboardEvent } from 'react';
|
|
5
5
|
import { ThemeMode, ProductContext } from '@xsolla/xui-core';
|
|
@@ -44,11 +44,7 @@ interface DatePickerProps extends Omit<CalendarProps, "onChange"> {
|
|
|
44
44
|
* Placeholder text for the input
|
|
45
45
|
*/
|
|
46
46
|
placeholder?: string;
|
|
47
|
-
/**
|
|
48
|
-
* date-fns format string used to both render and parse the field's text value
|
|
49
|
-
* (e.g. `"dd/MM/yyyy"`). Applies to each date in a range. Defaults to
|
|
50
|
-
* `"MM/dd/yyyy"`.
|
|
51
|
-
*/
|
|
47
|
+
/** `date-fns` format for the field, defaulting to `"MM/dd/yyyy"`. Numeric `d`/`dd`, `M`/`MM` and `y`/`yyyy` tokens only, in any order (mind the casing — `mm` is minutes); anything else warns in development and falls back to the default. */
|
|
52
48
|
dateFormat?: string;
|
|
53
49
|
/**
|
|
54
50
|
* Size of the input
|
|
@@ -130,7 +126,6 @@ interface DatePickerProps extends Omit<CalendarProps, "onChange"> {
|
|
|
130
126
|
|
|
131
127
|
declare const DatePicker: react.ForwardRefExoticComponent<DatePickerProps & react.RefAttributes<any>>;
|
|
132
128
|
|
|
133
|
-
declare function formatDate(date: Date, formatStr: string, locale?: CalendarLocaleType): string;
|
|
134
129
|
type DateSegmentType = "day" | "month" | "year";
|
|
135
130
|
/** A date broken into editable parts; `null` means "not yet filled". */
|
|
136
131
|
interface DateParts {
|
|
@@ -152,6 +147,28 @@ type FormatToken = {
|
|
|
152
147
|
kind: "literal";
|
|
153
148
|
text: string;
|
|
154
149
|
};
|
|
150
|
+
/** Default format for rendering/parsing the field's text value (overridable via `dateFormat`). */
|
|
151
|
+
declare const DEFAULT_DATE_FORMAT = "MM/dd/yyyy";
|
|
152
|
+
/** What is wrong with one token of a `dateFormat`. */
|
|
153
|
+
interface DateFormatIssue {
|
|
154
|
+
/** Offending token as written, or the segment name for `kind: "missing"`. */
|
|
155
|
+
token: string;
|
|
156
|
+
kind: "casing" | "unsupported" | "missing";
|
|
157
|
+
/** The `date-fns` token to use instead (`casing` only). */
|
|
158
|
+
suggestion?: string;
|
|
159
|
+
}
|
|
160
|
+
interface DateFormatAnalysis {
|
|
161
|
+
/** The format the field should actually use (repaired input, or the fallback). */
|
|
162
|
+
format: string;
|
|
163
|
+
usedFallback: boolean;
|
|
164
|
+
issues: DateFormatIssue[];
|
|
165
|
+
}
|
|
166
|
+
/** Repair a caller's `dateFormat` where the case is unambiguously wrong, else fall back when it is unusable (unsupported token, or a missing segment). */
|
|
167
|
+
declare function analyzeDateFormat(format: string, fallback?: string): DateFormatAnalysis;
|
|
168
|
+
/** Human-readable warning for a failed {@link analyzeDateFormat}. */
|
|
169
|
+
declare function describeDateFormatIssues(format: string, analysis: DateFormatAnalysis): string;
|
|
170
|
+
/** Resolve a `dateFormat` to one the field can render, warning once per offending format in development. See {@link analyzeDateFormat}. */
|
|
171
|
+
declare function resolveDateFormat(format: string, fallback?: string): string;
|
|
155
172
|
/** Split a date format into ordered segment tokens and literal separators. */
|
|
156
173
|
declare function parseDateFormat(format: string): FormatToken[];
|
|
157
174
|
declare function datePartsFromDate(date: Date | null | undefined): DateParts;
|
|
@@ -202,4 +219,4 @@ declare function getDateSegments(format: string, value: string): DateSegment[];
|
|
|
202
219
|
*/
|
|
203
220
|
declare function stepDateSegment(date: Date, type: DateSegmentType, delta: number): Date;
|
|
204
221
|
|
|
205
|
-
export { type DateParts, DatePicker, type DatePickerAlignment, type DatePickerDropdownPosition, type DatePickerPlacement, type DatePickerProps, type DatePickerSide, type DateRangeType, type DateSegment, type DateSegmentType, type SegmentRange, buildSegmentedValue, clampDatePart, datePartsFromDate, datePartsToDate, daysInMonth,
|
|
222
|
+
export { DEFAULT_DATE_FORMAT, type DateFormatAnalysis, type DateFormatIssue, type DateParts, DatePicker, type DatePickerAlignment, type DatePickerDropdownPosition, type DatePickerPlacement, type DatePickerProps, type DatePickerSide, type DateRangeType, type DateSegment, type DateSegmentType, type SegmentRange, analyzeDateFormat, buildSegmentedValue, clampDatePart, datePartsFromDate, datePartsToDate, daysInMonth, describeDateFormatIssues, getDateSegments, isDatePartsComplete, isSegmentComplete, parseDateFormat, resolveDateFormat, stepDatePart, stepDateSegment };
|
package/native/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CalendarProps
|
|
2
|
-
export { Calendar, CalendarChipOption, CalendarChips, CalendarChipsProps, CalendarGrid, CalendarGridProps, CalendarHeader, CalendarHeaderProps, CalendarLocaleType, CalendarProps, DualCalendar, DualCalendarProps } from '@xsolla/xui-calendar';
|
|
1
|
+
import { CalendarProps } from '@xsolla/xui-calendar';
|
|
2
|
+
export { Calendar, CalendarChipOption, CalendarChips, CalendarChipsProps, CalendarGrid, CalendarGridProps, CalendarHeader, CalendarHeaderProps, CalendarLocaleType, CalendarProps, DualCalendar, DualCalendarProps, formatDate } from '@xsolla/xui-calendar';
|
|
3
3
|
import * as react from 'react';
|
|
4
4
|
import { ReactNode, FocusEvent, MouseEvent, KeyboardEvent, ClipboardEvent } from 'react';
|
|
5
5
|
import { ThemeMode, ProductContext } from '@xsolla/xui-core';
|
|
@@ -44,11 +44,7 @@ interface DatePickerProps extends Omit<CalendarProps, "onChange"> {
|
|
|
44
44
|
* Placeholder text for the input
|
|
45
45
|
*/
|
|
46
46
|
placeholder?: string;
|
|
47
|
-
/**
|
|
48
|
-
* date-fns format string used to both render and parse the field's text value
|
|
49
|
-
* (e.g. `"dd/MM/yyyy"`). Applies to each date in a range. Defaults to
|
|
50
|
-
* `"MM/dd/yyyy"`.
|
|
51
|
-
*/
|
|
47
|
+
/** `date-fns` format for the field, defaulting to `"MM/dd/yyyy"`. Numeric `d`/`dd`, `M`/`MM` and `y`/`yyyy` tokens only, in any order (mind the casing — `mm` is minutes); anything else warns in development and falls back to the default. */
|
|
52
48
|
dateFormat?: string;
|
|
53
49
|
/**
|
|
54
50
|
* Size of the input
|
|
@@ -130,7 +126,6 @@ interface DatePickerProps extends Omit<CalendarProps, "onChange"> {
|
|
|
130
126
|
|
|
131
127
|
declare const DatePicker: react.ForwardRefExoticComponent<DatePickerProps & react.RefAttributes<any>>;
|
|
132
128
|
|
|
133
|
-
declare function formatDate(date: Date, formatStr: string, locale?: CalendarLocaleType): string;
|
|
134
129
|
type DateSegmentType = "day" | "month" | "year";
|
|
135
130
|
/** A date broken into editable parts; `null` means "not yet filled". */
|
|
136
131
|
interface DateParts {
|
|
@@ -152,6 +147,28 @@ type FormatToken = {
|
|
|
152
147
|
kind: "literal";
|
|
153
148
|
text: string;
|
|
154
149
|
};
|
|
150
|
+
/** Default format for rendering/parsing the field's text value (overridable via `dateFormat`). */
|
|
151
|
+
declare const DEFAULT_DATE_FORMAT = "MM/dd/yyyy";
|
|
152
|
+
/** What is wrong with one token of a `dateFormat`. */
|
|
153
|
+
interface DateFormatIssue {
|
|
154
|
+
/** Offending token as written, or the segment name for `kind: "missing"`. */
|
|
155
|
+
token: string;
|
|
156
|
+
kind: "casing" | "unsupported" | "missing";
|
|
157
|
+
/** The `date-fns` token to use instead (`casing` only). */
|
|
158
|
+
suggestion?: string;
|
|
159
|
+
}
|
|
160
|
+
interface DateFormatAnalysis {
|
|
161
|
+
/** The format the field should actually use (repaired input, or the fallback). */
|
|
162
|
+
format: string;
|
|
163
|
+
usedFallback: boolean;
|
|
164
|
+
issues: DateFormatIssue[];
|
|
165
|
+
}
|
|
166
|
+
/** Repair a caller's `dateFormat` where the case is unambiguously wrong, else fall back when it is unusable (unsupported token, or a missing segment). */
|
|
167
|
+
declare function analyzeDateFormat(format: string, fallback?: string): DateFormatAnalysis;
|
|
168
|
+
/** Human-readable warning for a failed {@link analyzeDateFormat}. */
|
|
169
|
+
declare function describeDateFormatIssues(format: string, analysis: DateFormatAnalysis): string;
|
|
170
|
+
/** Resolve a `dateFormat` to one the field can render, warning once per offending format in development. See {@link analyzeDateFormat}. */
|
|
171
|
+
declare function resolveDateFormat(format: string, fallback?: string): string;
|
|
155
172
|
/** Split a date format into ordered segment tokens and literal separators. */
|
|
156
173
|
declare function parseDateFormat(format: string): FormatToken[];
|
|
157
174
|
declare function datePartsFromDate(date: Date | null | undefined): DateParts;
|
|
@@ -202,4 +219,4 @@ declare function getDateSegments(format: string, value: string): DateSegment[];
|
|
|
202
219
|
*/
|
|
203
220
|
declare function stepDateSegment(date: Date, type: DateSegmentType, delta: number): Date;
|
|
204
221
|
|
|
205
|
-
export { type DateParts, DatePicker, type DatePickerAlignment, type DatePickerDropdownPosition, type DatePickerPlacement, type DatePickerProps, type DatePickerSide, type DateRangeType, type DateSegment, type DateSegmentType, type SegmentRange, buildSegmentedValue, clampDatePart, datePartsFromDate, datePartsToDate, daysInMonth,
|
|
222
|
+
export { DEFAULT_DATE_FORMAT, type DateFormatAnalysis, type DateFormatIssue, type DateParts, DatePicker, type DatePickerAlignment, type DatePickerDropdownPosition, type DatePickerPlacement, type DatePickerProps, type DatePickerSide, type DateRangeType, type DateSegment, type DateSegmentType, type SegmentRange, analyzeDateFormat, buildSegmentedValue, clampDatePart, datePartsFromDate, datePartsToDate, daysInMonth, describeDateFormatIssues, getDateSegments, isDatePartsComplete, isSegmentComplete, parseDateFormat, resolveDateFormat, stepDatePart, stepDateSegment };
|
package/native/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.tsx
|
|
@@ -34,18 +24,22 @@ __export(index_exports, {
|
|
|
34
24
|
CalendarChips: () => import_xui_calendar2.CalendarChips,
|
|
35
25
|
CalendarGrid: () => import_xui_calendar2.CalendarGrid,
|
|
36
26
|
CalendarHeader: () => import_xui_calendar2.CalendarHeader,
|
|
27
|
+
DEFAULT_DATE_FORMAT: () => DEFAULT_DATE_FORMAT,
|
|
37
28
|
DatePicker: () => DatePicker,
|
|
38
29
|
DualCalendar: () => import_xui_calendar2.DualCalendar,
|
|
30
|
+
analyzeDateFormat: () => analyzeDateFormat,
|
|
39
31
|
buildSegmentedValue: () => buildSegmentedValue,
|
|
40
32
|
clampDatePart: () => clampDatePart,
|
|
41
33
|
datePartsFromDate: () => datePartsFromDate,
|
|
42
34
|
datePartsToDate: () => datePartsToDate,
|
|
43
35
|
daysInMonth: () => daysInMonth,
|
|
44
|
-
|
|
36
|
+
describeDateFormatIssues: () => describeDateFormatIssues,
|
|
37
|
+
formatDate: () => import_xui_calendar2.formatDate,
|
|
45
38
|
getDateSegments: () => getDateSegments,
|
|
46
39
|
isDatePartsComplete: () => isDatePartsComplete,
|
|
47
40
|
isSegmentComplete: () => isSegmentComplete,
|
|
48
41
|
parseDateFormat: () => parseDateFormat,
|
|
42
|
+
resolveDateFormat: () => resolveDateFormat,
|
|
49
43
|
stepDatePart: () => stepDatePart,
|
|
50
44
|
stepDateSegment: () => stepDateSegment
|
|
51
45
|
});
|
|
@@ -298,20 +292,11 @@ var isNative = true;
|
|
|
298
292
|
|
|
299
293
|
// src/DatePicker.tsx
|
|
300
294
|
var import_xui_input = require("@xsolla/xui-input");
|
|
301
|
-
var
|
|
295
|
+
var import_date_fns = require("date-fns");
|
|
302
296
|
var import_xui_calendar = require("@xsolla/xui-calendar");
|
|
303
297
|
var import_xui_icons_base = require("@xsolla/xui-icons-base");
|
|
304
298
|
|
|
305
299
|
// src/utils.ts
|
|
306
|
-
var import_date_fns = require("date-fns");
|
|
307
|
-
var locales = __toESM(require("date-fns/locale"));
|
|
308
|
-
var defaultLocale = "enUS";
|
|
309
|
-
function formatDate(date, formatStr, locale = defaultLocale) {
|
|
310
|
-
const localeObj = locales[locale] || locales[defaultLocale];
|
|
311
|
-
return (0, import_date_fns.format)(date, formatStr, {
|
|
312
|
-
locale: localeObj
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
300
|
var SEGMENT_WIDTH = {
|
|
316
301
|
day: 2,
|
|
317
302
|
month: 2,
|
|
@@ -322,21 +307,109 @@ var SEGMENT_TOKEN = {
|
|
|
322
307
|
month: "mm",
|
|
323
308
|
year: "yyyy"
|
|
324
309
|
};
|
|
325
|
-
|
|
310
|
+
var DEFAULT_DATE_FORMAT = "MM/dd/yyyy";
|
|
311
|
+
var SEGMENT_LETTER = {
|
|
312
|
+
d: "day",
|
|
313
|
+
M: "month",
|
|
314
|
+
y: "year"
|
|
315
|
+
};
|
|
316
|
+
var CASING_REPAIRS = { D: "d", Y: "y", m: "M" };
|
|
317
|
+
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.";
|
|
318
|
+
var ALL_SEGMENTS = ["day", "month", "year"];
|
|
319
|
+
var isLetter = (c) => /[A-Za-z]/.test(c);
|
|
320
|
+
var isSupportedRun = (letter, length) => {
|
|
321
|
+
if (letter === "y") return true;
|
|
322
|
+
if (letter === "d" || letter === "M") return length <= 2;
|
|
323
|
+
return false;
|
|
324
|
+
};
|
|
325
|
+
function analyzeDateFormat(format, fallback = DEFAULT_DATE_FORMAT) {
|
|
326
|
+
const issues = [];
|
|
327
|
+
const used = /* @__PURE__ */ new Set();
|
|
328
|
+
for (let i2 = 0; i2 < format.length; i2++) {
|
|
329
|
+
if (isLetter(format[i2])) used.add(format[i2]);
|
|
330
|
+
}
|
|
331
|
+
let repaired = "";
|
|
332
|
+
let hasUnsupported = false;
|
|
333
|
+
let i = 0;
|
|
334
|
+
while (i < format.length) {
|
|
335
|
+
const c = format[i];
|
|
336
|
+
if (!isLetter(c)) {
|
|
337
|
+
repaired += c;
|
|
338
|
+
i++;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
let end = i;
|
|
342
|
+
while (end < format.length && format[end] === c) end++;
|
|
343
|
+
const run = format.slice(i, end);
|
|
344
|
+
i = end;
|
|
345
|
+
if (isSupportedRun(c, run.length)) {
|
|
346
|
+
repaired += run;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const repair = CASING_REPAIRS[c];
|
|
350
|
+
if (repair && !used.has(repair) && isSupportedRun(repair, run.length)) {
|
|
351
|
+
const fixed = repair.repeat(run.length);
|
|
352
|
+
issues.push({ token: run, kind: "casing", suggestion: fixed });
|
|
353
|
+
repaired += fixed;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
issues.push({ token: run, kind: "unsupported" });
|
|
357
|
+
hasUnsupported = true;
|
|
358
|
+
repaired += run;
|
|
359
|
+
}
|
|
360
|
+
const present = new Set(
|
|
361
|
+
parseDateFormat(repaired).filter(
|
|
362
|
+
(t) => t.kind === "segment"
|
|
363
|
+
).map((t) => t.type)
|
|
364
|
+
);
|
|
365
|
+
const missing = ALL_SEGMENTS.filter((type) => !present.has(type));
|
|
366
|
+
for (const type of missing) issues.push({ token: type, kind: "missing" });
|
|
367
|
+
const usable = !hasUnsupported && missing.length === 0;
|
|
368
|
+
return {
|
|
369
|
+
format: usable ? repaired : fallback,
|
|
370
|
+
usedFallback: !usable,
|
|
371
|
+
issues
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function describeDateFormatIssues(format, analysis) {
|
|
375
|
+
const clauses = analysis.issues.map((issue) => {
|
|
376
|
+
if (issue.kind === "missing") {
|
|
377
|
+
return `it has no ${issue.token} segment, so the field can never be completed`;
|
|
378
|
+
}
|
|
379
|
+
if (issue.kind === "casing") {
|
|
380
|
+
return `\`${issue.token}\` is not a \`date-fns\` date token \u2014 use \`${issue.suggestion}\``;
|
|
381
|
+
}
|
|
382
|
+
return `\`${issue.token}\` is not a supported token`;
|
|
383
|
+
});
|
|
384
|
+
const resolution = analysis.usedFallback ? `Falling back to \`${analysis.format}\`.` : `Reading it as \`${analysis.format}\`.`;
|
|
385
|
+
return `[DatePicker] Invalid \`dateFormat\` \`${format}\`: ${clauses.join("; ")}. ${resolution} Note ${SUPPORTED_TOKENS_HINT}`;
|
|
386
|
+
}
|
|
387
|
+
var warnedFormats = /* @__PURE__ */ new Set();
|
|
388
|
+
function resolveDateFormat(format, fallback = DEFAULT_DATE_FORMAT) {
|
|
389
|
+
const analysis = analyzeDateFormat(format, fallback);
|
|
390
|
+
if (analysis.issues.length > 0) {
|
|
391
|
+
const key = `${format}|${fallback}`;
|
|
392
|
+
const isProduction = globalThis.process?.env?.NODE_ENV === "production";
|
|
393
|
+
if (!isProduction && !warnedFormats.has(key)) {
|
|
394
|
+
warnedFormats.add(key);
|
|
395
|
+
console.warn(describeDateFormatIssues(format, analysis));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return analysis.format;
|
|
399
|
+
}
|
|
400
|
+
function parseDateFormat(format) {
|
|
326
401
|
const tokens = [];
|
|
327
402
|
let i = 0;
|
|
328
|
-
while (i <
|
|
329
|
-
const c =
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
type: c === "d" ? "day" : c === "M" ? "month" : "year"
|
|
335
|
-
});
|
|
403
|
+
while (i < format.length) {
|
|
404
|
+
const c = format[i];
|
|
405
|
+
const type = SEGMENT_LETTER[c];
|
|
406
|
+
if (type) {
|
|
407
|
+
while (i < format.length && format[i] === c) i++;
|
|
408
|
+
tokens.push({ kind: "segment", type });
|
|
336
409
|
} else {
|
|
337
410
|
let text = "";
|
|
338
|
-
while (i <
|
|
339
|
-
text +=
|
|
411
|
+
while (i < format.length && !SEGMENT_LETTER[format[i]]) {
|
|
412
|
+
text += format[i];
|
|
340
413
|
i++;
|
|
341
414
|
}
|
|
342
415
|
tokens.push({ kind: "literal", text });
|
|
@@ -380,8 +453,8 @@ function stepDatePart(type, current, delta, parts) {
|
|
|
380
453
|
const dim = daysInMonth(year, monthIndex);
|
|
381
454
|
return ((current - 1 + delta) % dim + dim) % dim + 1;
|
|
382
455
|
}
|
|
383
|
-
function buildSegmentedValue(
|
|
384
|
-
const tokens = parseDateFormat(
|
|
456
|
+
function buildSegmentedValue(format, parts, active) {
|
|
457
|
+
const tokens = parseDateFormat(format);
|
|
385
458
|
let text = "";
|
|
386
459
|
const ranges = [];
|
|
387
460
|
for (const token of tokens) {
|
|
@@ -422,14 +495,14 @@ function isSegmentComplete(type, buffer) {
|
|
|
422
495
|
function daysInMonth(year, monthIndex) {
|
|
423
496
|
return new Date(year, monthIndex + 1, 0).getDate();
|
|
424
497
|
}
|
|
425
|
-
function getDateSegments(
|
|
498
|
+
function getDateSegments(format, value) {
|
|
426
499
|
const segments = [];
|
|
427
500
|
let fi = 0;
|
|
428
501
|
let vi = 0;
|
|
429
|
-
while (fi <
|
|
430
|
-
const fc =
|
|
502
|
+
while (fi < format.length && vi < value.length) {
|
|
503
|
+
const fc = format[fi];
|
|
431
504
|
if (fc === "d" || fc === "M" || fc === "y") {
|
|
432
|
-
while (fi <
|
|
505
|
+
while (fi < format.length && format[fi] === fc) fi++;
|
|
433
506
|
const start = vi;
|
|
434
507
|
while (vi < value.length && value[vi] >= "0" && value[vi] <= "9") vi++;
|
|
435
508
|
if (vi > start) {
|
|
@@ -474,7 +547,6 @@ function stepDateSegment(date, type, delta) {
|
|
|
474
547
|
// src/DatePicker.tsx
|
|
475
548
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
476
549
|
var PLACEMENT_OFFSET = 4;
|
|
477
|
-
var DEFAULT_DATE_FORMAT = "MM/dd/yyyy";
|
|
478
550
|
var RANGE_SEPARATOR = /\s*[-–—]\s*/;
|
|
479
551
|
var RANGE_DISPLAY_SEP = " - ";
|
|
480
552
|
var CalendarFieldIcon = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_xui_icons_base.Calendar, { size: "100%" });
|
|
@@ -576,6 +648,10 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
576
648
|
const editable = !readOnly && !rest.disabled;
|
|
577
649
|
const isDual = variant === "dual";
|
|
578
650
|
const isRange = isDual || selectsRange;
|
|
651
|
+
const resolvedFormat = (0, import_react.useMemo)(
|
|
652
|
+
() => resolveDateFormat(dateFormat),
|
|
653
|
+
[dateFormat]
|
|
654
|
+
);
|
|
579
655
|
const propGroups = (0, import_react.useCallback)(
|
|
580
656
|
() => isRange ? [datePartsFromDate(startDate), datePartsFromDate(endDate)] : [datePartsFromDate(selectedDate)],
|
|
581
657
|
[isRange, startDate, endDate, selectedDate]
|
|
@@ -623,7 +699,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
623
699
|
};
|
|
624
700
|
const composeField = () => {
|
|
625
701
|
const build = (group) => buildSegmentedValue(
|
|
626
|
-
|
|
702
|
+
resolvedFormat,
|
|
627
703
|
groups[group] ?? { day: null, month: null, year: null },
|
|
628
704
|
edit && edit.group === group ? { type: edit.type, buffer: edit.buffer } : void 0
|
|
629
705
|
);
|
|
@@ -870,8 +946,8 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
870
946
|
const parseOne = (token) => {
|
|
871
947
|
const trimmed = token.trim();
|
|
872
948
|
if (!trimmed) return null;
|
|
873
|
-
const parsed = (0,
|
|
874
|
-
return (0,
|
|
949
|
+
const parsed = (0, import_date_fns.parse)(trimmed, resolvedFormat, /* @__PURE__ */ new Date());
|
|
950
|
+
return (0, import_date_fns.isValid)(parsed) ? parsed : null;
|
|
875
951
|
};
|
|
876
952
|
const handlePaste = (e) => {
|
|
877
953
|
onPasteProp?.(e);
|
|
@@ -887,7 +963,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
887
963
|
const end = parseOne(endText ?? "");
|
|
888
964
|
if (!start) {
|
|
889
965
|
setParseError(
|
|
890
|
-
`Enter a valid date range (${
|
|
966
|
+
`Enter a valid date range (${resolvedFormat} - ${resolvedFormat})`
|
|
891
967
|
);
|
|
892
968
|
return;
|
|
893
969
|
}
|
|
@@ -895,7 +971,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
895
971
|
} else {
|
|
896
972
|
const date = parseOne(text);
|
|
897
973
|
if (!date) {
|
|
898
|
-
setParseError(`Enter a valid date (${
|
|
974
|
+
setParseError(`Enter a valid date (${resolvedFormat})`);
|
|
899
975
|
return;
|
|
900
976
|
}
|
|
901
977
|
next = [datePartsFromDate(date)];
|
|
@@ -1091,7 +1167,7 @@ var DatePicker = (0, import_react.forwardRef)(
|
|
|
1091
1167
|
...rest,
|
|
1092
1168
|
ref: inputRef,
|
|
1093
1169
|
size,
|
|
1094
|
-
placeholder: placeholder || (isRange ? "
|
|
1170
|
+
placeholder: placeholder || (isRange ? "Choose a date range" : "Choose a date"),
|
|
1095
1171
|
value: inputValue,
|
|
1096
1172
|
readOnly,
|
|
1097
1173
|
onFocus: handleFieldFocus,
|
|
@@ -1139,18 +1215,22 @@ DatePicker.displayName = "DatePicker";
|
|
|
1139
1215
|
CalendarChips,
|
|
1140
1216
|
CalendarGrid,
|
|
1141
1217
|
CalendarHeader,
|
|
1218
|
+
DEFAULT_DATE_FORMAT,
|
|
1142
1219
|
DatePicker,
|
|
1143
1220
|
DualCalendar,
|
|
1221
|
+
analyzeDateFormat,
|
|
1144
1222
|
buildSegmentedValue,
|
|
1145
1223
|
clampDatePart,
|
|
1146
1224
|
datePartsFromDate,
|
|
1147
1225
|
datePartsToDate,
|
|
1148
1226
|
daysInMonth,
|
|
1227
|
+
describeDateFormatIssues,
|
|
1149
1228
|
formatDate,
|
|
1150
1229
|
getDateSegments,
|
|
1151
1230
|
isDatePartsComplete,
|
|
1152
1231
|
isSegmentComplete,
|
|
1153
1232
|
parseDateFormat,
|
|
1233
|
+
resolveDateFormat,
|
|
1154
1234
|
stepDatePart,
|
|
1155
1235
|
stepDateSegment
|
|
1156
1236
|
});
|