@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.
Files changed (48) hide show
  1. package/.eslintrc.js +29 -0
  2. package/.turbo/turbo-build.log +19 -0
  3. package/.turbo/turbo-lint.log +14 -0
  4. package/.turbo/turbo-test.log +261 -0
  5. package/.turbo/turbo-typecheck.log +1 -0
  6. package/CHANGELOG.md +127 -0
  7. package/dist/index.d.mts +679 -0
  8. package/dist/index.d.ts +679 -0
  9. package/dist/index.js +2 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/index.mjs +2 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/package.json +47 -0
  14. package/src/components/Calendar.stories.tsx +226 -0
  15. package/src/components/Calendar.tsx +224 -0
  16. package/src/components/CalendarItemDay.tsx +385 -0
  17. package/src/components/CalendarItemEmpty.tsx +30 -0
  18. package/src/components/CalendarItemWeekName.tsx +67 -0
  19. package/src/components/CalendarList.stories.tsx +326 -0
  20. package/src/components/CalendarList.tsx +373 -0
  21. package/src/components/CalendarRowMonth.tsx +62 -0
  22. package/src/components/CalendarRowWeek.tsx +46 -0
  23. package/src/components/CalendarThemeProvider.tsx +43 -0
  24. package/src/components/HStack.tsx +67 -0
  25. package/src/components/VStack.tsx +67 -0
  26. package/src/components/index.ts +108 -0
  27. package/src/developer/decorators.tsx +54 -0
  28. package/src/developer/loggginHandler.tsx +6 -0
  29. package/src/developer/useRenderCount.ts +7 -0
  30. package/src/helpers/dates.test.ts +567 -0
  31. package/src/helpers/dates.ts +163 -0
  32. package/src/helpers/functions.ts +327 -0
  33. package/src/helpers/numbers.ts +11 -0
  34. package/src/helpers/strings.ts +2 -0
  35. package/src/helpers/tokens.ts +71 -0
  36. package/src/helpers/types.ts +3 -0
  37. package/src/hooks/useCalendar.test.ts +381 -0
  38. package/src/hooks/useCalendar.ts +351 -0
  39. package/src/hooks/useCalendarList.test.ts +382 -0
  40. package/src/hooks/useCalendarList.tsx +291 -0
  41. package/src/hooks/useDateRange.test.ts +128 -0
  42. package/src/hooks/useDateRange.ts +94 -0
  43. package/src/hooks/useOptimizedDayMetadata.test.ts +582 -0
  44. package/src/hooks/useOptimizedDayMetadata.ts +93 -0
  45. package/src/hooks/useTheme.ts +14 -0
  46. package/src/index.ts +24 -0
  47. package/tsconfig.json +13 -0
  48. 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
+ };