@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,326 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-native";
|
|
2
|
+
import {
|
|
3
|
+
addDays,
|
|
4
|
+
addMonths,
|
|
5
|
+
endOfYear,
|
|
6
|
+
startOfMonth,
|
|
7
|
+
startOfYear,
|
|
8
|
+
subMonths,
|
|
9
|
+
} from "date-fns";
|
|
10
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
11
|
+
import { Button, Text, View } from "react-native";
|
|
12
|
+
|
|
13
|
+
import type { CalendarListProps, CalendarListRef } from "@/components";
|
|
14
|
+
import { Calendar } from "@/components";
|
|
15
|
+
import { HStack } from "@/components/HStack";
|
|
16
|
+
import { VStack } from "@/components/VStack";
|
|
17
|
+
import { paddingDecorator } from "@/developer/decorators";
|
|
18
|
+
import { loggingHandler } from "@/developer/loggginHandler";
|
|
19
|
+
import { fromDateId, toDateId } from "@/helpers/dates";
|
|
20
|
+
import type { CalendarActiveDateRange } from "@/hooks/useCalendar";
|
|
21
|
+
import { useDateRange } from "@/hooks/useDateRange";
|
|
22
|
+
import { useTheme } from "@/hooks/useTheme";
|
|
23
|
+
|
|
24
|
+
const today = new Date();
|
|
25
|
+
|
|
26
|
+
const startOfThisMonth = startOfMonth(today);
|
|
27
|
+
|
|
28
|
+
const CalendarListMeta: Meta<typeof Calendar.List> = {
|
|
29
|
+
title: "Calendar.List",
|
|
30
|
+
component: Calendar.List,
|
|
31
|
+
argTypes: {},
|
|
32
|
+
args: {
|
|
33
|
+
onCalendarDayPress: loggingHandler("onCalendarDayPress"),
|
|
34
|
+
calendarRowVerticalSpacing: 8,
|
|
35
|
+
calendarRowHorizontalSpacing: 8,
|
|
36
|
+
calendarFutureScrollRangeInMonths: 12,
|
|
37
|
+
calendarPastScrollRangeInMonths: 12,
|
|
38
|
+
|
|
39
|
+
calendarFirstDayOfWeek: "sunday",
|
|
40
|
+
calendarInitialMonthId: toDateId(startOfThisMonth),
|
|
41
|
+
},
|
|
42
|
+
decorators: [paddingDecorator],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default CalendarListMeta;
|
|
46
|
+
|
|
47
|
+
export const Default: StoryObj<typeof Calendar.List> = {};
|
|
48
|
+
|
|
49
|
+
export const SpacingDense: StoryObj<typeof Calendar.List> = {
|
|
50
|
+
args: {
|
|
51
|
+
calendarRowVerticalSpacing: 0,
|
|
52
|
+
calendarRowHorizontalSpacing: 4,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const WithDateRangeAndDisabledDates: StoryObj<typeof Calendar.List> = {
|
|
57
|
+
args: {
|
|
58
|
+
calendarActiveDateRanges: [
|
|
59
|
+
{
|
|
60
|
+
startId: "2024-01-15",
|
|
61
|
+
endId: "2024-01-28",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
calendarDisabledDateIds: ["2024-01-01", "2024-01-02"],
|
|
65
|
+
calendarInitialMonthId: "2024-01-01",
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const WithShortFutureScrollRange: StoryObj<typeof Calendar.List> = {
|
|
70
|
+
args: {
|
|
71
|
+
calendarPastScrollRangeInMonths: 1,
|
|
72
|
+
calendarFutureScrollRangeInMonths: 1,
|
|
73
|
+
calendarInitialMonthId: "2024-02-01",
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function SpacingSparse() {
|
|
78
|
+
const [selectedDate, setSelectedDate] = useState<undefined | string>(
|
|
79
|
+
undefined
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const onCalendarDayPress = (dateId: string) => {
|
|
83
|
+
setSelectedDate(dateId);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<VStack grow spacing={24}>
|
|
88
|
+
<Text>Selected date: {selectedDate}</Text>
|
|
89
|
+
|
|
90
|
+
<Calendar.List
|
|
91
|
+
calendarActiveDateRanges={[
|
|
92
|
+
{
|
|
93
|
+
startId: selectedDate,
|
|
94
|
+
endId: selectedDate,
|
|
95
|
+
},
|
|
96
|
+
]}
|
|
97
|
+
calendarDayHeight={50}
|
|
98
|
+
calendarInitialMonthId={selectedDate}
|
|
99
|
+
calendarMonthHeaderHeight={20}
|
|
100
|
+
calendarRowHorizontalSpacing={16}
|
|
101
|
+
calendarRowVerticalSpacing={16}
|
|
102
|
+
calendarSpacing={48}
|
|
103
|
+
calendarWeekHeaderHeight={32}
|
|
104
|
+
onCalendarDayPress={onCalendarDayPress}
|
|
105
|
+
/>
|
|
106
|
+
</VStack>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function ImperativeScrolling() {
|
|
111
|
+
const [currentMonth, setCurrentMonth] = useState(startOfMonth(new Date()));
|
|
112
|
+
|
|
113
|
+
const [activeDateId, setActiveDateId] = useState<string | undefined>(
|
|
114
|
+
toDateId(addDays(currentMonth, 3))
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const calendarActiveDateRanges = useMemo<CalendarActiveDateRange[]>(() => {
|
|
118
|
+
if (!activeDateId) return [];
|
|
119
|
+
|
|
120
|
+
return [{ startId: activeDateId, endId: activeDateId }];
|
|
121
|
+
}, [activeDateId]);
|
|
122
|
+
|
|
123
|
+
const ref = useRef<CalendarListRef>(null);
|
|
124
|
+
|
|
125
|
+
const onCalendarDayPress = useCallback((dateId: string) => {
|
|
126
|
+
ref.current?.scrollToDate(fromDateId(dateId), true);
|
|
127
|
+
setActiveDateId(dateId);
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<View style={{ paddingTop: 20, flex: 1 }}>
|
|
132
|
+
<VStack alignItems="center" grow spacing={20}>
|
|
133
|
+
<HStack spacing={12}>
|
|
134
|
+
<Button
|
|
135
|
+
onPress={() => {
|
|
136
|
+
const pastMonth = subMonths(currentMonth, 1);
|
|
137
|
+
setCurrentMonth(pastMonth);
|
|
138
|
+
ref.current?.scrollToMonth(pastMonth, true);
|
|
139
|
+
}}
|
|
140
|
+
title="Past month"
|
|
141
|
+
/>
|
|
142
|
+
<Text>Current: {toDateId(currentMonth)}</Text>
|
|
143
|
+
<Button
|
|
144
|
+
onPress={() => {
|
|
145
|
+
const nextMonth = addMonths(currentMonth, 1);
|
|
146
|
+
setCurrentMonth(nextMonth);
|
|
147
|
+
ref.current?.scrollToMonth(nextMonth, true);
|
|
148
|
+
}}
|
|
149
|
+
title="Next month"
|
|
150
|
+
/>
|
|
151
|
+
</HStack>
|
|
152
|
+
<Button
|
|
153
|
+
onPress={() => {
|
|
154
|
+
const thisMonth = startOfMonth(new Date());
|
|
155
|
+
setCurrentMonth(thisMonth);
|
|
156
|
+
ref.current?.scrollToMonth(thisMonth, true);
|
|
157
|
+
}}
|
|
158
|
+
title="Today"
|
|
159
|
+
/>
|
|
160
|
+
<View style={{ flex: 1, width: "100%" }}>
|
|
161
|
+
<Calendar.List
|
|
162
|
+
calendarActiveDateRanges={calendarActiveDateRanges}
|
|
163
|
+
calendarInitialMonthId={toDateId(currentMonth)}
|
|
164
|
+
onCalendarDayPress={onCalendarDayPress}
|
|
165
|
+
ref={ref}
|
|
166
|
+
/>
|
|
167
|
+
</View>
|
|
168
|
+
</VStack>
|
|
169
|
+
</View>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function MinAndMaxDates() {
|
|
174
|
+
return (
|
|
175
|
+
<VStack alignItems="center" grow spacing={20}>
|
|
176
|
+
<Text>This calendar list is only available for the 2024 period</Text>
|
|
177
|
+
<View style={{ flex: 1, width: "100%" }}>
|
|
178
|
+
<Calendar.List
|
|
179
|
+
calendarInitialMonthId="2024-02-13"
|
|
180
|
+
calendarMaxDateId="2024-12-31"
|
|
181
|
+
calendarMinDateId="2024-01-01"
|
|
182
|
+
onCalendarDayPress={loggingHandler("onCalendarDayPress")}
|
|
183
|
+
/>
|
|
184
|
+
</View>
|
|
185
|
+
</VStack>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function DateRangePicker() {
|
|
190
|
+
const calendarListProps = useMemo<Partial<CalendarListProps>>(() => {
|
|
191
|
+
const today = new Date();
|
|
192
|
+
return {
|
|
193
|
+
calendarInitialMonthId: toDateId(today),
|
|
194
|
+
calendarMinDateId: toDateId(startOfYear(today)),
|
|
195
|
+
calendarMaxDateId: toDateId(endOfYear(today)),
|
|
196
|
+
};
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
const calendarDateRangeProps = useDateRange();
|
|
200
|
+
const { onClearDateRange, dateRange, isDateRangeValid } =
|
|
201
|
+
calendarDateRangeProps;
|
|
202
|
+
|
|
203
|
+
const { colors } = useTheme();
|
|
204
|
+
|
|
205
|
+
const textProps = {
|
|
206
|
+
style: { color: colors.content.primary },
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<VStack alignItems="center" grow spacing={20}>
|
|
211
|
+
<Text style={{ ...textProps.style, fontWeight: "bold" }}>
|
|
212
|
+
This shows how to build a date range picker bounded by the current year
|
|
213
|
+
</Text>
|
|
214
|
+
<View style={{ flex: 1, width: "100%" }}>
|
|
215
|
+
<Calendar.List {...calendarListProps} {...calendarDateRangeProps} />
|
|
216
|
+
</View>
|
|
217
|
+
<HStack justifyContent="space-between" width="100%">
|
|
218
|
+
<Button onPress={onClearDateRange} title="Clear range" />
|
|
219
|
+
<VStack spacing={4}>
|
|
220
|
+
<Text {...textProps}>Start: {dateRange.startId ?? "?"}</Text>
|
|
221
|
+
<Text {...textProps}>End: {dateRange.endId ?? "?"}</Text>
|
|
222
|
+
</VStack>
|
|
223
|
+
<VStack alignItems="flex-end" spacing={4}>
|
|
224
|
+
<Text {...textProps}>Is range valid?</Text>
|
|
225
|
+
<Text {...textProps}>{isDateRangeValid ? "✅" : "❌"}</Text>
|
|
226
|
+
</VStack>
|
|
227
|
+
</HStack>
|
|
228
|
+
</VStack>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const DatePicker = () => {
|
|
233
|
+
const [activeDateId, setActiveDateId] = useState<string | undefined>(
|
|
234
|
+
toDateId(addDays(startOfThisMonth, 3))
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const calendarActiveDateRanges = useMemo<CalendarActiveDateRange[]>(() => {
|
|
238
|
+
if (!activeDateId) return [];
|
|
239
|
+
|
|
240
|
+
return [{ startId: activeDateId, endId: activeDateId }];
|
|
241
|
+
}, [activeDateId]);
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<Calendar.List
|
|
245
|
+
calendarActiveDateRanges={calendarActiveDateRanges}
|
|
246
|
+
calendarInitialMonthId={toDateId(startOfThisMonth)}
|
|
247
|
+
onCalendarDayPress={setActiveDateId}
|
|
248
|
+
/>
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export const ScrollingBackwardsWorkaround = () => {
|
|
253
|
+
return (
|
|
254
|
+
<VStack alignItems="stretch" grow spacing={12}>
|
|
255
|
+
<Text>This preloads all past months between Jan 1st 2020 and today</Text>
|
|
256
|
+
|
|
257
|
+
<Calendar.List
|
|
258
|
+
calendarFutureScrollRangeInMonths={1}
|
|
259
|
+
calendarInitialMonthId="2024-02-01"
|
|
260
|
+
calendarMaxDateId="2024-05-01"
|
|
261
|
+
calendarMinDateId="2020-01-01"
|
|
262
|
+
calendarPastScrollRangeInMonths={50}
|
|
263
|
+
onCalendarDayPress={loggingHandler("onCalendarDayPress")}
|
|
264
|
+
/>
|
|
265
|
+
</VStack>
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const TwoCalendarListsMounted = () => {
|
|
270
|
+
return (
|
|
271
|
+
<VStack grow spacing={48}>
|
|
272
|
+
<CalendarInstanceDemo instanceId="First" startingIndex={3} />
|
|
273
|
+
<CalendarInstanceDemo instanceId="Second" startingIndex={10} />
|
|
274
|
+
</VStack>
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
function CalendarInstanceDemo({
|
|
279
|
+
instanceId,
|
|
280
|
+
startingIndex,
|
|
281
|
+
}: {
|
|
282
|
+
instanceId: string;
|
|
283
|
+
startingIndex: number;
|
|
284
|
+
}) {
|
|
285
|
+
const calendarDateRangeProps = useDateRange();
|
|
286
|
+
const rerender = useRef(0);
|
|
287
|
+
rerender.current += 1;
|
|
288
|
+
return (
|
|
289
|
+
<VStack grow spacing={8}>
|
|
290
|
+
<Calendar.List
|
|
291
|
+
{...calendarDateRangeProps}
|
|
292
|
+
calendarInstanceId={instanceId}
|
|
293
|
+
/>
|
|
294
|
+
<Text>
|
|
295
|
+
{instanceId}: {calendarDateRangeProps.dateRange.startId} -{" "}
|
|
296
|
+
{calendarDateRangeProps.dateRange.endId} (re-renders: {rerender.current}
|
|
297
|
+
⚡)
|
|
298
|
+
</Text>
|
|
299
|
+
</VStack>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export const Demo = () => {
|
|
304
|
+
const dateRangeOne = useDateRange();
|
|
305
|
+
const dateRangeTwo = useDateRange();
|
|
306
|
+
return (
|
|
307
|
+
<VStack grow spacing={48}>
|
|
308
|
+
<VStack grow spacing={4}>
|
|
309
|
+
<Text>First calendar</Text>
|
|
310
|
+
<Calendar
|
|
311
|
+
calendarInstanceId="First"
|
|
312
|
+
calendarMonthId="2024-08-01"
|
|
313
|
+
{...dateRangeOne}
|
|
314
|
+
/>
|
|
315
|
+
</VStack>
|
|
316
|
+
<VStack grow spacing={4}>
|
|
317
|
+
<Text>Second calendar</Text>
|
|
318
|
+
<Calendar
|
|
319
|
+
calendarInstanceId="Second"
|
|
320
|
+
calendarMonthId="2024-08-01"
|
|
321
|
+
{...dateRangeTwo}
|
|
322
|
+
/>
|
|
323
|
+
</VStack>
|
|
324
|
+
</VStack>
|
|
325
|
+
);
|
|
326
|
+
};
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LegendList as LegendListBase,
|
|
3
|
+
type LegendListProps,
|
|
4
|
+
type LegendListRef,
|
|
5
|
+
} from "@legendapp/list";
|
|
6
|
+
import { memo, useCallback, useImperativeHandle, useMemo, useRef } from "react";
|
|
7
|
+
import { View } from "react-native";
|
|
8
|
+
|
|
9
|
+
import type { CalendarProps } from "@/components/Calendar";
|
|
10
|
+
import { Calendar } from "@/components/Calendar";
|
|
11
|
+
import { getWeekOfMonth, startOfMonth, toDateId } from "@/helpers/dates";
|
|
12
|
+
import type { CalendarMonth } from "@/hooks/useCalendarList";
|
|
13
|
+
import { getHeightForMonth, useCalendarList } from "@/hooks/useCalendarList";
|
|
14
|
+
|
|
15
|
+
// Type assertion to make LegendList compatible with React 19
|
|
16
|
+
const LegendList = LegendListBase as <T>(
|
|
17
|
+
props: LegendListProps<T> & { ref?: React.Ref<LegendListRef> }
|
|
18
|
+
) => React.ReactElement;
|
|
19
|
+
/**
|
|
20
|
+
* Represents each `CalendarList` item. It's enhanced with the required
|
|
21
|
+
* `Calendar` props to simplify building custom `Calendar` components.
|
|
22
|
+
*/
|
|
23
|
+
export type CalendarMonthEnhanced = CalendarMonth & {
|
|
24
|
+
calendarProps: Omit<CalendarProps, "calendarMonthId">;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const keyExtractor = (month: CalendarMonth) => month.id;
|
|
28
|
+
|
|
29
|
+
export interface CalendarListProps
|
|
30
|
+
extends Omit<CalendarProps, "calendarMonthId">,
|
|
31
|
+
Omit<
|
|
32
|
+
LegendListProps<CalendarMonthEnhanced>,
|
|
33
|
+
"renderItem" | "data" | "children"
|
|
34
|
+
> {
|
|
35
|
+
/**
|
|
36
|
+
* How many months to show before the current month. Once the user scrolls
|
|
37
|
+
* past this range and if they haven't exceeded the `calendarMinDateId`, new
|
|
38
|
+
* months are prepended in this increment.
|
|
39
|
+
* @defaultValue 12
|
|
40
|
+
*/
|
|
41
|
+
calendarPastScrollRangeInMonths?: number;
|
|
42
|
+
/**
|
|
43
|
+
* How many months to show after the current month. Once the user scrolls
|
|
44
|
+
* past this range and if they haven't exceeded the `calendarMaxDateId`, new
|
|
45
|
+
* months are appended in this increment.
|
|
46
|
+
* @defaultValue 12
|
|
47
|
+
*/
|
|
48
|
+
calendarFutureScrollRangeInMonths?: number;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* An additional height to use in the month's height calculation. Useful when
|
|
52
|
+
* providing a custom `Calendar` component with extra content such as a
|
|
53
|
+
* footer.
|
|
54
|
+
*/
|
|
55
|
+
calendarAdditionalHeight?: number;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The vertical spacing between each `<Calendar />` component.
|
|
59
|
+
* @defaultValue 20
|
|
60
|
+
*/
|
|
61
|
+
calendarSpacing?: number;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The initial month to open the calendar to, as a `YYYY-MM-DD` string.
|
|
65
|
+
* Defaults to the current month.
|
|
66
|
+
*
|
|
67
|
+
* **Tip**: To convert to date ID, use `toDateId(date)`.
|
|
68
|
+
*/
|
|
69
|
+
calendarInitialMonthId?: string;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Overwrites the default `Calendar` component.
|
|
73
|
+
*
|
|
74
|
+
* **Important**: when providing a custom implementation, make sure to
|
|
75
|
+
* manually set all the spacing and height props to ensure the list scrolls
|
|
76
|
+
* to the right offset:
|
|
77
|
+
* - calendarDayHeight
|
|
78
|
+
* - calendarMonthHeaderHeight
|
|
79
|
+
* - calendarWeekHeaderHeight
|
|
80
|
+
* - calendarAdditionalHeight
|
|
81
|
+
* - calendarRowVerticalSpacing
|
|
82
|
+
* - calendarSpacing
|
|
83
|
+
*/
|
|
84
|
+
renderItem?: LegendListProps<CalendarMonthEnhanced>["renderItem"];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface ImperativeScrollParams {
|
|
88
|
+
/**
|
|
89
|
+
* An additional offset to add to the final scroll position. Useful when
|
|
90
|
+
* you need to slightly change the final scroll position.
|
|
91
|
+
*/
|
|
92
|
+
additionalOffset?: number;
|
|
93
|
+
}
|
|
94
|
+
export interface CalendarListRef {
|
|
95
|
+
scrollToMonth: (
|
|
96
|
+
date: Date,
|
|
97
|
+
animated: boolean,
|
|
98
|
+
params?: ImperativeScrollParams
|
|
99
|
+
) => void;
|
|
100
|
+
scrollToDate: (
|
|
101
|
+
date: Date,
|
|
102
|
+
animated: boolean,
|
|
103
|
+
params?: ImperativeScrollParams
|
|
104
|
+
) => void;
|
|
105
|
+
scrollToOffset: (offset: number, animated: boolean) => void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const CalendarList = memo(function CalendarList({
|
|
109
|
+
ref,
|
|
110
|
+
...props
|
|
111
|
+
}: CalendarListProps & { ref?: React.Ref<CalendarListRef> }) {
|
|
112
|
+
const {
|
|
113
|
+
// List-related props
|
|
114
|
+
calendarInitialMonthId,
|
|
115
|
+
calendarPastScrollRangeInMonths = 12,
|
|
116
|
+
calendarFutureScrollRangeInMonths = 12,
|
|
117
|
+
calendarFirstDayOfWeek = "sunday",
|
|
118
|
+
calendarFormatLocale,
|
|
119
|
+
|
|
120
|
+
// Spacings
|
|
121
|
+
calendarSpacing = 20,
|
|
122
|
+
calendarRowHorizontalSpacing,
|
|
123
|
+
calendarRowVerticalSpacing = 8,
|
|
124
|
+
|
|
125
|
+
// Heights
|
|
126
|
+
calendarMonthHeaderHeight = 20,
|
|
127
|
+
calendarDayHeight = 32,
|
|
128
|
+
calendarWeekHeaderHeight = calendarDayHeight,
|
|
129
|
+
calendarAdditionalHeight = 0,
|
|
130
|
+
|
|
131
|
+
// Other props
|
|
132
|
+
calendarColorScheme,
|
|
133
|
+
theme,
|
|
134
|
+
onEndReached,
|
|
135
|
+
onStartReached,
|
|
136
|
+
...otherProps
|
|
137
|
+
} = props;
|
|
138
|
+
|
|
139
|
+
const {
|
|
140
|
+
calendarActiveDateRanges,
|
|
141
|
+
calendarDisabledDateIds,
|
|
142
|
+
calendarInstanceId,
|
|
143
|
+
calendarMaxDateId,
|
|
144
|
+
calendarMinDateId,
|
|
145
|
+
getCalendarDayFormat,
|
|
146
|
+
getCalendarMonthFormat,
|
|
147
|
+
getCalendarWeekDayFormat,
|
|
148
|
+
onCalendarDayPress,
|
|
149
|
+
CalendarPressableComponent,
|
|
150
|
+
...flatListProps
|
|
151
|
+
} = otherProps;
|
|
152
|
+
|
|
153
|
+
const calendarProps = useMemo(
|
|
154
|
+
(): CalendarMonthEnhanced["calendarProps"] => ({
|
|
155
|
+
calendarActiveDateRanges,
|
|
156
|
+
calendarColorScheme,
|
|
157
|
+
calendarDayHeight,
|
|
158
|
+
calendarDisabledDateIds,
|
|
159
|
+
calendarFirstDayOfWeek,
|
|
160
|
+
calendarFormatLocale,
|
|
161
|
+
calendarInstanceId,
|
|
162
|
+
calendarMaxDateId,
|
|
163
|
+
calendarMinDateId,
|
|
164
|
+
calendarMonthHeaderHeight,
|
|
165
|
+
calendarRowHorizontalSpacing,
|
|
166
|
+
calendarRowVerticalSpacing,
|
|
167
|
+
calendarWeekHeaderHeight,
|
|
168
|
+
getCalendarDayFormat,
|
|
169
|
+
getCalendarMonthFormat,
|
|
170
|
+
getCalendarWeekDayFormat,
|
|
171
|
+
onCalendarDayPress,
|
|
172
|
+
theme,
|
|
173
|
+
CalendarPressableComponent,
|
|
174
|
+
}),
|
|
175
|
+
[
|
|
176
|
+
calendarColorScheme,
|
|
177
|
+
calendarActiveDateRanges,
|
|
178
|
+
calendarDayHeight,
|
|
179
|
+
calendarDisabledDateIds,
|
|
180
|
+
calendarFirstDayOfWeek,
|
|
181
|
+
calendarFormatLocale,
|
|
182
|
+
calendarMaxDateId,
|
|
183
|
+
calendarMinDateId,
|
|
184
|
+
calendarMonthHeaderHeight,
|
|
185
|
+
calendarRowHorizontalSpacing,
|
|
186
|
+
calendarRowVerticalSpacing,
|
|
187
|
+
calendarWeekHeaderHeight,
|
|
188
|
+
getCalendarDayFormat,
|
|
189
|
+
getCalendarMonthFormat,
|
|
190
|
+
getCalendarWeekDayFormat,
|
|
191
|
+
calendarInstanceId,
|
|
192
|
+
onCalendarDayPress,
|
|
193
|
+
theme,
|
|
194
|
+
CalendarPressableComponent,
|
|
195
|
+
]
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const {
|
|
199
|
+
initialMonthIndex,
|
|
200
|
+
monthList,
|
|
201
|
+
appendMonths,
|
|
202
|
+
prependMonths,
|
|
203
|
+
addMissingMonths,
|
|
204
|
+
} = useCalendarList({
|
|
205
|
+
calendarFirstDayOfWeek,
|
|
206
|
+
calendarFutureScrollRangeInMonths,
|
|
207
|
+
calendarPastScrollRangeInMonths,
|
|
208
|
+
calendarInitialMonthId,
|
|
209
|
+
calendarMaxDateId,
|
|
210
|
+
calendarMinDateId,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const monthListWithCalendarProps = useMemo(() => {
|
|
214
|
+
return monthList.map((month) => ({
|
|
215
|
+
...month,
|
|
216
|
+
calendarProps,
|
|
217
|
+
}));
|
|
218
|
+
}, [calendarProps, monthList]);
|
|
219
|
+
|
|
220
|
+
const handleOnEndReached = useCallback(
|
|
221
|
+
(info: { distanceFromEnd: number }) => {
|
|
222
|
+
appendMonths(calendarFutureScrollRangeInMonths);
|
|
223
|
+
onEndReached?.(info);
|
|
224
|
+
},
|
|
225
|
+
[appendMonths, calendarFutureScrollRangeInMonths, onEndReached]
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const handleOnStartReached = useCallback(
|
|
229
|
+
(info: { distanceFromStart: number }) => {
|
|
230
|
+
prependMonths(calendarPastScrollRangeInMonths);
|
|
231
|
+
onStartReached?.(info);
|
|
232
|
+
},
|
|
233
|
+
[prependMonths, calendarPastScrollRangeInMonths, onStartReached]
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const handleGetFixedItemSize = useCallback(
|
|
237
|
+
(_index: number, item: CalendarMonth) => {
|
|
238
|
+
return getHeightForMonth({
|
|
239
|
+
calendarMonth: item,
|
|
240
|
+
calendarSpacing,
|
|
241
|
+
calendarDayHeight,
|
|
242
|
+
calendarMonthHeaderHeight,
|
|
243
|
+
calendarRowVerticalSpacing,
|
|
244
|
+
calendarAdditionalHeight,
|
|
245
|
+
calendarWeekHeaderHeight,
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
[
|
|
249
|
+
calendarAdditionalHeight,
|
|
250
|
+
calendarDayHeight,
|
|
251
|
+
calendarMonthHeaderHeight,
|
|
252
|
+
calendarRowVerticalSpacing,
|
|
253
|
+
calendarSpacing,
|
|
254
|
+
calendarWeekHeaderHeight,
|
|
255
|
+
]
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Returns the offset for the given month (how much the user needs to
|
|
260
|
+
* scroll to reach the month).
|
|
261
|
+
*/
|
|
262
|
+
const getScrollOffsetForMonth = useCallback(
|
|
263
|
+
(date: Date) => {
|
|
264
|
+
const monthId = toDateId(startOfMonth(date));
|
|
265
|
+
|
|
266
|
+
let baseMonthList = monthList;
|
|
267
|
+
let index = baseMonthList.findIndex((month) => month.id === monthId);
|
|
268
|
+
|
|
269
|
+
if (index === -1) {
|
|
270
|
+
baseMonthList = addMissingMonths(monthId);
|
|
271
|
+
index = baseMonthList.findIndex((month) => month.id === monthId);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return baseMonthList.slice(0, index).reduce((acc, month) => {
|
|
275
|
+
const currentHeight = getHeightForMonth({
|
|
276
|
+
calendarMonth: month,
|
|
277
|
+
calendarSpacing,
|
|
278
|
+
calendarDayHeight,
|
|
279
|
+
calendarMonthHeaderHeight,
|
|
280
|
+
calendarRowVerticalSpacing,
|
|
281
|
+
calendarWeekHeaderHeight,
|
|
282
|
+
calendarAdditionalHeight,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return acc + currentHeight;
|
|
286
|
+
}, 0);
|
|
287
|
+
},
|
|
288
|
+
[
|
|
289
|
+
addMissingMonths,
|
|
290
|
+
calendarAdditionalHeight,
|
|
291
|
+
calendarDayHeight,
|
|
292
|
+
calendarMonthHeaderHeight,
|
|
293
|
+
calendarRowVerticalSpacing,
|
|
294
|
+
calendarSpacing,
|
|
295
|
+
calendarWeekHeaderHeight,
|
|
296
|
+
monthList,
|
|
297
|
+
]
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const legendListRef = useRef<LegendListRef>(null);
|
|
301
|
+
|
|
302
|
+
useImperativeHandle(ref, () => ({
|
|
303
|
+
scrollToMonth(
|
|
304
|
+
date,
|
|
305
|
+
animated,
|
|
306
|
+
{ additionalOffset = 0 } = { additionalOffset: 0 }
|
|
307
|
+
) {
|
|
308
|
+
// Wait for the next render cycle to ensure the list has been
|
|
309
|
+
// updated with the new months.
|
|
310
|
+
setTimeout(() => {
|
|
311
|
+
legendListRef.current?.scrollToOffset({
|
|
312
|
+
offset: getScrollOffsetForMonth(date) + additionalOffset,
|
|
313
|
+
animated,
|
|
314
|
+
});
|
|
315
|
+
}, 0);
|
|
316
|
+
},
|
|
317
|
+
scrollToDate(
|
|
318
|
+
date,
|
|
319
|
+
animated,
|
|
320
|
+
{ additionalOffset = 0 } = {
|
|
321
|
+
additionalOffset: 0,
|
|
322
|
+
}
|
|
323
|
+
) {
|
|
324
|
+
const currentMonthOffset = getScrollOffsetForMonth(date);
|
|
325
|
+
const weekOfMonthIndex = getWeekOfMonth(date, calendarFirstDayOfWeek);
|
|
326
|
+
const rowHeight = calendarDayHeight + calendarRowVerticalSpacing;
|
|
327
|
+
|
|
328
|
+
let weekOffset = calendarWeekHeaderHeight + rowHeight * weekOfMonthIndex;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* We need to subtract one vertical spacing to avoid cutting off the
|
|
332
|
+
* desired date. A simple way of understanding why is imagining we
|
|
333
|
+
* want to scroll exactly to the given date, but leave a little bit of
|
|
334
|
+
* breathing room (`calendarRowVerticalSpacing`) above it.
|
|
335
|
+
*/
|
|
336
|
+
weekOffset = weekOffset - calendarRowVerticalSpacing;
|
|
337
|
+
|
|
338
|
+
legendListRef.current?.scrollToOffset({
|
|
339
|
+
offset: currentMonthOffset + weekOffset + additionalOffset,
|
|
340
|
+
animated,
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
scrollToOffset(offset, animated) {
|
|
344
|
+
legendListRef.current?.scrollToOffset({ offset, animated });
|
|
345
|
+
},
|
|
346
|
+
}));
|
|
347
|
+
|
|
348
|
+
const calendarContainerStyle = useMemo(() => {
|
|
349
|
+
return { paddingBottom: calendarSpacing };
|
|
350
|
+
}, [calendarSpacing]);
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<LegendList
|
|
354
|
+
data={monthListWithCalendarProps}
|
|
355
|
+
estimatedItemSize={273}
|
|
356
|
+
getFixedItemSize={handleGetFixedItemSize}
|
|
357
|
+
initialScrollIndex={initialMonthIndex}
|
|
358
|
+
keyExtractor={keyExtractor}
|
|
359
|
+
onEndReached={handleOnEndReached}
|
|
360
|
+
onStartReached={handleOnStartReached}
|
|
361
|
+
recycleItems
|
|
362
|
+
ref={legendListRef}
|
|
363
|
+
renderItem={({ item }: { item: CalendarMonthEnhanced }) => (
|
|
364
|
+
<View style={calendarContainerStyle}>
|
|
365
|
+
<Calendar calendarMonthId={item.id} {...item.calendarProps} />
|
|
366
|
+
</View>
|
|
367
|
+
)}
|
|
368
|
+
showsVerticalScrollIndicator={false}
|
|
369
|
+
style={{ flex: 1 }}
|
|
370
|
+
{...flatListProps}
|
|
371
|
+
/>
|
|
372
|
+
);
|
|
373
|
+
});
|