Skip to content

Commit 72f9b5d

Browse files
authored
Add Optional InfiniteList Support to AgendaList (#2270)
* Start working on migration of the agenda list * Use prop useInfiniteList to enable the new agnedaList * Remove console.log * Implement update date in scroll * Mark the new field as experimental * Add support on onEndReached, set debounce for date updates on viewed item change * remove nl * Add support on onEndReachedThreshold * Fix Inbar PR comments * Add disableScrollOnDataChange on InfiniteListProps * Add comment on _onEndReached applying callback * use disableScrollOnDataChange as a variable * remove props from all places
1 parent 93d6423 commit 72f9b5d

File tree

3 files changed

+297
-11
lines changed

3 files changed

+297
-11
lines changed

src/expandableCalendar/agendaList.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {UpdateSources, todayString} from './commons';
3434
import constants from '../commons/constants';
3535
import styleConstructor from './style';
3636
import Context from './Context';
37+
import InfiniteAgendaList from './infiniteAgendaList';
3738

3839
const viewabilityConfig = {
3940
itemVisiblePercentThreshold: 20 // 50 means if 50% of the item is visible
@@ -59,6 +60,14 @@ export interface AgendaListProps extends SectionListProps<any, DefaultSectionT>
5960
viewOffset?: number;
6061
/** enable scrolling the agenda list to the next date with content when pressing a day without content */
6162
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+
};
6271
}
6372

6473
/**
@@ -68,6 +77,10 @@ export interface AgendaListProps extends SectionListProps<any, DefaultSectionT>
6877
* @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/expandableCalendar.js
6978
*/
7079
const AgendaList = (props: AgendaListProps) => {
80+
if (props.infiniteListProps) {
81+
return <InfiniteAgendaList {...props} />;
82+
}
83+
7184
const {
7285
theme,
7386
sections,
@@ -89,7 +102,7 @@ const AgendaList = (props: AgendaListProps) => {
89102
} = props;
90103

91104
const {date, updateSource, setDate, setDisabled} = useContext(Context);
92-
105+
93106
const style = useRef(styleConstructor(theme));
94107
const list = useRef<any>();
95108
const _topSection = useRef(sections[0]?.title);
@@ -271,15 +284,15 @@ const AgendaList = (props: AgendaListProps) => {
271284

272285
interface AgendaSectionHeaderProps {
273286
title?: string;
274-
onLayout: TextProps['onLayout'];
287+
onLayout?: TextProps['onLayout'];
275288
style: TextProps['style'];
276289
}
277290

278291
function areTextPropsEqual(prev: AgendaSectionHeaderProps, next: AgendaSectionHeaderProps): boolean {
279292
return isEqual(prev.style, next.style) && prev.title === next.title;
280293
}
281294

282-
const AgendaSectionHeader = React.memo((props: AgendaSectionHeaderProps) => {
295+
export const AgendaSectionHeader = React.memo((props: AgendaSectionHeaderProps) => {
283296
return (
284297
<Text allowFontScaling={false} style={props.style} onLayout={props.onLayout}>
285298
{props.title}
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import PropTypes from 'prop-types';
2+
3+
import isUndefined from 'lodash/isUndefined';
4+
import debounce from 'lodash/debounce';
5+
import InfiniteList from '../infinite-list';
6+
7+
import XDate from 'xdate';
8+
9+
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
10+
import {
11+
DefaultSectionT,
12+
SectionListData,
13+
} from 'react-native';
14+
15+
import {useDidUpdate} from '../hooks';
16+
import {getMoment} from '../momentResolver';
17+
import {isGTE, isToday} from '../dateutils';
18+
import {getDefaultLocale} from '../services';
19+
import {UpdateSources, todayString} from './commons';
20+
import styleConstructor from './style';
21+
import Context from './Context';
22+
import constants from "../commons/constants";
23+
import {parseDate} from "../interface";
24+
import {LayoutProvider} from "recyclerlistview/dist/reactnative/core/dependencies/LayoutProvider";
25+
import {AgendaListProps, AgendaSectionHeader} from "./agendaList";
26+
27+
/**
28+
* @description: AgendaList component that use InfiniteList to improve performance
29+
* @note: Should be wrapped with 'CalendarProvider'
30+
* @extends: InfiniteList
31+
* @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/expandableCalendar.js
32+
*/
33+
const InfiniteAgendaList = (props: AgendaListProps) => {
34+
const {
35+
theme,
36+
sections,
37+
scrollToNextEvent,
38+
avoidDateUpdates,
39+
onScroll,
40+
renderSectionHeader,
41+
sectionStyle,
42+
dayFormatter,
43+
dayFormat = 'dddd, MMM d',
44+
useMoment,
45+
markToday = true,
46+
infiniteListProps,
47+
renderItem,
48+
onEndReached,
49+
onEndReachedThreshold
50+
} = props;
51+
52+
const {date, updateSource, setDate} = useContext(Context);
53+
54+
const style = useRef(styleConstructor(theme));
55+
const list = useRef<any>();
56+
const _topSection = useRef(sections[0]?.title);
57+
const didScroll = useRef(false);
58+
const sectionScroll = useRef(false);
59+
const [data, setData] = useState([] as any[]);
60+
61+
useEffect(() => {
62+
const items = sections.reduce((acc: any, cur: any) => {
63+
return [...acc, {title: cur.title, isTitle: true}, ...cur.data];
64+
}, []);
65+
setData(items);
66+
67+
if (date !== _topSection.current) {
68+
setTimeout(() => {
69+
scrollToSection(date);
70+
}, 500);
71+
}
72+
}, [sections]);
73+
74+
useDidUpdate(() => {
75+
// NOTE: on first init data should set first section to the current date!!!
76+
if (updateSource !== UpdateSources.LIST_DRAG && updateSource !== UpdateSources.CALENDAR_INIT) {
77+
scrollToSection(date);
78+
}
79+
}, [date]);
80+
81+
const getSectionIndex = (date: string) => {
82+
let dataIndex = 0;
83+
84+
for (let i = 0; i < sections.length; i++) {
85+
if (sections[i].title === date) {
86+
return dataIndex;
87+
}
88+
dataIndex += sections[i].data.length + 1;
89+
}
90+
};
91+
92+
const getNextSectionIndex = (date: string) => {
93+
const cur = new XDate(date);
94+
let dataIndex = 0;
95+
96+
for (let i = 0; i < sections.length; i++) {
97+
const titleDate = parseDate(sections[i].title);
98+
if (isGTE(titleDate,cur)) {
99+
return dataIndex;
100+
}
101+
dataIndex += sections[i].data.length + 1;
102+
}
103+
};
104+
105+
const getSectionTitle = useCallback((title: string) => {
106+
if (!title) return;
107+
108+
let sectionTitle = title;
109+
110+
if (dayFormatter) {
111+
sectionTitle = dayFormatter(title);
112+
} else if (dayFormat) {
113+
if (useMoment) {
114+
const moment = getMoment();
115+
sectionTitle = moment(title).format(dayFormat);
116+
} else {
117+
sectionTitle = new XDate(title).toString(dayFormat);
118+
}
119+
}
120+
121+
if (markToday) {
122+
const string = getDefaultLocale().today || todayString;
123+
const today = isToday(title);
124+
sectionTitle = today ? `${string}, ${sectionTitle}` : sectionTitle;
125+
}
126+
127+
return sectionTitle;
128+
}, []);
129+
130+
const scrollToSection = useCallback(debounce((d) => {
131+
const sectionIndex = scrollToNextEvent ? getNextSectionIndex(d) : getSectionIndex(d);
132+
if (isUndefined(sectionIndex)) {
133+
return;
134+
}
135+
136+
if (list?.current && sectionIndex !== undefined) {
137+
sectionScroll.current = true; // to avoid setDate() in _onVisibleIndicesChanged
138+
_topSection.current = sections[findItemTitleIndex(sectionIndex)]?.title;
139+
140+
list.current?.scrollToIndex(sectionIndex, true);
141+
}
142+
}, 1000, {leading: false, trailing: true}), [ sections]);
143+
144+
const layoutProvider = useMemo(
145+
() => new LayoutProvider(
146+
(index) => data[index]?.isTitle ? 'title': 'page',
147+
(type, dim) => {
148+
dim.width = constants.screenWidth;
149+
dim.height = type === 'title' ? infiniteListProps?.titleHeight ?? 60 : infiniteListProps?.itemHeight ?? 80;
150+
}
151+
),
152+
[data]
153+
);
154+
155+
const _onScroll = useCallback((rawEvent: any) => {
156+
if (!didScroll.current) {
157+
didScroll.current = true;
158+
scrollToSection.cancel();
159+
}
160+
161+
// Convert to a format similar to NativeSyntheticEvent<NativeScrollEvent>
162+
const event = {
163+
nativeEvent: {
164+
contentOffset: rawEvent.nativeEvent.contentOffset,
165+
layoutMeasurement: rawEvent.nativeEvent.layoutMeasurement,
166+
contentSize: rawEvent.nativeEvent.contentSize,
167+
},
168+
};
169+
onScroll?.(event as any);
170+
}, [onScroll]);
171+
172+
const _onVisibleIndicesChanged = useCallback(debounce((all: number[]) => {
173+
if (all && all.length && !sectionScroll.current) {
174+
const topItemIndex = all[0];
175+
const topSection = data[findItemTitleIndex(topItemIndex)];
176+
if (topSection && topSection !== _topSection.current) {
177+
_topSection.current = topSection.title;
178+
if (didScroll.current && !avoidDateUpdates) {
179+
// to avoid setDate() on first load (while setting the initial context.date value)
180+
setDate?.(topSection.title, UpdateSources.LIST_DRAG);
181+
}
182+
}
183+
}
184+
}, 1000, {leading: false, trailing: true},), [avoidDateUpdates, setDate, data]);
185+
186+
const findItemTitleIndex = useCallback((itemIndex: number) => {
187+
let titleIndex = itemIndex;
188+
while (titleIndex > 0 && !data[titleIndex]?.isTitle) {
189+
titleIndex--;
190+
}
191+
192+
return titleIndex;
193+
}, [data]);
194+
195+
const _onMomentumScrollEnd = useCallback(() => {
196+
sectionScroll.current = false;
197+
}, []);
198+
199+
const headerTextStyle = useMemo(() => [style.current.sectionText, sectionStyle], [sectionStyle]);
200+
201+
const _renderSectionHeader = useCallback((info: {section: SectionListData<any, DefaultSectionT>}) => {
202+
const title = info?.section?.title;
203+
204+
if (renderSectionHeader) {
205+
return renderSectionHeader(title);
206+
}
207+
208+
const headerTitle = getSectionTitle(title);
209+
return <AgendaSectionHeader title={headerTitle} style={headerTextStyle}/>;
210+
}, [headerTextStyle]);
211+
212+
const _renderItem = useCallback((_type: any, item: any) => {
213+
if (item?.isTitle) {
214+
return _renderSectionHeader({section: item});
215+
}
216+
217+
if (renderItem) {
218+
return renderItem({item} as any);
219+
}
220+
221+
return <></>;
222+
}, [renderItem]);
223+
224+
const _onEndReached = useCallback(() => {
225+
if (onEndReached) {
226+
onEndReached({distanceFromEnd: 0}); // The RecyclerListView doesn't provide the distanceFromEnd, so we just pass 0
227+
}
228+
}, [onEndReached]);
229+
230+
return (
231+
<InfiniteList
232+
ref={list}
233+
renderItem={_renderItem}
234+
data={data}
235+
style={style.current.container}
236+
layoutProvider={layoutProvider}
237+
onScroll={_onScroll}
238+
onVisibleIndicesChanged={_onVisibleIndicesChanged}
239+
scrollViewProps={{onMomentumScrollEnd: _onMomentumScrollEnd}}
240+
onEndReached={_onEndReached}
241+
onEndReachedThreshold={onEndReachedThreshold as number | undefined}
242+
disableScrollOnDataChange
243+
/>
244+
);
245+
};
246+
247+
248+
export default InfiniteAgendaList;
249+
250+
InfiniteAgendaList.displayName = 'InfiniteAgendaList';
251+
InfiniteAgendaList.propTypes = {
252+
dayFormat: PropTypes.string,
253+
dayFormatter: PropTypes.func,
254+
useMoment: PropTypes.bool,
255+
markToday: PropTypes.bool,
256+
sectionStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
257+
avoidDateUpdates: PropTypes.bool
258+
};

0 commit comments

Comments
 (0)