@lazerlen/legend-calendar 1.0.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/.eslintrc.js +29 -0
- package/.turbo/turbo-build.log +19 -0
- package/.turbo/turbo-lint.log +14 -0
- package/.turbo/turbo-test.log +261 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/CHANGELOG.md +127 -0
- package/dist/index.d.mts +679 -0
- package/dist/index.d.ts +679 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
- package/src/components/Calendar.stories.tsx +226 -0
- package/src/components/Calendar.tsx +224 -0
- package/src/components/CalendarItemDay.tsx +385 -0
- package/src/components/CalendarItemEmpty.tsx +30 -0
- package/src/components/CalendarItemWeekName.tsx +67 -0
- package/src/components/CalendarList.stories.tsx +326 -0
- package/src/components/CalendarList.tsx +373 -0
- package/src/components/CalendarRowMonth.tsx +62 -0
- package/src/components/CalendarRowWeek.tsx +46 -0
- package/src/components/CalendarThemeProvider.tsx +43 -0
- package/src/components/HStack.tsx +67 -0
- package/src/components/VStack.tsx +67 -0
- package/src/components/index.ts +108 -0
- package/src/developer/decorators.tsx +54 -0
- package/src/developer/loggginHandler.tsx +6 -0
- package/src/developer/useRenderCount.ts +7 -0
- package/src/helpers/dates.test.ts +567 -0
- package/src/helpers/dates.ts +163 -0
- package/src/helpers/functions.ts +327 -0
- package/src/helpers/numbers.ts +11 -0
- package/src/helpers/strings.ts +2 -0
- package/src/helpers/tokens.ts +71 -0
- package/src/helpers/types.ts +3 -0
- package/src/hooks/useCalendar.test.ts +381 -0
- package/src/hooks/useCalendar.ts +351 -0
- package/src/hooks/useCalendarList.test.ts +382 -0
- package/src/hooks/useCalendarList.tsx +291 -0
- package/src/hooks/useDateRange.test.ts +128 -0
- package/src/hooks/useDateRange.ts +94 -0
- package/src/hooks/useOptimizedDayMetadata.test.ts +582 -0
- package/src/hooks/useOptimizedDayMetadata.ts +93 -0
- package/src/hooks/useTheme.ts +14 -0
- package/src/index.ts +24 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +15 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react-hooks";
|
|
2
|
+
import { describe, it, expect } from "bun:test";
|
|
3
|
+
|
|
4
|
+
import { fromDateId } from "@/helpers/dates";
|
|
5
|
+
import { getHeightForMonth, useCalendarList } from "@/hooks/useCalendarList";
|
|
6
|
+
|
|
7
|
+
describe("getHeightForMonth", () => {
|
|
8
|
+
it("Measures months with 5 weeks (no calendar spacing)", () => {
|
|
9
|
+
const calendarMonthHeaderHeight = 50;
|
|
10
|
+
const calendarRowVerticalSpacing = 5;
|
|
11
|
+
const calendarWeekHeaderHeight = 30;
|
|
12
|
+
const calendarDayHeight = 20;
|
|
13
|
+
const calendarSpacing = 0;
|
|
14
|
+
const calendarAdditionalHeight = 0;
|
|
15
|
+
|
|
16
|
+
expect(
|
|
17
|
+
getHeightForMonth({
|
|
18
|
+
calendarDayHeight,
|
|
19
|
+
calendarAdditionalHeight,
|
|
20
|
+
calendarMonthHeaderHeight,
|
|
21
|
+
calendarRowVerticalSpacing,
|
|
22
|
+
calendarWeekHeaderHeight,
|
|
23
|
+
calendarSpacing,
|
|
24
|
+
calendarMonth: {
|
|
25
|
+
id: "some",
|
|
26
|
+
date: new Date(),
|
|
27
|
+
numberOfWeeks: 5,
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
).toBe(210);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("Measures months with 5 weeks (with calendar spacing)", () => {
|
|
34
|
+
const calendarMonthHeaderHeight = 50;
|
|
35
|
+
const calendarRowVerticalSpacing = 5;
|
|
36
|
+
const calendarWeekHeaderHeight = 30;
|
|
37
|
+
const calendarDayHeight = 20;
|
|
38
|
+
const calendarSpacing = 99;
|
|
39
|
+
const calendarAdditionalHeight = 0;
|
|
40
|
+
|
|
41
|
+
expect(
|
|
42
|
+
getHeightForMonth({
|
|
43
|
+
calendarDayHeight,
|
|
44
|
+
calendarAdditionalHeight,
|
|
45
|
+
calendarMonthHeaderHeight,
|
|
46
|
+
calendarRowVerticalSpacing,
|
|
47
|
+
calendarWeekHeaderHeight,
|
|
48
|
+
calendarSpacing,
|
|
49
|
+
calendarMonth: {
|
|
50
|
+
id: "some",
|
|
51
|
+
date: new Date(),
|
|
52
|
+
numberOfWeeks: 5,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
).toBe(309);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("Accounts for additional height", () => {
|
|
59
|
+
const calendarMonthHeaderHeight = 50;
|
|
60
|
+
const calendarRowVerticalSpacing = 5;
|
|
61
|
+
const calendarWeekHeaderHeight = 30;
|
|
62
|
+
const calendarDayHeight = 20;
|
|
63
|
+
const calendarSpacing = 0;
|
|
64
|
+
const calendarAdditionalHeight = 24;
|
|
65
|
+
|
|
66
|
+
expect(
|
|
67
|
+
getHeightForMonth({
|
|
68
|
+
calendarDayHeight,
|
|
69
|
+
calendarAdditionalHeight,
|
|
70
|
+
calendarMonthHeaderHeight,
|
|
71
|
+
calendarRowVerticalSpacing,
|
|
72
|
+
calendarWeekHeaderHeight,
|
|
73
|
+
calendarSpacing,
|
|
74
|
+
calendarMonth: {
|
|
75
|
+
id: "some",
|
|
76
|
+
date: new Date(),
|
|
77
|
+
numberOfWeeks: 5,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
).toBe(234);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("Measures months with 6 weeks", () => {
|
|
84
|
+
const calendarMonthHeaderHeight = 50;
|
|
85
|
+
const calendarRowVerticalSpacing = 5;
|
|
86
|
+
const calendarWeekHeaderHeight = 30;
|
|
87
|
+
const calendarDayHeight = 20;
|
|
88
|
+
const calendarSpacing = 0;
|
|
89
|
+
const calendarAdditionalHeight = 0;
|
|
90
|
+
|
|
91
|
+
expect(
|
|
92
|
+
getHeightForMonth({
|
|
93
|
+
calendarDayHeight,
|
|
94
|
+
calendarAdditionalHeight,
|
|
95
|
+
calendarMonthHeaderHeight,
|
|
96
|
+
calendarRowVerticalSpacing,
|
|
97
|
+
calendarWeekHeaderHeight,
|
|
98
|
+
calendarSpacing,
|
|
99
|
+
calendarMonth: {
|
|
100
|
+
id: "some",
|
|
101
|
+
date: new Date(),
|
|
102
|
+
numberOfWeeks: 6,
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
).toBe(235);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("February 24 has the right height with base options", () => {
|
|
109
|
+
const calendarMonthHeaderHeight = 20;
|
|
110
|
+
const calendarRowVerticalSpacing = 8;
|
|
111
|
+
const calendarWeekHeaderHeight = 32;
|
|
112
|
+
const calendarDayHeight = 32;
|
|
113
|
+
const calendarSpacing = 20;
|
|
114
|
+
const calendarAdditionalHeight = 0;
|
|
115
|
+
|
|
116
|
+
expect(
|
|
117
|
+
getHeightForMonth({
|
|
118
|
+
calendarDayHeight,
|
|
119
|
+
calendarAdditionalHeight,
|
|
120
|
+
calendarMonthHeaderHeight,
|
|
121
|
+
calendarRowVerticalSpacing,
|
|
122
|
+
calendarWeekHeaderHeight,
|
|
123
|
+
calendarSpacing,
|
|
124
|
+
calendarMonth: {
|
|
125
|
+
id: "some",
|
|
126
|
+
date: fromDateId("2024-02-01"),
|
|
127
|
+
numberOfWeeks: 5,
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
).toBe(280);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("useCalendarList", () => {
|
|
135
|
+
describe("Min/Max dates", () => {
|
|
136
|
+
it("Max: virtualization still works as expected", () => {
|
|
137
|
+
const { result } = renderHook(() =>
|
|
138
|
+
useCalendarList({
|
|
139
|
+
calendarFirstDayOfWeek: "sunday",
|
|
140
|
+
calendarFutureScrollRangeInMonths: 2,
|
|
141
|
+
calendarPastScrollRangeInMonths: 2,
|
|
142
|
+
calendarInitialMonthId: "2024-07-01",
|
|
143
|
+
calendarMinDateId: "2024-01-01",
|
|
144
|
+
calendarMaxDateId: "2024-12-31",
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Initially, we render 5 months: the initial month +- 2
|
|
149
|
+
expect(result.current.monthList).toHaveLength(5);
|
|
150
|
+
const initialIds = [
|
|
151
|
+
"2024-05-01",
|
|
152
|
+
"2024-06-01",
|
|
153
|
+
"2024-07-01",
|
|
154
|
+
"2024-08-01",
|
|
155
|
+
"2024-09-01",
|
|
156
|
+
];
|
|
157
|
+
expect(result.current.monthList.map((m) => m.id)).toEqual(initialIds);
|
|
158
|
+
|
|
159
|
+
// After we scroll to the end, we append 2 more months since we're not at the max date yet.
|
|
160
|
+
act(() => {
|
|
161
|
+
result.current.appendMonths(2);
|
|
162
|
+
});
|
|
163
|
+
expect(result.current.monthList).toHaveLength(7);
|
|
164
|
+
expect(result.current.monthList.map((m) => m.id)).toEqual([
|
|
165
|
+
...initialIds,
|
|
166
|
+
"2024-10-01",
|
|
167
|
+
"2024-11-01",
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
// After we scroll to the end again, we append just another month since we're at the max date.
|
|
171
|
+
act(() => {
|
|
172
|
+
result.current.appendMonths(2); // no change here
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(result.current.monthList).toHaveLength(8);
|
|
176
|
+
|
|
177
|
+
const finalIds = [
|
|
178
|
+
...initialIds,
|
|
179
|
+
"2024-10-01",
|
|
180
|
+
"2024-11-01",
|
|
181
|
+
"2024-12-01",
|
|
182
|
+
];
|
|
183
|
+
expect(result.current.monthList.map((m) => m.id)).toEqual(finalIds);
|
|
184
|
+
|
|
185
|
+
// If we try scrolling once more, it's a no-op
|
|
186
|
+
act(() => {
|
|
187
|
+
result.current.appendMonths(2);
|
|
188
|
+
});
|
|
189
|
+
expect(result.current.monthList.map((m) => m.id)).toEqual(finalIds);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("Min: virtualization still works as expected", () => {
|
|
193
|
+
const { result } = renderHook(() =>
|
|
194
|
+
useCalendarList({
|
|
195
|
+
calendarFirstDayOfWeek: "sunday",
|
|
196
|
+
calendarFutureScrollRangeInMonths: 2,
|
|
197
|
+
calendarPastScrollRangeInMonths: 2,
|
|
198
|
+
calendarInitialMonthId: "2024-07-01",
|
|
199
|
+
calendarMinDateId: "2024-01-01",
|
|
200
|
+
calendarMaxDateId: "2024-12-31",
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Initially, we render 5 months: the initial month +- 2
|
|
205
|
+
expect(result.current.monthList).toHaveLength(5);
|
|
206
|
+
const initialIds = [
|
|
207
|
+
"2024-05-01",
|
|
208
|
+
"2024-06-01",
|
|
209
|
+
"2024-07-01",
|
|
210
|
+
"2024-08-01",
|
|
211
|
+
"2024-09-01",
|
|
212
|
+
];
|
|
213
|
+
expect(result.current.monthList.map((m) => m.id)).toEqual(initialIds);
|
|
214
|
+
|
|
215
|
+
// After we scroll to the beginning, we prepend 2 more months since we're not at the min date yet.
|
|
216
|
+
act(() => {
|
|
217
|
+
result.current.prependMonths(2);
|
|
218
|
+
});
|
|
219
|
+
expect(result.current.monthList).toHaveLength(7);
|
|
220
|
+
expect(result.current.monthList.map((m) => m.id)).toEqual([
|
|
221
|
+
"2024-03-01",
|
|
222
|
+
"2024-04-01",
|
|
223
|
+
...initialIds,
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
// After we scroll to the beginning again, we prepend two months again, reaching the min date.
|
|
227
|
+
act(() => {
|
|
228
|
+
result.current.prependMonths(2); // no change here
|
|
229
|
+
});
|
|
230
|
+
expect(result.current.monthList).toHaveLength(9);
|
|
231
|
+
|
|
232
|
+
const finalIds = [
|
|
233
|
+
"2024-01-01",
|
|
234
|
+
"2024-02-01",
|
|
235
|
+
"2024-03-01",
|
|
236
|
+
"2024-04-01",
|
|
237
|
+
...initialIds,
|
|
238
|
+
];
|
|
239
|
+
expect(result.current.monthList.map((m) => m.id)).toEqual(finalIds);
|
|
240
|
+
|
|
241
|
+
// If we try scrolling once more, it's a no-op
|
|
242
|
+
act(() => {
|
|
243
|
+
result.current.prependMonths(2);
|
|
244
|
+
});
|
|
245
|
+
expect(result.current.monthList.map((m) => m.id)).toEqual(finalIds);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("Returns a single month when min and max are in the same month", () => {
|
|
249
|
+
const { result } = renderHook(() =>
|
|
250
|
+
useCalendarList({
|
|
251
|
+
calendarInitialMonthId: "2024-01-01",
|
|
252
|
+
calendarFirstDayOfWeek: "sunday",
|
|
253
|
+
calendarFutureScrollRangeInMonths: 12,
|
|
254
|
+
calendarPastScrollRangeInMonths: 12,
|
|
255
|
+
calendarMinDateId: "2024-01-01",
|
|
256
|
+
calendarMaxDateId: "2024-01-31",
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
expect(result.current.monthList).toHaveLength(1);
|
|
261
|
+
const [january] = result.current.monthList;
|
|
262
|
+
expect(january.numberOfWeeks).toBe(5);
|
|
263
|
+
expect(january.id).toBe("2024-01-01");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("Returns a range of months bounded by the min and max dates", () => {
|
|
267
|
+
const { result } = renderHook(() =>
|
|
268
|
+
useCalendarList({
|
|
269
|
+
calendarFirstDayOfWeek: "sunday",
|
|
270
|
+
calendarFutureScrollRangeInMonths: 12,
|
|
271
|
+
calendarPastScrollRangeInMonths: 12,
|
|
272
|
+
calendarInitialMonthId: "2024-03-01",
|
|
273
|
+
calendarMinDateId: "2024-01-01",
|
|
274
|
+
calendarMaxDateId: "2024-07-15",
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(result.current.monthList).toHaveLength(7);
|
|
279
|
+
const [january, february, march, april, may, jun, jul] =
|
|
280
|
+
result.current.monthList;
|
|
281
|
+
|
|
282
|
+
expect(january.numberOfWeeks).toBe(5);
|
|
283
|
+
expect(january.id).toBe("2024-01-01");
|
|
284
|
+
|
|
285
|
+
expect(february.numberOfWeeks).toBe(5);
|
|
286
|
+
expect(february.id).toBe("2024-02-01");
|
|
287
|
+
|
|
288
|
+
expect(march.numberOfWeeks).toBe(6);
|
|
289
|
+
expect(march.id).toBe("2024-03-01");
|
|
290
|
+
|
|
291
|
+
expect(april.numberOfWeeks).toBe(5);
|
|
292
|
+
expect(april.id).toBe("2024-04-01");
|
|
293
|
+
|
|
294
|
+
expect(may.numberOfWeeks).toBe(5);
|
|
295
|
+
expect(may.id).toBe("2024-05-01");
|
|
296
|
+
|
|
297
|
+
expect(jun.numberOfWeeks).toBe(6);
|
|
298
|
+
expect(jun.id).toBe("2024-06-01");
|
|
299
|
+
|
|
300
|
+
// Although half of July is outside the max date, it's still included
|
|
301
|
+
expect(jul.numberOfWeeks).toBe(5);
|
|
302
|
+
expect(jul.id).toBe("2024-07-01");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("Ignores past/future scroll ranges when min/max dates are used", () => {
|
|
306
|
+
const { result } = renderHook(() =>
|
|
307
|
+
useCalendarList({
|
|
308
|
+
calendarFirstDayOfWeek: "sunday",
|
|
309
|
+
calendarFutureScrollRangeInMonths: 90,
|
|
310
|
+
calendarPastScrollRangeInMonths: 90,
|
|
311
|
+
calendarInitialMonthId: "2024-03-01",
|
|
312
|
+
calendarMinDateId: "2024-01-01",
|
|
313
|
+
calendarMaxDateId: "2024-03-03",
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
expect(result.current.monthList).toHaveLength(3);
|
|
317
|
+
expect(result.current.monthList[0].id).toBe("2024-01-01");
|
|
318
|
+
expect(result.current.monthList[2].id).toBe("2024-03-01");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("Append/Prepend are no-op if min/max are reached", () => {
|
|
322
|
+
const { result } = renderHook(() =>
|
|
323
|
+
useCalendarList({
|
|
324
|
+
calendarFirstDayOfWeek: "sunday",
|
|
325
|
+
calendarFutureScrollRangeInMonths: 12,
|
|
326
|
+
calendarPastScrollRangeInMonths: 12,
|
|
327
|
+
calendarInitialMonthId: "2024-09-01",
|
|
328
|
+
calendarMinDateId: "2024-01-01",
|
|
329
|
+
calendarMaxDateId: "2024-12-31",
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
const currentMonthList = result.current.monthList;
|
|
334
|
+
expect(currentMonthList).toHaveLength(12);
|
|
335
|
+
|
|
336
|
+
// Appending is a no-op
|
|
337
|
+
act(() => {
|
|
338
|
+
result.current.appendMonths(12);
|
|
339
|
+
});
|
|
340
|
+
expect(result.current.monthList).toEqual(currentMonthList);
|
|
341
|
+
|
|
342
|
+
// Prepending is a no-op
|
|
343
|
+
act(() => {
|
|
344
|
+
result.current.prependMonths(12);
|
|
345
|
+
});
|
|
346
|
+
expect(result.current.monthList).toEqual(currentMonthList);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("Data is correctly built when future/past ranges are 0", () => {
|
|
351
|
+
const { result } = renderHook(() =>
|
|
352
|
+
useCalendarList({
|
|
353
|
+
calendarFirstDayOfWeek: "sunday",
|
|
354
|
+
calendarFutureScrollRangeInMonths: 0,
|
|
355
|
+
calendarPastScrollRangeInMonths: 0,
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const currentMonthList = result.current.monthList;
|
|
360
|
+
expect(currentMonthList).toHaveLength(1);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("github issues", () => {
|
|
364
|
+
it("#16: Incorrect scroll position when setting calendarMinDateId", () => {
|
|
365
|
+
const { result } = renderHook(() =>
|
|
366
|
+
useCalendarList({
|
|
367
|
+
calendarInitialMonthId: "2024-01-05",
|
|
368
|
+
calendarMinDateId: "2023-02-27",
|
|
369
|
+
calendarFirstDayOfWeek: "sunday",
|
|
370
|
+
calendarFutureScrollRangeInMonths: 12,
|
|
371
|
+
calendarPastScrollRangeInMonths: 12,
|
|
372
|
+
})
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const { monthList, initialMonthIndex } = result.current;
|
|
376
|
+
|
|
377
|
+
expect(monthList[0].id).toBe("2023-02-01");
|
|
378
|
+
expect(initialMonthIndex).toBe(11);
|
|
379
|
+
expect(monthList.at(-1)?.id).toBe("2025-01-01");
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import type { CalendarProps } from "@/components/Calendar";
|
|
4
|
+
import {
|
|
5
|
+
fromDateId,
|
|
6
|
+
toDateId,
|
|
7
|
+
startOfMonth,
|
|
8
|
+
addMonths,
|
|
9
|
+
subMonths,
|
|
10
|
+
differenceInMonths,
|
|
11
|
+
getWeeksInMonth,
|
|
12
|
+
} from "@/helpers/dates";
|
|
13
|
+
import type { UseCalendarParams } from "@/hooks/useCalendar";
|
|
14
|
+
import { pipe } from "@/helpers/functions";
|
|
15
|
+
|
|
16
|
+
export interface CalendarMonth {
|
|
17
|
+
id: string;
|
|
18
|
+
date: Date;
|
|
19
|
+
numberOfWeeks: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const buildMonthList = (
|
|
23
|
+
startingMonth: Date,
|
|
24
|
+
endingMonth: Date,
|
|
25
|
+
firstDayOfWeek: CalendarProps["calendarFirstDayOfWeek"] = "sunday"
|
|
26
|
+
): CalendarMonth[] => {
|
|
27
|
+
const startingMonthId = toDateId(startingMonth);
|
|
28
|
+
const endingMonthId = toDateId(endingMonth);
|
|
29
|
+
|
|
30
|
+
if (endingMonthId < startingMonthId) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const months = [
|
|
35
|
+
{
|
|
36
|
+
id: toDateId(startingMonth),
|
|
37
|
+
date: startingMonth,
|
|
38
|
+
numberOfWeeks: getWeeksInMonth(startingMonth, firstDayOfWeek),
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
if (startingMonthId === endingMonthId) {
|
|
43
|
+
return months;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const numberOfMonths = differenceInMonths(endingMonth, startingMonth);
|
|
47
|
+
|
|
48
|
+
for (let i = 1; i <= numberOfMonths; i++) {
|
|
49
|
+
const month = addMonths(startingMonth, i);
|
|
50
|
+
const numberOfWeeks = getWeeksInMonth(month, firstDayOfWeek);
|
|
51
|
+
|
|
52
|
+
months.push({
|
|
53
|
+
id: toDateId(month),
|
|
54
|
+
date: month,
|
|
55
|
+
numberOfWeeks,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return months;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export interface UseCalendarListParams
|
|
62
|
+
extends Pick<UseCalendarParams, "calendarMinDateId" | "calendarMaxDateId"> {
|
|
63
|
+
/**
|
|
64
|
+
* The initial month to open the calendar to, as a `YYYY-MM-DD` string.
|
|
65
|
+
* @defaultValue today
|
|
66
|
+
*/
|
|
67
|
+
calendarInitialMonthId?: string;
|
|
68
|
+
/**
|
|
69
|
+
* How many months to show before the current month. Only applicable if
|
|
70
|
+
* `calendarMinDateId` is not set.
|
|
71
|
+
*/
|
|
72
|
+
calendarPastScrollRangeInMonths: number;
|
|
73
|
+
/**
|
|
74
|
+
* How many months to show after the current month. Applicable if
|
|
75
|
+
* `calendarMaxDateId` is not set.
|
|
76
|
+
*/
|
|
77
|
+
calendarFutureScrollRangeInMonths: number;
|
|
78
|
+
calendarFirstDayOfWeek: "monday" | "sunday";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const getEndingMonth = (
|
|
82
|
+
calendarFutureScrollRange: number,
|
|
83
|
+
calendarMaxDateId: string | undefined,
|
|
84
|
+
baseDate: Date
|
|
85
|
+
) => {
|
|
86
|
+
const endingMonthFromRange = addMonths(baseDate, calendarFutureScrollRange);
|
|
87
|
+
const newEndingMonthId = toDateId(endingMonthFromRange);
|
|
88
|
+
const safeMaxDateId = calendarMaxDateId ?? newEndingMonthId;
|
|
89
|
+
|
|
90
|
+
// We've exceeded the max date
|
|
91
|
+
return startOfMonth(
|
|
92
|
+
newEndingMonthId > safeMaxDateId
|
|
93
|
+
? fromDateId(safeMaxDateId)
|
|
94
|
+
: endingMonthFromRange
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const getStartingMonth = (
|
|
99
|
+
calendarPastScrollRange: number,
|
|
100
|
+
calendarMinDateId: string | undefined,
|
|
101
|
+
baseDate: Date
|
|
102
|
+
) => {
|
|
103
|
+
const startingMonthFromRange = subMonths(baseDate, calendarPastScrollRange);
|
|
104
|
+
const newStartingMonthId = toDateId(startingMonthFromRange);
|
|
105
|
+
const safeMinDateId = calendarMinDateId ?? newStartingMonthId;
|
|
106
|
+
|
|
107
|
+
// We've exceeded the min date.
|
|
108
|
+
return safeMinDateId > newStartingMonthId
|
|
109
|
+
? // Normalize to start of month since each month ID is represented by the first day of month
|
|
110
|
+
pipe(fromDateId(safeMinDateId), startOfMonth)
|
|
111
|
+
: startingMonthFromRange;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns a list of months to display in the calendar, and methods to append
|
|
116
|
+
* and prepend months to the list.
|
|
117
|
+
*/
|
|
118
|
+
export const useCalendarList = ({
|
|
119
|
+
calendarInitialMonthId,
|
|
120
|
+
calendarPastScrollRangeInMonths,
|
|
121
|
+
calendarFutureScrollRangeInMonths,
|
|
122
|
+
calendarFirstDayOfWeek,
|
|
123
|
+
calendarMaxDateId,
|
|
124
|
+
calendarMinDateId,
|
|
125
|
+
}: UseCalendarListParams) => {
|
|
126
|
+
// Initialize key values
|
|
127
|
+
const { initialMonth, initialMonthId } = useMemo(() => {
|
|
128
|
+
const baseDate = calendarInitialMonthId
|
|
129
|
+
? fromDateId(calendarInitialMonthId)
|
|
130
|
+
: fromDateId(toDateId(new Date()));
|
|
131
|
+
|
|
132
|
+
// Normalize to start of month since each month ID is represented by the first day of month
|
|
133
|
+
const baseStartOfMonth = startOfMonth(baseDate);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
initialMonth: baseStartOfMonth,
|
|
137
|
+
initialMonthId: toDateId(baseStartOfMonth),
|
|
138
|
+
};
|
|
139
|
+
}, [calendarInitialMonthId]);
|
|
140
|
+
|
|
141
|
+
const [monthList, setMonthList] = useState<CalendarMonth[]>(() => {
|
|
142
|
+
const currentMonth = startOfMonth(initialMonth);
|
|
143
|
+
|
|
144
|
+
const startingMonth = getStartingMonth(
|
|
145
|
+
calendarPastScrollRangeInMonths,
|
|
146
|
+
calendarMinDateId,
|
|
147
|
+
currentMonth
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const endingMonth = getEndingMonth(
|
|
151
|
+
calendarFutureScrollRangeInMonths,
|
|
152
|
+
calendarMaxDateId,
|
|
153
|
+
currentMonth
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return buildMonthList(startingMonth, endingMonth, calendarFirstDayOfWeek);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Append new months to the list.
|
|
161
|
+
*/
|
|
162
|
+
const appendMonths = useCallback(
|
|
163
|
+
(numberOfMonths: number) => {
|
|
164
|
+
// Last month + 1
|
|
165
|
+
const startingMonth = addMonths(monthList[monthList.length - 1].date, 1);
|
|
166
|
+
|
|
167
|
+
const endingMonth = getEndingMonth(
|
|
168
|
+
Math.max(numberOfMonths - 1, 0),
|
|
169
|
+
calendarMaxDateId,
|
|
170
|
+
startingMonth
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const hasReachedEndingMonth = monthList.find(
|
|
174
|
+
(m) => m.id === toDateId(endingMonth)
|
|
175
|
+
);
|
|
176
|
+
if (hasReachedEndingMonth) {
|
|
177
|
+
return monthList;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const newMonths = buildMonthList(
|
|
181
|
+
startingMonth,
|
|
182
|
+
endingMonth,
|
|
183
|
+
calendarFirstDayOfWeek
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const newMonthList = [...monthList, ...newMonths];
|
|
187
|
+
setMonthList(newMonthList);
|
|
188
|
+
return newMonthList;
|
|
189
|
+
},
|
|
190
|
+
[calendarFirstDayOfWeek, calendarMaxDateId, monthList]
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const prependMonths = useCallback(
|
|
194
|
+
(numberOfMonths: number) => {
|
|
195
|
+
const endingMonth = subMonths(monthList[0].date, 1);
|
|
196
|
+
|
|
197
|
+
const startingMonth = getStartingMonth(
|
|
198
|
+
Math.max(numberOfMonths - 1, 0),
|
|
199
|
+
calendarMinDateId,
|
|
200
|
+
endingMonth
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const newMonths = buildMonthList(
|
|
204
|
+
startingMonth,
|
|
205
|
+
endingMonth,
|
|
206
|
+
calendarFirstDayOfWeek
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const newMonthList = [...newMonths, ...monthList];
|
|
210
|
+
setMonthList(newMonthList);
|
|
211
|
+
return newMonthList;
|
|
212
|
+
},
|
|
213
|
+
[calendarFirstDayOfWeek, calendarMinDateId, monthList]
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const addMissingMonths = useCallback(
|
|
217
|
+
(targetMonthId: string) => {
|
|
218
|
+
const firstMonth = monthList[0];
|
|
219
|
+
const lastMonth = monthList[monthList.length - 1];
|
|
220
|
+
|
|
221
|
+
if (targetMonthId > lastMonth.id) {
|
|
222
|
+
return appendMonths(
|
|
223
|
+
differenceInMonths(fromDateId(targetMonthId), lastMonth.date)
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
return prependMonths(
|
|
227
|
+
differenceInMonths(firstMonth.date, fromDateId(targetMonthId))
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
[appendMonths, monthList, prependMonths]
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const initialMonthIndex = useMemo(() => {
|
|
235
|
+
const index = monthList.findIndex((i) => i.id === initialMonthId);
|
|
236
|
+
return index;
|
|
237
|
+
}, [initialMonthId, monthList]);
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
/**
|
|
241
|
+
* The list of months to display in the calendar.
|
|
242
|
+
*/
|
|
243
|
+
monthList,
|
|
244
|
+
/**
|
|
245
|
+
* The index of the initial month in the list.
|
|
246
|
+
*/
|
|
247
|
+
initialMonthIndex,
|
|
248
|
+
/**
|
|
249
|
+
* Appends new months to the list.
|
|
250
|
+
*/
|
|
251
|
+
appendMonths,
|
|
252
|
+
/**
|
|
253
|
+
* Prepends new months to the list.
|
|
254
|
+
*/
|
|
255
|
+
prependMonths,
|
|
256
|
+
/**
|
|
257
|
+
* Adds missing months to the list, so that the target month is included.
|
|
258
|
+
*/
|
|
259
|
+
addMissingMonths,
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Returns the absolute height for a month, accounting for the spacings and
|
|
265
|
+
* headers.
|
|
266
|
+
*/
|
|
267
|
+
export const getHeightForMonth = ({
|
|
268
|
+
calendarRowVerticalSpacing: vSpacing,
|
|
269
|
+
calendarDayHeight: day,
|
|
270
|
+
calendarWeekHeaderHeight: weekName,
|
|
271
|
+
calendarMonthHeaderHeight: header,
|
|
272
|
+
calendarAdditionalHeight: extraHeight,
|
|
273
|
+
calendarMonth,
|
|
274
|
+
calendarSpacing,
|
|
275
|
+
}: {
|
|
276
|
+
calendarAdditionalHeight: number;
|
|
277
|
+
calendarDayHeight: number;
|
|
278
|
+
calendarMonthHeaderHeight: number;
|
|
279
|
+
calendarRowVerticalSpacing: number;
|
|
280
|
+
calendarWeekHeaderHeight: number;
|
|
281
|
+
calendarMonth: CalendarMonth;
|
|
282
|
+
calendarSpacing: number;
|
|
283
|
+
}) => {
|
|
284
|
+
const headerHeight = header + vSpacing + weekName + vSpacing;
|
|
285
|
+
const daysHeight =
|
|
286
|
+
day * calendarMonth.numberOfWeeks +
|
|
287
|
+
// The last week doesn't have a bottom spacing (not referring to `calendarSpacing`)
|
|
288
|
+
(calendarMonth.numberOfWeeks - 1) * vSpacing;
|
|
289
|
+
|
|
290
|
+
return headerHeight + daysHeight + extraHeight + calendarSpacing;
|
|
291
|
+
};
|