react-native-calendars icon indicating copy to clipboard operation
react-native-calendars copied to clipboard

fix: Transparent ExpandableCalendar

Open freely322 opened this issue 8 months ago • 16 comments

The issue related to dynamic headerHeight was not set.

  • Introduced a ref to conditionally measure header height only when necessary.
  • Updated onHeaderLayout to prevent unnecessary state updates.
  • Adjusted onLayout prop for Animated.View to utilize the new measurement logic.

Related issues: #2625 #2693 #2681 #2670 #2657 #2571 #2722

freely322 avatar May 01 '25 11:05 freely322

Hi @freely322 , Is there a ETA for this? This is really causing a issues even while using a loading flag from redux.

Satyam-code143 avatar Jun 14 '25 05:06 Satyam-code143

@nitzanyiz @Inbal-Tish @ethanshar Take a look please

freely322 avatar Jul 08 '25 11:07 freely322

I upgraded to "react-native-calendars": "1.1313.0" today and applied this PR to fix the invisible ExpandableCalendar. While everything was working fine on iOS, I had a styling issue on Android because of a header height that was too small.

Right now I just did a "dirty" fix by adding 5px to the height when using android…

const shouldMeasureHeader = useRef(true);
    const onHeaderLayout = useCallback(
        ({
            nativeEvent: {
                layout: { height },
            },
        }) => {
            const _height = Platform.OS === "android" ? height + 5 : height;
            if (_height !== headerHeight) {
                setHeaderHeight(_height || DEFAULT_HEADER_HEIGHT);
            }
            shouldMeasureHeader.current = false;
        },
        [headerHeight]
    );
image

carlbleick avatar Aug 05 '25 14:08 carlbleick

I upgraded to "react-native-calendars": "1.1313.0" today and applied this PR to fix the invisible ExpandableCalendar. While everything was working fine on iOS, I had a styling issue on Android because of a header height that was too small.

Right now I just did a "dirty" fix by adding 5px to the height when using android…

const shouldMeasureHeader = useRef(true);
    const onHeaderLayout = useCallback(
        ({
            nativeEvent: {
                layout: { height },
            },
        }) => {
            const _height = Platform.OS === "android" ? height + 5 : height;
            if (_height !== headerHeight) {
                setHeaderHeight(_height || DEFAULT_HEADER_HEIGHT);
            }
            shouldMeasureHeader.current = false;
        },
        [headerHeight]
    );
image

thanks for testing, I will check soon

freely322 avatar Aug 08 '25 15:08 freely322

Here's a patch for this fix.

diff --git a/node_modules/react-native-calendars/src/expandableCalendar/index.js b/node_modules/react-native-calendars/src/expandableCalendar/index.js
index bea00e0..bf92441 100644
--- a/node_modules/react-native-calendars/src/expandableCalendar/index.js
+++ b/node_modules/react-native-calendars/src/expandableCalendar/index.js
@@ -61,9 +61,11 @@ const ExpandableCalendar = forwardRef((props, ref) => {
     horizontal = true, calendarStyle, theme, style: propsStyle, firstDay = 0, onDayPress, hideArrows, onPressArrowLeft, onPressArrowRight, renderArrow, testID, ...others } = props;
     const [screenReaderEnabled, setScreenReaderEnabled] = useState(false);
     const [headerHeight, setHeaderHeight] = useState(0);
+    const shouldMeasureHeader = useRef(true);
     const onHeaderLayout = useCallback(({ nativeEvent: { layout: { height } } }) => {
-        setHeaderHeight(height || DEFAULT_HEADER_HEIGHT);
-    }, []);
+      if (height !== headerHeight) { setHeaderHeight(height || DEFAULT_HEADER_HEIGHT); }
+      shouldMeasureHeader.current = false;
+    }, [headerHeight]);
     /** Date */
     const getYear = (date) => {
         const d = new XDate(date);
@@ -411,7 +413,7 @@ const ExpandableCalendar = forwardRef((props, ref) => {
         return (<CalendarList testID={`${testID}.calendarList`} horizontal={horizontal} firstDay={firstDay} calendarStyle={calendarStyle} onHeaderLayout={onHeaderLayout} {...others} current={date} theme={themeObject} ref={calendarList} onDayPress={_onDayPress} onVisibleMonthsChange={onVisibleMonthsChange} pagingEnabled scrollEnabled={isOpen} hideArrows={shouldHideArrows} onPressArrowLeft={_onPressArrowLeft} onPressArrowRight={_onPressArrowRight} hideExtraDays={!horizontal && isOpen} renderArrow={_renderArrow} staticHeader numberOfDays={numberOfDays} headerStyle={_headerStyle} timelineLeftInset={timelineLeftInset} context={_context}/>);
     };
     return (<View testID={testID} style={containerStyle}>
-      {screenReaderEnabled ? (<Calendar testID={`${testID}.calendarAccessible`} {...others} theme={themeObject} onHeaderLayout={onHeaderLayout} onDayPress={_onDayPress} hideExtraDays renderArrow={_renderArrow}/>) : (<Animated.View testID={`${testID}.expandableContainer`} ref={wrapper} style={wrapperStyle} {...panResponder.panHandlers}>
+      {screenReaderEnabled ? (<Calendar testID={`${testID}.calendarAccessible`} {...others} theme={themeObject} onHeaderLayout={onHeaderLayout} onDayPress={_onDayPress} hideExtraDays renderArrow={_renderArrow}/>) : (<Animated.View testID={`${testID}.expandableContainer`} ref={wrapper} style={wrapperStyle} onLayout={shouldMeasureHeader.current ? onHeaderLayout : undefined} {...panResponder.panHandlers}>
           {renderCalendarList()}
           {renderWeekCalendar()}
           {!hideKnob && renderKnob()}

ajp8164 avatar Aug 11 '25 16:08 ajp8164

Wow, this is a painful bug that wasted a bunch of time... When will this be merged in? I manually patched.

NorseGaud avatar Aug 21 '25 03:08 NorseGaud

Here's a patch for this fix.

diff --git a/node_modules/react-native-calendars/src/expandableCalendar/index.js b/node_modules/react-native-calendars/src/expandableCalendar/index.js
index bea00e0..bf92441 100644
--- a/node_modules/react-native-calendars/src/expandableCalendar/index.js
+++ b/node_modules/react-native-calendars/src/expandableCalendar/index.js
@@ -61,9 +61,11 @@ const ExpandableCalendar = forwardRef((props, ref) => {
     horizontal = true, calendarStyle, theme, style: propsStyle, firstDay = 0, onDayPress, hideArrows, onPressArrowLeft, onPressArrowRight, renderArrow, testID, ...others } = props;
     const [screenReaderEnabled, setScreenReaderEnabled] = useState(false);
     const [headerHeight, setHeaderHeight] = useState(0);
+    const shouldMeasureHeader = useRef(true);
     const onHeaderLayout = useCallback(({ nativeEvent: { layout: { height } } }) => {
-        setHeaderHeight(height || DEFAULT_HEADER_HEIGHT);
-    }, []);
+      if (height !== headerHeight) { setHeaderHeight(height || DEFAULT_HEADER_HEIGHT); }
+      shouldMeasureHeader.current = false;
+    }, [headerHeight]);
     /** Date */
     const getYear = (date) => {
         const d = new XDate(date);
@@ -411,7 +413,7 @@ const ExpandableCalendar = forwardRef((props, ref) => {
         return (<CalendarList testID={`${testID}.calendarList`} horizontal={horizontal} firstDay={firstDay} calendarStyle={calendarStyle} onHeaderLayout={onHeaderLayout} {...others} current={date} theme={themeObject} ref={calendarList} onDayPress={_onDayPress} onVisibleMonthsChange={onVisibleMonthsChange} pagingEnabled scrollEnabled={isOpen} hideArrows={shouldHideArrows} onPressArrowLeft={_onPressArrowLeft} onPressArrowRight={_onPressArrowRight} hideExtraDays={!horizontal && isOpen} renderArrow={_renderArrow} staticHeader numberOfDays={numberOfDays} headerStyle={_headerStyle} timelineLeftInset={timelineLeftInset} context={_context}/>);
     };
     return (<View testID={testID} style={containerStyle}>
-      {screenReaderEnabled ? (<Calendar testID={`${testID}.calendarAccessible`} {...others} theme={themeObject} onHeaderLayout={onHeaderLayout} onDayPress={_onDayPress} hideExtraDays renderArrow={_renderArrow}/>) : (<Animated.View testID={`${testID}.expandableContainer`} ref={wrapper} style={wrapperStyle} {...panResponder.panHandlers}>
+      {screenReaderEnabled ? (<Calendar testID={`${testID}.calendarAccessible`} {...others} theme={themeObject} onHeaderLayout={onHeaderLayout} onDayPress={_onDayPress} hideExtraDays renderArrow={_renderArrow}/>) : (<Animated.View testID={`${testID}.expandableContainer`} ref={wrapper} style={wrapperStyle} onLayout={shouldMeasureHeader.current ? onHeaderLayout : undefined} {...panResponder.panHandlers}>
           {renderCalendarList()}
           {renderWeekCalendar()}
           {!hideKnob && renderKnob()}

+1 the above fixed for me... only thing that I had to change was that I always had to add height + 5 for both android and iOS, else it would look like how @carlbleick mentioned in this comment. For him it was fine on iOS, but for me it was hiding the days slightly a bit for both OS.

So, pretty much what @ajp8164 had but with +5... so looks like this

@@ -0,0 +1,28 @@
diff --git a/src/expandableCalendar/index.js b/src/expandableCalendar/index.js
index bea00e0168a338eb19d3eee2ddd7ee8d0470b745..404ae259b00571b78deb000509d2f5d09816d29c 100644
--- a/src/expandableCalendar/index.js
+++ b/src/expandableCalendar/index.js
@@ -61,9 +61,12 @@ const ExpandableCalendar = forwardRef((props, ref) => {
     horizontal = true, calendarStyle, theme, style: propsStyle, firstDay = 0, onDayPress, hideArrows, onPressArrowLeft, onPressArrowRight, renderArrow, testID, ...others } = props;
     const [screenReaderEnabled, setScreenReaderEnabled] = useState(false);
     const [headerHeight, setHeaderHeight] = useState(0);
+    const shouldMeasureHeader = useRef(true);
     const onHeaderLayout = useCallback(({ nativeEvent: { layout: { height } } }) => {
-        setHeaderHeight(height || DEFAULT_HEADER_HEIGHT);
-    }, []);
+        const _height = (height || DEFAULT_HEADER_HEIGHT) + 5;
+      if (height !== headerHeight) { setHeaderHeight(_height); }
+      shouldMeasureHeader.current = false;
+    }, [headerHeight]);
     /** Date */
     const getYear = (date) => {
         const d = new XDate(date);
@@ -411,7 +414,7 @@ const ExpandableCalendar = forwardRef((props, ref) => {
         return (<CalendarList testID={`${testID}.calendarList`} horizontal={horizontal} firstDay={firstDay} calendarStyle={calendarStyle} onHeaderLayout={onHeaderLayout} {...others} current={date} theme={themeObject} ref={calendarList} onDayPress={_onDayPress} onVisibleMonthsChange={onVisibleMonthsChange} pagingEnabled scrollEnabled={isOpen} hideArrows={shouldHideArrows} onPressArrowLeft={_onPressArrowLeft} onPressArrowRight={_onPressArrowRight} hideExtraDays={!horizontal && isOpen} renderArrow={_renderArrow} staticHeader numberOfDays={numberOfDays} headerStyle={_headerStyle} timelineLeftInset={timelineLeftInset} context={_context}/>);
     };
     return (<View testID={testID} style={containerStyle}>
-      {screenReaderEnabled ? (<Calendar testID={`${testID}.calendarAccessible`} {...others} theme={themeObject} onHeaderLayout={onHeaderLayout} onDayPress={_onDayPress} hideExtraDays renderArrow={_renderArrow}/>) : (<Animated.View testID={`${testID}.expandableContainer`} ref={wrapper} style={wrapperStyle} {...panResponder.panHandlers}>
+      {screenReaderEnabled ? (<Calendar testID={`${testID}.calendarAccessible`} {...others} theme={themeObject} onHeaderLayout={onHeaderLayout} onDayPress={_onDayPress} hideExtraDays renderArrow={_renderArrow}/>) : (<Animated.View testID={`${testID}.expandableContainer`} ref={wrapper} style={wrapperStyle} onLayout={shouldMeasureHeader.current ? onHeaderLayout : undefined} {...panResponder.panHandlers}>
           {renderCalendarList()}
           {renderWeekCalendar()}
           {!hideKnob && renderKnob()}

NiharR027 avatar Aug 25 '25 02:08 NiharR027

Just applied fix of issue mentioned above.

freely322 avatar Aug 27 '25 15:08 freely322

Will also fix #2722

NiharR27 avatar Aug 28 '25 23:08 NiharR27

@Inbal-Tish @nitzanyiz - please have a look at this PR 🙏🏽

NiharR27 avatar Aug 29 '25 00:08 NiharR27

Screenshot 2025-08-31 at 11 30 45 AM

The fix is not ideal.  I render  no header  renderHeader={() => null}  and it will show two rows .
Screenshot 2025-08-31 at 11 33 54 AM
I am using this hack to force a re-render so it can properly measure the row height. 

  const [forceRenderKey, setForceRenderKey] = useState(0)
  // Force re-render to work around ExpandableCalendar headerHeight === 0 bug
  useEffect(() => {
    const timer = setTimeout(() => {
      setForceRenderKey(1)
    }, 10)
    return () => clearTimeout(timer)
  }, [])

         <ExpandableCalendar
            key={forceRenderKey}
            testID="expandableCalendar"

sdjg113 avatar Aug 31 '25 03:08 sdjg113

@ethanshar @Inbal-Tish @nitzanyiz can you take a look at this?

AndreMaz avatar Sep 01 '25 05:09 AndreMaz

also having this issue, @ethanshar @Inbal-Tish @nitzanyiz will anyone take a look at this?

francoangulo avatar Sep 09 '25 11:09 francoangulo

I'm migrating to expo v54 + react native v0.81 and this issue is back even after applying the patch https://github.com/wix/react-native-calendars/pull/2653#issuecomment-3175741492

Did anyone managed to solve this issue? If so, how?

AndreMaz avatar Oct 03 '25 11:10 AndreMaz

Not me, I had to roll back to v1.1289.0

francoangulo avatar Oct 03 '25 11:10 francoangulo

I’ve found that using nativewind 4.2.0 together with tailwind 3.4.17 works fine in my project — not sure if that’ll work in your case though.

elpeix avatar Nov 10 '25 15:11 elpeix