diff --git a/apps/docs/docs/widgets/calendar/index.mdx b/apps/docs/docs/widgets/calendar/index.mdx
index 60095aabe..5db4e9b30 100644
--- a/apps/docs/docs/widgets/calendar/index.mdx
+++ b/apps/docs/docs/widgets/calendar/index.mdx
@@ -63,7 +63,9 @@ import withoutIntegrations from './img/without-integrations.png';
### Interactions
-- View calendar events by clicking on the marked dates.
+- Hover over a marked date to preview its events in a popover.
+- Click on a marked date to pin the popover open (it will stay visible even after moving the mouse away).
+- Click the date again or click outside to close the pinned popover.
### Adding the widget
diff --git a/packages/widgets/src/calendar/calender-day.tsx b/packages/widgets/src/calendar/calender-day.tsx
index 790cc0a82..70f2f26a8 100644
--- a/packages/widgets/src/calendar/calender-day.tsx
+++ b/packages/widgets/src/calendar/calender-day.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useCallback, useState } from "react";
import { Box, Container, Flex, Popover, Text, useMantineTheme } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
@@ -13,10 +13,12 @@ interface CalendarDayProps {
disabled: boolean;
rootWidth: number;
rootHeight: number;
+ onOpen: (close: () => void) => void;
}
-export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: CalendarDayProps) => {
+export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth, onOpen }: CalendarDayProps) => {
const [opened, setOpened] = useState(false);
+ const [pinned, setPinned] = useState(false);
const { primaryColor } = useMantineTheme();
const { ref, height } = useElementSize();
const board = useRequiredBoard();
@@ -29,6 +31,19 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
const isTooSmallForIndicators = height < 30;
+ const handleMouseEnter = useCallback(() => {
+ if (disabled) return;
+ onOpen(() => {
+ setOpened(false);
+ setPinned(false);
+ });
+ setOpened(true);
+ }, [disabled, onOpen]);
+
+ const handleMouseLeave = useCallback(() => {
+ if (!pinned) setOpened(false);
+ }, [pinned]);
+
return (
{
+ setOpened(value);
+ if (!value) setPinned(false);
+ }}
opened={opened}
disabled={disabled}
>
@@ -59,10 +77,10 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
cursor: disabled ? "default" : "pointer",
}}
onClick={() => {
- if (disabled) return;
-
- setOpened((prev) => !prev);
+ if (!disabled) setPinned((prev) => !prev);
}}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
>
{date.getDate()}
@@ -71,7 +89,15 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
{/* Popover has some offset on the left side, padding is removed because of scrollarea paddings */}
-
+ setOpened(true)}
+ onMouseLeave={handleMouseLeave}
+ >
diff --git a/packages/widgets/src/calendar/component.tsx b/packages/widgets/src/calendar/component.tsx
index ac3d8b37b..fb0940720 100644
--- a/packages/widgets/src/calendar/component.tsx
+++ b/packages/widgets/src/calendar/component.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useMemo, useState } from "react";
+import { useCallback, useMemo, useRef, useState } from "react";
import { useParams } from "next/navigation";
import { useMantineTheme } from "@mantine/core";
import { Calendar } from "@mantine/dates";
@@ -16,11 +16,15 @@ import type { WidgetComponentProps } from "../definition";
import { CalendarDay } from "./calender-day";
import classes from "./component.module.css";
-export default function CalendarWidget(props: WidgetComponentProps<"calendar">) {
+export default function CalendarWidget(
+ props: WidgetComponentProps<"calendar">,
+) {
const [month, setMonth] = useState(new Date());
if (props.integrationIds.length === 0) {
- return ;
+ return (
+
+ );
}
return ;
@@ -31,7 +35,13 @@ interface FetchCalendarProps extends WidgetComponentProps<"calendar"> {
setMonth: (date: Date) => void;
}
-const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }: FetchCalendarProps) => {
+const FetchCalendar = ({
+ month,
+ setMonth,
+ isEditMode,
+ integrationIds,
+ options,
+}: FetchCalendarProps) => {
const input = {
integrationIds,
month: month.getMonth(),
@@ -61,9 +71,20 @@ const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }:
},
});
- const events = useMemo(() => data?.flatMap((item) => item.events) ?? [], [data]);
+ const events = useMemo(
+ () => data?.flatMap((item) => item.events) ?? [],
+ [data],
+ );
- return ;
+ return (
+
+ );
};
interface CalendarBaseProps {
@@ -74,7 +95,13 @@ interface CalendarBaseProps {
options: WidgetComponentProps<"calendar">["options"];
}
-const CalendarBase = ({ isEditMode, events, month, setMonth, options }: CalendarBaseProps) => {
+const CalendarBase = ({
+ isEditMode,
+ events,
+ month,
+ setMonth,
+ options,
+}: CalendarBaseProps) => {
const params = useParams();
const locale = params.locale as string;
const { firstDayOfWeek } = useSettings();
@@ -85,6 +112,11 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
const isSmall = width < 256;
const normalizedEvents = useMemo(() => splitEvents(events), [events]);
+ const activeCloseRef = useRef<(() => void) | null>(null);
+ const onDayOpen = useCallback((close: () => void) => {
+ activeCloseRef.current?.();
+ activeCloseRef.current = close;
+ }, []);
return (
dayjs(event.startDate).isSame(tileDate, "day"))
.filter(
- (event) => event.metadata?.type !== "radarr" || options.releaseType.includes(event.metadata.releaseType),
+ (event) =>
+ event.metadata?.type !== "radarr" ||
+ options.releaseType.includes(event.metadata.releaseType),
)
- .toSorted((eventA, eventB) => eventA.startDate.getTime() - eventB.startDate.getTime());
+ .toSorted(
+ (eventA, eventB) =>
+ eventA.startDate.getTime() - eventB.startDate.getTime(),
+ );
return (
);
}}
@@ -198,7 +236,9 @@ export const splitEvents = (events: CalendarEvent[]): CalendarEvent[] => {
splitEvents.push({
...event,
startDate: currentStart.toDate(),
- endDate: currentStart.endOf("day").isAfter(event.endDate) ? event.endDate : currentStart.endOf("day").toDate(),
+ endDate: currentStart.endOf("day").isAfter(event.endDate)
+ ? event.endDate
+ : currentStart.endOf("day").toDate(),
});
currentStart = currentStart.add(1, "day").startOf("day");