@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,582 @@
|
|
|
1
|
+
import { renderHook, act } from "@testing-library/react-hooks";
|
|
2
|
+
import { describe, it, expect } from "bun:test";
|
|
3
|
+
|
|
4
|
+
import type { CalendarDayMetadata } from "@/hooks/useCalendar";
|
|
5
|
+
import { fromDateId } from "@/helpers/dates";
|
|
6
|
+
import {
|
|
7
|
+
activeDateRangesEmitter,
|
|
8
|
+
useOptimizedDayMetadata,
|
|
9
|
+
} from "@/hooks/useOptimizedDayMetadata";
|
|
10
|
+
|
|
11
|
+
describe("useOptimizedDayMetadata", () => {
|
|
12
|
+
it("return the base metadata as the initial value", () => {
|
|
13
|
+
const baseMetadata: CalendarDayMetadata = {
|
|
14
|
+
date: fromDateId("2024-02-16"),
|
|
15
|
+
displayLabel: "16",
|
|
16
|
+
id: "2024-02-16",
|
|
17
|
+
isDifferentMonth: false,
|
|
18
|
+
isDisabled: false,
|
|
19
|
+
isEndOfMonth: false,
|
|
20
|
+
isEndOfRange: false,
|
|
21
|
+
isEndOfWeek: false,
|
|
22
|
+
isRangeValid: false,
|
|
23
|
+
isStartOfMonth: false,
|
|
24
|
+
isStartOfRange: false,
|
|
25
|
+
isStartOfWeek: false,
|
|
26
|
+
isToday: false,
|
|
27
|
+
isWeekend: false,
|
|
28
|
+
state: "idle",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const { result } = renderHook(() => useOptimizedDayMetadata(baseMetadata));
|
|
32
|
+
|
|
33
|
+
expect(result.current).toEqual(baseMetadata);
|
|
34
|
+
expect(result.all).toHaveLength(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("if the base changes, the returned value should match it.", () => {
|
|
38
|
+
const baseMetadata: CalendarDayMetadata = {
|
|
39
|
+
date: fromDateId("2024-02-16"),
|
|
40
|
+
displayLabel: "16",
|
|
41
|
+
id: "2024-02-16",
|
|
42
|
+
isDifferentMonth: false,
|
|
43
|
+
isDisabled: false,
|
|
44
|
+
isEndOfMonth: false,
|
|
45
|
+
isEndOfRange: false,
|
|
46
|
+
isEndOfWeek: false,
|
|
47
|
+
isRangeValid: false,
|
|
48
|
+
isStartOfMonth: false,
|
|
49
|
+
isStartOfRange: false,
|
|
50
|
+
isStartOfWeek: false,
|
|
51
|
+
isToday: false,
|
|
52
|
+
isWeekend: false,
|
|
53
|
+
state: "idle",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const { result, rerender } = renderHook(
|
|
57
|
+
(initial) => useOptimizedDayMetadata(initial),
|
|
58
|
+
{
|
|
59
|
+
initialProps: baseMetadata,
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(result.current).toEqual(baseMetadata);
|
|
64
|
+
rerender({
|
|
65
|
+
...baseMetadata,
|
|
66
|
+
isDisabled: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result.current).toEqual({
|
|
70
|
+
...baseMetadata,
|
|
71
|
+
isDisabled: true,
|
|
72
|
+
});
|
|
73
|
+
// `useEffect` is called after the hook renders with the new baseMetadata.
|
|
74
|
+
// Hence, the length of the `result.all` array is 3.
|
|
75
|
+
expect(result.all).toHaveLength(3);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("endOfRange: returns the updated metadata once an event is emitted", () => {
|
|
79
|
+
const baseMetadata: CalendarDayMetadata = {
|
|
80
|
+
date: fromDateId("2024-02-16"),
|
|
81
|
+
displayLabel: "16",
|
|
82
|
+
id: "2024-02-16",
|
|
83
|
+
isDifferentMonth: false,
|
|
84
|
+
isDisabled: false,
|
|
85
|
+
isEndOfMonth: false,
|
|
86
|
+
isEndOfRange: false,
|
|
87
|
+
isEndOfWeek: false,
|
|
88
|
+
isRangeValid: false,
|
|
89
|
+
isStartOfMonth: false,
|
|
90
|
+
isStartOfRange: false,
|
|
91
|
+
isStartOfWeek: false,
|
|
92
|
+
isToday: false,
|
|
93
|
+
isWeekend: false,
|
|
94
|
+
state: "idle",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Initial state
|
|
98
|
+
const { result } = renderHook(
|
|
99
|
+
(initial) => useOptimizedDayMetadata(initial),
|
|
100
|
+
{
|
|
101
|
+
initialProps: baseMetadata,
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
expect(result.current).toEqual(baseMetadata);
|
|
105
|
+
|
|
106
|
+
// Emit event
|
|
107
|
+
act(() => {
|
|
108
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
109
|
+
ranges: [
|
|
110
|
+
{
|
|
111
|
+
startId: "2024-02-01",
|
|
112
|
+
endId: "2024-02-16",
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(result.current).toEqual({
|
|
119
|
+
...baseMetadata,
|
|
120
|
+
state: "active",
|
|
121
|
+
isEndOfRange: true,
|
|
122
|
+
isRangeValid: true,
|
|
123
|
+
});
|
|
124
|
+
expect(result.all).toHaveLength(2);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("startOfRange: returns the updated metadata once an event is emitted", () => {
|
|
128
|
+
const baseMetadata: CalendarDayMetadata = {
|
|
129
|
+
date: fromDateId("2024-02-01"),
|
|
130
|
+
displayLabel: "01",
|
|
131
|
+
id: "2024-02-01",
|
|
132
|
+
isDifferentMonth: false,
|
|
133
|
+
isDisabled: false,
|
|
134
|
+
isEndOfMonth: false,
|
|
135
|
+
isEndOfRange: false,
|
|
136
|
+
isEndOfWeek: false,
|
|
137
|
+
isRangeValid: false,
|
|
138
|
+
isStartOfMonth: false,
|
|
139
|
+
isStartOfRange: false,
|
|
140
|
+
isStartOfWeek: false,
|
|
141
|
+
isToday: false,
|
|
142
|
+
isWeekend: false,
|
|
143
|
+
state: "idle",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Initial state
|
|
147
|
+
const { result } = renderHook(
|
|
148
|
+
(initial) => useOptimizedDayMetadata(initial),
|
|
149
|
+
{
|
|
150
|
+
initialProps: baseMetadata,
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
expect(result.current).toEqual(baseMetadata);
|
|
154
|
+
|
|
155
|
+
// Emit event
|
|
156
|
+
act(() => {
|
|
157
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
158
|
+
ranges: [
|
|
159
|
+
{
|
|
160
|
+
startId: "2024-02-01",
|
|
161
|
+
endId: "2024-02-16",
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(result.current).toEqual({
|
|
168
|
+
...baseMetadata,
|
|
169
|
+
state: "active",
|
|
170
|
+
isStartOfRange: true,
|
|
171
|
+
isRangeValid: true,
|
|
172
|
+
});
|
|
173
|
+
expect(result.all).toHaveLength(2);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("startOfRange|endOfRange: returns the updated metadata once an event is emitted", () => {
|
|
177
|
+
const baseMetadata: CalendarDayMetadata = {
|
|
178
|
+
date: fromDateId("2024-02-01"),
|
|
179
|
+
displayLabel: "01",
|
|
180
|
+
id: "2024-02-01",
|
|
181
|
+
isDifferentMonth: false,
|
|
182
|
+
isDisabled: false,
|
|
183
|
+
isEndOfMonth: false,
|
|
184
|
+
isEndOfRange: false,
|
|
185
|
+
isEndOfWeek: false,
|
|
186
|
+
isRangeValid: false,
|
|
187
|
+
isStartOfMonth: false,
|
|
188
|
+
isStartOfRange: false,
|
|
189
|
+
isStartOfWeek: false,
|
|
190
|
+
isToday: false,
|
|
191
|
+
isWeekend: false,
|
|
192
|
+
state: "idle",
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Initial state
|
|
196
|
+
const { result } = renderHook(
|
|
197
|
+
(initial) => useOptimizedDayMetadata(initial),
|
|
198
|
+
{
|
|
199
|
+
initialProps: baseMetadata,
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
expect(result.current).toEqual(baseMetadata);
|
|
203
|
+
|
|
204
|
+
// Emit event
|
|
205
|
+
act(() => {
|
|
206
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
207
|
+
ranges: [
|
|
208
|
+
{
|
|
209
|
+
startId: "2024-02-01",
|
|
210
|
+
endId: "2024-02-01",
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(result.current).toEqual({
|
|
217
|
+
...baseMetadata,
|
|
218
|
+
state: "active",
|
|
219
|
+
isStartOfRange: true,
|
|
220
|
+
isEndOfRange: true,
|
|
221
|
+
isRangeValid: true,
|
|
222
|
+
});
|
|
223
|
+
expect(result.all).toHaveLength(2);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("doesn't re-render if the active dates don't overlap", () => {
|
|
227
|
+
const baseMetadata: CalendarDayMetadata = {
|
|
228
|
+
date: fromDateId("2024-02-18"),
|
|
229
|
+
displayLabel: "18",
|
|
230
|
+
id: "2024-02-18",
|
|
231
|
+
isDifferentMonth: false,
|
|
232
|
+
isDisabled: false,
|
|
233
|
+
isEndOfMonth: false,
|
|
234
|
+
isEndOfRange: false,
|
|
235
|
+
isEndOfWeek: false,
|
|
236
|
+
isRangeValid: false,
|
|
237
|
+
isStartOfMonth: false,
|
|
238
|
+
isStartOfRange: false,
|
|
239
|
+
isStartOfWeek: false,
|
|
240
|
+
isToday: false,
|
|
241
|
+
isWeekend: false,
|
|
242
|
+
state: "idle",
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Initial state
|
|
246
|
+
const { result } = renderHook(
|
|
247
|
+
(initial) => useOptimizedDayMetadata(initial),
|
|
248
|
+
{
|
|
249
|
+
initialProps: baseMetadata,
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
expect(result.current).toEqual(baseMetadata);
|
|
253
|
+
|
|
254
|
+
// Emit event
|
|
255
|
+
act(() => {
|
|
256
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
257
|
+
ranges: [
|
|
258
|
+
{
|
|
259
|
+
startId: "2024-02-01",
|
|
260
|
+
endId: "2024-02-17",
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(result.current).toEqual(baseMetadata);
|
|
267
|
+
expect(result.all).toHaveLength(1);
|
|
268
|
+
|
|
269
|
+
// Emit another event
|
|
270
|
+
act(() => {
|
|
271
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
272
|
+
ranges: [
|
|
273
|
+
{
|
|
274
|
+
startId: "2024-02-19",
|
|
275
|
+
endId: "2024-02-23",
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(result.current).toEqual(baseMetadata);
|
|
282
|
+
expect(result.all).toHaveLength(1);
|
|
283
|
+
|
|
284
|
+
// Emit an incomplete range
|
|
285
|
+
act(() => {
|
|
286
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
287
|
+
ranges: [
|
|
288
|
+
{
|
|
289
|
+
startId: "2024-02-01",
|
|
290
|
+
endId: undefined,
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(result.current).toEqual(baseMetadata);
|
|
297
|
+
expect(result.all).toHaveLength(1);
|
|
298
|
+
|
|
299
|
+
// Check if the metadata is updated when the range is complete
|
|
300
|
+
act(() => {
|
|
301
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
302
|
+
ranges: [
|
|
303
|
+
{
|
|
304
|
+
startId: "2024-02-01",
|
|
305
|
+
endId: "2024-02-20",
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
expect(result.current).toEqual({
|
|
311
|
+
...baseMetadata,
|
|
312
|
+
state: "active",
|
|
313
|
+
isRangeValid: true,
|
|
314
|
+
});
|
|
315
|
+
expect(result.all).toHaveLength(2);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("resets the state once a new range is selected", () => {
|
|
319
|
+
const baseMetadata: CalendarDayMetadata = {
|
|
320
|
+
date: fromDateId("2024-02-01"),
|
|
321
|
+
displayLabel: "01",
|
|
322
|
+
id: "2024-02-01",
|
|
323
|
+
isDifferentMonth: false,
|
|
324
|
+
isDisabled: false,
|
|
325
|
+
isEndOfMonth: false,
|
|
326
|
+
isEndOfRange: false,
|
|
327
|
+
isEndOfWeek: false,
|
|
328
|
+
isRangeValid: false,
|
|
329
|
+
isStartOfMonth: false,
|
|
330
|
+
isStartOfRange: false,
|
|
331
|
+
isStartOfWeek: false,
|
|
332
|
+
isToday: false,
|
|
333
|
+
isWeekend: false,
|
|
334
|
+
state: "idle",
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Initial state
|
|
338
|
+
const { result } = renderHook(
|
|
339
|
+
(initial) => useOptimizedDayMetadata(initial),
|
|
340
|
+
{
|
|
341
|
+
initialProps: baseMetadata,
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
expect(result.current).toEqual(baseMetadata);
|
|
345
|
+
|
|
346
|
+
// Emit event
|
|
347
|
+
act(() => {
|
|
348
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
349
|
+
ranges: [
|
|
350
|
+
{
|
|
351
|
+
startId: "2024-02-01",
|
|
352
|
+
endId: "2024-02-01",
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(result.current).toEqual({
|
|
359
|
+
...baseMetadata,
|
|
360
|
+
state: "active",
|
|
361
|
+
isStartOfRange: true,
|
|
362
|
+
isEndOfRange: true,
|
|
363
|
+
isRangeValid: true,
|
|
364
|
+
});
|
|
365
|
+
expect(result.all).toHaveLength(2);
|
|
366
|
+
|
|
367
|
+
// Emit another range
|
|
368
|
+
act(() => {
|
|
369
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
370
|
+
ranges: [
|
|
371
|
+
{
|
|
372
|
+
startId: "2024-02-03",
|
|
373
|
+
endId: "2024-02-03",
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Back to the initial state
|
|
380
|
+
expect(result.current).toEqual(baseMetadata);
|
|
381
|
+
expect(result.all).toHaveLength(3);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe("useOptimizedDayMetadata with calendarInstanceId", () => {
|
|
386
|
+
const getBaseMetadata = (date: string): CalendarDayMetadata => ({
|
|
387
|
+
date: fromDateId(date),
|
|
388
|
+
displayLabel: date.split("-")[2],
|
|
389
|
+
id: date,
|
|
390
|
+
isDifferentMonth: false,
|
|
391
|
+
isDisabled: false,
|
|
392
|
+
isEndOfMonth: false,
|
|
393
|
+
isEndOfRange: false,
|
|
394
|
+
isEndOfWeek: false,
|
|
395
|
+
isRangeValid: false,
|
|
396
|
+
isStartOfMonth: false,
|
|
397
|
+
isStartOfRange: false,
|
|
398
|
+
isStartOfWeek: false,
|
|
399
|
+
isToday: false,
|
|
400
|
+
isWeekend: false,
|
|
401
|
+
state: "idle",
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("uses the default instance ID when not provided", () => {
|
|
405
|
+
const baseMetadata = getBaseMetadata("2024-02-16");
|
|
406
|
+
const { result } = renderHook(() => useOptimizedDayMetadata(baseMetadata));
|
|
407
|
+
|
|
408
|
+
act(() => {
|
|
409
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
410
|
+
ranges: [
|
|
411
|
+
{
|
|
412
|
+
startId: "2024-02-16",
|
|
413
|
+
endId: "2024-02-16",
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(result.current).toEqual({
|
|
420
|
+
...baseMetadata,
|
|
421
|
+
state: "active",
|
|
422
|
+
isStartOfRange: true,
|
|
423
|
+
isEndOfRange: true,
|
|
424
|
+
isRangeValid: true,
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("responds to events for the correct instance ID", () => {
|
|
429
|
+
const instanceId = "test-calendar-1";
|
|
430
|
+
const baseMetadata = getBaseMetadata("2024-02-16");
|
|
431
|
+
const { result } = renderHook(() =>
|
|
432
|
+
useOptimizedDayMetadata(baseMetadata, instanceId)
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
act(() => {
|
|
436
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
437
|
+
instanceId,
|
|
438
|
+
ranges: [
|
|
439
|
+
{
|
|
440
|
+
startId: "2024-02-16",
|
|
441
|
+
endId: "2024-02-16",
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
expect(result.current).toEqual({
|
|
448
|
+
...baseMetadata,
|
|
449
|
+
state: "active",
|
|
450
|
+
isStartOfRange: true,
|
|
451
|
+
isEndOfRange: true,
|
|
452
|
+
isRangeValid: true,
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("ignores events for different instance IDs", () => {
|
|
457
|
+
const instanceId = "test-calendar-2";
|
|
458
|
+
const baseMetadata = getBaseMetadata("2024-02-16");
|
|
459
|
+
const { result } = renderHook(() =>
|
|
460
|
+
useOptimizedDayMetadata(baseMetadata, instanceId)
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
act(() => {
|
|
464
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
465
|
+
instanceId: "different-calendar",
|
|
466
|
+
ranges: [
|
|
467
|
+
{
|
|
468
|
+
startId: "2024-02-16",
|
|
469
|
+
endId: "2024-02-16",
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// The metadata should not change
|
|
476
|
+
expect(result.current).toEqual(baseMetadata);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("handles multiple instances correctly", () => {
|
|
480
|
+
const instanceId1 = "test-calendar-3";
|
|
481
|
+
const instanceId2 = "test-calendar-4";
|
|
482
|
+
|
|
483
|
+
const baseMetadata1 = getBaseMetadata("2024-02-16");
|
|
484
|
+
const baseMetadata2 = getBaseMetadata("2024-02-16");
|
|
485
|
+
|
|
486
|
+
const { result: result1 } = renderHook(() =>
|
|
487
|
+
useOptimizedDayMetadata(baseMetadata1, instanceId1)
|
|
488
|
+
);
|
|
489
|
+
const { result: result2 } = renderHook(() =>
|
|
490
|
+
useOptimizedDayMetadata(baseMetadata2, instanceId2)
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
act(() => {
|
|
494
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
495
|
+
instanceId: instanceId1,
|
|
496
|
+
ranges: [
|
|
497
|
+
{
|
|
498
|
+
startId: "2024-02-16",
|
|
499
|
+
endId: "2024-02-16",
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
expect(result1.current).toEqual({
|
|
506
|
+
...baseMetadata1,
|
|
507
|
+
state: "active",
|
|
508
|
+
isStartOfRange: true,
|
|
509
|
+
isEndOfRange: true,
|
|
510
|
+
isRangeValid: true,
|
|
511
|
+
});
|
|
512
|
+
expect(result2.current).toEqual(baseMetadata2);
|
|
513
|
+
|
|
514
|
+
act(() => {
|
|
515
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
516
|
+
instanceId: instanceId2,
|
|
517
|
+
ranges: [
|
|
518
|
+
{
|
|
519
|
+
startId: "2024-02-15",
|
|
520
|
+
endId: "2024-02-17",
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
expect(result1.current).toEqual({
|
|
527
|
+
...baseMetadata1,
|
|
528
|
+
state: "active",
|
|
529
|
+
isStartOfRange: true,
|
|
530
|
+
isEndOfRange: true,
|
|
531
|
+
isRangeValid: true,
|
|
532
|
+
});
|
|
533
|
+
expect(result2.current).toEqual({
|
|
534
|
+
...baseMetadata2,
|
|
535
|
+
state: "active",
|
|
536
|
+
isRangeValid: true,
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("resets state for the correct instance when a new range is selected", () => {
|
|
541
|
+
const instanceId = "test-calendar-5";
|
|
542
|
+
const baseMetadata = getBaseMetadata("2024-02-16");
|
|
543
|
+
const { result } = renderHook(() =>
|
|
544
|
+
useOptimizedDayMetadata(baseMetadata, instanceId)
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
act(() => {
|
|
548
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
549
|
+
instanceId,
|
|
550
|
+
ranges: [
|
|
551
|
+
{
|
|
552
|
+
startId: "2024-02-16",
|
|
553
|
+
endId: "2024-02-16",
|
|
554
|
+
},
|
|
555
|
+
],
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
expect(result.current).toEqual({
|
|
560
|
+
...baseMetadata,
|
|
561
|
+
state: "active",
|
|
562
|
+
isStartOfRange: true,
|
|
563
|
+
isEndOfRange: true,
|
|
564
|
+
isRangeValid: true,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
act(() => {
|
|
568
|
+
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
|
|
569
|
+
instanceId,
|
|
570
|
+
ranges: [
|
|
571
|
+
{
|
|
572
|
+
startId: "2024-02-18",
|
|
573
|
+
endId: "2024-02-20",
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Should reset to base state
|
|
580
|
+
expect(result.current).toEqual(baseMetadata);
|
|
581
|
+
});
|
|
582
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import mitt from "mitt";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getStateFields,
|
|
6
|
+
type CalendarActiveDateRange,
|
|
7
|
+
type CalendarDayMetadata,
|
|
8
|
+
} from "@/hooks/useCalendar";
|
|
9
|
+
|
|
10
|
+
interface OnSetActiveDateRangesPayload {
|
|
11
|
+
instanceId?: string;
|
|
12
|
+
ranges: CalendarActiveDateRange[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* An event emitter for the active date ranges. This notifies the calendar items
|
|
17
|
+
* when their state changes, allowing just the affected items to re-render.
|
|
18
|
+
*
|
|
19
|
+
* While this is an implementation detail focused on improving performance, it's
|
|
20
|
+
* exported in case you need to build your own calendar. Check the source code
|
|
21
|
+
* for a reference implementation.
|
|
22
|
+
*/
|
|
23
|
+
export const activeDateRangesEmitter = mitt<{
|
|
24
|
+
onSetActiveDateRanges: OnSetActiveDateRangesPayload;
|
|
25
|
+
}>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The default calendar instance ID. This is used when no instance ID is provided.
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_CALENDAR_INSTANCE_ID = "legend-calendar-default-instance";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns an optimized metadata for a particular day. This hook listens to the
|
|
34
|
+
* `activeDateRanges` emitter, enabling only the affected calendar items to
|
|
35
|
+
* re-render.
|
|
36
|
+
*
|
|
37
|
+
* While this is an implementation detail focused on improving performance, it's
|
|
38
|
+
* exported in case you need to build your own calendar. Check the source code
|
|
39
|
+
* for a reference implementation.
|
|
40
|
+
*/
|
|
41
|
+
export const useOptimizedDayMetadata = (
|
|
42
|
+
baseMetadata: CalendarDayMetadata,
|
|
43
|
+
calendarInstanceId?: string
|
|
44
|
+
) => {
|
|
45
|
+
const [metadata, setMetadata] = useState(baseMetadata);
|
|
46
|
+
|
|
47
|
+
const safeCalendarInstanceId =
|
|
48
|
+
calendarInstanceId ?? DEFAULT_CALENDAR_INSTANCE_ID;
|
|
49
|
+
|
|
50
|
+
// Ensure the metadata is updated when the base changes.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
setMetadata(baseMetadata);
|
|
53
|
+
}, [baseMetadata]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const handler = (payload: OnSetActiveDateRangesPayload) => {
|
|
57
|
+
const { ranges, instanceId = DEFAULT_CALENDAR_INSTANCE_ID } = payload;
|
|
58
|
+
if (instanceId !== safeCalendarInstanceId) {
|
|
59
|
+
// This event is not for this instance, ignore it.
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// We're only interested in the active date ranges, no need to worry about
|
|
64
|
+
// disabled states. These are already covered by the base metadata.
|
|
65
|
+
const { isStartOfRange, isEndOfRange, isRangeValid, state } =
|
|
66
|
+
getStateFields({
|
|
67
|
+
id: metadata.id,
|
|
68
|
+
calendarActiveDateRanges: ranges,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (state === "active") {
|
|
72
|
+
setMetadata((prev) => ({
|
|
73
|
+
...prev,
|
|
74
|
+
isStartOfRange,
|
|
75
|
+
isEndOfRange,
|
|
76
|
+
isRangeValid,
|
|
77
|
+
state,
|
|
78
|
+
}));
|
|
79
|
+
} else {
|
|
80
|
+
// Resets the state when it's no longer active.
|
|
81
|
+
setMetadata(baseMetadata);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
activeDateRangesEmitter.on("onSetActiveDateRanges", handler);
|
|
86
|
+
|
|
87
|
+
return () => {
|
|
88
|
+
activeDateRangesEmitter.off("onSetActiveDateRanges", handler);
|
|
89
|
+
};
|
|
90
|
+
}, [baseMetadata, safeCalendarInstanceId, metadata]);
|
|
91
|
+
|
|
92
|
+
return metadata;
|
|
93
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useColorScheme } from "react-native";
|
|
2
|
+
|
|
3
|
+
import type { BaseTheme } from "@/helpers/tokens";
|
|
4
|
+
import { darkTheme, lightTheme } from "@/helpers/tokens";
|
|
5
|
+
import { useCalendarTheme } from "@/components/CalendarThemeProvider";
|
|
6
|
+
|
|
7
|
+
export const useTheme = (): BaseTheme => {
|
|
8
|
+
const appearance = useColorScheme();
|
|
9
|
+
const { colorScheme } = useCalendarTheme();
|
|
10
|
+
|
|
11
|
+
const theme = colorScheme ?? appearance;
|
|
12
|
+
|
|
13
|
+
return theme === "dark" ? darkTheme : lightTheme;
|
|
14
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export * from "./components";
|
|
2
|
+
export { toDateId, fromDateId } from "./helpers/dates";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
type UseCalendarParams,
|
|
6
|
+
type CalendarDayMetadata,
|
|
7
|
+
type CalendarActiveDateRange,
|
|
8
|
+
useCalendar,
|
|
9
|
+
buildCalendar,
|
|
10
|
+
} from "./hooks/useCalendar";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
type CalendarMonth,
|
|
14
|
+
type UseCalendarListParams,
|
|
15
|
+
getHeightForMonth,
|
|
16
|
+
useCalendarList,
|
|
17
|
+
} from "./hooks/useCalendarList";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
useOptimizedDayMetadata,
|
|
21
|
+
activeDateRangesEmitter,
|
|
22
|
+
} from "./hooks/useOptimizedDayMetadata";
|
|
23
|
+
|
|
24
|
+
export { useDateRange } from "./hooks/useDateRange";
|