From db3d58821e4de8d46b110125d0fa36919765248f Mon Sep 17 00:00:00 2001 From: euduardo Date: Sat, 27 Jun 2026 17:49:26 -0300 Subject: [PATCH] feat(widgets): show calendar events on hover with pin on click (#2637) (#6081) feat(widgets): show calendar events on hover with pin on click (#2637) docs(widgets): update calendar interactions section for hover behavior --- apps/docs/docs/widgets/calendar/index.mdx | 4 +- .../widgets/src/calendar/calender-day.tsx | 40 ++++++++++--- packages/widgets/src/calendar/component.tsx | 60 +++++++++++++++---- 3 files changed, 86 insertions(+), 18 deletions(-) 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");