-
-
Notifications
You must be signed in to change notification settings - Fork 100
Description
Before submitting a new issue
- I tested using the latest version of the library, as the bug might be already fixed.
- I tested using a supported version of react native.
- I checked for possible duplicate issues, with possible answers.
Bug summary
The code:
return (
<CalendarContainer>
<CalendarHeader />
<CalendarBody />
</CalendarContainer>
)
The CalendarHeader and CalendarBody components can scroll independently on Android, causing the onChange callback to only fire when the body is scrolled, not when the header is scrolled. This issue appears to be random and is caused by a race condition in the scroll synchronization logic.
Description
When using CalendarContainer with CalendarHeader and CalendarBody as children, the header and body can become desynchronized on Android. When the user scrolls the header, the onChange callback does not fire, but when scrolling the body, it works correctly. This suggests that the scroll synchronization mechanism has a race condition that prevents the header's scroll events from being properly recognized.
Root Cause Analysis
The issue seems to be located in src/hooks/useSyncedList.tsx at lines 50-52:
const activeId = linkedScrollGroup.getActiveId() || ScrollType.calendarGrid;
if (activeId === id.toString() && visibleColumns && visibleDates) {
// Date change processing only happens if activeId matches
}The Problem
-
Active ID Check: The code only processes date changes when
activeId === id.toString(). This means:- Header (
ScrollType.dayBar) only processes changes whenactiveId === 'dayBar' - Body (
ScrollType.calendarGrid) only processes changes whenactiveId === 'calendarGrid'
- Header (
-
Default Fallback: When
getActiveId()returnsnull, it defaults toScrollType.calendarGrid, which means:- If the active ID is not set (null), only body scrolls are processed
- Header scrolls are ignored when active ID is null or still set to
calendarGrid
-
Race Condition on Android:
- The active ID is set via
onTouchStarthandler inuseLinkedScrollGroup.tsx(line 103-121) - On Android,
onTouchStartevents can be unreliable or delayed - If a user quickly scrolls the header without a proper
onTouchStartevent, the active ID may not be set toScrollType.dayBar - This causes the header's scroll events to be ignored
- The active ID is set via
-
Touch Event Timing: The
onTouchStarthandler inuseLinkedScrollGroup.tsxrelies on touch events to set the active scroll ID:const onTouchStartHandler = useCallback( (triggerId: string) => { // Sets activeId.current = triggerId // Sets activeTag.value = elementTag }, [activeTag, peers] );
On Android, these touch events may not fire reliably before scroll events, causing the active ID to remain unset or incorrect.
Code References
Problematic Code
File: src/hooks/useSyncedList.tsx
- Lines: 50-52
- Issue: Restrictive active ID check prevents header scroll events from being processed
File: src/hooks/useLinkedScrollGroup/useLinkedScrollGroup.tsx
- Lines: 103-121
- Issue:
onTouchStarthandler may not fire reliably on Android before scroll events
File: src/CalendarHeader.tsx
- Line: 75-78, 93-95
- Context: Header uses
ScrollType.dayBaranduseSyncedListhook
File: src/CalendarBody.tsx
- Line: 104-107, 112-114
- Context: Body uses
ScrollType.calendarGridanduseSyncedListhook
Suggested Fix
The fix should ensure that date changes are processed from both scroll views, not just the "active" one. Here are two possible approaches:
Option 1: Remove Active ID Restriction (Recommended)
Modify useSyncedList.tsx to process date changes from both scroll views:
// Current (problematic):
const activeId = linkedScrollGroup.getActiveId() || ScrollType.calendarGrid;
if (activeId === id.toString() && visibleColumns && visibleDates) {
// Process date changes
}
// Suggested fix:
if (visibleColumns && visibleDates) {
// Process date changes regardless of active ID
// The linkedScrollGroup already handles scroll synchronization
// We just need to detect date changes from either scroll view
}Option 2: Improve Active ID Detection
Ensure the active ID is set more reliably:
- Set active ID based on scroll events, not just touch events
- Add a fallback mechanism that checks which scroll view is actually moving
- Remove the default fallback to
ScrollType.calendarGridto avoid bias
Option 3: Process Both Scroll Views
Allow both header and body to process date changes, but use the active ID only for scroll synchronization (not for date change detection):
// Process date changes from both views
// Use active ID only for determining which view initiated the scroll
const isActiveScroll = activeId === id.toString();
if (visibleColumns && visibleDates) {
// Always process date changes, but prioritize active scroll
if (isActiveScroll || !linkedScrollGroup.getActiveId()) {
// Process date changes
}
}Priority
High - This affects core functionality (date change detection) and user experience on Android devices.
Library version
2.5.6
Environment info
- **Library**: `@howljs/calendar-kit` (version 2.5.6)
- **Platform**: Android (iOS behavior may differ)
- **React Native**: 0.76.9 (although reports include latests versions of the lib)
- **Reproducibility**: Random/IntermittentSteps to reproduce
- Create a
CalendarContainerwithCalendarHeaderandCalendarBodyas children - On Android device/emulator, attempt to scroll the calendar by touching and dragging the header
- Observe that
onChangecallback does not fire when scrolling the header - Scroll the body instead -
onChangefires correctly - The issue is intermittent - sometimes header scrolling works, sometimes it doesn't
Reproducible example repository
https://github.com/howljs/react-native-calendar-kit/