Skip to content

Commit 95418db

Browse files
committed
Merge branch 'master' of github.com:wix/react-native-calendars into release
2 parents b1f5441 + cea10c8 commit 95418db

File tree

12 files changed

+114
-90
lines changed

12 files changed

+114
-90
lines changed

example/src/screens/timelineCalendarScreen.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export default class TimelineCalendarScreen extends Component {
3434
[`${getDate(4)}`]: {marked: true}
3535
};
3636

37-
onDateChanged = (date: string) => {
37+
onDateChanged = (date: string, source: string) => {
38+
console.log('TimelineCalendarScreen onDateChanged: ', date, source);
3839
this.setState({currentDate: date});
3940
};
4041

src/calendar-list/index.tsx

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import findIndex from 'lodash/findIndex';
22
import PropTypes from 'prop-types';
33
import XDate from 'xdate';
44

5-
import React, {forwardRef, useImperativeHandle, useRef, useEffect, useState, useCallback, useMemo} from 'react';
6-
import {FlatList, View, ViewStyle, FlatListProps} from 'react-native';
5+
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
6+
import {FlatList, FlatListProps, View, ViewStyle} from 'react-native';
77

8-
import {extractHeaderProps, extractCalendarProps} from '../componentUpdater';
9-
import {xdateToData, parseDate, toMarkingFormat} from '../interface';
8+
import {extractCalendarProps, extractHeaderProps} from '../componentUpdater';
9+
import {parseDate, toMarkingFormat, xdateToData} from '../interface';
1010
import {page, sameDate, sameMonth} from '../dateutils';
1111
import constants from '../commons/constants';
1212
import {useDidUpdate} from '../hooks';
@@ -15,6 +15,7 @@ import styleConstructor from './style';
1515
import Calendar, {CalendarProps} from '../calendar';
1616
import CalendarListItem from './item';
1717
import CalendarHeader from '../calendar/header/index';
18+
import isEqual from 'lodash/isEqual';
1819

1920
const CALENDAR_WIDTH = constants.screenWidth;
2021
const CALENDAR_HEIGHT = 360;
@@ -108,13 +109,15 @@ const CalendarList = (props: CalendarListProps & ContextProp, ref: any) => {
108109

109110
const [currentMonth, setCurrentMonth] = useState(parseDate(current));
110111

112+
const shouldUseAndroidRTLFix = useMemo(() => constants.isAndroidRTL && horizontal, [horizontal]);
113+
111114
const style = useRef(styleConstructor(theme));
112115
const list = useRef();
113116
const range = useRef(horizontal ? 1 : 3);
114117
const initialDate = useRef(parseDate(current) || new XDate());
115118
const visibleMonth = useRef(currentMonth);
116119

117-
const items = useMemo(() => {
120+
const items: XDate[] = useMemo(() => {
118121
const months: any[] = [];
119122
for (let i = 0; i <= pastScrollRange + futureScrollRange; i++) {
120123
const rangeDate = initialDate.current?.clone().addMonths(i - pastScrollRange, true);
@@ -184,13 +187,13 @@ const CalendarList = (props: CalendarListProps & ContextProp, ref: any) => {
184187
const scrollToMonth = useCallback((date: XDate | string) => {
185188
const scrollTo = parseDate(date);
186189
const diffMonths = Math.round(initialDate?.current?.clone().setDate(1).diffMonths(scrollTo?.clone().setDate(1)));
187-
const scrollAmount = calendarSize * pastScrollRange + diffMonths * calendarSize;
190+
const scrollAmount = calendarSize * (shouldUseAndroidRTLFix ? pastScrollRange - diffMonths : pastScrollRange + diffMonths);
188191

189192
if (scrollAmount !== 0) {
190193
// @ts-expect-error
191194
list?.current?.scrollToOffset({offset: scrollAmount, animated: animateScroll});
192195
}
193-
}, [calendarSize]);
196+
}, [calendarSize, shouldUseAndroidRTLFix, pastScrollRange, animateScroll]);
194197

195198
const addMonth = useCallback((count: number) => {
196199
const day = currentMonth?.clone().addMonths(count, true);
@@ -274,24 +277,32 @@ const CalendarList = (props: CalendarListProps & ContextProp, ref: any) => {
274277

275278
const onViewableItemsChanged = useCallback(({viewableItems}: any) => {
276279
const newVisibleMonth = parseDate(viewableItems[0]?.item);
277-
if (!sameDate(visibleMonth?.current, newVisibleMonth)) {
278-
visibleMonth.current = newVisibleMonth;
280+
if (shouldUseAndroidRTLFix) {
281+
const centerIndex = items.findIndex((item) => isEqual(parseDate(current), item));
282+
const adjustedOffset = centerIndex - items.findIndex((item) => isEqual(newVisibleMonth, item));
283+
visibleMonth.current = items[centerIndex + adjustedOffset];
279284
setCurrentMonth(visibleMonth.current);
285+
} else {
286+
if (!sameDate(visibleMonth?.current, newVisibleMonth)) {
287+
visibleMonth.current = newVisibleMonth;
288+
setCurrentMonth(visibleMonth.current);
289+
}
280290
}
281-
}, []);
291+
}, [items, shouldUseAndroidRTLFix, current]);
282292

283293
const viewabilityConfigCallbackPairs = useRef([
284294
{
285295
viewabilityConfig: viewabilityConfig.current,
286296
onViewableItemsChanged
287297
},
288298
]);
289-
299+
290300
return (
291301
<View style={style.current.flatListContainer} testID={testID}>
292302
<FlatList
293303
// @ts-expect-error
294304
ref={list}
305+
windowSize={shouldUseAndroidRTLFix ? pastScrollRange + futureScrollRange + 1 : undefined}
295306
style={listStyle}
296307
showsVerticalScrollIndicator={showScrollIndicator}
297308
showsHorizontalScrollIndicator={showScrollIndicator}

src/commons/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ const isAndroid = Platform.OS === 'android';
77
const isIOS = Platform.OS === 'ios';
88
const screenAspectRatio = screenWidth < screenHeight ? screenHeight / screenWidth : screenWidth / screenHeight;
99
const isTablet = (Platform as PlatformIOSStatic).isPad || (screenAspectRatio < 1.6 && Math.max(screenWidth, screenHeight) >= 900);
10+
const isAndroidRTL = isAndroid && isRTL;
1011

1112
export default {
1213
screenWidth,
1314
screenHeight,
1415
isRTL,
1516
isAndroid,
1617
isIOS,
17-
isTablet
18+
isTablet,
19+
isAndroidRTL
1820
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import isEqual from 'lodash/isEqual';
2+
import React from 'react';
3+
import {DefaultSectionT, SectionListProps, Text, TextProps, ViewStyle} from 'react-native';
4+
import {Theme} from '../types';
5+
6+
export interface AgendaListProps extends SectionListProps<any, DefaultSectionT> {
7+
/** Specify theme properties to override specific styles for calendar parts */
8+
theme?: Theme;
9+
/** day format in section title. Formatting values: http://arshaw.com/xdate/#Formatting */
10+
dayFormat?: string;
11+
/** a function to custom format the section header's title */
12+
dayFormatter?: (arg0: string) => string;
13+
/** whether to use moment.js for date string formatting
14+
* (remember to pass 'dayFormat' with appropriate format, like 'dddd, MMM D') */
15+
useMoment?: boolean;
16+
/** whether to mark today's title with the "Today, ..." string. Default = true */
17+
markToday?: boolean;
18+
/** style passed to the section view */
19+
sectionStyle?: ViewStyle;
20+
/** whether to block the date change in calendar (and calendar context provider) when agenda scrolls */
21+
avoidDateUpdates?: boolean;
22+
/** offset scroll to section */
23+
viewOffset?: number;
24+
/** enable scrolling the agenda list to the next date with content when pressing a day without content */
25+
scrollToNextEvent?: boolean;
26+
/**
27+
* @experimental
28+
* If defined, uses InfiniteList instead of SectionList. This feature is experimental and subject to change.
29+
*/
30+
infiniteListProps?: {
31+
itemHeight?: number;
32+
titleHeight?: number;
33+
visibleIndicesChangedDebounce?: number;
34+
renderFooter?: () => React.ReactElement | null;
35+
};
36+
}
37+
38+
interface AgendaSectionHeaderProps {
39+
title?: string;
40+
onLayout?: TextProps['onLayout'];
41+
style: TextProps['style'];
42+
}
43+
44+
function areTextPropsEqual(prev: AgendaSectionHeaderProps, next: AgendaSectionHeaderProps): boolean {
45+
return isEqual(prev.style, next.style) && prev.title === next.title;
46+
}
47+
48+
export const AgendaSectionHeader = React.memo((props: AgendaSectionHeaderProps) => {
49+
return (
50+
<Text allowFontScaling={false} style={props.style} onLayout={props.onLayout}>
51+
{props.title}
52+
</Text>
53+
);
54+
}, areTextPropsEqual);

src/expandableCalendar/WeekCalendar/index.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {useDidUpdate} from '../../hooks';
1818

1919
export const NUMBER_OF_PAGES = 6;
2020
const NUM_OF_ITEMS = NUMBER_OF_PAGES * 2 + 1; // NUMBER_OF_PAGES before + NUMBER_OF_PAGES after + current
21-
const APPLY_ANDROID_FIX = constants.isAndroid && constants.isRTL;
2221

2322
export interface WeekCalendarProps extends CalendarListProps {
2423
/** whether to have shadow/elevation for the calendar */
@@ -70,14 +69,15 @@ const WeekCalendar = (props: WeekCalendarProps) => {
7069
}) :
7170
sameWeek(item, date, firstDay));
7271
if (pageIndex !== currentIndex.current) {
72+
const adjustedIndexFrScroll = constants.isAndroidRTL ? NUM_OF_ITEMS - 1 - pageIndex : pageIndex;
7373
if (pageIndex >= 0) {
74-
visibleWeek.current = items.current[pageIndex];
75-
currentIndex.current = pageIndex;
74+
visibleWeek.current = items.current[adjustedIndexFrScroll];
75+
currentIndex.current = adjustedIndexFrScroll;
7676
} else {
7777
visibleWeek.current = date;
7878
currentIndex.current = NUMBER_OF_PAGES;
7979
}
80-
pageIndex <= 0 ? onEndReached() : list?.current?.scrollToIndex({index: pageIndex, animated: false});
80+
pageIndex <= 0 ? onEndReached() : list?.current?.scrollToIndex({index: adjustedIndexFrScroll, animated: false});
8181
}
8282
}
8383
}, [date, updateSource]);
@@ -179,7 +179,7 @@ const WeekCalendar = (props: WeekCalendarProps) => {
179179
const currItems = items.current;
180180
const newDate = viewableItems[0]?.item;
181181
if (newDate !== visibleWeek.current) {
182-
if (APPLY_ANDROID_FIX) {
182+
if (constants.isAndroidRTL) {
183183
//in android RTL the item we see is the one in the opposite direction
184184
const newDateOffset = -1 * (NUMBER_OF_PAGES - currItems.indexOf(newDate));
185185
const adjustedNewDate = currItems[NUMBER_OF_PAGES - newDateOffset];

src/expandableCalendar/__test__/index.spec.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,8 @@ describe('ExpandableCalendar', () => {
243243
beforeEach(() => {
244244
driver.render();
245245
});
246-
it.each([[Direction.LEFT], [Direction.RIGHT]])(`should call onDateChanged to next week first day when pressing %s arrow`, (direction) => {
247-
const currentDay = today.getUTCDay();
246+
it.each([['last', Direction.LEFT], ['next', Direction.RIGHT]])(`should call onDateChanged to %s week first day when pressing %s arrow`, (direction) => {
247+
const currentDay = today.getDay();
248248
const expectedDate = today.clone().addDays(direction === Direction.LEFT ? - (currentDay + 7) : (7 - currentDay));
249249
driver.pressOnHeaderArrow({left: direction === Direction.LEFT});
250250
expect(onDateChanged).toHaveBeenCalledWith(toMarkingFormat(expectedDate), UpdateSources.PAGE_SCROLL);
@@ -259,7 +259,7 @@ describe('ExpandableCalendar', () => {
259259

260260
it('should fetch next weeks when in last week of the list', () => {
261261
times(NUMBER_OF_PAGES + 1, () => driver.pressOnHeaderArrow({left: false}));
262-
const currentDay = today.getUTCDay();
262+
const currentDay = today.getDay();
263263
const expectedDate = today.clone().addDays(7 * (NUMBER_OF_PAGES + 1) - currentDay);
264264
const day = driver.getWeekDay(toMarkingFormat(expectedDate));
265265
expect(day).toBeDefined();
@@ -269,7 +269,6 @@ describe('ExpandableCalendar', () => {
269269
const endOfMonth = new XDate(today.getFullYear(), today.getMonth() + 1, 0, 0, 0 ,0 , 0, true);
270270
const diff = Math.ceil(((endOfMonth.getUTCDate() + 1) - today.getUTCDate()) / 7) + ((today.getUTCDay() > endOfMonth.getUTCDay()) ? 1 : 0);
271271
const expectedDate = today.clone().setDate(today.getDate() + 7 * diff - today.getDay());
272-
console.log(diff, endOfMonth, expectedDate);
273272
times(diff, () => driver.pressOnHeaderArrow({left: false}));
274273
expect(onMonthChange).toHaveBeenCalledWith(xdateToData(expectedDate), UpdateSources.PAGE_SCROLL);
275274
});

src/expandableCalendar/agendaList.tsx

Lines changed: 1 addition & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,73 +5,36 @@ import map from 'lodash/map';
55
import isFunction from 'lodash/isFunction';
66
import isUndefined from 'lodash/isUndefined';
77
import debounce from 'lodash/debounce';
8-
import isEqual from 'lodash/isEqual';
98

109
import XDate from 'xdate';
1110

1211
import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react';
1312
import {
14-
Text,
1513
SectionList,
16-
SectionListProps,
1714
DefaultSectionT,
1815
SectionListData,
19-
ViewStyle,
2016
NativeSyntheticEvent,
2117
NativeScrollEvent,
2218
LayoutChangeEvent,
2319
ViewToken,
24-
TextProps
2520
} from 'react-native';
2621

2722
import {useDidUpdate} from '../hooks';
2823
import {getMoment} from '../momentResolver';
2924
import {isToday, isGTE, sameDate} from '../dateutils';
3025
import {parseDate} from '../interface';
3126
import {getDefaultLocale} from '../services';
32-
import {Theme} from '../types';
3327
import {UpdateSources, todayString} from './commons';
3428
import constants from '../commons/constants';
3529
import styleConstructor from './style';
3630
import Context from './Context';
3731
import InfiniteAgendaList from './infiniteAgendaList';
32+
import {AgendaListProps, AgendaSectionHeader} from './AgendaListsCommon';
3833

3934
const viewabilityConfig = {
4035
itemVisiblePercentThreshold: 20 // 50 means if 50% of the item is visible
4136
};
4237

43-
export interface AgendaListProps extends SectionListProps<any, DefaultSectionT> {
44-
/** Specify theme properties to override specific styles for calendar parts */
45-
theme?: Theme;
46-
/** day format in section title. Formatting values: http://arshaw.com/xdate/#Formatting */
47-
dayFormat?: string;
48-
/** a function to custom format the section header's title */
49-
dayFormatter?: (arg0: string) => string;
50-
/** whether to use moment.js for date string formatting
51-
* (remember to pass 'dayFormat' with appropriate format, like 'dddd, MMM D') */
52-
useMoment?: boolean;
53-
/** whether to mark today's title with the "Today, ..." string. Default = true */
54-
markToday?: boolean;
55-
/** style passed to the section view */
56-
sectionStyle?: ViewStyle;
57-
/** whether to block the date change in calendar (and calendar context provider) when agenda scrolls */
58-
avoidDateUpdates?: boolean;
59-
/** offset scroll to section */
60-
viewOffset?: number;
61-
/** enable scrolling the agenda list to the next date with content when pressing a day without content */
62-
scrollToNextEvent?: boolean;
63-
/**
64-
* @experimental
65-
* If defined, uses InfiniteList instead of SectionList. This feature is experimental and subject to change.
66-
*/
67-
infiniteListProps?: {
68-
itemHeight?: number;
69-
titleHeight?: number;
70-
visibleIndicesChangedDebounce?: number;
71-
renderFooter?: () => React.ReactElement | null;
72-
};
73-
}
74-
7538
/**
7639
* @description: AgendaList component
7740
* @note: Should be wrapped with 'CalendarProvider'
@@ -284,24 +247,6 @@ const AgendaList = (props: AgendaListProps) => {
284247
// }
285248
};
286249

287-
interface AgendaSectionHeaderProps {
288-
title?: string;
289-
onLayout?: TextProps['onLayout'];
290-
style: TextProps['style'];
291-
}
292-
293-
function areTextPropsEqual(prev: AgendaSectionHeaderProps, next: AgendaSectionHeaderProps): boolean {
294-
return isEqual(prev.style, next.style) && prev.title === next.title;
295-
}
296-
297-
export const AgendaSectionHeader = React.memo((props: AgendaSectionHeaderProps) => {
298-
return (
299-
<Text allowFontScaling={false} style={props.style} onLayout={props.onLayout}>
300-
{props.title}
301-
</Text>
302-
);
303-
}, areTextPropsEqual);
304-
305250
export default AgendaList;
306251

307252
AgendaList.displayName = 'AgendaList';

src/expandableCalendar/infiniteAgendaList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import Context from './Context';
2222
import constants from "../commons/constants";
2323
import {parseDate} from "../interface";
2424
import {LayoutProvider} from "recyclerlistview/dist/reactnative/core/dependencies/LayoutProvider";
25-
import {AgendaListProps, AgendaSectionHeader} from "./agendaList";
25+
import {AgendaSectionHeader, AgendaListProps} from "./AgendaListsCommon";
2626

2727
/**
2828
* @description: AgendaList component that use InfiniteList to improve performance

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export type {ExpandableCalendarProps} from './expandableCalendar';
1010
export {default as WeekCalendar} from './expandableCalendar/WeekCalendar/new';
1111
export type {WeekCalendarProps} from './expandableCalendar/WeekCalendar';
1212
export {default as AgendaList} from './expandableCalendar/agendaList';
13-
export type {AgendaListProps} from './expandableCalendar/agendaList';
13+
export type {AgendaListProps} from './expandableCalendar/AgendaListsCommon';
1414
export {default as CalendarContext} from './expandableCalendar/Context';
1515
export {default as CalendarProvider} from './expandableCalendar/Context/Provider';
1616
export type {CalendarContextProviderProps} from './expandableCalendar/Context/Provider';

0 commit comments

Comments
 (0)