@carbon/utilities 0.23.0 → 0.24.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 +21 -0
- package/es/date-picker/index.d.ts +993 -0
- package/es/date-picker/index.js +2015 -0
- package/lib/date-picker/index.js +2055 -0
- package/package.json +2 -2
|
@@ -0,0 +1,2015 @@
|
|
|
1
|
+
//#region src/date-picker/primitives/states.ts
|
|
2
|
+
/**
|
|
3
|
+
* Copyright IBM Corp. 2026
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the Apache-2.0 license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Date picker state machine states
|
|
10
|
+
*/
|
|
11
|
+
let DatePickerState = /* @__PURE__ */ function(DatePickerState) {
|
|
12
|
+
/**
|
|
13
|
+
* Initial state - calendar closed, no focus
|
|
14
|
+
*/
|
|
15
|
+
DatePickerState["IDLE"] = "idle";
|
|
16
|
+
/**
|
|
17
|
+
* Input has focus, calendar closed
|
|
18
|
+
*/
|
|
19
|
+
DatePickerState["FOCUSED"] = "focused";
|
|
20
|
+
/**
|
|
21
|
+
* Calendar dropdown is open
|
|
22
|
+
*/
|
|
23
|
+
DatePickerState["CALENDAR_OPEN"] = "calendar_open";
|
|
24
|
+
/**
|
|
25
|
+
* User is selecting the start date (range mode)
|
|
26
|
+
*/
|
|
27
|
+
DatePickerState["SELECTING_START"] = "selecting_start";
|
|
28
|
+
/**
|
|
29
|
+
* User is selecting the end date (range mode)
|
|
30
|
+
*/
|
|
31
|
+
DatePickerState["SELECTING_END"] = "selecting_end";
|
|
32
|
+
/**
|
|
33
|
+
* Date(s) have been selected
|
|
34
|
+
*/
|
|
35
|
+
DatePickerState["DATE_SELECTED"] = "date_selected";
|
|
36
|
+
/**
|
|
37
|
+
* Component is disabled
|
|
38
|
+
*/
|
|
39
|
+
DatePickerState["DISABLED"] = "disabled";
|
|
40
|
+
/**
|
|
41
|
+
* Component is read-only
|
|
42
|
+
*/
|
|
43
|
+
DatePickerState["READONLY"] = "readonly";
|
|
44
|
+
/**
|
|
45
|
+
* Component is in an error state
|
|
46
|
+
*/
|
|
47
|
+
DatePickerState["ERROR"] = "error";
|
|
48
|
+
return DatePickerState;
|
|
49
|
+
}({});
|
|
50
|
+
/**
|
|
51
|
+
* Date picker events
|
|
52
|
+
*/
|
|
53
|
+
let DatePickerEvent = /* @__PURE__ */ function(DatePickerEvent) {
|
|
54
|
+
DatePickerEvent["INPUT_FOCUS"] = "INPUT_FOCUS";
|
|
55
|
+
DatePickerEvent["INPUT_BLUR"] = "INPUT_BLUR";
|
|
56
|
+
DatePickerEvent["INPUT_CHANGE"] = "INPUT_CHANGE";
|
|
57
|
+
DatePickerEvent["CALENDAR_ICON_CLICK"] = "CALENDAR_ICON_CLICK";
|
|
58
|
+
DatePickerEvent["CALENDAR_OPEN"] = "CALENDAR_OPEN";
|
|
59
|
+
DatePickerEvent["CALENDAR_CLOSE"] = "CALENDAR_CLOSE";
|
|
60
|
+
DatePickerEvent["PREV_MONTH"] = "PREV_MONTH";
|
|
61
|
+
DatePickerEvent["NEXT_MONTH"] = "NEXT_MONTH";
|
|
62
|
+
DatePickerEvent["PREV_YEAR"] = "PREV_YEAR";
|
|
63
|
+
DatePickerEvent["NEXT_YEAR"] = "NEXT_YEAR";
|
|
64
|
+
DatePickerEvent["GO_TO_TODAY"] = "GO_TO_TODAY";
|
|
65
|
+
DatePickerEvent["DATE_SELECT"] = "DATE_SELECT";
|
|
66
|
+
DatePickerEvent["RANGE_START_SELECT"] = "RANGE_START_SELECT";
|
|
67
|
+
DatePickerEvent["RANGE_END_SELECT"] = "RANGE_END_SELECT";
|
|
68
|
+
DatePickerEvent["OUTSIDE_CLICK"] = "OUTSIDE_CLICK";
|
|
69
|
+
DatePickerEvent["ESCAPE_KEY"] = "ESCAPE_KEY";
|
|
70
|
+
DatePickerEvent["TAB_KEY"] = "TAB_KEY";
|
|
71
|
+
DatePickerEvent["SHIFT_TAB_KEY"] = "SHIFT_TAB_KEY";
|
|
72
|
+
DatePickerEvent["ENTER_KEY"] = "ENTER_KEY";
|
|
73
|
+
DatePickerEvent["ARROW_UP"] = "ARROW_UP";
|
|
74
|
+
DatePickerEvent["ARROW_DOWN"] = "ARROW_DOWN";
|
|
75
|
+
DatePickerEvent["ARROW_LEFT"] = "ARROW_LEFT";
|
|
76
|
+
DatePickerEvent["ARROW_RIGHT"] = "ARROW_RIGHT";
|
|
77
|
+
DatePickerEvent["PAGE_UP"] = "PAGE_UP";
|
|
78
|
+
DatePickerEvent["PAGE_DOWN"] = "PAGE_DOWN";
|
|
79
|
+
DatePickerEvent["HOME_KEY"] = "HOME_KEY";
|
|
80
|
+
DatePickerEvent["END_KEY"] = "END_KEY";
|
|
81
|
+
DatePickerEvent["DISABLE"] = "DISABLE";
|
|
82
|
+
DatePickerEvent["ENABLE"] = "ENABLE";
|
|
83
|
+
DatePickerEvent["SET_READONLY"] = "SET_READONLY";
|
|
84
|
+
DatePickerEvent["UNSET_READONLY"] = "UNSET_READONLY";
|
|
85
|
+
DatePickerEvent["VALUE_CHANGE"] = "VALUE_CHANGE";
|
|
86
|
+
DatePickerEvent["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
87
|
+
DatePickerEvent["CLEAR_ERROR"] = "CLEAR_ERROR";
|
|
88
|
+
DatePickerEvent["SET_MIN_DATE"] = "SET_MIN_DATE";
|
|
89
|
+
DatePickerEvent["SET_MAX_DATE"] = "SET_MAX_DATE";
|
|
90
|
+
DatePickerEvent["SET_DATE_FORMAT"] = "SET_DATE_FORMAT";
|
|
91
|
+
return DatePickerEvent;
|
|
92
|
+
}({});
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/date-picker/primitives/temporal-utils.ts
|
|
95
|
+
/**
|
|
96
|
+
* Copyright IBM Corp. 2026
|
|
97
|
+
*
|
|
98
|
+
* This source code is licensed under the Apache-2.0 license found in the
|
|
99
|
+
* LICENSE file in the root directory of this source tree.
|
|
100
|
+
*/
|
|
101
|
+
/**
|
|
102
|
+
* Temporal API utilities for date picker
|
|
103
|
+
* Uses the modern Temporal API for robust date handling
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* Convert a Date object to Temporal.PlainDate
|
|
107
|
+
*
|
|
108
|
+
* @param {Date} date - JavaScript Date object
|
|
109
|
+
* @returns Temporal.PlainDate
|
|
110
|
+
*/
|
|
111
|
+
function dateToPlainDate(date) {
|
|
112
|
+
return Temporal.PlainDate.from({
|
|
113
|
+
year: date.getFullYear(),
|
|
114
|
+
month: date.getMonth() + 1,
|
|
115
|
+
day: date.getDate()
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Convert Temporal.PlainDate to Date object
|
|
120
|
+
*
|
|
121
|
+
* @param {Temporal.PlainDate} plainDate - Temporal.PlainDate
|
|
122
|
+
* @returns JavaScript Date object
|
|
123
|
+
*/
|
|
124
|
+
function plainDateToDate(plainDate) {
|
|
125
|
+
return new Date(plainDate.year, plainDate.month - 1, plainDate.day);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Convert Temporal.PlainDate to ISO date string (YYYY-MM-DD)
|
|
129
|
+
*
|
|
130
|
+
* @param {Temporal.PlainDate} plainDate - Temporal.PlainDate
|
|
131
|
+
* @returns ISO date string
|
|
132
|
+
*/
|
|
133
|
+
function plainDateToISOString(plainDate) {
|
|
134
|
+
return plainDate.toString();
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Parse ISO date string to Temporal.PlainDate
|
|
138
|
+
*
|
|
139
|
+
* @param {string} isoString - ISO date string (YYYY-MM-DD)
|
|
140
|
+
* @returns Temporal.PlainDate or null if invalid
|
|
141
|
+
*/
|
|
142
|
+
function parseISOToPlainDate(isoString) {
|
|
143
|
+
try {
|
|
144
|
+
return Temporal.PlainDate.from(isoString);
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Parse a date string in various formats to Temporal.PlainDate
|
|
151
|
+
* Supports: ISO (YYYY-MM-DD), US (MM/DD/YYYY), and JavaScript Date objects
|
|
152
|
+
*
|
|
153
|
+
* @param {string | Date} dateInput - Date string or Date object
|
|
154
|
+
* @returns Temporal.PlainDate or null if invalid
|
|
155
|
+
*/
|
|
156
|
+
function parseDateToPlainDate(dateInput) {
|
|
157
|
+
if (!dateInput) return null;
|
|
158
|
+
if (dateInput instanceof Date) return dateToPlainDate(dateInput);
|
|
159
|
+
try {
|
|
160
|
+
return Temporal.PlainDate.from(dateInput);
|
|
161
|
+
} catch {
|
|
162
|
+
const parts = dateInput.split("/");
|
|
163
|
+
if (parts.length === 3) {
|
|
164
|
+
const month = parseInt(parts[0], 10);
|
|
165
|
+
const day = parseInt(parts[1], 10);
|
|
166
|
+
const year = parseInt(parts[2], 10);
|
|
167
|
+
if (!isNaN(month) && !isNaN(day) && !isNaN(year)) try {
|
|
168
|
+
return Temporal.PlainDate.from({
|
|
169
|
+
year,
|
|
170
|
+
month,
|
|
171
|
+
day
|
|
172
|
+
});
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Compare two Temporal.PlainDate objects
|
|
182
|
+
*
|
|
183
|
+
* @param {Temporal.PlainDate} date1 - First date
|
|
184
|
+
* @param {Temporal.PlainDate} date2 - Second date
|
|
185
|
+
* @returns -1 if date1 < date2, 0 if equal, 1 if date1 > date2
|
|
186
|
+
*/
|
|
187
|
+
function comparePlainDates(date1, date2) {
|
|
188
|
+
return Temporal.PlainDate.compare(date1, date2);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Check if a date is within a range
|
|
192
|
+
*
|
|
193
|
+
* @param {Temporal.PlainDate} date - Date to check
|
|
194
|
+
* @param {Temporal.PlainDate | null} minDate - Minimum date (inclusive)
|
|
195
|
+
* @param {Temporal.PlainDate | null} maxDate - Maximum date (inclusive)
|
|
196
|
+
* @returns True if date is within range
|
|
197
|
+
*/
|
|
198
|
+
function isDateInRange(date, minDate, maxDate) {
|
|
199
|
+
if (minDate && comparePlainDates(date, minDate) < 0) return false;
|
|
200
|
+
if (maxDate && comparePlainDates(date, maxDate) > 0) return false;
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Format a Temporal.PlainDate according to a format string
|
|
205
|
+
* Supports Flatpickr-compatible format tokens:
|
|
206
|
+
* - Y: 4-digit year (e.g., 2026)
|
|
207
|
+
* - y: 2-digit year (e.g., 26)
|
|
208
|
+
* - m: 2-digit month with leading zero (01-12)
|
|
209
|
+
* - n: month without leading zero (1-12)
|
|
210
|
+
* - d: 2-digit day with leading zero (01-31)
|
|
211
|
+
* - j: day without leading zero (1-31)
|
|
212
|
+
*
|
|
213
|
+
* @param {Temporal.PlainDate} date - Date to format
|
|
214
|
+
* @param {string} format - Format string (e.g., 'd/m/Y', 'm/d/Y', 'Y-m-d')
|
|
215
|
+
* @returns Formatted date string
|
|
216
|
+
*/
|
|
217
|
+
function formatPlainDate(date, format) {
|
|
218
|
+
const year4 = date.year.toString();
|
|
219
|
+
const year2 = year4.slice(-2);
|
|
220
|
+
const month2 = date.month.toString().padStart(2, "0");
|
|
221
|
+
const month1 = date.month.toString();
|
|
222
|
+
const day2 = date.day.toString().padStart(2, "0");
|
|
223
|
+
const day1 = date.day.toString();
|
|
224
|
+
return format.replace(/Y/g, year4).replace(/y/g, year2).replace(/m/g, month2).replace(/n/g, month1).replace(/d/g, day2).replace(/j/g, day1);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Get today's date as Temporal.PlainDate
|
|
228
|
+
*
|
|
229
|
+
* @returns Today's date
|
|
230
|
+
*/
|
|
231
|
+
function getToday() {
|
|
232
|
+
return Temporal.Now.plainDateISO();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Add days to a date
|
|
236
|
+
*
|
|
237
|
+
* @param {Temporal.PlainDate} date - Starting date
|
|
238
|
+
* @param {number} days - Number of days to add (can be negative)
|
|
239
|
+
* @returns New date
|
|
240
|
+
*/
|
|
241
|
+
function addDays(date, days) {
|
|
242
|
+
return date.add({ days });
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Add months to a date
|
|
246
|
+
*
|
|
247
|
+
* @param {Temporal.PlainDate} date - Starting date
|
|
248
|
+
* @param {number} months - Number of months to add (can be negative)
|
|
249
|
+
* @returns New date
|
|
250
|
+
*/
|
|
251
|
+
function addMonths(date, months) {
|
|
252
|
+
return date.add({ months });
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Get the number of days between two dates
|
|
256
|
+
*
|
|
257
|
+
* @param {Temporal.PlainDate} date1 - First date
|
|
258
|
+
* @param {Temporal.PlainDate} date2 - Second date
|
|
259
|
+
* @returns Number of days (positive if date2 is after date1)
|
|
260
|
+
*/
|
|
261
|
+
function daysBetween(date1, date2) {
|
|
262
|
+
return date1.until(date2).days;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Check if two dates are equal
|
|
266
|
+
*
|
|
267
|
+
* @param {Temporal.PlainDate} date1 - First date
|
|
268
|
+
* @param {Temporal.PlainDate} date2 - Second date
|
|
269
|
+
* @returns True if dates are equal
|
|
270
|
+
*/
|
|
271
|
+
function areDatesEqual(date1, date2) {
|
|
272
|
+
if (date1 === null && date2 === null) return true;
|
|
273
|
+
if (date1 === null || date2 === null) return false;
|
|
274
|
+
return comparePlainDates(date1, date2) === 0;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Get the start of the month for a given date
|
|
278
|
+
*
|
|
279
|
+
* @param {Temporal.PlainDate} date - Input date
|
|
280
|
+
* @returns First day of the month
|
|
281
|
+
*/
|
|
282
|
+
function getMonthStart(date) {
|
|
283
|
+
return date.with({ day: 1 });
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get the end of the month for a given date
|
|
287
|
+
*
|
|
288
|
+
* @param {Temporal.PlainDate} date - Input date
|
|
289
|
+
* @returns Last day of the month
|
|
290
|
+
*/
|
|
291
|
+
function getMonthEnd(date) {
|
|
292
|
+
return date.with({ day: date.daysInMonth });
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Check if a date is today
|
|
296
|
+
*
|
|
297
|
+
* @param {Temporal.PlainDate} date - Date to check
|
|
298
|
+
* @returns True if date is today
|
|
299
|
+
*/
|
|
300
|
+
function isToday(date) {
|
|
301
|
+
return areDatesEqual(date, getToday());
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Check if a date is in the past
|
|
305
|
+
*
|
|
306
|
+
* @param {Temporal.PlainDate} date - Date to check
|
|
307
|
+
* @returns True if date is before today
|
|
308
|
+
*/
|
|
309
|
+
function isPast(date) {
|
|
310
|
+
return comparePlainDates(date, getToday()) < 0;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Check if a date is in the future
|
|
314
|
+
*
|
|
315
|
+
* @param {Temporal.PlainDate} date - Date to check
|
|
316
|
+
* @returns True if date is after today
|
|
317
|
+
*/
|
|
318
|
+
function isFuture(date) {
|
|
319
|
+
return comparePlainDates(date, getToday()) > 0;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Parse a date string with a specific format
|
|
323
|
+
* Supports common format tokens: Y, m, d
|
|
324
|
+
*
|
|
325
|
+
* @param {string} dateString - Date string to parse
|
|
326
|
+
* @param {string} format - Format string (e.g., 'm/d/Y', 'Y-m-d')
|
|
327
|
+
* @returns Temporal.PlainDate or null if invalid
|
|
328
|
+
*/
|
|
329
|
+
function parseDateString(dateString, format) {
|
|
330
|
+
try {
|
|
331
|
+
const formatParts = format.split(/[^YmdHis]/);
|
|
332
|
+
const dateParts = dateString.split(/[^0-9]/);
|
|
333
|
+
if (formatParts.length !== dateParts.length) return null;
|
|
334
|
+
let year = 0;
|
|
335
|
+
let month = 0;
|
|
336
|
+
let day = 0;
|
|
337
|
+
formatParts.forEach((part, index) => {
|
|
338
|
+
const value = parseInt(dateParts[index], 10);
|
|
339
|
+
if (part === "Y") year = value;
|
|
340
|
+
else if (part === "m") month = value;
|
|
341
|
+
else if (part === "d") day = value;
|
|
342
|
+
});
|
|
343
|
+
return Temporal.PlainDate.from({
|
|
344
|
+
year,
|
|
345
|
+
month,
|
|
346
|
+
day
|
|
347
|
+
});
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Polyfill check for Temporal API
|
|
354
|
+
*
|
|
355
|
+
* @returns True if Temporal API is available
|
|
356
|
+
*/
|
|
357
|
+
function isTemporalAvailable() {
|
|
358
|
+
return typeof Temporal !== "undefined" && typeof Temporal.PlainDate !== "undefined";
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Get a fallback date handler if Temporal is not available
|
|
362
|
+
* This provides a migration path for browsers without Temporal support
|
|
363
|
+
*/
|
|
364
|
+
function getDateHandler() {
|
|
365
|
+
if (isTemporalAvailable()) return {
|
|
366
|
+
type: "temporal",
|
|
367
|
+
toISOString: plainDateToISOString,
|
|
368
|
+
fromISOString: parseISOToPlainDate,
|
|
369
|
+
compare: comparePlainDates,
|
|
370
|
+
format: formatPlainDate,
|
|
371
|
+
isInRange: isDateInRange
|
|
372
|
+
};
|
|
373
|
+
return {
|
|
374
|
+
type: "date",
|
|
375
|
+
/**
|
|
376
|
+
*
|
|
377
|
+
* @param {Temporal.PlainDate} date - The date to convert
|
|
378
|
+
*/
|
|
379
|
+
toISOString: (date) => date.toISOString().split("T")[0],
|
|
380
|
+
/**
|
|
381
|
+
*
|
|
382
|
+
* @param {string} str - The ISO string to parse
|
|
383
|
+
*/
|
|
384
|
+
fromISOString: (str) => {
|
|
385
|
+
const date = new Date(str);
|
|
386
|
+
return isNaN(date.getTime()) ? null : date;
|
|
387
|
+
},
|
|
388
|
+
/**
|
|
389
|
+
*
|
|
390
|
+
* @param {Temporal.PlainDate} d1 - First date
|
|
391
|
+
* @param {Temporal.PlainDate} d2 - Second date
|
|
392
|
+
*/
|
|
393
|
+
compare: (d1, d2) => d1.getTime() - d2.getTime(),
|
|
394
|
+
/**
|
|
395
|
+
*
|
|
396
|
+
* @param {Temporal.PlainDate} date - The date to format
|
|
397
|
+
* @param {string} format - The format string
|
|
398
|
+
*/
|
|
399
|
+
format: (date, format) => {
|
|
400
|
+
const year = date.getFullYear().toString();
|
|
401
|
+
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
|
402
|
+
const day = date.getDate().toString().padStart(2, "0");
|
|
403
|
+
return format.replace("Y", year).replace("m", month).replace("d", day);
|
|
404
|
+
},
|
|
405
|
+
/**
|
|
406
|
+
*
|
|
407
|
+
* @param {Temporal.PlainDate} date - The date to check
|
|
408
|
+
* @param {Temporal.PlainDate | null} min - Minimum date
|
|
409
|
+
* @param {Temporal.PlainDate | null} max - Maximum date
|
|
410
|
+
*/
|
|
411
|
+
isInRange: (date, min, max) => {
|
|
412
|
+
if (min && date < min) return false;
|
|
413
|
+
if (max && date > max) return false;
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region src/date-picker/primitives/guards.ts
|
|
420
|
+
/**
|
|
421
|
+
* Check if the component is interactive (not disabled or readonly)
|
|
422
|
+
*
|
|
423
|
+
* @param {DatePickerContext} context - The current context
|
|
424
|
+
* @returns {boolean} True if interactive
|
|
425
|
+
*/
|
|
426
|
+
const isInteractive = (context) => {
|
|
427
|
+
return !context.isDisabled && !context.isReadonly;
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* Check if component is in range mode
|
|
431
|
+
*
|
|
432
|
+
* @param {DatePickerContext} context - The current context
|
|
433
|
+
* @returns {boolean} True if in range mode
|
|
434
|
+
*/
|
|
435
|
+
const isRangeMode = (context) => {
|
|
436
|
+
return context.mode === "range";
|
|
437
|
+
};
|
|
438
|
+
/**
|
|
439
|
+
* Check if component is in single mode
|
|
440
|
+
* @param {DatePickerContext} context - Current context
|
|
441
|
+
* @returns {boolean} True if in single mode
|
|
442
|
+
*/
|
|
443
|
+
const isSingleMode = (context) => {
|
|
444
|
+
return context.mode === "single";
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* Guards for state transitions
|
|
448
|
+
*/
|
|
449
|
+
const guards = {
|
|
450
|
+
["idle"]: {},
|
|
451
|
+
["focused"]: { INPUT_FOCUS: isInteractive },
|
|
452
|
+
["calendar_open"]: {
|
|
453
|
+
CALENDAR_OPEN: isInteractive,
|
|
454
|
+
CALENDAR_ICON_CLICK: isInteractive,
|
|
455
|
+
TAB_KEY: isInteractive
|
|
456
|
+
},
|
|
457
|
+
["selecting_start"]: {
|
|
458
|
+
/**
|
|
459
|
+
* Guard for calendar open in range mode
|
|
460
|
+
*
|
|
461
|
+
* @param {DatePickerContext} context - The current context
|
|
462
|
+
* @returns {boolean} True if allowed
|
|
463
|
+
*/
|
|
464
|
+
CALENDAR_OPEN: (context) => isInteractive(context) && isRangeMode(context) },
|
|
465
|
+
["selecting_end"]: {
|
|
466
|
+
/**
|
|
467
|
+
* Guard for range start selection
|
|
468
|
+
*
|
|
469
|
+
* @param {DatePickerContext} context - The current context
|
|
470
|
+
* @param {DatePickerEvent} event - The event
|
|
471
|
+
* @returns {boolean} True if allowed
|
|
472
|
+
*/
|
|
473
|
+
RANGE_START_SELECT: (context, event) => {
|
|
474
|
+
if (!isInteractive(context) || !isRangeMode(context)) return false;
|
|
475
|
+
const payload = event.payload;
|
|
476
|
+
if (!payload?.date) return false;
|
|
477
|
+
return isDateInRange(payload.date, context.minDate, context.maxDate);
|
|
478
|
+
} },
|
|
479
|
+
["date_selected"]: {
|
|
480
|
+
/**
|
|
481
|
+
* Guard for date selection
|
|
482
|
+
*
|
|
483
|
+
* @param {DatePickerContext} context - The current context
|
|
484
|
+
* @param {DatePickerEvent} event - The event
|
|
485
|
+
* @returns {boolean} True if allowed
|
|
486
|
+
*/
|
|
487
|
+
DATE_SELECT: (context, event) => {
|
|
488
|
+
if (!isInteractive(context)) return false;
|
|
489
|
+
const payload = event.payload;
|
|
490
|
+
if (!payload?.date) return false;
|
|
491
|
+
return isDateInRange(payload.date, context.minDate, context.maxDate);
|
|
492
|
+
},
|
|
493
|
+
/**
|
|
494
|
+
* Guard for range end selection
|
|
495
|
+
*
|
|
496
|
+
* @param {DatePickerContext} context - The current context
|
|
497
|
+
* @param {DatePickerEvent} event - The event
|
|
498
|
+
* @returns {boolean} True if allowed
|
|
499
|
+
*/
|
|
500
|
+
RANGE_END_SELECT: (context, event) => {
|
|
501
|
+
if (!isInteractive(context) || !isRangeMode(context)) return false;
|
|
502
|
+
const payload = event.payload;
|
|
503
|
+
if (!payload?.date) return false;
|
|
504
|
+
if (!context.startDate) return false;
|
|
505
|
+
return isDateInRange(payload.date, context.minDate, context.maxDate);
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
["disabled"]: {},
|
|
509
|
+
["readonly"]: {},
|
|
510
|
+
["error"]: {}
|
|
511
|
+
};
|
|
512
|
+
/**
|
|
513
|
+
* Get guard for a specific state and event
|
|
514
|
+
*
|
|
515
|
+
* @param {DatePickerState} state - The current state
|
|
516
|
+
* @param {string} eventType - The event type
|
|
517
|
+
* @returns {StateGuard | undefined} The guard function or undefined
|
|
518
|
+
*/
|
|
519
|
+
function getGuard(state, eventType) {
|
|
520
|
+
return guards[state]?.[eventType];
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Check if a transition is guarded and allowed
|
|
524
|
+
*
|
|
525
|
+
* @param {DatePickerState} state - The current state
|
|
526
|
+
* @param {string} eventType - The event type
|
|
527
|
+
* @param {DatePickerContext} context - The current context
|
|
528
|
+
* @param {DatePickerEvent} event - The event
|
|
529
|
+
* @returns {boolean} True if guard passes
|
|
530
|
+
*/
|
|
531
|
+
function checkGuard(state, eventType, context, event) {
|
|
532
|
+
const guard = getGuard(state, eventType);
|
|
533
|
+
if (!guard) return true;
|
|
534
|
+
return guard(context, event);
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/date-picker/primitives/actions.ts
|
|
538
|
+
/**
|
|
539
|
+
* Actions for state transitions
|
|
540
|
+
*/
|
|
541
|
+
const actions = {
|
|
542
|
+
["idle"]: {
|
|
543
|
+
/**
|
|
544
|
+
* Action for CALENDAR_ICON_CLICK event
|
|
545
|
+
*
|
|
546
|
+
* @param {DatePickerContext} context - Current context
|
|
547
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
548
|
+
*/
|
|
549
|
+
CALENDAR_ICON_CLICK: (context) => {
|
|
550
|
+
return {
|
|
551
|
+
isOpen: true,
|
|
552
|
+
viewDate: context.viewDate || context.startDate || Temporal.Now.plainDateISO(),
|
|
553
|
+
focusedDate: context.focusedDate || context.startDate || null
|
|
554
|
+
};
|
|
555
|
+
},
|
|
556
|
+
/**
|
|
557
|
+
* Action for INPUT_FOCUS event
|
|
558
|
+
*
|
|
559
|
+
* @param {DatePickerContext} _context - The current context (unused)
|
|
560
|
+
* @param {DatePickerEvent} event - The event
|
|
561
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
562
|
+
*/
|
|
563
|
+
INPUT_FOCUS: (_context, event) => {
|
|
564
|
+
return {
|
|
565
|
+
isFocused: true,
|
|
566
|
+
lastFocusedInput: event.payload?.inputType || "from"
|
|
567
|
+
};
|
|
568
|
+
},
|
|
569
|
+
/** Action for INPUT_BLUR event */
|
|
570
|
+
INPUT_BLUR: () => ({ isFocused: false }),
|
|
571
|
+
/** Action for OUTSIDE_CLICK event */
|
|
572
|
+
OUTSIDE_CLICK: () => ({
|
|
573
|
+
isOpen: false,
|
|
574
|
+
isFocused: false,
|
|
575
|
+
restoreFocusTo: null,
|
|
576
|
+
shouldRestoreFocus: false
|
|
577
|
+
}),
|
|
578
|
+
/**
|
|
579
|
+
* Action for CALENDAR_CLOSE event
|
|
580
|
+
*
|
|
581
|
+
* @param {DatePickerContext} context - Current context
|
|
582
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
583
|
+
*/
|
|
584
|
+
CALENDAR_CLOSE: (context) => ({
|
|
585
|
+
isOpen: false,
|
|
586
|
+
restoreFocusTo: context.shouldRestoreFocus ? context.restoreFocusTo : null,
|
|
587
|
+
shouldRestoreFocus: context.shouldRestoreFocus
|
|
588
|
+
})
|
|
589
|
+
},
|
|
590
|
+
["focused"]: {
|
|
591
|
+
/**
|
|
592
|
+
* Action for INPUT_FOCUS event
|
|
593
|
+
*
|
|
594
|
+
* @param {DatePickerContext} _context - The current context (unused)
|
|
595
|
+
* @param {DatePickerEvent} event - The event
|
|
596
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
597
|
+
*/
|
|
598
|
+
INPUT_FOCUS: (_context, event) => {
|
|
599
|
+
return {
|
|
600
|
+
isFocused: true,
|
|
601
|
+
lastFocusedInput: event.payload?.inputType || "from"
|
|
602
|
+
};
|
|
603
|
+
},
|
|
604
|
+
/**
|
|
605
|
+
* Action for CALENDAR_OPEN event from FOCUSED state
|
|
606
|
+
*
|
|
607
|
+
* @param {DatePickerContext} context - Current context
|
|
608
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
609
|
+
*/
|
|
610
|
+
CALENDAR_OPEN: (context) => {
|
|
611
|
+
const focusedDate = context.startDate || Temporal.Now.plainDateISO();
|
|
612
|
+
return {
|
|
613
|
+
isOpen: true,
|
|
614
|
+
viewDate: focusedDate,
|
|
615
|
+
focusedDate
|
|
616
|
+
};
|
|
617
|
+
},
|
|
618
|
+
/**
|
|
619
|
+
* Action for CALENDAR_ICON_CLICK event from FOCUSED state
|
|
620
|
+
* Behaves identically to CALENDAR_OPEN — opens the calendar preserving
|
|
621
|
+
* any existing viewDate/focusedDate context.
|
|
622
|
+
*
|
|
623
|
+
* @param {DatePickerContext} context - Current context
|
|
624
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
625
|
+
*/
|
|
626
|
+
CALENDAR_ICON_CLICK: (context) => {
|
|
627
|
+
return {
|
|
628
|
+
isOpen: true,
|
|
629
|
+
viewDate: context.viewDate || context.startDate || Temporal.Now.plainDateISO(),
|
|
630
|
+
focusedDate: context.focusedDate || context.startDate || null
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
["calendar_open"]: {
|
|
635
|
+
/**
|
|
636
|
+
* Action for CALENDAR_OPEN event
|
|
637
|
+
*
|
|
638
|
+
* @param {DatePickerContext} context - Current context
|
|
639
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
640
|
+
*/
|
|
641
|
+
CALENDAR_OPEN: (context) => {
|
|
642
|
+
const viewDate = context.viewDate || context.startDate || Temporal.Now.plainDateISO();
|
|
643
|
+
return {
|
|
644
|
+
isOpen: true,
|
|
645
|
+
viewDate,
|
|
646
|
+
focusedDate: context.startDate || viewDate
|
|
647
|
+
};
|
|
648
|
+
},
|
|
649
|
+
/**
|
|
650
|
+
* Action for OUTSIDE_CLICK event
|
|
651
|
+
* Close the calendar when clicking outside
|
|
652
|
+
*
|
|
653
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
654
|
+
*/
|
|
655
|
+
OUTSIDE_CLICK: () => ({
|
|
656
|
+
isOpen: false,
|
|
657
|
+
isFocused: false,
|
|
658
|
+
restoreFocusTo: null,
|
|
659
|
+
shouldRestoreFocus: false
|
|
660
|
+
}),
|
|
661
|
+
/**
|
|
662
|
+
* Action for CALENDAR_ICON_CLICK event
|
|
663
|
+
*
|
|
664
|
+
* @param {DatePickerContext} context - Current context
|
|
665
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
666
|
+
*/
|
|
667
|
+
CALENDAR_ICON_CLICK: (context) => {
|
|
668
|
+
const viewDate = context.viewDate || context.startDate || Temporal.Now.plainDateISO();
|
|
669
|
+
return {
|
|
670
|
+
isOpen: true,
|
|
671
|
+
viewDate,
|
|
672
|
+
focusedDate: context.startDate || viewDate
|
|
673
|
+
};
|
|
674
|
+
},
|
|
675
|
+
/**
|
|
676
|
+
* Action for RANGE_START_SELECT event
|
|
677
|
+
*
|
|
678
|
+
* @param {DatePickerContext} _context - Current context
|
|
679
|
+
* @param {DatePickerEvent} event - The event
|
|
680
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
681
|
+
*/
|
|
682
|
+
RANGE_START_SELECT: (_context, event) => {
|
|
683
|
+
const startDate = event.payload?.date;
|
|
684
|
+
if (!startDate) return {};
|
|
685
|
+
return {
|
|
686
|
+
startDate,
|
|
687
|
+
endDate: null,
|
|
688
|
+
value: plainDateToISOString(startDate),
|
|
689
|
+
viewDate: startDate,
|
|
690
|
+
focusedDate: startDate,
|
|
691
|
+
isOpen: true,
|
|
692
|
+
restoreFocusTo: "from",
|
|
693
|
+
shouldRestoreFocus: false
|
|
694
|
+
};
|
|
695
|
+
},
|
|
696
|
+
/**
|
|
697
|
+
* Action for DATE_SELECT event (single mode)
|
|
698
|
+
*
|
|
699
|
+
* @param {DatePickerContext} _context - Current context
|
|
700
|
+
* @param {DatePickerEvent} event - The event
|
|
701
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
702
|
+
*/
|
|
703
|
+
DATE_SELECT: (_context, event) => {
|
|
704
|
+
const startDate = event.payload?.date;
|
|
705
|
+
if (!startDate) return {};
|
|
706
|
+
return {
|
|
707
|
+
startDate,
|
|
708
|
+
value: plainDateToISOString(startDate),
|
|
709
|
+
isOpen: _context.closeOnSelect ? false : _context.isOpen,
|
|
710
|
+
restoreFocusTo: _context.lastFocusedInput || "from",
|
|
711
|
+
shouldRestoreFocus: _context.closeOnSelect
|
|
712
|
+
};
|
|
713
|
+
},
|
|
714
|
+
/**
|
|
715
|
+
* Action for PREV_MONTH event
|
|
716
|
+
*
|
|
717
|
+
* @param {DatePickerContext} context - Current context
|
|
718
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
719
|
+
*/
|
|
720
|
+
PREV_MONTH: (context) => {
|
|
721
|
+
if (!context.viewDate) return {};
|
|
722
|
+
return {
|
|
723
|
+
viewDate: context.viewDate.add({ months: -1 }),
|
|
724
|
+
focusedDate: context.focusedDate ? context.focusedDate.add({ months: -1 }) : null
|
|
725
|
+
};
|
|
726
|
+
},
|
|
727
|
+
/**
|
|
728
|
+
* Action for NEXT_MONTH event
|
|
729
|
+
*
|
|
730
|
+
* @param {DatePickerContext} context - Current context
|
|
731
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
732
|
+
*/
|
|
733
|
+
NEXT_MONTH: (context) => {
|
|
734
|
+
if (!context.viewDate) return {};
|
|
735
|
+
return {
|
|
736
|
+
viewDate: context.viewDate.add({ months: 1 }),
|
|
737
|
+
focusedDate: context.focusedDate ? context.focusedDate.add({ months: 1 }) : null
|
|
738
|
+
};
|
|
739
|
+
},
|
|
740
|
+
/**
|
|
741
|
+
* Action for PREV_YEAR event
|
|
742
|
+
*
|
|
743
|
+
* @param {DatePickerContext} context - Current context
|
|
744
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
745
|
+
*/
|
|
746
|
+
PREV_YEAR: (context) => {
|
|
747
|
+
if (!context.viewDate) return {};
|
|
748
|
+
return { viewDate: context.viewDate.add({ years: -1 }) };
|
|
749
|
+
},
|
|
750
|
+
/**
|
|
751
|
+
* Action for NEXT_YEAR event
|
|
752
|
+
*
|
|
753
|
+
* @param {DatePickerContext} context - Current context
|
|
754
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
755
|
+
*/
|
|
756
|
+
NEXT_YEAR: (context) => {
|
|
757
|
+
if (!context.viewDate) return {};
|
|
758
|
+
return { viewDate: context.viewDate.add({ years: 1 }) };
|
|
759
|
+
},
|
|
760
|
+
/** Action for GO_TO_TODAY event */
|
|
761
|
+
GO_TO_TODAY: () => ({ viewDate: Temporal.Now.plainDateISO() }),
|
|
762
|
+
/**
|
|
763
|
+
* Action for ESCAPE_KEY event - close calendar
|
|
764
|
+
*
|
|
765
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
766
|
+
*/
|
|
767
|
+
ESCAPE_KEY: () => ({ isOpen: false }),
|
|
768
|
+
/**
|
|
769
|
+
* Action for TAB_KEY event - close calendar
|
|
770
|
+
*
|
|
771
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
772
|
+
*/
|
|
773
|
+
TAB_KEY: () => ({ isOpen: false }),
|
|
774
|
+
/**
|
|
775
|
+
* Action for ENTER_KEY event - select focused date
|
|
776
|
+
*
|
|
777
|
+
* @param {DatePickerContext} context - Current context
|
|
778
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
779
|
+
*/
|
|
780
|
+
ENTER_KEY: (context) => {
|
|
781
|
+
if (!context.focusedDate) return {};
|
|
782
|
+
return {
|
|
783
|
+
startDate: context.focusedDate,
|
|
784
|
+
value: plainDateToISOString(context.focusedDate),
|
|
785
|
+
isOpen: context.closeOnSelect ? false : context.isOpen,
|
|
786
|
+
restoreFocusTo: context.lastFocusedInput || "from",
|
|
787
|
+
shouldRestoreFocus: context.closeOnSelect
|
|
788
|
+
};
|
|
789
|
+
},
|
|
790
|
+
/**
|
|
791
|
+
* Action for ARROW_UP event - move focus up one week
|
|
792
|
+
*
|
|
793
|
+
* @param {DatePickerContext} context - Current context
|
|
794
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
795
|
+
*/
|
|
796
|
+
ARROW_UP: (context) => {
|
|
797
|
+
const focusedDate = context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO();
|
|
798
|
+
let newFocusedDate = focusedDate.add({ days: -7 });
|
|
799
|
+
if (context.minDate && Temporal.PlainDate.compare(newFocusedDate, context.minDate) < 0) newFocusedDate = context.minDate;
|
|
800
|
+
const viewDate = newFocusedDate.month !== focusedDate.month ? newFocusedDate : context.viewDate;
|
|
801
|
+
return {
|
|
802
|
+
focusedDate: newFocusedDate,
|
|
803
|
+
viewDate
|
|
804
|
+
};
|
|
805
|
+
},
|
|
806
|
+
/**
|
|
807
|
+
* Action for ARROW_DOWN event - move focus down one week
|
|
808
|
+
*
|
|
809
|
+
* @param {DatePickerContext} context - Current context
|
|
810
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
811
|
+
*/
|
|
812
|
+
ARROW_DOWN: (context) => {
|
|
813
|
+
const focusedDate = context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO();
|
|
814
|
+
let newFocusedDate = focusedDate.add({ days: 7 });
|
|
815
|
+
if (context.maxDate && Temporal.PlainDate.compare(newFocusedDate, context.maxDate) > 0) newFocusedDate = context.maxDate;
|
|
816
|
+
const viewDate = newFocusedDate.month !== focusedDate.month ? newFocusedDate : context.viewDate;
|
|
817
|
+
return {
|
|
818
|
+
focusedDate: newFocusedDate,
|
|
819
|
+
viewDate
|
|
820
|
+
};
|
|
821
|
+
},
|
|
822
|
+
/**
|
|
823
|
+
* Action for ARROW_LEFT event - move focus left one day
|
|
824
|
+
*
|
|
825
|
+
* @param {DatePickerContext} context - Current context
|
|
826
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
827
|
+
*/
|
|
828
|
+
ARROW_LEFT: (context) => {
|
|
829
|
+
const focusedDate = context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO();
|
|
830
|
+
let newFocusedDate = focusedDate.add({ days: -1 });
|
|
831
|
+
if (context.minDate && Temporal.PlainDate.compare(newFocusedDate, context.minDate) < 0) newFocusedDate = context.minDate;
|
|
832
|
+
const viewDate = newFocusedDate.month !== focusedDate.month ? newFocusedDate : context.viewDate;
|
|
833
|
+
return {
|
|
834
|
+
focusedDate: newFocusedDate,
|
|
835
|
+
viewDate
|
|
836
|
+
};
|
|
837
|
+
},
|
|
838
|
+
/**
|
|
839
|
+
* Action for ARROW_RIGHT event - move focus right one day
|
|
840
|
+
*
|
|
841
|
+
* @param {DatePickerContext} context - Current context
|
|
842
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
843
|
+
*/
|
|
844
|
+
ARROW_RIGHT: (context) => {
|
|
845
|
+
const focusedDate = context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO();
|
|
846
|
+
let newFocusedDate = focusedDate.add({ days: 1 });
|
|
847
|
+
if (context.maxDate && Temporal.PlainDate.compare(newFocusedDate, context.maxDate) > 0) newFocusedDate = context.maxDate;
|
|
848
|
+
const viewDate = newFocusedDate.month !== focusedDate.month ? newFocusedDate : context.viewDate;
|
|
849
|
+
return {
|
|
850
|
+
focusedDate: newFocusedDate,
|
|
851
|
+
viewDate
|
|
852
|
+
};
|
|
853
|
+
},
|
|
854
|
+
/**
|
|
855
|
+
* Action for PAGE_UP event - move to previous month
|
|
856
|
+
*
|
|
857
|
+
* @param {DatePickerContext} context - Current context
|
|
858
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
859
|
+
*/
|
|
860
|
+
PAGE_UP: (context) => {
|
|
861
|
+
if (!context.viewDate) return {};
|
|
862
|
+
const newViewDate = context.viewDate.add({ months: -1 });
|
|
863
|
+
let focusedDate = context.focusedDate ? context.focusedDate.add({ months: -1 }) : null;
|
|
864
|
+
if (focusedDate && context.minDate && Temporal.PlainDate.compare(focusedDate, context.minDate) < 0) focusedDate = context.minDate;
|
|
865
|
+
return {
|
|
866
|
+
viewDate: newViewDate,
|
|
867
|
+
focusedDate
|
|
868
|
+
};
|
|
869
|
+
},
|
|
870
|
+
/**
|
|
871
|
+
* Action for PAGE_DOWN event - move to next month
|
|
872
|
+
*
|
|
873
|
+
* @param {DatePickerContext} context - Current context
|
|
874
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
875
|
+
*/
|
|
876
|
+
PAGE_DOWN: (context) => {
|
|
877
|
+
if (!context.viewDate) return {};
|
|
878
|
+
const newViewDate = context.viewDate.add({ months: 1 });
|
|
879
|
+
let focusedDate = context.focusedDate ? context.focusedDate.add({ months: 1 }) : null;
|
|
880
|
+
if (focusedDate && context.maxDate && Temporal.PlainDate.compare(focusedDate, context.maxDate) > 0) focusedDate = context.maxDate;
|
|
881
|
+
return {
|
|
882
|
+
viewDate: newViewDate,
|
|
883
|
+
focusedDate
|
|
884
|
+
};
|
|
885
|
+
},
|
|
886
|
+
/**
|
|
887
|
+
* Action for HOME_KEY event - move to start of week
|
|
888
|
+
*
|
|
889
|
+
* @param {DatePickerContext} context - Current context
|
|
890
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
891
|
+
*/
|
|
892
|
+
HOME_KEY: (context) => {
|
|
893
|
+
const focusedDate = context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO();
|
|
894
|
+
const daysToSubtract = new Date(focusedDate.year, focusedDate.month - 1, focusedDate.day).getDay();
|
|
895
|
+
let newFocusedDate = focusedDate.add({ days: -daysToSubtract });
|
|
896
|
+
if (context.minDate && Temporal.PlainDate.compare(newFocusedDate, context.minDate) < 0) newFocusedDate = context.minDate;
|
|
897
|
+
const viewDate = newFocusedDate.month !== focusedDate.month ? newFocusedDate : context.viewDate;
|
|
898
|
+
return {
|
|
899
|
+
focusedDate: newFocusedDate,
|
|
900
|
+
viewDate
|
|
901
|
+
};
|
|
902
|
+
},
|
|
903
|
+
/**
|
|
904
|
+
* Action for END_KEY event - move to end of week
|
|
905
|
+
*
|
|
906
|
+
* @param {DatePickerContext} context - Current context
|
|
907
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
908
|
+
*/
|
|
909
|
+
END_KEY: (context) => {
|
|
910
|
+
const focusedDate = context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO();
|
|
911
|
+
const daysToAdd = 6 - new Date(focusedDate.year, focusedDate.month - 1, focusedDate.day).getDay();
|
|
912
|
+
let newFocusedDate = focusedDate.add({ days: daysToAdd });
|
|
913
|
+
if (context.maxDate && Temporal.PlainDate.compare(newFocusedDate, context.maxDate) > 0) newFocusedDate = context.maxDate;
|
|
914
|
+
const viewDate = newFocusedDate.month !== focusedDate.month ? newFocusedDate : context.viewDate;
|
|
915
|
+
return {
|
|
916
|
+
focusedDate: newFocusedDate,
|
|
917
|
+
viewDate
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
["selecting_start"]: {
|
|
922
|
+
/**
|
|
923
|
+
* Action for CALENDAR_OPEN event
|
|
924
|
+
*
|
|
925
|
+
* @param {DatePickerContext} context - Current context
|
|
926
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
927
|
+
*/
|
|
928
|
+
CALENDAR_OPEN: (context) => {
|
|
929
|
+
if (context.mode === "range") return {
|
|
930
|
+
isOpen: true,
|
|
931
|
+
startDate: null,
|
|
932
|
+
endDate: null
|
|
933
|
+
};
|
|
934
|
+
return { isOpen: true };
|
|
935
|
+
} },
|
|
936
|
+
["selecting_end"]: {
|
|
937
|
+
/**
|
|
938
|
+
* Action for RANGE_START_SELECT event
|
|
939
|
+
*
|
|
940
|
+
* @param {DatePickerContext} _context - Current context
|
|
941
|
+
* @param {DatePickerEvent} event - The event
|
|
942
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
943
|
+
*/
|
|
944
|
+
RANGE_START_SELECT: (_context, event) => {
|
|
945
|
+
const startDate = event.payload?.date;
|
|
946
|
+
if (!startDate) return {};
|
|
947
|
+
return {
|
|
948
|
+
startDate,
|
|
949
|
+
endDate: null,
|
|
950
|
+
value: plainDateToISOString(startDate),
|
|
951
|
+
restoreFocusTo: "from",
|
|
952
|
+
shouldRestoreFocus: false
|
|
953
|
+
};
|
|
954
|
+
},
|
|
955
|
+
/**
|
|
956
|
+
* Action for RANGE_END_SELECT event
|
|
957
|
+
*
|
|
958
|
+
* @param {DatePickerContext} context - Current context
|
|
959
|
+
* @param {DatePickerEvent} event - The event
|
|
960
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
961
|
+
*/
|
|
962
|
+
RANGE_END_SELECT: (context, event) => {
|
|
963
|
+
const endDate = event.payload?.date;
|
|
964
|
+
const { startDate } = context;
|
|
965
|
+
if (!endDate || !startDate) return {};
|
|
966
|
+
let finalStartDate = startDate;
|
|
967
|
+
let finalEndDate = endDate;
|
|
968
|
+
if (comparePlainDates(endDate, startDate) < 0) {
|
|
969
|
+
finalStartDate = endDate;
|
|
970
|
+
finalEndDate = startDate;
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
startDate: finalStartDate,
|
|
974
|
+
endDate: finalEndDate,
|
|
975
|
+
value: `${plainDateToISOString(finalStartDate)}/${plainDateToISOString(finalEndDate)}`,
|
|
976
|
+
isOpen: false,
|
|
977
|
+
lastFocusedInput: "to",
|
|
978
|
+
restoreFocusTo: "to",
|
|
979
|
+
shouldRestoreFocus: true
|
|
980
|
+
};
|
|
981
|
+
},
|
|
982
|
+
/**
|
|
983
|
+
* Action for ESCAPE_KEY event - close calendar
|
|
984
|
+
*
|
|
985
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
986
|
+
*/
|
|
987
|
+
ESCAPE_KEY: () => ({
|
|
988
|
+
isOpen: false,
|
|
989
|
+
restoreFocusTo: null,
|
|
990
|
+
shouldRestoreFocus: false
|
|
991
|
+
}),
|
|
992
|
+
/**
|
|
993
|
+
* Action for TAB_KEY event - close calendar
|
|
994
|
+
*
|
|
995
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
996
|
+
*/
|
|
997
|
+
TAB_KEY: () => ({
|
|
998
|
+
isOpen: false,
|
|
999
|
+
restoreFocusTo: null,
|
|
1000
|
+
shouldRestoreFocus: false
|
|
1001
|
+
}),
|
|
1002
|
+
/**
|
|
1003
|
+
* Action for ARROW_UP event - move focus up one week
|
|
1004
|
+
*
|
|
1005
|
+
* @param {DatePickerContext} context - Current context
|
|
1006
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1007
|
+
*/
|
|
1008
|
+
ARROW_UP: (context) => {
|
|
1009
|
+
const focusedDate = context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO();
|
|
1010
|
+
let newFocusedDate = focusedDate.add({ days: -7 });
|
|
1011
|
+
if (context.minDate && Temporal.PlainDate.compare(newFocusedDate, context.minDate) < 0) newFocusedDate = context.minDate;
|
|
1012
|
+
const viewDate = newFocusedDate.month !== focusedDate.month ? newFocusedDate : context.viewDate;
|
|
1013
|
+
return {
|
|
1014
|
+
focusedDate: newFocusedDate,
|
|
1015
|
+
viewDate
|
|
1016
|
+
};
|
|
1017
|
+
},
|
|
1018
|
+
/**
|
|
1019
|
+
* Action for ARROW_DOWN event - move focus down one week
|
|
1020
|
+
*
|
|
1021
|
+
* @param {DatePickerContext} context - Current context
|
|
1022
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1023
|
+
*/
|
|
1024
|
+
ARROW_DOWN: (context) => {
|
|
1025
|
+
const focusedDate = context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO();
|
|
1026
|
+
let newFocusedDate = focusedDate.add({ days: 7 });
|
|
1027
|
+
if (context.maxDate && Temporal.PlainDate.compare(newFocusedDate, context.maxDate) > 0) newFocusedDate = context.maxDate;
|
|
1028
|
+
const viewDate = newFocusedDate.month !== focusedDate.month ? newFocusedDate : context.viewDate;
|
|
1029
|
+
return {
|
|
1030
|
+
focusedDate: newFocusedDate,
|
|
1031
|
+
viewDate
|
|
1032
|
+
};
|
|
1033
|
+
},
|
|
1034
|
+
/**
|
|
1035
|
+
* Action for ARROW_LEFT event - move focus left one day
|
|
1036
|
+
*
|
|
1037
|
+
* @param {DatePickerContext} context - Current context
|
|
1038
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1039
|
+
*/
|
|
1040
|
+
ARROW_LEFT: (context) => {
|
|
1041
|
+
const focusedDate = context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO();
|
|
1042
|
+
let newFocusedDate = focusedDate.add({ days: -1 });
|
|
1043
|
+
if (context.minDate && Temporal.PlainDate.compare(newFocusedDate, context.minDate) < 0) newFocusedDate = context.minDate;
|
|
1044
|
+
const viewDate = newFocusedDate.month !== focusedDate.month ? newFocusedDate : context.viewDate;
|
|
1045
|
+
return {
|
|
1046
|
+
focusedDate: newFocusedDate,
|
|
1047
|
+
viewDate
|
|
1048
|
+
};
|
|
1049
|
+
},
|
|
1050
|
+
/**
|
|
1051
|
+
* Action for ARROW_RIGHT event - move focus right one day
|
|
1052
|
+
*
|
|
1053
|
+
* @param {DatePickerContext} context - Current context
|
|
1054
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1055
|
+
*/
|
|
1056
|
+
ARROW_RIGHT: (context) => {
|
|
1057
|
+
const focusedDate = context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO();
|
|
1058
|
+
let newFocusedDate = focusedDate.add({ days: 1 });
|
|
1059
|
+
if (context.maxDate && Temporal.PlainDate.compare(newFocusedDate, context.maxDate) > 0) newFocusedDate = context.maxDate;
|
|
1060
|
+
const viewDate = newFocusedDate.month !== focusedDate.month ? newFocusedDate : context.viewDate;
|
|
1061
|
+
return {
|
|
1062
|
+
focusedDate: newFocusedDate,
|
|
1063
|
+
viewDate
|
|
1064
|
+
};
|
|
1065
|
+
},
|
|
1066
|
+
/**
|
|
1067
|
+
* Action for PAGE_UP event - move to previous month
|
|
1068
|
+
*
|
|
1069
|
+
* @param {DatePickerContext} context - Current context
|
|
1070
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1071
|
+
*/
|
|
1072
|
+
PAGE_UP: (context) => {
|
|
1073
|
+
let newFocusedDate = (context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO()).add({ months: -1 });
|
|
1074
|
+
if (context.minDate && Temporal.PlainDate.compare(newFocusedDate, context.minDate) < 0) newFocusedDate = context.minDate;
|
|
1075
|
+
return {
|
|
1076
|
+
focusedDate: newFocusedDate,
|
|
1077
|
+
viewDate: newFocusedDate
|
|
1078
|
+
};
|
|
1079
|
+
},
|
|
1080
|
+
/**
|
|
1081
|
+
* Action for PAGE_DOWN event - move to next month
|
|
1082
|
+
*
|
|
1083
|
+
* @param {DatePickerContext} context - Current context
|
|
1084
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1085
|
+
*/
|
|
1086
|
+
PAGE_DOWN: (context) => {
|
|
1087
|
+
let newFocusedDate = (context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO()).add({ months: 1 });
|
|
1088
|
+
if (context.maxDate && Temporal.PlainDate.compare(newFocusedDate, context.maxDate) > 0) newFocusedDate = context.maxDate;
|
|
1089
|
+
return {
|
|
1090
|
+
focusedDate: newFocusedDate,
|
|
1091
|
+
viewDate: newFocusedDate
|
|
1092
|
+
};
|
|
1093
|
+
},
|
|
1094
|
+
/**
|
|
1095
|
+
* Action for HOME_KEY event - move to start of week
|
|
1096
|
+
*
|
|
1097
|
+
* @param {DatePickerContext} context - Current context
|
|
1098
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1099
|
+
*/
|
|
1100
|
+
HOME_KEY: (context) => {
|
|
1101
|
+
const focusedDate = context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO();
|
|
1102
|
+
const daysToSubtract = new Date(focusedDate.year, focusedDate.month - 1, focusedDate.day).getDay();
|
|
1103
|
+
let newFocusedDate = focusedDate.add({ days: -daysToSubtract });
|
|
1104
|
+
if (context.minDate && Temporal.PlainDate.compare(newFocusedDate, context.minDate) < 0) newFocusedDate = context.minDate;
|
|
1105
|
+
const viewDate = newFocusedDate.month !== focusedDate.month ? newFocusedDate : context.viewDate;
|
|
1106
|
+
return {
|
|
1107
|
+
focusedDate: newFocusedDate,
|
|
1108
|
+
viewDate
|
|
1109
|
+
};
|
|
1110
|
+
},
|
|
1111
|
+
/**
|
|
1112
|
+
* Action for END_KEY event - move to end of week
|
|
1113
|
+
*
|
|
1114
|
+
* @param {DatePickerContext} context - Current context
|
|
1115
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1116
|
+
*/
|
|
1117
|
+
END_KEY: (context) => {
|
|
1118
|
+
const focusedDate = context.focusedDate || context.startDate || context.viewDate || Temporal.Now.plainDateISO();
|
|
1119
|
+
const dayOfWeek = new Date(focusedDate.year, focusedDate.month - 1, focusedDate.day).getDay();
|
|
1120
|
+
const daysToAdd = dayOfWeek === 6 ? 0 : 6 - dayOfWeek;
|
|
1121
|
+
let newFocusedDate = focusedDate.add({ days: daysToAdd });
|
|
1122
|
+
if (context.maxDate && Temporal.PlainDate.compare(newFocusedDate, context.maxDate) > 0) newFocusedDate = context.maxDate;
|
|
1123
|
+
const viewDate = newFocusedDate.month !== focusedDate.month ? newFocusedDate : context.viewDate;
|
|
1124
|
+
return {
|
|
1125
|
+
focusedDate: newFocusedDate,
|
|
1126
|
+
viewDate
|
|
1127
|
+
};
|
|
1128
|
+
},
|
|
1129
|
+
/**
|
|
1130
|
+
* Action for ENTER_KEY event - select focused date as end date
|
|
1131
|
+
*
|
|
1132
|
+
* @param {DatePickerContext} context - Current context
|
|
1133
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1134
|
+
*/
|
|
1135
|
+
ENTER_KEY: (context) => {
|
|
1136
|
+
if (!context.focusedDate || !context.startDate) return {};
|
|
1137
|
+
const endDate = context.focusedDate;
|
|
1138
|
+
const { startDate } = context;
|
|
1139
|
+
let finalStartDate = startDate;
|
|
1140
|
+
let finalEndDate = endDate;
|
|
1141
|
+
if (comparePlainDates(endDate, startDate) < 0) {
|
|
1142
|
+
finalStartDate = endDate;
|
|
1143
|
+
finalEndDate = startDate;
|
|
1144
|
+
}
|
|
1145
|
+
return {
|
|
1146
|
+
startDate: finalStartDate,
|
|
1147
|
+
endDate: finalEndDate,
|
|
1148
|
+
value: `${plainDateToISOString(finalStartDate)}/${plainDateToISOString(finalEndDate)}`,
|
|
1149
|
+
isOpen: false,
|
|
1150
|
+
restoreFocusTo: "to",
|
|
1151
|
+
shouldRestoreFocus: true
|
|
1152
|
+
};
|
|
1153
|
+
},
|
|
1154
|
+
/**
|
|
1155
|
+
* Action for PREV_MONTH event
|
|
1156
|
+
*
|
|
1157
|
+
* @param {DatePickerContext} context - Current context
|
|
1158
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1159
|
+
*/
|
|
1160
|
+
PREV_MONTH: (context) => {
|
|
1161
|
+
if (!context.viewDate) return {};
|
|
1162
|
+
return {
|
|
1163
|
+
viewDate: context.viewDate.add({ months: -1 }),
|
|
1164
|
+
focusedDate: context.focusedDate ? context.focusedDate.add({ months: -1 }) : null
|
|
1165
|
+
};
|
|
1166
|
+
},
|
|
1167
|
+
/**
|
|
1168
|
+
* Action for NEXT_MONTH event
|
|
1169
|
+
*
|
|
1170
|
+
* @param {DatePickerContext} context - Current context
|
|
1171
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1172
|
+
*/
|
|
1173
|
+
NEXT_MONTH: (context) => {
|
|
1174
|
+
if (!context.viewDate) return {};
|
|
1175
|
+
return {
|
|
1176
|
+
viewDate: context.viewDate.add({ months: 1 }),
|
|
1177
|
+
focusedDate: context.focusedDate ? context.focusedDate.add({ months: 1 }) : null
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
},
|
|
1181
|
+
["date_selected"]: {
|
|
1182
|
+
/**
|
|
1183
|
+
* Action for DATE_SELECT event
|
|
1184
|
+
*
|
|
1185
|
+
* @param {DatePickerContext} context - Current context
|
|
1186
|
+
* @param {DatePickerEvent} event - The event
|
|
1187
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1188
|
+
*/
|
|
1189
|
+
DATE_SELECT: (context, event) => {
|
|
1190
|
+
const date = event.payload?.date;
|
|
1191
|
+
if (!date) return {};
|
|
1192
|
+
if (context.mode === "single") return {
|
|
1193
|
+
startDate: date,
|
|
1194
|
+
value: plainDateToISOString(date),
|
|
1195
|
+
isOpen: context.closeOnSelect ? false : context.isOpen,
|
|
1196
|
+
restoreFocusTo: context.lastFocusedInput || "from",
|
|
1197
|
+
shouldRestoreFocus: context.closeOnSelect
|
|
1198
|
+
};
|
|
1199
|
+
return {};
|
|
1200
|
+
},
|
|
1201
|
+
/**
|
|
1202
|
+
* Action for RANGE_END_SELECT event
|
|
1203
|
+
*
|
|
1204
|
+
* @param {DatePickerContext} context - Current context
|
|
1205
|
+
* @param {DatePickerEvent} event - The event
|
|
1206
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1207
|
+
*/
|
|
1208
|
+
RANGE_END_SELECT: (context, event) => {
|
|
1209
|
+
const endDate = event.payload?.date;
|
|
1210
|
+
const { startDate } = context;
|
|
1211
|
+
if (!endDate || !startDate) return {};
|
|
1212
|
+
let finalStartDate = startDate;
|
|
1213
|
+
let finalEndDate = endDate;
|
|
1214
|
+
if (comparePlainDates(endDate, startDate) < 0) {
|
|
1215
|
+
finalStartDate = endDate;
|
|
1216
|
+
finalEndDate = startDate;
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
startDate: finalStartDate,
|
|
1220
|
+
endDate: finalEndDate,
|
|
1221
|
+
value: `${plainDateToISOString(finalStartDate)}/${plainDateToISOString(finalEndDate)}`,
|
|
1222
|
+
isOpen: context.closeOnSelect ? false : context.isOpen,
|
|
1223
|
+
restoreFocusTo: "to",
|
|
1224
|
+
shouldRestoreFocus: context.closeOnSelect
|
|
1225
|
+
};
|
|
1226
|
+
},
|
|
1227
|
+
/**
|
|
1228
|
+
* Action for CALENDAR_CLOSE event
|
|
1229
|
+
*
|
|
1230
|
+
* @param {DatePickerContext} context - Current context
|
|
1231
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1232
|
+
*/
|
|
1233
|
+
CALENDAR_CLOSE: (context) => ({
|
|
1234
|
+
isOpen: false,
|
|
1235
|
+
restoreFocusTo: context.shouldRestoreFocus ? context.restoreFocusTo : null,
|
|
1236
|
+
shouldRestoreFocus: context.shouldRestoreFocus
|
|
1237
|
+
})
|
|
1238
|
+
},
|
|
1239
|
+
["disabled"]: {
|
|
1240
|
+
/** Action for a DISABLE event */
|
|
1241
|
+
DISABLE: () => ({
|
|
1242
|
+
isDisabled: true,
|
|
1243
|
+
isOpen: false
|
|
1244
|
+
}),
|
|
1245
|
+
/** Action for ENABLE event */
|
|
1246
|
+
ENABLE: () => ({ isDisabled: false })
|
|
1247
|
+
},
|
|
1248
|
+
["readonly"]: {
|
|
1249
|
+
/** Action for SET_READONLY event */
|
|
1250
|
+
SET_READONLY: () => ({
|
|
1251
|
+
isReadonly: true,
|
|
1252
|
+
isOpen: false
|
|
1253
|
+
}),
|
|
1254
|
+
/** Action for UNSET_READONLY event */
|
|
1255
|
+
UNSET_READONLY: () => ({ isReadonly: false })
|
|
1256
|
+
},
|
|
1257
|
+
["error"]: {
|
|
1258
|
+
/**
|
|
1259
|
+
* Action for VALIDATION_ERROR event
|
|
1260
|
+
*
|
|
1261
|
+
* @param {DatePickerContext} _context - Current context (unused)
|
|
1262
|
+
* @param {DatePickerEvent} event - The event
|
|
1263
|
+
* @returns {Partial<DatePickerContext>} Updated context
|
|
1264
|
+
*/
|
|
1265
|
+
VALIDATION_ERROR: (_context, event) => {
|
|
1266
|
+
return {
|
|
1267
|
+
isInvalid: true,
|
|
1268
|
+
errorMessage: event.payload?.message || "Invalid date"
|
|
1269
|
+
};
|
|
1270
|
+
},
|
|
1271
|
+
/** Action for CLEAR_ERROR event */
|
|
1272
|
+
CLEAR_ERROR: () => ({
|
|
1273
|
+
isInvalid: false,
|
|
1274
|
+
errorMessage: void 0
|
|
1275
|
+
}),
|
|
1276
|
+
/** Action for VALUE_CHANGE event */
|
|
1277
|
+
VALUE_CHANGE: () => ({
|
|
1278
|
+
isInvalid: false,
|
|
1279
|
+
errorMessage: void 0
|
|
1280
|
+
})
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
/**
|
|
1284
|
+
* Get action for a specific state and event
|
|
1285
|
+
*
|
|
1286
|
+
* @param {DatePickerState} state - The current state
|
|
1287
|
+
* @param {string} eventType - The event type
|
|
1288
|
+
* @returns {StateAction | undefined} The action function or undefined
|
|
1289
|
+
*/
|
|
1290
|
+
function getAction(state, eventType) {
|
|
1291
|
+
return actions[state]?.[eventType];
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Execute an action and return context updates
|
|
1295
|
+
*
|
|
1296
|
+
* @param {DatePickerState} state - The current state
|
|
1297
|
+
* @param {string} eventType - The event type
|
|
1298
|
+
* @param {DatePickerContext} context - The current context
|
|
1299
|
+
* @param {DatePickerEvent} event - The event
|
|
1300
|
+
* @returns {Partial<DatePickerContext>} Partial context updates
|
|
1301
|
+
*/
|
|
1302
|
+
function executeAction(state, eventType, context, event) {
|
|
1303
|
+
const action = getAction(state, eventType);
|
|
1304
|
+
if (!action) return {};
|
|
1305
|
+
return action(context, event);
|
|
1306
|
+
}
|
|
1307
|
+
//#endregion
|
|
1308
|
+
//#region src/date-picker/primitives/effects.ts
|
|
1309
|
+
/**
|
|
1310
|
+
* Side effects for state transitions
|
|
1311
|
+
* These are executed after the state transition is complete
|
|
1312
|
+
*/
|
|
1313
|
+
const effects = {
|
|
1314
|
+
["idle"]: {},
|
|
1315
|
+
["focused"]: {},
|
|
1316
|
+
["calendar_open"]: {
|
|
1317
|
+
/**
|
|
1318
|
+
* Focus on the calendar when it opens
|
|
1319
|
+
*/
|
|
1320
|
+
CALENDAR_OPEN: () => {},
|
|
1321
|
+
/**
|
|
1322
|
+
* Focus the calendar when opened via a Tab key
|
|
1323
|
+
*/
|
|
1324
|
+
TAB_KEY: () => {}
|
|
1325
|
+
},
|
|
1326
|
+
["selecting_start"]: {},
|
|
1327
|
+
["selecting_end"]: {},
|
|
1328
|
+
["date_selected"]: {
|
|
1329
|
+
/**
|
|
1330
|
+
* Dispatch custom event when a date is selected
|
|
1331
|
+
*/
|
|
1332
|
+
DATE_SELECT: () => {},
|
|
1333
|
+
/**
|
|
1334
|
+
* Dispatch custom event when the range end is selected
|
|
1335
|
+
*/
|
|
1336
|
+
RANGE_END_SELECT: () => {}
|
|
1337
|
+
},
|
|
1338
|
+
["disabled"]: {},
|
|
1339
|
+
["readonly"]: {},
|
|
1340
|
+
["error"]: {
|
|
1341
|
+
/**
|
|
1342
|
+
* Log validation errors
|
|
1343
|
+
* @param {DatePickerContext} _context - Current context
|
|
1344
|
+
* @param {DatePickerEvent} event - The event
|
|
1345
|
+
*/
|
|
1346
|
+
VALIDATION_ERROR: (_context, event) => {
|
|
1347
|
+
if (process.env.NODE_ENV !== "production") console.warn("DatePicker validation error:", event.payload);
|
|
1348
|
+
} }
|
|
1349
|
+
};
|
|
1350
|
+
/**
|
|
1351
|
+
* Get effect for a specific state and event
|
|
1352
|
+
*
|
|
1353
|
+
* @param {DatePickerState} state - The current state
|
|
1354
|
+
* @param {string} eventType - The event type
|
|
1355
|
+
* @returns The effect function or undefined
|
|
1356
|
+
*/
|
|
1357
|
+
function getEffect(state, eventType) {
|
|
1358
|
+
return effects[state]?.[eventType];
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Execute an effect
|
|
1362
|
+
*
|
|
1363
|
+
* @param {DatePickerState} state - The current state
|
|
1364
|
+
* @param {string} eventType - The event type
|
|
1365
|
+
* @param {DatePickerContext} context - The current context
|
|
1366
|
+
* @param {DatePickerEvent} event - The event
|
|
1367
|
+
*/
|
|
1368
|
+
function executeEffect(state, eventType, context, event) {
|
|
1369
|
+
const effect = getEffect(state, eventType);
|
|
1370
|
+
if (effect) effect(context, event);
|
|
1371
|
+
}
|
|
1372
|
+
//#endregion
|
|
1373
|
+
//#region src/date-picker/primitives/machine.ts
|
|
1374
|
+
/**
|
|
1375
|
+
* State transition configuration
|
|
1376
|
+
* Maps the current state + event type to the next state
|
|
1377
|
+
*/
|
|
1378
|
+
const stateTransitions = {
|
|
1379
|
+
["idle"]: {
|
|
1380
|
+
["INPUT_FOCUS"]: "focused",
|
|
1381
|
+
["CALENDAR_ICON_CLICK"]: "calendar_open",
|
|
1382
|
+
["DISABLE"]: "disabled",
|
|
1383
|
+
["SET_READONLY"]: "readonly"
|
|
1384
|
+
},
|
|
1385
|
+
["focused"]: {
|
|
1386
|
+
["INPUT_BLUR"]: "idle",
|
|
1387
|
+
["CALENDAR_OPEN"]: "calendar_open",
|
|
1388
|
+
["CALENDAR_ICON_CLICK"]: "calendar_open",
|
|
1389
|
+
["DISABLE"]: "disabled"
|
|
1390
|
+
},
|
|
1391
|
+
["calendar_open"]: {
|
|
1392
|
+
["DATE_SELECT"]: "date_selected",
|
|
1393
|
+
["RANGE_START_SELECT"]: "selecting_end",
|
|
1394
|
+
["OUTSIDE_CLICK"]: "idle",
|
|
1395
|
+
["ESCAPE_KEY"]: "focused",
|
|
1396
|
+
["TAB_KEY"]: "idle",
|
|
1397
|
+
["CALENDAR_CLOSE"]: "focused",
|
|
1398
|
+
["PREV_MONTH"]: "calendar_open",
|
|
1399
|
+
["NEXT_MONTH"]: "calendar_open",
|
|
1400
|
+
["PREV_YEAR"]: "calendar_open",
|
|
1401
|
+
["NEXT_YEAR"]: "calendar_open",
|
|
1402
|
+
["GO_TO_TODAY"]: "calendar_open",
|
|
1403
|
+
["ARROW_UP"]: "calendar_open",
|
|
1404
|
+
["ARROW_DOWN"]: "calendar_open",
|
|
1405
|
+
["ARROW_LEFT"]: "calendar_open",
|
|
1406
|
+
["ARROW_RIGHT"]: "calendar_open",
|
|
1407
|
+
["PAGE_UP"]: "calendar_open",
|
|
1408
|
+
["PAGE_DOWN"]: "calendar_open",
|
|
1409
|
+
["HOME_KEY"]: "calendar_open",
|
|
1410
|
+
["END_KEY"]: "calendar_open"
|
|
1411
|
+
},
|
|
1412
|
+
["selecting_start"]: {
|
|
1413
|
+
["RANGE_START_SELECT"]: "selecting_end",
|
|
1414
|
+
["OUTSIDE_CLICK"]: "idle",
|
|
1415
|
+
["ESCAPE_KEY"]: "focused"
|
|
1416
|
+
},
|
|
1417
|
+
["selecting_end"]: {
|
|
1418
|
+
["RANGE_END_SELECT"]: "date_selected",
|
|
1419
|
+
["RANGE_START_SELECT"]: "selecting_end",
|
|
1420
|
+
["OUTSIDE_CLICK"]: "idle",
|
|
1421
|
+
["ESCAPE_KEY"]: "focused",
|
|
1422
|
+
["TAB_KEY"]: "idle",
|
|
1423
|
+
["ARROW_UP"]: "selecting_end",
|
|
1424
|
+
["ARROW_DOWN"]: "selecting_end",
|
|
1425
|
+
["ARROW_LEFT"]: "selecting_end",
|
|
1426
|
+
["ARROW_RIGHT"]: "selecting_end",
|
|
1427
|
+
["PAGE_UP"]: "selecting_end",
|
|
1428
|
+
["PAGE_DOWN"]: "selecting_end",
|
|
1429
|
+
["HOME_KEY"]: "selecting_end",
|
|
1430
|
+
["END_KEY"]: "selecting_end",
|
|
1431
|
+
["ENTER_KEY"]: "date_selected",
|
|
1432
|
+
["PREV_MONTH"]: "selecting_end",
|
|
1433
|
+
["NEXT_MONTH"]: "selecting_end"
|
|
1434
|
+
},
|
|
1435
|
+
["date_selected"]: {
|
|
1436
|
+
["CALENDAR_CLOSE"]: "idle",
|
|
1437
|
+
["INPUT_FOCUS"]: "focused",
|
|
1438
|
+
["CALENDAR_ICON_CLICK"]: "calendar_open"
|
|
1439
|
+
},
|
|
1440
|
+
["disabled"]: { ["ENABLE"]: "idle" },
|
|
1441
|
+
["readonly"]: { ["UNSET_READONLY"]: "idle" },
|
|
1442
|
+
["error"]: {
|
|
1443
|
+
["VALUE_CHANGE"]: "idle",
|
|
1444
|
+
["CLEAR_ERROR"]: "idle"
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
/**
|
|
1448
|
+
* Date picker state machine
|
|
1449
|
+
* Manages state transitions and context updates for the date picker
|
|
1450
|
+
*/
|
|
1451
|
+
var DatePickerStateMachine = class {
|
|
1452
|
+
/**
|
|
1453
|
+
* Create a new date picker state machine
|
|
1454
|
+
*
|
|
1455
|
+
* @param {Partial<DatePickerContext>} initialContext - Initial context values
|
|
1456
|
+
*/
|
|
1457
|
+
constructor(initialContext = {}) {
|
|
1458
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1459
|
+
this.currentState = "idle";
|
|
1460
|
+
this.context = this.createInitialContext(initialContext);
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Create initial context with defaults
|
|
1464
|
+
*
|
|
1465
|
+
* @param {Partial<DatePickerContext>} partial - Partial context to merge with defaults
|
|
1466
|
+
* @returns Complete context object
|
|
1467
|
+
*/
|
|
1468
|
+
createInitialContext(partial) {
|
|
1469
|
+
return {
|
|
1470
|
+
mode: "single",
|
|
1471
|
+
value: "",
|
|
1472
|
+
startDate: null,
|
|
1473
|
+
endDate: null,
|
|
1474
|
+
isOpen: false,
|
|
1475
|
+
isFocused: false,
|
|
1476
|
+
isDisabled: false,
|
|
1477
|
+
isReadonly: false,
|
|
1478
|
+
isInvalid: false,
|
|
1479
|
+
lastFocusedInput: null,
|
|
1480
|
+
restoreFocusTo: null,
|
|
1481
|
+
shouldRestoreFocus: false,
|
|
1482
|
+
minDate: null,
|
|
1483
|
+
maxDate: null,
|
|
1484
|
+
dateFormat: "m/d/Y",
|
|
1485
|
+
allowInput: true,
|
|
1486
|
+
closeOnSelect: true,
|
|
1487
|
+
viewDate: null,
|
|
1488
|
+
focusedDate: null,
|
|
1489
|
+
...partial
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Send an event to the state machine
|
|
1494
|
+
* This is the primary method for triggering state transitions
|
|
1495
|
+
*
|
|
1496
|
+
* @param {string} eventType - The type of event to send
|
|
1497
|
+
* @param {unknown} payload - Optional event payload
|
|
1498
|
+
* @returns The updated context
|
|
1499
|
+
*/
|
|
1500
|
+
send(eventType, payload) {
|
|
1501
|
+
const event = {
|
|
1502
|
+
type: eventType,
|
|
1503
|
+
payload,
|
|
1504
|
+
timestamp: Date.now()
|
|
1505
|
+
};
|
|
1506
|
+
const nextState = this.getNextState(this.currentState, event);
|
|
1507
|
+
if (!nextState) return this.context;
|
|
1508
|
+
if (!checkGuard(nextState, event.type, this.context, event)) return this.context;
|
|
1509
|
+
const contextUpdates = executeAction(this.currentState, event.type, this.context, event);
|
|
1510
|
+
const newContext = {
|
|
1511
|
+
...this.context,
|
|
1512
|
+
...contextUpdates
|
|
1513
|
+
};
|
|
1514
|
+
const stateTransition = {
|
|
1515
|
+
from: this.currentState,
|
|
1516
|
+
to: nextState,
|
|
1517
|
+
event,
|
|
1518
|
+
context: newContext
|
|
1519
|
+
};
|
|
1520
|
+
this.currentState = nextState;
|
|
1521
|
+
this.context = newContext;
|
|
1522
|
+
executeEffect(nextState, event.type, this.context, event);
|
|
1523
|
+
this.notifyListeners(stateTransition);
|
|
1524
|
+
return this.context;
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Get the current state
|
|
1528
|
+
*
|
|
1529
|
+
* @returns The current state
|
|
1530
|
+
*/
|
|
1531
|
+
getState() {
|
|
1532
|
+
return this.currentState;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Get the current context
|
|
1536
|
+
*
|
|
1537
|
+
* @returns A copy of the current context
|
|
1538
|
+
*/
|
|
1539
|
+
getContext() {
|
|
1540
|
+
return { ...this.context };
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Update context directly (use sparingly)
|
|
1544
|
+
*
|
|
1545
|
+
* @param {Partial<DatePickerContext>} updates - Partial context updates
|
|
1546
|
+
* @returns The updated context
|
|
1547
|
+
*/
|
|
1548
|
+
updateContext(updates) {
|
|
1549
|
+
this.context = {
|
|
1550
|
+
...this.context,
|
|
1551
|
+
...updates
|
|
1552
|
+
};
|
|
1553
|
+
return this.context;
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Subscribe to state transitions
|
|
1557
|
+
*
|
|
1558
|
+
* @param {TransitionListener} listener - Function to call on each transition
|
|
1559
|
+
* @returns Unsubscribe function
|
|
1560
|
+
*/
|
|
1561
|
+
subscribe(listener) {
|
|
1562
|
+
this.listeners.add(listener);
|
|
1563
|
+
return () => this.listeners.delete(listener);
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Check if a transition is valid from the current state
|
|
1567
|
+
*
|
|
1568
|
+
* @param {string} eventType - The event type to check
|
|
1569
|
+
* @returns True if the transition is valid
|
|
1570
|
+
*/
|
|
1571
|
+
canTransition(eventType) {
|
|
1572
|
+
const event = {
|
|
1573
|
+
type: eventType,
|
|
1574
|
+
timestamp: Date.now()
|
|
1575
|
+
};
|
|
1576
|
+
const nextState = this.getNextState(this.currentState, event);
|
|
1577
|
+
if (!nextState) return false;
|
|
1578
|
+
return checkGuard(nextState, eventType, this.context, event);
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Get the next state for a given event
|
|
1582
|
+
*
|
|
1583
|
+
* @param {DatePickerState} from - Current state
|
|
1584
|
+
* @param {DatePickerEvent} event - The event
|
|
1585
|
+
* @returns Next state or null if no transition exists
|
|
1586
|
+
*/
|
|
1587
|
+
getNextState(from, event) {
|
|
1588
|
+
const transitions = stateTransitions[from];
|
|
1589
|
+
if (!transitions) return null;
|
|
1590
|
+
const nextState = transitions[event.type];
|
|
1591
|
+
if (!nextState) return null;
|
|
1592
|
+
return nextState;
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Notify all listeners of a state transition
|
|
1596
|
+
*
|
|
1597
|
+
* @param {StateTransition} transition - The state transition
|
|
1598
|
+
*/
|
|
1599
|
+
notifyListeners(transition) {
|
|
1600
|
+
this.listeners.forEach((listener) => listener(transition));
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Reset the state machine to initial state
|
|
1604
|
+
*
|
|
1605
|
+
* @param {Partial<DatePickerContext>} initialContext - Optional new initial context
|
|
1606
|
+
*/
|
|
1607
|
+
reset(initialContext) {
|
|
1608
|
+
this.currentState = "idle";
|
|
1609
|
+
this.context = this.createInitialContext(initialContext || {});
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
//#endregion
|
|
1613
|
+
//#region src/date-picker/primitives/click-outside-handler.ts
|
|
1614
|
+
/**
|
|
1615
|
+
* Creates a click outside handler with proper lifecycle management
|
|
1616
|
+
*
|
|
1617
|
+
* This utility provides a framework-agnostic way to detect clicks outside
|
|
1618
|
+
* a component and trigger a callback. It combines best practices from both
|
|
1619
|
+
* React and Web Components implementations:
|
|
1620
|
+
*
|
|
1621
|
+
* - Uses defensive timing (from React) to prevent the opening click from closing
|
|
1622
|
+
* - Supports flexible element detection (from Web Components)
|
|
1623
|
+
* - Provides clean lifecycle management
|
|
1624
|
+
* - Fully testable and reusable
|
|
1625
|
+
*
|
|
1626
|
+
* @example React usage
|
|
1627
|
+
* ```typescript
|
|
1628
|
+
* useEffect(() => {
|
|
1629
|
+
* if (!context.isOpen) {
|
|
1630
|
+
* return;
|
|
1631
|
+
* }
|
|
1632
|
+
*
|
|
1633
|
+
* const handler = new ClickOutsideHandler({
|
|
1634
|
+
* isOpen: context.isOpen,
|
|
1635
|
+
* containsNode: (node: Node) => {
|
|
1636
|
+
* const calendarEl = calendarRef.current;
|
|
1637
|
+
* const startInputEl = startInputRef.current;
|
|
1638
|
+
* const endInputEl = endInputRef.current;
|
|
1639
|
+
*
|
|
1640
|
+
* return (
|
|
1641
|
+
* (calendarEl?.contains(node) ?? false) ||
|
|
1642
|
+
* (startInputEl?.contains(node) ?? false) ||
|
|
1643
|
+
* (endInputEl?.contains(node) ?? false)
|
|
1644
|
+
* );
|
|
1645
|
+
* },
|
|
1646
|
+
* onOutsideClick: () => send(DatePickerEvent.OUTSIDE_CLICK),
|
|
1647
|
+
* useCapture: true,
|
|
1648
|
+
* attachDelay: 0,
|
|
1649
|
+
* });
|
|
1650
|
+
*
|
|
1651
|
+
* handler.attach();
|
|
1652
|
+
*
|
|
1653
|
+
* return () => {
|
|
1654
|
+
* handler.detach();
|
|
1655
|
+
* };
|
|
1656
|
+
* }, [context.isOpen, send]);
|
|
1657
|
+
* ```
|
|
1658
|
+
*
|
|
1659
|
+
* @example Web Components usage
|
|
1660
|
+
* ```typescript
|
|
1661
|
+
* connectedCallback() {
|
|
1662
|
+
* super.connectedCallback();
|
|
1663
|
+
*
|
|
1664
|
+
* this._clickOutsideHandler = new ClickOutsideHandler({
|
|
1665
|
+
* isOpen: this.open,
|
|
1666
|
+
* containsNode: (node: Node) => {
|
|
1667
|
+
* return this.contains(node) || (this.shadowRoot?.contains(node) ?? false);
|
|
1668
|
+
* },
|
|
1669
|
+
* onOutsideClick: () => {
|
|
1670
|
+
* if (this._adapter) {
|
|
1671
|
+
* this._adapter.send(DatePickerEvent.OUTSIDE_CLICK);
|
|
1672
|
+
* }
|
|
1673
|
+
* },
|
|
1674
|
+
* useCapture: true,
|
|
1675
|
+
* attachDelay: 0,
|
|
1676
|
+
* });
|
|
1677
|
+
*
|
|
1678
|
+
* this._clickOutsideHandler.attach();
|
|
1679
|
+
* }
|
|
1680
|
+
*
|
|
1681
|
+
* disconnectedCallback() {
|
|
1682
|
+
* super.disconnectedCallback();
|
|
1683
|
+
* this._clickOutsideHandler?.detach();
|
|
1684
|
+
* this._clickOutsideHandler = null;
|
|
1685
|
+
* }
|
|
1686
|
+
*
|
|
1687
|
+
* updated(changedProperties: PropertyValues) {
|
|
1688
|
+
* super.updated(changedProperties);
|
|
1689
|
+
*
|
|
1690
|
+
* if (changedProperties.has('open')) {
|
|
1691
|
+
* this._clickOutsideHandler?.updateConfig({ isOpen: this.open });
|
|
1692
|
+
* }
|
|
1693
|
+
* }
|
|
1694
|
+
* ```
|
|
1695
|
+
*/
|
|
1696
|
+
var ClickOutsideHandler = class {
|
|
1697
|
+
/**
|
|
1698
|
+
* Creates a new ClickOutsideHandler instance
|
|
1699
|
+
*
|
|
1700
|
+
* @param {ClickOutsideConfig} config - Configuration for the click outside handler
|
|
1701
|
+
*/
|
|
1702
|
+
constructor(config) {
|
|
1703
|
+
this.handler = null;
|
|
1704
|
+
this.timeoutId = null;
|
|
1705
|
+
this.handleClick = (event) => {
|
|
1706
|
+
if (!this.config.isOpen) return;
|
|
1707
|
+
const target = event.target;
|
|
1708
|
+
if (!this.config.containsNode(target)) this.config.onOutsideClick();
|
|
1709
|
+
};
|
|
1710
|
+
this.config = config;
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Attach the click outside listener
|
|
1714
|
+
*
|
|
1715
|
+
* This method adds the click event listener to the document.
|
|
1716
|
+
* If attachDelay is configured, it will delay the attachment to prevent
|
|
1717
|
+
* the opening click from immediately triggering a close.
|
|
1718
|
+
*
|
|
1719
|
+
* Calling attach() multiple times is safe - it will only attach once.
|
|
1720
|
+
*/
|
|
1721
|
+
attach() {
|
|
1722
|
+
if (this.handler) return;
|
|
1723
|
+
const useCapture = this.config.useCapture ?? true;
|
|
1724
|
+
const delay = this.config.attachDelay ?? 0;
|
|
1725
|
+
this.timeoutId = window.setTimeout(() => {
|
|
1726
|
+
document.addEventListener("click", this.handleClick, useCapture);
|
|
1727
|
+
this.handler = this.handleClick;
|
|
1728
|
+
this.timeoutId = null;
|
|
1729
|
+
}, delay);
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Detach the click outside listener
|
|
1733
|
+
*
|
|
1734
|
+
* This method removes the click event listener and cleans up any pending timeouts.
|
|
1735
|
+
* It's safe to call detach() multiple times or even if attach() was never called.
|
|
1736
|
+
*
|
|
1737
|
+
* Always call detach() when the component unmounts or when you no longer need
|
|
1738
|
+
* click outside detection to prevent memory leaks.
|
|
1739
|
+
*/
|
|
1740
|
+
detach() {
|
|
1741
|
+
if (this.timeoutId !== null) {
|
|
1742
|
+
clearTimeout(this.timeoutId);
|
|
1743
|
+
this.timeoutId = null;
|
|
1744
|
+
}
|
|
1745
|
+
if (this.handler) {
|
|
1746
|
+
const useCapture = this.config.useCapture ?? true;
|
|
1747
|
+
document.removeEventListener("click", this.handler, useCapture);
|
|
1748
|
+
this.handler = null;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Update configuration
|
|
1753
|
+
*
|
|
1754
|
+
* This is useful in React when dependencies change, or in Web Components
|
|
1755
|
+
* when properties update. You can update any part of the configuration
|
|
1756
|
+
* without needing to detach and reattach the handler.
|
|
1757
|
+
*
|
|
1758
|
+
* @param {Partial<ClickOutsideConfig>} config - Partial configuration to update
|
|
1759
|
+
*
|
|
1760
|
+
* @example
|
|
1761
|
+
* ```typescript
|
|
1762
|
+
* // Update just the isOpen state
|
|
1763
|
+
* handler.updateConfig({ isOpen: true });
|
|
1764
|
+
*
|
|
1765
|
+
* // Update multiple properties
|
|
1766
|
+
* handler.updateConfig({
|
|
1767
|
+
* isOpen: true,
|
|
1768
|
+
* onOutsideClick: newCallback
|
|
1769
|
+
* });
|
|
1770
|
+
* ```
|
|
1771
|
+
*/
|
|
1772
|
+
updateConfig(config) {
|
|
1773
|
+
this.config = {
|
|
1774
|
+
...this.config,
|
|
1775
|
+
...config
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Check if the handler is currently attached
|
|
1780
|
+
*
|
|
1781
|
+
* @returns true if the handler is attached, false otherwise
|
|
1782
|
+
*/
|
|
1783
|
+
isAttached() {
|
|
1784
|
+
return this.handler !== null;
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Check if there's a pending attachment
|
|
1788
|
+
*
|
|
1789
|
+
* @returns true if attachment is pending (waiting for delay), false otherwise
|
|
1790
|
+
*/
|
|
1791
|
+
isPending() {
|
|
1792
|
+
return this.timeoutId !== null;
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
//#endregion
|
|
1796
|
+
//#region src/date-picker/primitives/keyboard-utils.ts
|
|
1797
|
+
/**
|
|
1798
|
+
* Map a keyboard event to a state machine event
|
|
1799
|
+
* This provides a single source of truth for keyboard shortcuts across React and Web Components
|
|
1800
|
+
*
|
|
1801
|
+
* @param {KeyboardEventInfo} info - Information about the keyboard event and current state
|
|
1802
|
+
* @returns {KeyboardEventResult | null} The state machine event to dispatch, or null if key not handled
|
|
1803
|
+
*/
|
|
1804
|
+
function mapKeyboardToStateMachineEvent(info) {
|
|
1805
|
+
const { key, mode, state, focusedDate } = info;
|
|
1806
|
+
if (key === "Escape") return {
|
|
1807
|
+
eventType: "ESCAPE_KEY",
|
|
1808
|
+
preventDefault: true
|
|
1809
|
+
};
|
|
1810
|
+
if (key === "Enter") {
|
|
1811
|
+
if (!focusedDate) return null;
|
|
1812
|
+
if (mode === "range") if (state === "selecting_end") return {
|
|
1813
|
+
eventType: "RANGE_END_SELECT",
|
|
1814
|
+
payload: { date: focusedDate },
|
|
1815
|
+
preventDefault: true
|
|
1816
|
+
};
|
|
1817
|
+
else return {
|
|
1818
|
+
eventType: "RANGE_START_SELECT",
|
|
1819
|
+
payload: { date: focusedDate },
|
|
1820
|
+
preventDefault: true
|
|
1821
|
+
};
|
|
1822
|
+
else return {
|
|
1823
|
+
eventType: "DATE_SELECT",
|
|
1824
|
+
payload: { date: focusedDate },
|
|
1825
|
+
preventDefault: true
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
if (key === "ArrowUp") return {
|
|
1829
|
+
eventType: "ARROW_UP",
|
|
1830
|
+
preventDefault: true
|
|
1831
|
+
};
|
|
1832
|
+
if (key === "ArrowDown") return {
|
|
1833
|
+
eventType: "ARROW_DOWN",
|
|
1834
|
+
preventDefault: true
|
|
1835
|
+
};
|
|
1836
|
+
if (key === "ArrowLeft") return {
|
|
1837
|
+
eventType: "ARROW_LEFT",
|
|
1838
|
+
preventDefault: true
|
|
1839
|
+
};
|
|
1840
|
+
if (key === "ArrowRight") return {
|
|
1841
|
+
eventType: "ARROW_RIGHT",
|
|
1842
|
+
preventDefault: true
|
|
1843
|
+
};
|
|
1844
|
+
if (key === "PageUp") return {
|
|
1845
|
+
eventType: "PAGE_UP",
|
|
1846
|
+
preventDefault: true
|
|
1847
|
+
};
|
|
1848
|
+
if (key === "PageDown") return {
|
|
1849
|
+
eventType: "PAGE_DOWN",
|
|
1850
|
+
preventDefault: true
|
|
1851
|
+
};
|
|
1852
|
+
if (key === "Home") return {
|
|
1853
|
+
eventType: "HOME_KEY",
|
|
1854
|
+
preventDefault: true
|
|
1855
|
+
};
|
|
1856
|
+
if (key === "End") return {
|
|
1857
|
+
eventType: "END_KEY",
|
|
1858
|
+
preventDefault: true
|
|
1859
|
+
};
|
|
1860
|
+
return null;
|
|
1861
|
+
}
|
|
1862
|
+
//#endregion
|
|
1863
|
+
//#region src/date-picker/primitives/calendar-grid.ts
|
|
1864
|
+
/**
|
|
1865
|
+
* Copyright IBM Corp. 2026
|
|
1866
|
+
*
|
|
1867
|
+
* This source code is licensed under the Apache-2.0 license found in the
|
|
1868
|
+
* LICENSE file in the root directory of this source tree.
|
|
1869
|
+
*/
|
|
1870
|
+
/**
|
|
1871
|
+
* Shared calendar-grid generation for date picker renderers.
|
|
1872
|
+
*/
|
|
1873
|
+
/**
|
|
1874
|
+
* Number of weeks rendered per month. Fixed at 6 so the grid height stays
|
|
1875
|
+
* constant regardless of how the month falls across weeks.
|
|
1876
|
+
*/
|
|
1877
|
+
const WEEKS_IN_GRID = 6;
|
|
1878
|
+
/**
|
|
1879
|
+
* Number of days in a week.
|
|
1880
|
+
*/
|
|
1881
|
+
const DAYS_IN_WEEK$1 = 7;
|
|
1882
|
+
/**
|
|
1883
|
+
* Generate the calendar grid for the month containing `viewDate`.
|
|
1884
|
+
*
|
|
1885
|
+
*
|
|
1886
|
+
* @param {Temporal.PlainDate} viewDate - The date to generate calendar for
|
|
1887
|
+
* @param {Temporal.PlainDate | null} minDate - Minimum selectable date (inclusive), or `null` for no lower bound.
|
|
1888
|
+
* @param {Temporal.PlainDate | null} maxDate - Maximum selectable date (inclusive), or `null` for no upper bound.
|
|
1889
|
+
* @param {number} weekStartsOn - First day of the week: 0 = Sunday (default) through 6 = Saturday.
|
|
1890
|
+
* @returns {CalendarDay[][]} 6×7 grid of day cells (weeks × days).
|
|
1891
|
+
*/
|
|
1892
|
+
function generateCalendarGrid(viewDate, minDate = null, maxDate = null, weekStartsOn = 0) {
|
|
1893
|
+
const firstDayOfMonth = getMonthStart(viewDate);
|
|
1894
|
+
const startDate = addDays(firstDayOfMonth, -((plainDateToDate(firstDayOfMonth).getDay() - weekStartsOn + 7) % 7));
|
|
1895
|
+
const weeks = [];
|
|
1896
|
+
let currentDate = startDate;
|
|
1897
|
+
for (let week = 0; week < WEEKS_IN_GRID; week++) {
|
|
1898
|
+
const days = [];
|
|
1899
|
+
for (let day = 0; day < DAYS_IN_WEEK$1; day++) {
|
|
1900
|
+
days.push({
|
|
1901
|
+
date: currentDate,
|
|
1902
|
+
isCurrentMonth: currentDate.month === viewDate.month,
|
|
1903
|
+
isToday: isToday(currentDate),
|
|
1904
|
+
isDisabled: !isDateInRange(currentDate, minDate, maxDate)
|
|
1905
|
+
});
|
|
1906
|
+
currentDate = addDays(currentDate, 1);
|
|
1907
|
+
}
|
|
1908
|
+
weeks.push(days);
|
|
1909
|
+
}
|
|
1910
|
+
return weeks;
|
|
1911
|
+
}
|
|
1912
|
+
//#endregion
|
|
1913
|
+
//#region src/date-picker/primitives/calendar-labels.ts
|
|
1914
|
+
/**
|
|
1915
|
+
* Copyright IBM Corp. 2026
|
|
1916
|
+
*
|
|
1917
|
+
* This source code is licensed under the Apache-2.0 license found in the
|
|
1918
|
+
* LICENSE file in the root directory of this source tree.
|
|
1919
|
+
*/
|
|
1920
|
+
/**
|
|
1921
|
+
* Localized label helpers for date picker calendar renderers.
|
|
1922
|
+
*/
|
|
1923
|
+
/**
|
|
1924
|
+
* Number of days in a week.
|
|
1925
|
+
*/
|
|
1926
|
+
const DAYS_IN_WEEK = 7;
|
|
1927
|
+
/**
|
|
1928
|
+
* A date known to be a Sunday, used as the anchor for generating weekday names
|
|
1929
|
+
* in order. Can be any Sunday.
|
|
1930
|
+
*/
|
|
1931
|
+
const SUNDAY_ANCHOR = new Date(2024, 0, 7);
|
|
1932
|
+
/**
|
|
1933
|
+
* Locale used when the caller supplies a structurally invalid BCP 47 tag.
|
|
1934
|
+
*/
|
|
1935
|
+
const FALLBACK_LOCALE = "en";
|
|
1936
|
+
/**
|
|
1937
|
+
* Resolve a caller-supplied locale to one that is safe to hand to `Intl`.
|
|
1938
|
+
*
|
|
1939
|
+
* Gracefully fallback to FALLBACK_LOCALE when an invalid BCP 47 locale is provided
|
|
1940
|
+
*
|
|
1941
|
+
* @param {string} locale - Candidate BCP 47 locale tag.
|
|
1942
|
+
* @returns {string} The original locale if structurally valid, else `'en'`.
|
|
1943
|
+
*/
|
|
1944
|
+
function resolveLocale(locale) {
|
|
1945
|
+
try {
|
|
1946
|
+
Intl.getCanonicalLocales(locale);
|
|
1947
|
+
return locale;
|
|
1948
|
+
} catch {
|
|
1949
|
+
return FALLBACK_LOCALE;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Whether a locale is English. Carbon renders single-letter weekday headers
|
|
1954
|
+
* for English only; other locales keep their localized short names.
|
|
1955
|
+
*
|
|
1956
|
+
* @param {string} locale - BCP 47 locale tag.
|
|
1957
|
+
* @returns {boolean} True if the locale is a form of English.
|
|
1958
|
+
*/
|
|
1959
|
+
function isEnglishLocale(locale) {
|
|
1960
|
+
return locale.toLowerCase().startsWith("en");
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Get the month-and-year heading for the calendar (e.g. "January 2026").
|
|
1964
|
+
*
|
|
1965
|
+
* @param {Temporal.PlainDate} viewDate - Any date within the displayed month.
|
|
1966
|
+
* @param {string} locale - BCP 47 locale tag (default 'en').
|
|
1967
|
+
* @returns {string} Localized "month year" label.
|
|
1968
|
+
*/
|
|
1969
|
+
function getMonthYearLabel(viewDate, locale = "en") {
|
|
1970
|
+
return new Intl.DateTimeFormat(resolveLocale(locale), {
|
|
1971
|
+
month: "long",
|
|
1972
|
+
year: "numeric"
|
|
1973
|
+
}).format(plainDateToDate(viewDate));
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Get a full, localized label for a single date (e.g. "January 1, 2026"),
|
|
1977
|
+
* suitable for a day cell's `aria-label`.
|
|
1978
|
+
*
|
|
1979
|
+
* @param {Temporal.PlainDate} date - The date to label.
|
|
1980
|
+
* @param {string} locale - BCP 47 locale tag (default 'en').
|
|
1981
|
+
* @returns {string} Localized full-date label.
|
|
1982
|
+
*/
|
|
1983
|
+
function getFullDateLabel(date, locale = "en") {
|
|
1984
|
+
return new Intl.DateTimeFormat(resolveLocale(locale), {
|
|
1985
|
+
year: "numeric",
|
|
1986
|
+
month: "long",
|
|
1987
|
+
day: "numeric"
|
|
1988
|
+
}).format(plainDateToDate(date));
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Get the ordered weekday header labels.
|
|
1992
|
+
*
|
|
1993
|
+
* For English locales this returns `S M T W Th F S`,
|
|
1994
|
+
* with `Th` disambiguating Thursday from Tuesday). Other
|
|
1995
|
+
* locales get their `Intl` short weekday names unchanged.
|
|
1996
|
+
*
|
|
1997
|
+
* @param {string} locale - BCP 47 locale tag (default 'en').
|
|
1998
|
+
* @param {number} weekStartsOn - First day of the week: 0 = Sunday (default) through 6 = Saturday.
|
|
1999
|
+
* @returns {string[]} Seven weekday labels, ordered from `weekStartsOn`.
|
|
2000
|
+
*/
|
|
2001
|
+
function getWeekdayLabels(locale = "en", weekStartsOn = 0) {
|
|
2002
|
+
const safeLocale = resolveLocale(locale);
|
|
2003
|
+
const formatter = new Intl.DateTimeFormat(safeLocale, { weekday: "short" });
|
|
2004
|
+
const english = isEnglishLocale(safeLocale);
|
|
2005
|
+
const labels = [];
|
|
2006
|
+
for (let i = 0; i < DAYS_IN_WEEK; i++) {
|
|
2007
|
+
const offset = (weekStartsOn + i) % DAYS_IN_WEEK;
|
|
2008
|
+
const date = new Date(SUNDAY_ANCHOR.getFullYear(), SUNDAY_ANCHOR.getMonth(), SUNDAY_ANCHOR.getDate() + offset);
|
|
2009
|
+
const short = formatter.format(date);
|
|
2010
|
+
labels.push(english ? short === "Thu" ? "Th" : short.charAt(0) : short);
|
|
2011
|
+
}
|
|
2012
|
+
return labels;
|
|
2013
|
+
}
|
|
2014
|
+
//#endregion
|
|
2015
|
+
export { ClickOutsideHandler, DatePickerEvent, DatePickerState, DatePickerStateMachine, actions, addDays, addMonths, areDatesEqual, checkGuard, comparePlainDates, dateToPlainDate, daysBetween, effects, executeAction, executeEffect, formatPlainDate, generateCalendarGrid, getAction, getDateHandler, getEffect, getFullDateLabel, getGuard, getMonthEnd, getMonthStart, getMonthYearLabel, getToday, getWeekdayLabels, guards, isDateInRange, isFuture, isPast, isSingleMode, isTemporalAvailable, isToday, mapKeyboardToStateMachineEvent, parseDateString, parseDateToPlainDate, parseISOToPlainDate, plainDateToDate, plainDateToISOString };
|