forked from wix/react-native-calendars
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Optional InfiniteList Support to AgendaList (wix#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
- Loading branch information
Showing
3 changed files
with
297 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
import PropTypes from 'prop-types'; | ||
|
||
import isUndefined from 'lodash/isUndefined'; | ||
import debounce from 'lodash/debounce'; | ||
import InfiniteList from '../infinite-list'; | ||
|
||
import XDate from 'xdate'; | ||
|
||
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; | ||
import { | ||
DefaultSectionT, | ||
SectionListData, | ||
} from 'react-native'; | ||
|
||
import {useDidUpdate} from '../hooks'; | ||
import {getMoment} from '../momentResolver'; | ||
import {isGTE, isToday} from '../dateutils'; | ||
import {getDefaultLocale} from '../services'; | ||
import {UpdateSources, todayString} from './commons'; | ||
import styleConstructor from './style'; | ||
import Context from './Context'; | ||
import constants from "../commons/constants"; | ||
import {parseDate} from "../interface"; | ||
import {LayoutProvider} from "recyclerlistview/dist/reactnative/core/dependencies/LayoutProvider"; | ||
import {AgendaListProps, AgendaSectionHeader} from "./agendaList"; | ||
|
||
/** | ||
* @description: AgendaList component that use InfiniteList to improve performance | ||
* @note: Should be wrapped with 'CalendarProvider' | ||
* @extends: InfiniteList | ||
* @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/expandableCalendar.js | ||
*/ | ||
const InfiniteAgendaList = (props: AgendaListProps) => { | ||
const { | ||
theme, | ||
sections, | ||
scrollToNextEvent, | ||
avoidDateUpdates, | ||
onScroll, | ||
renderSectionHeader, | ||
sectionStyle, | ||
dayFormatter, | ||
dayFormat = 'dddd, MMM d', | ||
useMoment, | ||
markToday = true, | ||
infiniteListProps, | ||
renderItem, | ||
onEndReached, | ||
onEndReachedThreshold | ||
} = props; | ||
|
||
const {date, updateSource, setDate} = useContext(Context); | ||
|
||
const style = useRef(styleConstructor(theme)); | ||
const list = useRef<any>(); | ||
const _topSection = useRef(sections[0]?.title); | ||
const didScroll = useRef(false); | ||
const sectionScroll = useRef(false); | ||
const [data, setData] = useState([] as any[]); | ||
|
||
useEffect(() => { | ||
const items = sections.reduce((acc: any, cur: any) => { | ||
return [...acc, {title: cur.title, isTitle: true}, ...cur.data]; | ||
}, []); | ||
setData(items); | ||
|
||
if (date !== _topSection.current) { | ||
setTimeout(() => { | ||
scrollToSection(date); | ||
}, 500); | ||
} | ||
}, [sections]); | ||
|
||
useDidUpdate(() => { | ||
// NOTE: on first init data should set first section to the current date!!! | ||
if (updateSource !== UpdateSources.LIST_DRAG && updateSource !== UpdateSources.CALENDAR_INIT) { | ||
scrollToSection(date); | ||
} | ||
}, [date]); | ||
|
||
const getSectionIndex = (date: string) => { | ||
let dataIndex = 0; | ||
|
||
for (let i = 0; i < sections.length; i++) { | ||
if (sections[i].title === date) { | ||
return dataIndex; | ||
} | ||
dataIndex += sections[i].data.length + 1; | ||
} | ||
}; | ||
|
||
const getNextSectionIndex = (date: string) => { | ||
const cur = new XDate(date); | ||
let dataIndex = 0; | ||
|
||
for (let i = 0; i < sections.length; i++) { | ||
const titleDate = parseDate(sections[i].title); | ||
if (isGTE(titleDate,cur)) { | ||
return dataIndex; | ||
} | ||
dataIndex += sections[i].data.length + 1; | ||
} | ||
}; | ||
|
||
const getSectionTitle = useCallback((title: string) => { | ||
if (!title) return; | ||
|
||
let sectionTitle = title; | ||
|
||
if (dayFormatter) { | ||
sectionTitle = dayFormatter(title); | ||
} else if (dayFormat) { | ||
if (useMoment) { | ||
const moment = getMoment(); | ||
sectionTitle = moment(title).format(dayFormat); | ||
} else { | ||
sectionTitle = new XDate(title).toString(dayFormat); | ||
} | ||
} | ||
|
||
if (markToday) { | ||
const string = getDefaultLocale().today || todayString; | ||
const today = isToday(title); | ||
sectionTitle = today ? `${string}, ${sectionTitle}` : sectionTitle; | ||
} | ||
|
||
return sectionTitle; | ||
}, []); | ||
|
||
const scrollToSection = useCallback(debounce((d) => { | ||
const sectionIndex = scrollToNextEvent ? getNextSectionIndex(d) : getSectionIndex(d); | ||
if (isUndefined(sectionIndex)) { | ||
return; | ||
} | ||
|
||
if (list?.current && sectionIndex !== undefined) { | ||
sectionScroll.current = true; // to avoid setDate() in _onVisibleIndicesChanged | ||
_topSection.current = sections[findItemTitleIndex(sectionIndex)]?.title; | ||
|
||
list.current?.scrollToIndex(sectionIndex, true); | ||
} | ||
}, 1000, {leading: false, trailing: true}), [ sections]); | ||
|
||
const layoutProvider = useMemo( | ||
() => new LayoutProvider( | ||
(index) => data[index]?.isTitle ? 'title': 'page', | ||
(type, dim) => { | ||
dim.width = constants.screenWidth; | ||
dim.height = type === 'title' ? infiniteListProps?.titleHeight ?? 60 : infiniteListProps?.itemHeight ?? 80; | ||
} | ||
), | ||
[data] | ||
); | ||
|
||
const _onScroll = useCallback((rawEvent: any) => { | ||
if (!didScroll.current) { | ||
didScroll.current = true; | ||
scrollToSection.cancel(); | ||
} | ||
|
||
// Convert to a format similar to NativeSyntheticEvent<NativeScrollEvent> | ||
const event = { | ||
nativeEvent: { | ||
contentOffset: rawEvent.nativeEvent.contentOffset, | ||
layoutMeasurement: rawEvent.nativeEvent.layoutMeasurement, | ||
contentSize: rawEvent.nativeEvent.contentSize, | ||
}, | ||
}; | ||
onScroll?.(event as any); | ||
}, [onScroll]); | ||
|
||
const _onVisibleIndicesChanged = useCallback(debounce((all: number[]) => { | ||
if (all && all.length && !sectionScroll.current) { | ||
const topItemIndex = all[0]; | ||
const topSection = data[findItemTitleIndex(topItemIndex)]; | ||
if (topSection && topSection !== _topSection.current) { | ||
_topSection.current = topSection.title; | ||
if (didScroll.current && !avoidDateUpdates) { | ||
// to avoid setDate() on first load (while setting the initial context.date value) | ||
setDate?.(topSection.title, UpdateSources.LIST_DRAG); | ||
} | ||
} | ||
} | ||
}, 1000, {leading: false, trailing: true},), [avoidDateUpdates, setDate, data]); | ||
|
||
const findItemTitleIndex = useCallback((itemIndex: number) => { | ||
let titleIndex = itemIndex; | ||
while (titleIndex > 0 && !data[titleIndex]?.isTitle) { | ||
titleIndex--; | ||
} | ||
|
||
return titleIndex; | ||
}, [data]); | ||
|
||
const _onMomentumScrollEnd = useCallback(() => { | ||
sectionScroll.current = false; | ||
}, []); | ||
|
||
const headerTextStyle = useMemo(() => [style.current.sectionText, sectionStyle], [sectionStyle]); | ||
|
||
const _renderSectionHeader = useCallback((info: {section: SectionListData<any, DefaultSectionT>}) => { | ||
const title = info?.section?.title; | ||
|
||
if (renderSectionHeader) { | ||
return renderSectionHeader(title); | ||
} | ||
|
||
const headerTitle = getSectionTitle(title); | ||
return <AgendaSectionHeader title={headerTitle} style={headerTextStyle}/>; | ||
}, [headerTextStyle]); | ||
|
||
const _renderItem = useCallback((_type: any, item: any) => { | ||
if (item?.isTitle) { | ||
return _renderSectionHeader({section: item}); | ||
} | ||
|
||
if (renderItem) { | ||
return renderItem({item} as any); | ||
} | ||
|
||
return <></>; | ||
}, [renderItem]); | ||
|
||
const _onEndReached = useCallback(() => { | ||
if (onEndReached) { | ||
onEndReached({distanceFromEnd: 0}); // The RecyclerListView doesn't provide the distanceFromEnd, so we just pass 0 | ||
} | ||
}, [onEndReached]); | ||
|
||
return ( | ||
<InfiniteList | ||
ref={list} | ||
renderItem={_renderItem} | ||
data={data} | ||
style={style.current.container} | ||
layoutProvider={layoutProvider} | ||
onScroll={_onScroll} | ||
onVisibleIndicesChanged={_onVisibleIndicesChanged} | ||
scrollViewProps={{onMomentumScrollEnd: _onMomentumScrollEnd}} | ||
onEndReached={_onEndReached} | ||
onEndReachedThreshold={onEndReachedThreshold as number | undefined} | ||
disableScrollOnDataChange | ||
/> | ||
); | ||
}; | ||
|
||
|
||
export default InfiniteAgendaList; | ||
|
||
InfiniteAgendaList.displayName = 'InfiniteAgendaList'; | ||
InfiniteAgendaList.propTypes = { | ||
dayFormat: PropTypes.string, | ||
dayFormatter: PropTypes.func, | ||
useMoment: PropTypes.bool, | ||
markToday: PropTypes.bool, | ||
sectionStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), | ||
avoidDateUpdates: PropTypes.bool | ||
}; |
Oops, something went wrong.