From 19f66eb518f895524e95729c3a2eec79cb93d01a Mon Sep 17 00:00:00 2001 From: Shak Date: Wed, 26 Aug 2026 18:32:29 +0330 Subject: [PATCH 1/4] fix(calendar): stop tooltip click listener from swallowing day-select clicks ClickableTooltip attached its own native capture-phase click listener (with stopPropagation) directly on the trigger element to toggle open state. In the calendar, DayItem already has its own onClick that opens the tooltip explicitly, so the two handlers raced and the native listener could swallow the day cell's React onClick after the first click. Added a toggleOnTriggerClick opt-out and disabled it for the calendar's usage, and pass selectedDate into CalendarDayDetails as an explicit prop instead of re-deriving it from context. --- src/components/ui/tooltip/clickable-tooltip.tsx | 6 ++++-- src/layouts/widgets/calendar/components/calendar-grid.tsx | 7 +++++-- .../widgets/calendar/components/day/tool-tip-content.tsx | 5 ++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/ui/tooltip/clickable-tooltip.tsx b/src/components/ui/tooltip/clickable-tooltip.tsx index 2a5cbd52..e49f5a89 100644 --- a/src/components/ui/tooltip/clickable-tooltip.tsx +++ b/src/components/ui/tooltip/clickable-tooltip.tsx @@ -25,6 +25,7 @@ interface ClickableTooltipProps { triggerRef: RefObject isOpen: boolean setIsOpen: (isOpen: boolean) => void + toggleOnTriggerClick?: boolean } const ClickableTooltip = ({ @@ -37,6 +38,7 @@ const ClickableTooltip = ({ triggerRef, isOpen, setIsOpen, + toggleOnTriggerClick = true, }: ClickableTooltipProps) => { const [calculatedPosition, setCalculatedPosition] = useState(position) const [tooltipCoords, setTooltipCoords] = useState({ x: 0, y: 0 }) @@ -151,7 +153,7 @@ const ClickableTooltip = ({ }, [isOpen, closeOnClickOutside]) useEffect(() => { - if (!triggerRef?.current) return + if (!toggleOnTriggerClick || !triggerRef?.current) return const handleClick = (e: Event) => { e.preventDefault() @@ -165,7 +167,7 @@ const ClickableTooltip = ({ return () => { element.removeEventListener('click', handleClick, true) } - }, [triggerRef, isOpen]) + }, [triggerRef, isOpen, toggleOnTriggerClick]) const variants = { top: { diff --git a/src/layouts/widgets/calendar/components/calendar-grid.tsx b/src/layouts/widgets/calendar/components/calendar-grid.tsx index 0adc8120..1ba6f963 100644 --- a/src/layouts/widgets/calendar/components/calendar-grid.tsx +++ b/src/layouts/widgets/calendar/components/calendar-grid.tsx @@ -2,7 +2,7 @@ import { useAuth } from '@/context/auth.context' import { useGeneralSetting } from '@/context/general-setting.context' import { useGetEvents } from '@/services/hooks/date/get-events.hook' import type React from 'react' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { type WidgetifyDate, formatDateStr } from '../utils' import { DayItem } from './day/day' import { ClickableTooltip } from '@/components/ui' @@ -53,6 +53,7 @@ export const CalendarGrid: React.FC = ({ const nextMonthDays = totalCells - daysInMonth - emptyDays const selectedDateStr = formatDateStr(selectedDate) + const triggerRef = useMemo(() => ({ current: clickedElement }), [clickedElement]) return ( <> @@ -109,9 +110,11 @@ export const CalendarGrid: React.FC = ({ {clickedElement && ( refetch()} diff --git a/src/layouts/widgets/calendar/components/day/tool-tip-content.tsx b/src/layouts/widgets/calendar/components/day/tool-tip-content.tsx index 27c8d011..8643b4bb 100644 --- a/src/layouts/widgets/calendar/components/day/tool-tip-content.tsx +++ b/src/layouts/widgets/calendar/components/day/tool-tip-content.tsx @@ -5,6 +5,7 @@ import { getHijriEvents, getShamsiEvents, hijriMonthNames, + type WidgetifyDate, } from '../../utils' import { useDate } from '@/context/date.context' import type React from 'react' @@ -23,6 +24,7 @@ import { moodOptions } from '@/common/constant/moods' import { Icon } from '@/src/icons' interface CalendarDayDetailsProps { + selectedDate: WidgetifyDate events: FetchedAllEvents eventIcon?: string moods: MoodEntry[] @@ -30,11 +32,12 @@ interface CalendarDayDetailsProps { } export const CalendarDayDetails: React.FC = ({ + selectedDate, events, moods, onMoodChange, }) => { - const { selectedDate, today, getHijriDate } = useDate() + const { today, getHijriDate } = useDate() const { isAuthenticated } = useAuth() const { mutateAsync: upsertMoodLog } = useUpsertMoodLog() From 5c6ed9355ed9c2ee01bf2f1877f753579adf50ad Mon Sep 17 00:00:00 2001 From: Shak Date: Wed, 26 Aug 2026 18:32:37 +0330 Subject: [PATCH 2/4] fix(calendar): wrap default-UI calendar widget with DateProvider The calendar widget in widgetItems (used by the default/ADVANCED UI's ContentSection) rendered without a DateProvider ancestor, unlike the WIDGET_DEFINITIONS entry used by the custom canvas UI. Without the provider, every useDate() call fell back to a stub where setSelectedDate/setCurrentDate are no-ops and selectedDate/currentDate are recomputed to "now" on every render. That silently broke day selection and month navigation with no console errors, since the calendar was pinned to whatever "today" resolves to on each render. --- src/context/widget-visibility.context.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/context/widget-visibility.context.tsx b/src/context/widget-visibility.context.tsx index 37cf6a6d..9bbe6d38 100644 --- a/src/context/widget-visibility.context.tsx +++ b/src/context/widget-visibility.context.tsx @@ -24,6 +24,7 @@ import { } from '@/services/hooks/widgets/widget-sync.hook' import { useAuth } from './auth.context' import { CurrencyProvider } from './currency.context' +import { DateProvider } from './date.context' export enum WidgetKeys { comboWidget = 'comboWidget', @@ -67,7 +68,11 @@ export const widgetItems: WidgetItem[] = [ emoji: '📅', label: 'تقویم', order: 0, - node: , + node: ( + + + + ), canToggle: true, popular: true, }, From ccfaf9b70c12749e9a54764868499c2b45beba94 Mon Sep 17 00:00:00 2001 From: Shak Date: Wed, 26 Aug 2026 18:32:44 +0330 Subject: [PATCH 3/4] perf(date-context): memoize DateProvider context value goToToday, isToday and getHijriDate were recreated on every render, and the context value object was a fresh literal each time, so every consumer of useDate() re-rendered whenever DateProvider re-rendered for any reason, even when none of currentDate/selectedDate/today actually changed. Wrapped the callbacks in useCallback and the provided value in useMemo, keyed on their real dependencies. --- src/context/date.context.tsx | 66 +++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/src/context/date.context.tsx b/src/context/date.context.tsx index 13f56d3b..13dc0339 100644 --- a/src/context/date.context.tsx +++ b/src/context/date.context.tsx @@ -1,5 +1,5 @@ import type React from 'react' -import { createContext, useContext, useEffect, useState } from 'react' +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { convertShamsiToHijri, getCurrentDate, @@ -44,44 +44,54 @@ export const DateProvider: React.FC<{ children: React.ReactNode }> = ({ children setSelectedDate(newToday.clone()) }, [timezone]) - const goToToday = () => { + const goToToday = useCallback(() => { const newToday = getCurrentDate(timezone.value) setCurrentDate(newToday.clone()) setSelectedDate(newToday.clone()) - } + }, [timezone]) - const isToday = (date: WidgetifyDate): boolean => { - return ( - date.jDate() === today.jDate() && - date.jMonth() === today.jMonth() && - date.jYear() === today.jYear() - ) - } + const isToday = useCallback( + (date: WidgetifyDate): boolean => { + return ( + date.jDate() === today.jDate() && + date.jMonth() === today.jMonth() && + date.jYear() === today.jYear() + ) + }, + [today] + ) - const getHijriDate = (date: WidgetifyDate): string => { + const getHijriDate = useCallback((date: WidgetifyDate): string => { const hijriDate = convertShamsiToHijri(date) return `${hijriDate.iYear()}/${hijriDate.iMonth() + 1}/${hijriDate.iDate()}` - } + }, []) const todayIsHoliday = activeDate.day() === 5 - return ( - - {children} - + const value = useMemo( + () => ({ + currentDate, + selectedDate, + todayIsHoliday, + today, + setCurrentDate, + setSelectedDate, + goToToday, + isToday, + getHijriDate, + }), + [ + currentDate, + selectedDate, + todayIsHoliday, + today, + goToToday, + isToday, + getHijriDate, + ] ) + + return {children} } export const useDate = (): DateContextType => { From fabb0c8a720591d87e79499b0bda5f496b94ef95 Mon Sep 17 00:00:00 2001 From: Shak Date: Wed, 26 Aug 2026 18:53:53 +0330 Subject: [PATCH 4/4] refactor(widgets): delegate calendar widgetItems entry to WIDGET_DEFINITIONS widget-visibility.context.tsx is being phased out in favor of widget-registry.tsx, and duplicating the DateProvider wrap here was redundant with WIDGET_DEFINITIONS[calendar].node, which already wraps CalendarLayout correctly. Point the old registry's calendar entry at the new one instead of maintaining two copies of the same wiring. --- src/context/widget-visibility.context.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/context/widget-visibility.context.tsx b/src/context/widget-visibility.context.tsx index 9bbe6d38..5ec00321 100644 --- a/src/context/widget-visibility.context.tsx +++ b/src/context/widget-visibility.context.tsx @@ -9,13 +9,14 @@ import { import Analytics from '@/analytics' import { getFromStorage, setToStorage } from '@/common/storage' import { showToast } from '@/common/toast' -import CalendarLayout from '@/layouts/widgets/calendar/calendar' import { ComboWidget } from '@/layouts/widgets/combo-widget/combo-widget.layout' import { HabitsLayout } from '@/layouts/widgets/habit/habits.layout' +import { WidgetKeys as RegistryWidgetKeys } from '@/layouts/widgets/layout-engine/types' import { NetworkLayout } from '@/layouts/widgets/network/network.layout' import { NewsLayout } from '@/layouts/widgets/news/news.layout' import { ToolsLayout } from '@/layouts/widgets/tools/tools.layout' import { WeatherLayout } from '@/layouts/widgets/weather/weather.layout' +import { WIDGET_DEFINITIONS } from '@/layouts/widgets/widget-registry' import { WigiArzLayout } from '@/layouts/widgets/wigi-arz/wigi_arz.layout' import { YadkarWidget } from '@/layouts/widgets/yadkar/yadkar' import { @@ -24,7 +25,6 @@ import { } from '@/services/hooks/widgets/widget-sync.hook' import { useAuth } from './auth.context' import { CurrencyProvider } from './currency.context' -import { DateProvider } from './date.context' export enum WidgetKeys { comboWidget = 'comboWidget', @@ -68,11 +68,10 @@ export const widgetItems: WidgetItem[] = [ emoji: '📅', label: 'تقویم', order: 0, - node: ( - - - - ), + node: WIDGET_DEFINITIONS[RegistryWidgetKeys.calendar].node('calendar', { + w: 2, + h: 3, + }), canToggle: true, popular: true, },