Skip to content

CalendarHeader vs CalendarBody scroll synchronization issue #237

@kairos2109

Description

@kairos2109

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

header vs body scroll issue

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

  1. Active ID Check: The code only processes date changes when activeId === id.toString(). This means:

    • Header (ScrollType.dayBar) only processes changes when activeId === 'dayBar'
    • Body (ScrollType.calendarGrid) only processes changes when activeId === 'calendarGrid'
  2. Default Fallback: When getActiveId() returns null, it defaults to ScrollType.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
  3. Race Condition on Android:

    • The active ID is set via onTouchStart handler in useLinkedScrollGroup.tsx (line 103-121)
    • On Android, onTouchStart events can be unreliable or delayed
    • If a user quickly scrolls the header without a proper onTouchStart event, the active ID may not be set to ScrollType.dayBar
    • This causes the header's scroll events to be ignored
  4. Touch Event Timing: The onTouchStart handler in useLinkedScrollGroup.tsx relies 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: onTouchStart handler may not fire reliably on Android before scroll events

File: src/CalendarHeader.tsx

  • Line: 75-78, 93-95
  • Context: Header uses ScrollType.dayBar and useSyncedList hook

File: src/CalendarBody.tsx

  • Line: 104-107, 112-114
  • Context: Body uses ScrollType.calendarGrid and useSyncedList hook

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:

  1. Set active ID based on scroll events, not just touch events
  2. Add a fallback mechanism that checks which scroll view is actually moving
  3. Remove the default fallback to ScrollType.calendarGrid to 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/Intermittent

Steps to reproduce

  1. Create a CalendarContainer with CalendarHeader and CalendarBody as children
  2. On Android device/emulator, attempt to scroll the calendar by touching and dragging the header
  3. Observe that onChange callback does not fire when scrolling the header
  4. Scroll the body instead - onChange fires correctly
  5. The issue is intermittent - sometimes header scrolling works, sometimes it doesn't

Reproducible example repository

https://github.com/howljs/react-native-calendar-kit/

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions