Skip to content

Commit

Permalink
Mount many calendars at once (#59)
Browse files Browse the repository at this point in the history
* Use named functions and clean up the component signature

* Allow mounting multiple calendars

* Fix lint
  • Loading branch information
MarceloPrado authored Oct 2, 2024
1 parent a8d6f2a commit 863edce
Show file tree
Hide file tree
Showing 12 changed files with 812 additions and 390 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-pears-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@marceloterreiro/flash-calendar": minor
---

Add the ability to mount more than one calendar at once
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions apps/docs/docs/fundamentals/usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { HStack } from "@site/src/components/HStack";
import basicCalendar from "./assets/basic-calendar.png";
import basicCalendarList from "./assets/basic-calendar-list.png";
import dateRangeCalendarList from "./assets/date-range-calendar-list.png";
import twoCalendarsMounted from "./assets/two-calendars-mounted.png";
import customFormatting from "./assets/custom-formatting.png";
import customLocale from "./assets/custom-locale.png";
import disabledDates from "./assets/disabled-dates.png";
Expand Down Expand Up @@ -360,3 +361,47 @@ the
[source](https://github.com/marceloprado/flash-calendar/blob/d832d72867d40c9b375c30aec9985b181c0a80f3/kitchen-sink/expo/src/components/BottomSheetFlashList/BottomSheetFlashList.tsx).
Bear in mind this isn't a very performant implementation. Contributions
welcomed!

## Two calendars in the same screen

To render more than one calendar in the same screen, use the
`calendarInstanceId` prop. This works for both `Calendar` and `Calendar.List`:

<HStack spacing={24} alignItems="flex-start">

<div>

```tsx
import { Calendar, useDateRange } from "@marceloterreiro/flash-calendar";

export const TwoCalendarsMounted = () => {
const dateRangeOne = useDateRange();
const dateRangeTwo = useDateRange();
return (
<VStack grow spacing={48}>
<VStack grow spacing={4}>
<Text>First calendar</Text>
<Calendar
calendarInstanceId="First"
calendarMonthId="2024-08-01"
{...dateRangeOne}
/>
</VStack>
<VStack grow spacing={4}>
<Text>Second calendar</Text>
<Calendar
calendarInstanceId="Second"
calendarMonthId="2024-08-01"
{...dateRangeTwo}
/>
</VStack>
</VStack>
);
};
```

</div>

<Image img={twoCalendarsMounted} width={250} />

</HStack>
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,9 @@ BasePerfTestCalendar.displayName = "BasePerfTestCalendar";
export const PerfTestCalendar = memo(
({ calendarActiveDateRanges, calendarMonthId, ...props }: CalendarProps) => {
useEffect(() => {
activeDateRangesEmitter.emit(
"onSetActiveDateRanges",
calendarActiveDateRanges ?? []
);
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
ranges: calendarActiveDateRanges ?? [],
});
/**
* While `calendarMonthId` is not used by the effect, we still need it in
* the dependency array since [FlashList uses recycling
Expand Down
47 changes: 45 additions & 2 deletions packages/flash-calendar/src/components/Calendar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { Meta, StoryObj } from "@storybook/react";
import { addDays, subDays } from "date-fns";
import { format } from "date-fns/fp/format";
import { useState } from "react";
import { useRef, useState } from "react";
import { Text } from "react-native";

import { paddingDecorator } from "@/developer/decorators";
import { loggingHandler } from "@/developer/loggginHandler";
import { endOfMonth, startOfMonth, toDateId } from "@/helpers/dates";
import { useDateRange } from "@/hooks/useDateRange";
import { VStack } from "@/components/VStack";

import { Calendar } from "./Calendar";

Expand Down Expand Up @@ -128,7 +130,7 @@ export const WithCustomFormatting = (args: typeof KichenSink.args) => {
};

export const DatePicker = (args: typeof KichenSink.args) => {
const [activeDateId, setActiveDateId] = useState<string | undefined>(
const [activeDateId, setActiveDateId] = useState(
toDateId(addDays(startOfThisMonth, 3))
);

Expand Down Expand Up @@ -181,3 +183,44 @@ export const LightModeOnly = () => {
/>
);
};

export const TwoCalendarsMounted = () => {
return (
<VStack spacing={48}>
<CalendarInstanceDemo instanceId="First" startingIndex={3} />
<CalendarInstanceDemo instanceId="Second" startingIndex={10} />
</VStack>
);
};

function CalendarInstanceDemo({
instanceId,
startingIndex,
}: {
instanceId: string;
startingIndex: number;
}) {
const [date, setDate] = useState(
toDateId(addDays(startOfThisMonth, startingIndex))
);
const rerender = useRef(0);
rerender.current += 1;
return (
<VStack spacing={8}>
<Calendar
calendarActiveDateRanges={[
{
startId: date,
endId: date,
},
]}
calendarInstanceId={instanceId}
calendarMonthId={date}
onCalendarDayPress={setDate}
/>
<Text>
{instanceId} date: {date} (re-renders: {rerender.current}⚡)
</Text>
</VStack>
);
}
204 changes: 111 additions & 93 deletions packages/flash-calendar/src/components/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,18 @@ export interface CalendarTheme {
export type CalendarOnDayPress = (dateId: string) => void;

export interface CalendarProps extends UseCalendarParams {
onCalendarDayPress: CalendarOnDayPress;
/**
* A unique identifier for this calendar instance. This is useful if you
* need to render more than one calendar at once. This allows Flash Calendar
* to scope its state to the given instance.
*
* No need to get fancy with `uuid` or anything like that - a simple static
* string is enough.
*
* If not provided, Flash Calendar will use a default value which will hoist
* the state in a global scope.
*/
calendarInstanceId?: string;
/**
* The spacing between each calendar row (the month header, the week days row,
* and the weeks row)
Expand Down Expand Up @@ -78,119 +89,126 @@ export interface CalendarProps extends UseCalendarParams {
* @defaultValue undefined
*/
calendarColorScheme?: ColorSchemeName;
/**
* The callback to be called when a day is pressed.
*/
onCalendarDayPress: CalendarOnDayPress;
/** Theme to customize the calendar component. */
theme?: CalendarTheme;
}

const BaseCalendar = memo(
({
onCalendarDayPress,
const BaseCalendar = memo(function BaseCalendar(props: CalendarProps) {
const {
calendarInstanceId,
calendarRowVerticalSpacing = 8,
calendarRowHorizontalSpacing = 8,
theme,
calendarDayHeight = 32,
calendarMonthHeaderHeight = 20,
calendarWeekHeaderHeight = calendarDayHeight,
onCalendarDayPress,
theme,

...buildCalendarParams
}: CalendarProps) => {
const { calendarRowMonth, weeksList, weekDaysList } =
useCalendar(buildCalendarParams);
} = props;

return (
<VStack
alignItems="center"
spacing={calendarRowVerticalSpacing as keyof BaseTheme["spacing"]}
>
<CalendarRowMonth
height={calendarMonthHeaderHeight}
theme={theme?.rowMonth}
>
{uppercaseFirstLetter(calendarRowMonth)}
</CalendarRowMonth>
<CalendarRowWeek spacing={8} theme={theme?.rowWeek}>
{weekDaysList.map((weekDay, i) => (
<CalendarItemWeekName
height={calendarWeekHeaderHeight}
key={i}
theme={theme?.itemWeekName}
>
{weekDay}
</CalendarItemWeekName>
))}
</CalendarRowWeek>
{weeksList.map((week, index) => (
<CalendarRowWeek key={index}>
{week.map((dayProps) => {
if (dayProps.isDifferentMonth) {
return (
<CalendarItemDayContainer
dayHeight={calendarDayHeight}
daySpacing={calendarRowHorizontalSpacing}
isStartOfWeek={dayProps.isStartOfWeek}
key={dayProps.id}
theme={theme?.itemDayContainer}
>
<CalendarItemEmpty
height={calendarDayHeight}
theme={theme?.itemEmpty}
/>
</CalendarItemDayContainer>
);
}
const { calendarRowMonth, weeksList, weekDaysList } =
useCalendar(buildCalendarParams);

return (
<VStack
alignItems="center"
spacing={calendarRowVerticalSpacing as keyof BaseTheme["spacing"]}
>
<CalendarRowMonth
height={calendarMonthHeaderHeight}
theme={theme?.rowMonth}
>
{uppercaseFirstLetter(calendarRowMonth)}
</CalendarRowMonth>
<CalendarRowWeek spacing={8} theme={theme?.rowWeek}>
{weekDaysList.map((weekDay, i) => (
<CalendarItemWeekName
height={calendarWeekHeaderHeight}
key={i}
theme={theme?.itemWeekName}
>
{weekDay}
</CalendarItemWeekName>
))}
</CalendarRowWeek>
{weeksList.map((week, index) => (
<CalendarRowWeek key={index}>
{week.map((dayProps) => {
if (dayProps.isDifferentMonth) {
return (
<CalendarItemDayWithContainer
containerTheme={theme?.itemDayContainer}
<CalendarItemDayContainer
dayHeight={calendarDayHeight}
daySpacing={calendarRowHorizontalSpacing}
isStartOfWeek={dayProps.isStartOfWeek}
key={dayProps.id}
metadata={dayProps}
onPress={onCalendarDayPress}
theme={theme?.itemDay}
theme={theme?.itemDayContainer}
>
{dayProps.displayLabel}
</CalendarItemDayWithContainer>
<CalendarItemEmpty
height={calendarDayHeight}
theme={theme?.itemEmpty}
/>
</CalendarItemDayContainer>
);
})}
</CalendarRowWeek>
))}
</VStack>
);
}
);
BaseCalendar.displayName = "BaseCalendar";
}

export const Calendar = memo(
({
return (
<CalendarItemDayWithContainer
calendarInstanceId={calendarInstanceId}
containerTheme={theme?.itemDayContainer}
dayHeight={calendarDayHeight}
daySpacing={calendarRowHorizontalSpacing}
key={dayProps.id}
metadata={dayProps}
onPress={onCalendarDayPress}
theme={theme?.itemDay}
>
{dayProps.displayLabel}
</CalendarItemDayWithContainer>
);
})}
</CalendarRowWeek>
))}
</VStack>
);
});

export const Calendar = memo(function Calendar(props: CalendarProps) {
const {
calendarInstanceId,
calendarActiveDateRanges,
calendarMonthId,
calendarColorScheme,
...props
}: CalendarProps) => {
useEffect(() => {
activeDateRangesEmitter.emit(
"onSetActiveDateRanges",
calendarActiveDateRanges ?? []
);
/**
* While `calendarMonthId` is not used by the effect, we still need it in
* the dependency array since [FlashList uses recycling
* internally](https://shopify.github.io/flash-list/docs/recycling).
*
* This means `Calendar` can re-render with different props instead of
* getting re-mounted. Without it, we would see staled/invalid data, as
* reported by
* [#11](https://github.com/MarceloPrado/flash-calendar/issues/11).
*/
}, [calendarActiveDateRanges, calendarMonthId]);

return (
<CalendarThemeProvider colorScheme={calendarColorScheme}>
<BaseCalendar {...props} calendarMonthId={calendarMonthId} />
</CalendarThemeProvider>
);
}
);
...otherProps
} = props;
useEffect(() => {
activeDateRangesEmitter.emit("onSetActiveDateRanges", {
instanceId: calendarInstanceId,
ranges: calendarActiveDateRanges ?? [],
});
/**
* While `calendarMonthId` is not used by the effect, we still need it in
* the dependency array since [FlashList uses recycling
* internally](https://shopify.github.io/flash-list/docs/recycling).
*
* This means `Calendar` can re-render with different props instead of
* getting re-mounted. Without it, we would see staled/invalid data, as
* reported by
* [#11](https://github.com/MarceloPrado/flash-calendar/issues/11).
*/
}, [calendarActiveDateRanges, calendarInstanceId, calendarMonthId]);

Calendar.displayName = "Calendar";
return (
<CalendarThemeProvider colorScheme={calendarColorScheme}>
<BaseCalendar
{...otherProps}
calendarInstanceId={calendarInstanceId}
calendarMonthId={calendarMonthId}
/>
</CalendarThemeProvider>
);
});
Loading

0 comments on commit 863edce

Please sign in to comment.