@proyecto-viviana/solid-stately 0.3.0 → 0.4.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/dist/calendar/IncompleteDate.d.ts +61 -0
- package/dist/calendar/createCalendarState.d.ts +20 -10
- package/dist/calendar/createDateFieldState.d.ts +10 -3
- package/dist/calendar/createRangeCalendarState.d.ts +10 -2
- package/dist/calendar/index.d.ts +1 -1
- package/dist/collections/createListState.d.ts +4 -1
- package/dist/collections/createSelectionState.d.ts +17 -5
- package/dist/collections/getChildNodes.d.ts +11 -0
- package/dist/collections/index.d.ts +2 -1
- package/dist/combobox/createComboBoxState.d.ts +7 -0
- package/dist/dnd/index.d.ts +1 -1
- package/dist/dnd/types.d.ts +3 -1
- package/dist/grid/types.d.ts +17 -3
- package/dist/index.d.ts +6 -6
- package/dist/index.js +1570 -425
- package/dist/index.js.map +1 -1
- package/dist/select/createSelectState.d.ts +6 -0
- package/dist/selection/Selection.d.ts +16 -0
- package/dist/selection/SelectionManager.d.ts +110 -0
- package/dist/selection/createMultipleSelectionState.d.ts +59 -0
- package/dist/selection/index.d.ts +1 -0
- package/dist/table/TableCollection.d.ts +30 -0
- package/dist/table/createTreeGridState.d.ts +15 -0
- package/dist/table/index.d.ts +2 -1
- package/dist/table/types.d.ts +58 -0
- package/dist/toast/createToastState.d.ts +16 -0
- package/dist/toast/index.d.ts +1 -1
- package/dist/tree/types.d.ts +14 -6
- package/package.json +1 -1
- package/src/calendar/IncompleteDate.ts +430 -0
- package/src/calendar/createCalendarState.ts +104 -37
- package/src/calendar/createDateFieldState.ts +262 -261
- package/src/calendar/createRangeCalendarState.ts +131 -19
- package/src/calendar/index.ts +2 -0
- package/src/collections/createListState.ts +50 -19
- package/src/collections/createMenuState.ts +3 -0
- package/src/collections/createSelectionState.ts +38 -28
- package/src/collections/getChildNodes.ts +98 -0
- package/src/collections/index.ts +9 -0
- package/src/combobox/createComboBoxState.ts +8 -0
- package/src/dnd/index.d.ts +1 -0
- package/src/dnd/index.ts +1 -0
- package/src/dnd/types.d.ts +3 -1
- package/src/dnd/types.ts +4 -1
- package/src/grid/createGridState.ts +36 -2
- package/src/grid/types.ts +17 -3
- package/src/index.d.ts +1 -0
- package/src/index.ts +11 -0
- package/src/select/createSelectState.ts +15 -2
- package/src/selection/Selection.ts +28 -0
- package/src/selection/SelectionManager.ts +526 -0
- package/src/selection/createMultipleSelectionState.ts +247 -0
- package/src/selection/index.ts +6 -0
- package/src/table/TableCollection.ts +213 -43
- package/src/table/createTableState.ts +20 -0
- package/src/table/createTreeGridState.ts +146 -0
- package/src/table/index.ts +3 -0
- package/src/table/types.ts +66 -0
- package/src/tabs/createTabListState.ts +11 -9
- package/src/toast/createToastState.ts +43 -10
- package/src/toast/index.ts +1 -0
- package/src/tree/createTreeState.ts +37 -6
- package/src/tree/types.ts +14 -6
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IncompleteDate for Solid-Stately
|
|
3
|
+
*
|
|
4
|
+
* Represents a date that is incomplete or otherwise invalid as a result of user
|
|
5
|
+
* editing (for example February 31st, if the day is edited before the month).
|
|
6
|
+
* This lets a date field hold an invalid intermediate value while typing and
|
|
7
|
+
* only constrain it on blur, matching upstream behavior.
|
|
8
|
+
*
|
|
9
|
+
* Based on @react-stately/datepicker IncompleteDate
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type AnyDateTime,
|
|
14
|
+
type Calendar,
|
|
15
|
+
CalendarDate,
|
|
16
|
+
type DateValue,
|
|
17
|
+
ZonedDateTime,
|
|
18
|
+
} from "@internationalized/date";
|
|
19
|
+
import type { DateSegmentType } from "./createDateFieldState";
|
|
20
|
+
|
|
21
|
+
type HourCycle = "h12" | "h11" | "h23" | "h24";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* This class represents a date that is incomplete or otherwise invalid as a result of user editing.
|
|
25
|
+
* For example, it can represent temporary dates such as February 31st if the user edits the day
|
|
26
|
+
* before the month. Times are represented according to an hour cycle rather than always in 24 hour
|
|
27
|
+
* time. This enables the user to adjust the day period (e.g. am/pm) independently from the hour.
|
|
28
|
+
*/
|
|
29
|
+
export class IncompleteDate {
|
|
30
|
+
calendar: Calendar;
|
|
31
|
+
era: string | null;
|
|
32
|
+
year: number | null;
|
|
33
|
+
month: number | null;
|
|
34
|
+
day: number | null;
|
|
35
|
+
hour: number | null;
|
|
36
|
+
hourCycle: HourCycle;
|
|
37
|
+
dayPeriod: number | null;
|
|
38
|
+
minute: number | null;
|
|
39
|
+
second: number | null;
|
|
40
|
+
millisecond: number | null;
|
|
41
|
+
offset: number | null;
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
calendar: Calendar,
|
|
45
|
+
hourCycle: HourCycle,
|
|
46
|
+
dateValue?: Partial<Omit<AnyDateTime, "copy">> | null,
|
|
47
|
+
) {
|
|
48
|
+
this.era = dateValue?.era ?? null;
|
|
49
|
+
this.calendar = calendar;
|
|
50
|
+
this.year = dateValue?.year ?? null;
|
|
51
|
+
this.month = dateValue?.month ?? null;
|
|
52
|
+
this.day = dateValue?.day ?? null;
|
|
53
|
+
this.hour = dateValue?.hour ?? null;
|
|
54
|
+
this.hourCycle = hourCycle;
|
|
55
|
+
this.dayPeriod = null;
|
|
56
|
+
this.minute = dateValue?.minute ?? null;
|
|
57
|
+
this.second = dateValue?.second ?? null;
|
|
58
|
+
this.millisecond = dateValue?.millisecond ?? null;
|
|
59
|
+
this.offset = "offset" in (dateValue ?? {}) ? (dateValue as { offset: number }).offset : null;
|
|
60
|
+
|
|
61
|
+
// Convert the hour from 24 hour time to the given hour cycle.
|
|
62
|
+
if (this.hour != null) {
|
|
63
|
+
const [dayPeriod, hour] = toHourCycle(this.hour, hourCycle);
|
|
64
|
+
this.dayPeriod = dayPeriod;
|
|
65
|
+
this.hour = hour;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
copy(): IncompleteDate {
|
|
70
|
+
const res = new IncompleteDate(this.calendar, this.hourCycle);
|
|
71
|
+
res.era = this.era;
|
|
72
|
+
res.year = this.year;
|
|
73
|
+
res.month = this.month;
|
|
74
|
+
res.day = this.day;
|
|
75
|
+
res.hour = this.hour;
|
|
76
|
+
res.dayPeriod = this.dayPeriod;
|
|
77
|
+
res.minute = this.minute;
|
|
78
|
+
res.second = this.second;
|
|
79
|
+
res.millisecond = this.millisecond;
|
|
80
|
+
res.offset = this.offset;
|
|
81
|
+
return res;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Checks whether all the specified segments have a value. */
|
|
85
|
+
isComplete(segments: DateSegmentType[]): boolean {
|
|
86
|
+
return segments.every((segment) => this[segment as keyof IncompleteDate] != null);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Checks whether the given date value matches this value for the specified segments. */
|
|
90
|
+
validate(dt: DateValue, segments: DateSegmentType[]): boolean {
|
|
91
|
+
return segments.every((segment) => {
|
|
92
|
+
if ((segment === "hour" || segment === "dayPeriod") && "hour" in dt) {
|
|
93
|
+
const [dayPeriod, hour] = toHourCycle(dt.hour, this.hourCycle);
|
|
94
|
+
return this.dayPeriod === dayPeriod && this.hour === hour;
|
|
95
|
+
}
|
|
96
|
+
return this[segment as keyof IncompleteDate] === dt[segment as keyof DateValue];
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Checks if the date is empty (i.e. all specified segments are null). */
|
|
101
|
+
isCleared(segments: DateSegmentType[]): boolean {
|
|
102
|
+
return segments.every((segment) => this[segment as keyof IncompleteDate] === null);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Sets the given field. */
|
|
106
|
+
set(field: DateSegmentType, value: number | string, placeholder: DateValue): IncompleteDate {
|
|
107
|
+
const result = this.copy();
|
|
108
|
+
(result as unknown as Record<string, unknown>)[field] = value;
|
|
109
|
+
if (field === "hour" && result.dayPeriod == null && "hour" in placeholder) {
|
|
110
|
+
result.dayPeriod = toHourCycle(placeholder.hour, this.hourCycle)[0];
|
|
111
|
+
}
|
|
112
|
+
if (field === "year" && result.era == null) {
|
|
113
|
+
result.era = placeholder.era;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// clear offset when a date/time field changes since it may no longer be valid
|
|
117
|
+
if (field !== "second" && field !== "literal" && field !== "timeZoneName") {
|
|
118
|
+
result.offset = null;
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Sets the given field to null. */
|
|
124
|
+
clear(field: DateSegmentType): IncompleteDate {
|
|
125
|
+
const result = this.copy();
|
|
126
|
+
(result as unknown as Record<string, unknown>)[field] = null;
|
|
127
|
+
if (field === "year") {
|
|
128
|
+
result.era = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// clear offset when a field is cleared since it may no longer be valid
|
|
132
|
+
result.offset = null;
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Increments or decrements the given field. If it is null, then it is set to the placeholder
|
|
138
|
+
* value.
|
|
139
|
+
*/
|
|
140
|
+
cycle(
|
|
141
|
+
field: DateSegmentType,
|
|
142
|
+
amount: number,
|
|
143
|
+
placeholder: DateValue,
|
|
144
|
+
displaySegments: DateSegmentType[],
|
|
145
|
+
): IncompleteDate {
|
|
146
|
+
const res = this.copy();
|
|
147
|
+
|
|
148
|
+
// If field is null, default to placeholder.
|
|
149
|
+
if (res[field as keyof IncompleteDate] == null && field !== "dayPeriod" && field !== "era") {
|
|
150
|
+
if (field === "hour" && "hour" in placeholder) {
|
|
151
|
+
const [dayPeriod, hour] = toHourCycle(placeholder.hour, this.hourCycle);
|
|
152
|
+
res.dayPeriod = dayPeriod;
|
|
153
|
+
res.hour = hour;
|
|
154
|
+
} else {
|
|
155
|
+
(res as unknown as Record<string, unknown>)[field] = placeholder[field as keyof DateValue];
|
|
156
|
+
}
|
|
157
|
+
if (field === "year" && res.era == null) {
|
|
158
|
+
res.era = placeholder.era;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return res;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
switch (field) {
|
|
165
|
+
case "era": {
|
|
166
|
+
const eras = this.calendar.getEras();
|
|
167
|
+
let index = eras.indexOf(res.era!);
|
|
168
|
+
index = cycleValue(index, amount, 0, eras.length - 1);
|
|
169
|
+
res.era = eras[index];
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "year": {
|
|
173
|
+
// Use CalendarDate to cycle so that we update the era when going between 1 AD and 1 BC.
|
|
174
|
+
let date = new CalendarDate(
|
|
175
|
+
this.calendar,
|
|
176
|
+
this.era ?? placeholder.era,
|
|
177
|
+
this.year ?? placeholder.year,
|
|
178
|
+
this.month ?? 1,
|
|
179
|
+
this.day ?? 1,
|
|
180
|
+
);
|
|
181
|
+
date = date.cycle(field, amount, { round: field === "year" });
|
|
182
|
+
res.era = date.era;
|
|
183
|
+
res.year = date.year;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case "month":
|
|
187
|
+
res.month = cycleValue(res.month ?? 1, amount, 1, this.calendar.getMaximumMonthsInYear());
|
|
188
|
+
break;
|
|
189
|
+
case "day":
|
|
190
|
+
// Allow incrementing up to the maximum number of days in any month.
|
|
191
|
+
res.day = cycleValue(res.day ?? 1, amount, 1, this.calendar.getMaximumDaysInMonth());
|
|
192
|
+
break;
|
|
193
|
+
case "hour": {
|
|
194
|
+
// if date is fully defined or it is just a time field, and we have a time zone, use toValue to get a ZonedDateTime to cycle
|
|
195
|
+
// so DST fallback is properly handled
|
|
196
|
+
const hasDateSegements = displaySegments.some((s) => ["year", "month", "day"].includes(s));
|
|
197
|
+
if (
|
|
198
|
+
"timeZone" in placeholder &&
|
|
199
|
+
(!hasDateSegements || (res.year != null && res.month != null && res.day != null))
|
|
200
|
+
) {
|
|
201
|
+
let date = this.toValue(placeholder) as ZonedDateTime;
|
|
202
|
+
date = date.cycle("hour", amount, {
|
|
203
|
+
hourCycle: this.hourCycle === "h12" ? 12 : 24,
|
|
204
|
+
round: false,
|
|
205
|
+
});
|
|
206
|
+
const [dayPeriod, adjustedHour] = toHourCycle(date.hour, this.hourCycle);
|
|
207
|
+
res.hour = adjustedHour;
|
|
208
|
+
res.dayPeriod = dayPeriod;
|
|
209
|
+
res.offset = date.offset;
|
|
210
|
+
} else {
|
|
211
|
+
const hours = res.hour ?? 0;
|
|
212
|
+
const limits = this.getSegmentLimits("hour")!;
|
|
213
|
+
res.hour = cycleValue(hours, amount, limits.minValue, limits.maxValue);
|
|
214
|
+
if (res.dayPeriod == null && "hour" in placeholder) {
|
|
215
|
+
res.dayPeriod = toHourCycle(placeholder.hour, this.hourCycle)[0];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case "dayPeriod":
|
|
221
|
+
res.dayPeriod = cycleValue(res.dayPeriod ?? 0, amount, 0, 1);
|
|
222
|
+
break;
|
|
223
|
+
case "minute":
|
|
224
|
+
res.minute = cycleValue(res.minute ?? 0, amount, 0, 59, true);
|
|
225
|
+
break;
|
|
226
|
+
case "second":
|
|
227
|
+
res.second = cycleValue(res.second ?? 0, amount, 0, 59, true);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return res;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Converts the incomplete date to a full date value, using the provided value for any unset
|
|
236
|
+
* fields.
|
|
237
|
+
*/
|
|
238
|
+
toValue(value: DateValue): DateValue {
|
|
239
|
+
if ("hour" in value) {
|
|
240
|
+
let hour = this.hour;
|
|
241
|
+
if (hour != null) {
|
|
242
|
+
hour = fromHourCycle(hour, this.dayPeriod ?? 0, this.hourCycle);
|
|
243
|
+
} else if (this.hourCycle === "h12" || this.hourCycle === "h11") {
|
|
244
|
+
hour = this.dayPeriod === 1 ? 12 : 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let res = value.set({
|
|
248
|
+
era: this.era ?? value.era,
|
|
249
|
+
year: this.year ?? value.year,
|
|
250
|
+
month: this.month ?? value.month,
|
|
251
|
+
day: this.day ?? value.day,
|
|
252
|
+
hour: hour ?? value.hour,
|
|
253
|
+
minute: this.minute ?? value.minute,
|
|
254
|
+
second: this.second ?? value.second,
|
|
255
|
+
millisecond: this.millisecond ?? value.millisecond,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if ("offset" in res && this.offset != null && res.offset !== this.offset) {
|
|
259
|
+
res = res.add({ milliseconds: res.offset - this.offset });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return res;
|
|
263
|
+
} else {
|
|
264
|
+
return value.set({
|
|
265
|
+
era: this.era ?? value.era,
|
|
266
|
+
year: this.year ?? value.year,
|
|
267
|
+
month: this.month ?? value.month,
|
|
268
|
+
day: this.day ?? value.day,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
getSegmentLimits(
|
|
274
|
+
type: string,
|
|
275
|
+
): { value: number | null; minValue: number; maxValue: number } | undefined {
|
|
276
|
+
switch (type) {
|
|
277
|
+
case "era": {
|
|
278
|
+
const eras = this.calendar.getEras();
|
|
279
|
+
return {
|
|
280
|
+
value: this.era != null ? eras.indexOf(this.era) : eras.length - 1,
|
|
281
|
+
minValue: 0,
|
|
282
|
+
maxValue: eras.length - 1,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
case "year":
|
|
286
|
+
return {
|
|
287
|
+
value: this.year,
|
|
288
|
+
minValue: 1,
|
|
289
|
+
maxValue: 9999,
|
|
290
|
+
};
|
|
291
|
+
case "month":
|
|
292
|
+
return {
|
|
293
|
+
value: this.month,
|
|
294
|
+
minValue: 1,
|
|
295
|
+
maxValue: this.calendar.getMaximumMonthsInYear(),
|
|
296
|
+
};
|
|
297
|
+
case "day":
|
|
298
|
+
return {
|
|
299
|
+
value: this.day,
|
|
300
|
+
minValue: 1,
|
|
301
|
+
maxValue: this.calendar.getMaximumDaysInMonth(),
|
|
302
|
+
};
|
|
303
|
+
case "dayPeriod": {
|
|
304
|
+
return {
|
|
305
|
+
value: this.dayPeriod,
|
|
306
|
+
minValue: 0,
|
|
307
|
+
maxValue: 1,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
case "hour": {
|
|
311
|
+
let minValue = 0;
|
|
312
|
+
let maxValue = 23;
|
|
313
|
+
if (this.hourCycle === "h12") {
|
|
314
|
+
minValue = 1;
|
|
315
|
+
maxValue = 12;
|
|
316
|
+
} else if (this.hourCycle === "h11") {
|
|
317
|
+
minValue = 0;
|
|
318
|
+
maxValue = 11;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
value: this.hour,
|
|
323
|
+
minValue,
|
|
324
|
+
maxValue,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
case "minute":
|
|
328
|
+
return {
|
|
329
|
+
value: this.minute,
|
|
330
|
+
minValue: 0,
|
|
331
|
+
maxValue: 59,
|
|
332
|
+
};
|
|
333
|
+
case "second":
|
|
334
|
+
return {
|
|
335
|
+
value: this.second,
|
|
336
|
+
minValue: 0,
|
|
337
|
+
maxValue: 59,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function cycleValue(
|
|
344
|
+
value: number,
|
|
345
|
+
amount: number,
|
|
346
|
+
min: number,
|
|
347
|
+
max: number,
|
|
348
|
+
round = false,
|
|
349
|
+
): number {
|
|
350
|
+
if (round) {
|
|
351
|
+
value += Math.sign(amount);
|
|
352
|
+
|
|
353
|
+
if (value < min) {
|
|
354
|
+
value = max;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const div = Math.abs(amount);
|
|
358
|
+
if (amount > 0) {
|
|
359
|
+
value = Math.ceil(value / div) * div;
|
|
360
|
+
} else {
|
|
361
|
+
value = Math.floor(value / div) * div;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (value > max) {
|
|
365
|
+
value = min;
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
value += amount;
|
|
369
|
+
if (value < min) {
|
|
370
|
+
value = max - (min - value - 1);
|
|
371
|
+
} else if (value > max) {
|
|
372
|
+
value = min + (value - max - 1);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function toHourCycle(hour: number, hourCycle: HourCycle): [number | null, number] {
|
|
380
|
+
let dayPeriod: number | null = hour >= 12 ? 1 : 0;
|
|
381
|
+
switch (hourCycle) {
|
|
382
|
+
case "h11":
|
|
383
|
+
// Hours are numbered from 0 to 11. Used in Japan.
|
|
384
|
+
if (hour >= 12) {
|
|
385
|
+
hour -= 12;
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
case "h12":
|
|
389
|
+
// Hours are numbered from 12 (representing 0) to 11.
|
|
390
|
+
if (hour === 0) {
|
|
391
|
+
hour = 12;
|
|
392
|
+
} else if (hour > 12) {
|
|
393
|
+
hour -= 12;
|
|
394
|
+
}
|
|
395
|
+
break;
|
|
396
|
+
case "h23":
|
|
397
|
+
// 24 hour time, numbered 0 to 23.
|
|
398
|
+
dayPeriod = null;
|
|
399
|
+
break;
|
|
400
|
+
case "h24":
|
|
401
|
+
// 24 hour time numbered 24 to 23. Unused but supported by Intl.DateTimeFormat.
|
|
402
|
+
hour += 1;
|
|
403
|
+
dayPeriod = null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return [dayPeriod, hour];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function fromHourCycle(hour: number, dayPeriod: number, hourCycle: HourCycle): number {
|
|
410
|
+
switch (hourCycle) {
|
|
411
|
+
case "h11":
|
|
412
|
+
if (dayPeriod === 1) {
|
|
413
|
+
hour += 12;
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
case "h12":
|
|
417
|
+
if (hour === 12) {
|
|
418
|
+
hour = 0;
|
|
419
|
+
}
|
|
420
|
+
if (dayPeriod === 1) {
|
|
421
|
+
hour += 12;
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
case "h24":
|
|
425
|
+
hour -= 1;
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return hour;
|
|
430
|
+
}
|
|
@@ -35,13 +35,23 @@ export type CalendarDayOfWeek = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" |
|
|
|
35
35
|
|
|
36
36
|
const dayOfWeekNames: CalendarDayOfWeek[] = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
37
37
|
|
|
38
|
-
export
|
|
38
|
+
export type CalendarSelectionMode = "single" | "multiple";
|
|
39
|
+
export type CalendarValueType<T, M extends CalendarSelectionMode> = M extends "single"
|
|
40
|
+
? T
|
|
41
|
+
: readonly T[];
|
|
42
|
+
|
|
43
|
+
export interface CalendarStateProps<
|
|
44
|
+
T extends DateValue = DateValue,
|
|
45
|
+
M extends CalendarSelectionMode = "single",
|
|
46
|
+
> {
|
|
39
47
|
/** The current value (controlled). */
|
|
40
|
-
value?: MaybeAccessor<T | null>;
|
|
48
|
+
value?: MaybeAccessor<CalendarValueType<T | null, M> | undefined>;
|
|
41
49
|
/** The default value (uncontrolled). */
|
|
42
|
-
defaultValue?: T | null;
|
|
50
|
+
defaultValue?: CalendarValueType<T | null, M> | null;
|
|
43
51
|
/** Handler called when the value changes. */
|
|
44
|
-
onChange?: (value: T | null) => void;
|
|
52
|
+
onChange?: (value: CalendarValueType<T | null, M>) => void;
|
|
53
|
+
/** Whether single or multiple date selection is enabled. */
|
|
54
|
+
selectionMode?: MaybeAccessor<M>;
|
|
45
55
|
/** The minimum allowed date. */
|
|
46
56
|
minValue?: MaybeAccessor<DateValue | undefined>;
|
|
47
57
|
/** The maximum allowed date. */
|
|
@@ -82,15 +92,24 @@ export interface CalendarStateProps<T extends DateValue = DateValue> {
|
|
|
82
92
|
firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
|
-
export interface CalendarState<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
export interface CalendarState<
|
|
96
|
+
T extends DateValue = DateValue,
|
|
97
|
+
M extends CalendarSelectionMode = "single",
|
|
98
|
+
> {
|
|
99
|
+
/** Whether single or multiple selection is enabled. */
|
|
100
|
+
selectionMode: Accessor<CalendarSelectionMode>;
|
|
101
|
+
/** The currently selected date(s). */
|
|
102
|
+
value: Accessor<CalendarValueType<T | null, M>>;
|
|
103
|
+
/** Sets the selected date(s). */
|
|
104
|
+
setValue: (value: CalendarValueType<T | null, M>) => void;
|
|
90
105
|
/** The currently focused date. */
|
|
91
106
|
focusedDate: Accessor<CalendarDate>;
|
|
92
107
|
/** Sets the focused date. */
|
|
93
108
|
setFocusedDate: (date: CalendarDate) => void;
|
|
109
|
+
/** The minimum allowed date that a user may select. */
|
|
110
|
+
minValue: Accessor<DateValue | null>;
|
|
111
|
+
/** The maximum allowed date that a user may select. */
|
|
112
|
+
maxValue: Accessor<DateValue | null>;
|
|
94
113
|
/** Whether the calendar is disabled. */
|
|
95
114
|
isDisabled: Accessor<boolean>;
|
|
96
115
|
/** Whether the calendar is read-only. */
|
|
@@ -164,11 +183,17 @@ export interface CalendarState<T extends DateValue = DateValue> {
|
|
|
164
183
|
/**
|
|
165
184
|
* Provides state management for a calendar component.
|
|
166
185
|
*/
|
|
167
|
-
export function createCalendarState<
|
|
168
|
-
|
|
169
|
-
|
|
186
|
+
export function createCalendarState<
|
|
187
|
+
T extends DateValue = CalendarDate,
|
|
188
|
+
M extends CalendarSelectionMode = "single",
|
|
189
|
+
>(props: CalendarStateProps<T, M> = {}): CalendarState<T, M> {
|
|
170
190
|
const timeZone = getLocalTimeZone();
|
|
171
191
|
const locale = createMemo(() => access(props.locale) ?? "en-US");
|
|
192
|
+
const minValueState = createMemo(() => access(props.minValue) ?? null);
|
|
193
|
+
const maxValueState = createMemo(() => access(props.maxValue) ?? null);
|
|
194
|
+
const selectionMode = createMemo<CalendarSelectionMode>(
|
|
195
|
+
() => access(props.selectionMode) ?? "single",
|
|
196
|
+
);
|
|
172
197
|
const resolvedOptions = createMemo(() => new DateFormatter(locale()).resolvedOptions());
|
|
173
198
|
const calendar = createMemo(() =>
|
|
174
199
|
(props.createCalendar ?? intlCreateCalendar)(resolvedOptions().calendar as CalendarIdentifier),
|
|
@@ -181,19 +206,21 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
|
|
|
181
206
|
intlToCalendar(toCalendarDate(date), calendar());
|
|
182
207
|
|
|
183
208
|
const selectionAlignmentOffset = (): number => {
|
|
209
|
+
// Mirrors @react-stately/calendar useCalendarState, where selectionAlignment
|
|
210
|
+
// defaults to "center" (alignCenter) when not provided.
|
|
184
211
|
switch (props.selectionAlignment) {
|
|
212
|
+
case "start":
|
|
213
|
+
return 0;
|
|
185
214
|
case "end":
|
|
186
215
|
return Math.max(visibleMonths - 1, 0);
|
|
187
|
-
case "center":
|
|
216
|
+
case "center":
|
|
217
|
+
default: {
|
|
188
218
|
let halfDuration = Math.floor(visibleMonths / 2);
|
|
189
219
|
if (halfDuration > 0 && visibleMonths % 2 === 0) {
|
|
190
220
|
halfDuration--;
|
|
191
221
|
}
|
|
192
222
|
return halfDuration;
|
|
193
223
|
}
|
|
194
|
-
case "start":
|
|
195
|
-
default:
|
|
196
|
-
return 0;
|
|
197
224
|
}
|
|
198
225
|
};
|
|
199
226
|
|
|
@@ -238,18 +265,24 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
|
|
|
238
265
|
return toDisplayCalendarDate(props.defaultFocusedValue);
|
|
239
266
|
}
|
|
240
267
|
const controlledValue = access(props.value);
|
|
241
|
-
|
|
242
|
-
|
|
268
|
+
const controlledFirst = Array.isArray(controlledValue) ? controlledValue[0] : controlledValue;
|
|
269
|
+
if (controlledFirst) {
|
|
270
|
+
return toDisplayCalendarDate(controlledFirst);
|
|
243
271
|
}
|
|
244
|
-
|
|
245
|
-
|
|
272
|
+
const defaultFirst = Array.isArray(props.defaultValue)
|
|
273
|
+
? props.defaultValue[0]
|
|
274
|
+
: props.defaultValue;
|
|
275
|
+
if (defaultFirst) {
|
|
276
|
+
return toDisplayCalendarDate(defaultFirst);
|
|
246
277
|
}
|
|
247
278
|
return intlToCalendar(today(timeZone), calendar());
|
|
248
279
|
};
|
|
249
280
|
|
|
250
281
|
// State signals
|
|
251
282
|
const initialFocusedDate = constrainDate(getInitialFocusedDate());
|
|
252
|
-
const [internalValue, setInternalValue] = createSignal<T | null>(
|
|
283
|
+
const [internalValue, setInternalValue] = createSignal<T | T[] | null>(
|
|
284
|
+
(props.defaultValue as T | T[] | null) ?? null,
|
|
285
|
+
);
|
|
253
286
|
const [focusedDate, setFocusedDateInternal] = createSignal<CalendarDate>(initialFocusedDate);
|
|
254
287
|
const [visibleRangeStart, setVisibleRangeStart] = createSignal<CalendarDate>(
|
|
255
288
|
alignVisibleRangeStart(initialFocusedDate),
|
|
@@ -258,12 +291,15 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
|
|
|
258
291
|
const [isPaginating, setIsPaginating] = createSignal(false);
|
|
259
292
|
|
|
260
293
|
// Controlled vs uncontrolled value
|
|
261
|
-
const sourceValue = createMemo<T | null>(() => {
|
|
294
|
+
const sourceValue = createMemo<T | T[] | null>(() => {
|
|
262
295
|
const controlled = access(props.value);
|
|
263
|
-
return controlled !== undefined ? controlled : internalValue();
|
|
296
|
+
return controlled !== undefined ? (controlled as T | T[] | null) : internalValue();
|
|
264
297
|
});
|
|
265
|
-
const value = createMemo<T | null>(() => {
|
|
298
|
+
const value = createMemo<T | T[] | null>(() => {
|
|
266
299
|
const currentValue = sourceValue();
|
|
300
|
+
if (Array.isArray(currentValue)) {
|
|
301
|
+
return currentValue.map((v) => toDisplayCalendarDate(v) as unknown as T);
|
|
302
|
+
}
|
|
267
303
|
return currentValue ? (toDisplayCalendarDate(currentValue) as unknown as T) : null;
|
|
268
304
|
});
|
|
269
305
|
|
|
@@ -370,13 +406,22 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
|
|
|
370
406
|
});
|
|
371
407
|
|
|
372
408
|
// Set value with onChange callback
|
|
373
|
-
const setValue = (newValue: T | null) => {
|
|
409
|
+
const setValue = (newValue: T | T[] | null) => {
|
|
374
410
|
if (isDisabled() || isReadOnly()) return;
|
|
375
411
|
|
|
376
412
|
const oldValue = sourceValue();
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
413
|
+
const referenceValue = Array.isArray(oldValue) ? (oldValue[0] ?? null) : oldValue;
|
|
414
|
+
|
|
415
|
+
let nextValue: T | T[] | null;
|
|
416
|
+
if (newValue == null) {
|
|
417
|
+
nextValue = selectionMode() === "multiple" ? ([] as T[]) : null;
|
|
418
|
+
} else if (Array.isArray(newValue)) {
|
|
419
|
+
nextValue = newValue.map(
|
|
420
|
+
(v) => convertValue(constrainDate(toDisplayCalendarDate(v)), referenceValue) as T,
|
|
421
|
+
);
|
|
422
|
+
} else {
|
|
423
|
+
nextValue = convertValue(constrainDate(toDisplayCalendarDate(newValue)), referenceValue) as T;
|
|
424
|
+
}
|
|
380
425
|
|
|
381
426
|
const controlled = access(props.value);
|
|
382
427
|
if (controlled === undefined) {
|
|
@@ -384,7 +429,7 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
|
|
|
384
429
|
}
|
|
385
430
|
|
|
386
431
|
if (!Object.is(oldValue, nextValue) && props.onChange) {
|
|
387
|
-
props.onChange(nextValue);
|
|
432
|
+
props.onChange(nextValue as CalendarValueType<T | null, M>);
|
|
388
433
|
}
|
|
389
434
|
};
|
|
390
435
|
|
|
@@ -404,8 +449,12 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
|
|
|
404
449
|
// Check if a date is selected
|
|
405
450
|
const isSelected = (date: DateValue): boolean => {
|
|
406
451
|
const v = value();
|
|
407
|
-
if (
|
|
408
|
-
|
|
452
|
+
if (v == null) return false;
|
|
453
|
+
const target = toDisplayCalendarDate(date);
|
|
454
|
+
if (Array.isArray(v)) {
|
|
455
|
+
return v.some((d) => isSameDay(target, toDisplayCalendarDate(d)));
|
|
456
|
+
}
|
|
457
|
+
return isSameDay(target, toDisplayCalendarDate(v));
|
|
409
458
|
};
|
|
410
459
|
|
|
411
460
|
// Check if a date is focused
|
|
@@ -502,17 +551,32 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
|
|
|
502
551
|
const selectFocusedDate = () => {
|
|
503
552
|
if (isReadOnly() || isDisabled()) return;
|
|
504
553
|
const date = focusedDate();
|
|
505
|
-
if (!
|
|
506
|
-
|
|
554
|
+
if (!isCellUnavailable(date)) {
|
|
555
|
+
selectDate(date);
|
|
507
556
|
}
|
|
508
557
|
};
|
|
509
558
|
|
|
510
559
|
const selectDate = (date: CalendarDate) => {
|
|
511
560
|
if (isReadOnly() || isDisabled()) return;
|
|
512
|
-
if (
|
|
561
|
+
if (isCellDisabled(date) || isCellUnavailable(date)) return;
|
|
562
|
+
|
|
563
|
+
if (selectionMode() === "multiple") {
|
|
564
|
+
const current = value();
|
|
565
|
+
const base: CalendarDate[] = Array.isArray(current)
|
|
566
|
+
? current.map((v) => toDisplayCalendarDate(v))
|
|
567
|
+
: current != null
|
|
568
|
+
? [toDisplayCalendarDate(current)]
|
|
569
|
+
: [];
|
|
570
|
+
const target = toDisplayCalendarDate(date);
|
|
571
|
+
const index = base.findIndex((d) => isSameDay(d, target));
|
|
572
|
+
const nextValue =
|
|
573
|
+
index >= 0 ? [...base.slice(0, index), ...base.slice(index + 1)] : [...base, target];
|
|
574
|
+
setValue(nextValue as unknown as T[]);
|
|
575
|
+
} else {
|
|
513
576
|
setValue(date as unknown as T);
|
|
514
|
-
setFocusedDate(date);
|
|
515
577
|
}
|
|
578
|
+
|
|
579
|
+
setFocusedDate(date);
|
|
516
580
|
};
|
|
517
581
|
|
|
518
582
|
// Get dates in a specific week
|
|
@@ -548,8 +612,9 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
|
|
|
548
612
|
};
|
|
549
613
|
|
|
550
614
|
return {
|
|
551
|
-
|
|
552
|
-
|
|
615
|
+
selectionMode,
|
|
616
|
+
value: value as unknown as Accessor<CalendarValueType<T | null, M>>,
|
|
617
|
+
setValue: setValue as unknown as (value: CalendarValueType<T | null, M>) => void,
|
|
553
618
|
focusedDate,
|
|
554
619
|
setFocusedDate,
|
|
555
620
|
isDisabled,
|
|
@@ -557,6 +622,8 @@ export function createCalendarState<T extends DateValue = CalendarDate>(
|
|
|
557
622
|
visibleRange,
|
|
558
623
|
timeZone,
|
|
559
624
|
locale,
|
|
625
|
+
minValue: minValueState,
|
|
626
|
+
maxValue: maxValueState,
|
|
560
627
|
firstDayOfWeek: firstDayOfWeekName,
|
|
561
628
|
validationState,
|
|
562
629
|
isValueInvalid,
|