From 8ba2000e210551890e44f576250d50678c6bc9c2 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 4 Oct 2023 10:11:43 +0200 Subject: [PATCH 01/10] create MVCPScrollView --- .../MVCPScrollView/MVCPScrollView.js | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js diff --git a/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js b/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js new file mode 100644 index 000000000000..f0139a4ec39c --- /dev/null +++ b/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js @@ -0,0 +1,128 @@ +import React, {forwardRef, useEffect, useRef} from 'react'; +import {ScrollView, StyleSheet} from 'react-native'; +import PropTypes from 'prop-types'; + + +const MVCPScrollView = forwardRef(({maintainVisibleContentPosition, horizontal, ...props}, ref) => { + const scrollViewRef = useRef(null); + const prevFirstVisibleOffset = useRef(null); + const firstVisibleView = useRef(null); + const mutationObserver = useRef(null); + + const getContentView = () => scrollViewRef.current?.childNodes[0]; + + const prepareForMaintainVisibleContentPosition = () => { + if (maintainVisibleContentPosition == null || scrollViewRef.current == null) { + return; + } + + const contentView = getContentView(); + const minIdx = maintainVisibleContentPosition.minIndexForVisible; + for (let ii = minIdx; ii < contentView.childNodes.length; ii++) { + const subview = contentView.childNodes[ii]; + const hasNewView = horizontal ? subview.offsetLeft > scrollViewRef.current.scrollLeft : subview.offsetTop > scrollViewRef.current.scrollTop; + if (hasNewView || ii === contentView.childNodes.length - 1) { + prevFirstVisibleOffset.current = horizontal ? subview.offsetLeft : subview.offsetTop; + firstVisibleView.current = subview; + break; + } + } + }; + const scrollEventListener = useRef(() => { + prepareForMaintainVisibleContentPosition(); + }); + + const adjustForMaintainVisibleContentPosition = () => { + if (maintainVisibleContentPosition == null || scrollViewRef.current == null || firstVisibleView.current == null || prevFirstVisibleOffset.current == null) { + return; + } + + const autoscrollThreshold = maintainVisibleContentPosition.autoscrollToTopThreshold; + if (horizontal) { + const deltaX = firstVisibleView.current.offsetLeft - prevFirstVisibleOffset.current; + if (Math.abs(deltaX) > 0.5) { + const x = scrollViewRef.current.scrollLeft; + prevFirstVisibleOffset.current = firstVisibleView.current.offsetLeft; + scrollViewRef.current.scrollTo({x: x + deltaX, animated: false}); + if (autoscrollThreshold != null && x <= autoscrollThreshold) { + scrollViewRef.current.scrollTo({x: 0, animated: true}); + } + } + } else { + const deltaY = firstVisibleView.current.offsetTop - prevFirstVisibleOffset.current; + if (Math.abs(deltaY) > 0.5) { + const y = scrollViewRef.current.scrollTop; + prevFirstVisibleOffset.current = firstVisibleView.current.offsetTop; + scrollViewRef.current.scrollTo({y: y + deltaY, animated: false}); + if (autoscrollThreshold != null && y <= autoscrollThreshold) { + scrollViewRef.current.scrollTo({y: 0, animated: true}); + } + } + } + }; + + if (mutationObserver.current == null) { + mutationObserver.current = new MutationObserver(() => { + // This needs to execute after scroll events are dispatched, but + // in the same tick to avoid flickering. rAF provides the right timing. + requestAnimationFrame(adjustForMaintainVisibleContentPosition); + }); + } + + const onRef = (newRef) => { + scrollViewRef.current = newRef; + if (typeof ref === 'function') { + ref(newRef); + } else { + // eslint-disable-next-line no-param-reassign + ref.current = newRef; + } + prepareForMaintainVisibleContentPosition(); + mutationObserver.current.disconnect(); + mutationObserver.current.observe(getContentView(), { + attributes: true, + childList: true, + subtree: true, + }); + newRef.removeEventListener('scroll', scrollEventListener.current); + newRef.addEventListener('scroll', scrollEventListener.current); + }; + + useEffect(() => { + const currentObserver = mutationObserver.current; + const currentScrollEventListener = scrollEventListener.current; + return () => { + currentObserver.disconnect(); + scrollViewRef.current.removeEventListener('scroll', currentScrollEventListener); + }; + }, []); + + return ( + + ); +}); + +const styles = StyleSheet.create({ + inverted: { + transform: [{ scaleY: -1 }], + }, +}); + + +MVCPScrollView.propTypes = { + maintainVisibleContentPosition: PropTypes.shape({ + minIndexForVisible: PropTypes.number.isRequired, + autoscrollToTopThreshold: PropTypes.number, + }), + horizontal: PropTypes.bool, +}; + +export default MVCPScrollView; From ef49188d8f4c82342f51b3b830b289b5c4e682aa Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 4 Oct 2023 10:12:26 +0200 Subject: [PATCH 02/10] use renderScrollComponent --- src/components/InvertedFlatList/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js index d46cd5801605..caa49eaccf58 100644 --- a/src/components/InvertedFlatList/index.js +++ b/src/components/InvertedFlatList/index.js @@ -2,6 +2,7 @@ import React, {forwardRef, useEffect, useRef} from 'react'; import PropTypes from 'prop-types'; import {DeviceEventEmitter, FlatList, StyleSheet} from 'react-native'; import _ from 'underscore'; +import MVCPScrollView from './MVCPScrollView/MVCPScrollView'; import BaseInvertedFlatList from './BaseInvertedFlatList'; import styles from '../../styles/styles'; import CONST from '../../CONST'; @@ -122,6 +123,10 @@ function InvertedFlatList(props) { // We need to keep batch size to one to workaround a bug in react-native-web. // This can be removed once https://github.com/Expensify/App/pull/24482 is merged. maxToRenderPerBatch={1} + + // We need to use our own scroll component to workaround a maintainVisibleContentPosition for web + // eslint-disable-next-line react/jsx-props-no-spreading + renderScrollComponent={(_props) => } /> ); } From 3cc63f83d8953d7719429b15814f23847721e777 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 4 Oct 2023 10:11:43 +0200 Subject: [PATCH 03/10] create MVCPScrollView --- .../MVCPScrollView/MVCPScrollView.js | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js diff --git a/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js b/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js new file mode 100644 index 000000000000..f0139a4ec39c --- /dev/null +++ b/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js @@ -0,0 +1,128 @@ +import React, {forwardRef, useEffect, useRef} from 'react'; +import {ScrollView, StyleSheet} from 'react-native'; +import PropTypes from 'prop-types'; + + +const MVCPScrollView = forwardRef(({maintainVisibleContentPosition, horizontal, ...props}, ref) => { + const scrollViewRef = useRef(null); + const prevFirstVisibleOffset = useRef(null); + const firstVisibleView = useRef(null); + const mutationObserver = useRef(null); + + const getContentView = () => scrollViewRef.current?.childNodes[0]; + + const prepareForMaintainVisibleContentPosition = () => { + if (maintainVisibleContentPosition == null || scrollViewRef.current == null) { + return; + } + + const contentView = getContentView(); + const minIdx = maintainVisibleContentPosition.minIndexForVisible; + for (let ii = minIdx; ii < contentView.childNodes.length; ii++) { + const subview = contentView.childNodes[ii]; + const hasNewView = horizontal ? subview.offsetLeft > scrollViewRef.current.scrollLeft : subview.offsetTop > scrollViewRef.current.scrollTop; + if (hasNewView || ii === contentView.childNodes.length - 1) { + prevFirstVisibleOffset.current = horizontal ? subview.offsetLeft : subview.offsetTop; + firstVisibleView.current = subview; + break; + } + } + }; + const scrollEventListener = useRef(() => { + prepareForMaintainVisibleContentPosition(); + }); + + const adjustForMaintainVisibleContentPosition = () => { + if (maintainVisibleContentPosition == null || scrollViewRef.current == null || firstVisibleView.current == null || prevFirstVisibleOffset.current == null) { + return; + } + + const autoscrollThreshold = maintainVisibleContentPosition.autoscrollToTopThreshold; + if (horizontal) { + const deltaX = firstVisibleView.current.offsetLeft - prevFirstVisibleOffset.current; + if (Math.abs(deltaX) > 0.5) { + const x = scrollViewRef.current.scrollLeft; + prevFirstVisibleOffset.current = firstVisibleView.current.offsetLeft; + scrollViewRef.current.scrollTo({x: x + deltaX, animated: false}); + if (autoscrollThreshold != null && x <= autoscrollThreshold) { + scrollViewRef.current.scrollTo({x: 0, animated: true}); + } + } + } else { + const deltaY = firstVisibleView.current.offsetTop - prevFirstVisibleOffset.current; + if (Math.abs(deltaY) > 0.5) { + const y = scrollViewRef.current.scrollTop; + prevFirstVisibleOffset.current = firstVisibleView.current.offsetTop; + scrollViewRef.current.scrollTo({y: y + deltaY, animated: false}); + if (autoscrollThreshold != null && y <= autoscrollThreshold) { + scrollViewRef.current.scrollTo({y: 0, animated: true}); + } + } + } + }; + + if (mutationObserver.current == null) { + mutationObserver.current = new MutationObserver(() => { + // This needs to execute after scroll events are dispatched, but + // in the same tick to avoid flickering. rAF provides the right timing. + requestAnimationFrame(adjustForMaintainVisibleContentPosition); + }); + } + + const onRef = (newRef) => { + scrollViewRef.current = newRef; + if (typeof ref === 'function') { + ref(newRef); + } else { + // eslint-disable-next-line no-param-reassign + ref.current = newRef; + } + prepareForMaintainVisibleContentPosition(); + mutationObserver.current.disconnect(); + mutationObserver.current.observe(getContentView(), { + attributes: true, + childList: true, + subtree: true, + }); + newRef.removeEventListener('scroll', scrollEventListener.current); + newRef.addEventListener('scroll', scrollEventListener.current); + }; + + useEffect(() => { + const currentObserver = mutationObserver.current; + const currentScrollEventListener = scrollEventListener.current; + return () => { + currentObserver.disconnect(); + scrollViewRef.current.removeEventListener('scroll', currentScrollEventListener); + }; + }, []); + + return ( + + ); +}); + +const styles = StyleSheet.create({ + inverted: { + transform: [{ scaleY: -1 }], + }, +}); + + +MVCPScrollView.propTypes = { + maintainVisibleContentPosition: PropTypes.shape({ + minIndexForVisible: PropTypes.number.isRequired, + autoscrollToTopThreshold: PropTypes.number, + }), + horizontal: PropTypes.bool, +}; + +export default MVCPScrollView; From d82cf206706c0b6c566d54a1bb3fdfaf29693898 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 4 Oct 2023 10:12:26 +0200 Subject: [PATCH 04/10] use renderScrollComponent --- src/components/InvertedFlatList/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js index 564db6296c9b..e7f6c14f52d8 100644 --- a/src/components/InvertedFlatList/index.js +++ b/src/components/InvertedFlatList/index.js @@ -2,6 +2,7 @@ import React, {forwardRef, useEffect, useRef} from 'react'; import PropTypes from 'prop-types'; import {DeviceEventEmitter, FlatList, StyleSheet} from 'react-native'; import _ from 'underscore'; +import MVCPScrollView from './MVCPScrollView/MVCPScrollView'; import BaseInvertedFlatList from './BaseInvertedFlatList'; import styles from '../../styles/styles'; import CONST from '../../CONST'; @@ -121,6 +122,10 @@ function InvertedFlatList(props) { // We need to keep batch size to one to workaround a bug in react-native-web. // This can be removed once https://github.com/Expensify/App/pull/24482 is merged. maxToRenderPerBatch={1} + + // We need to use our own scroll component to workaround a maintainVisibleContentPosition for web + // eslint-disable-next-line react/jsx-props-no-spreading + renderScrollComponent={(_props) => } /> ); } From 3c5d2796c9e926cb7accd3396c43cda722f8851a Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 20 Oct 2023 13:08:06 -0400 Subject: [PATCH 05/10] update implementation --- src/components/FlatList/MVCPFlatList.js | 190 ++++++++++++++++++ src/components/FlatList/index.web.js | 3 + .../MVCPScrollView/MVCPScrollView.js | 128 ------------ src/components/InvertedFlatList/index.js | 5 - 4 files changed, 193 insertions(+), 133 deletions(-) create mode 100644 src/components/FlatList/MVCPFlatList.js create mode 100644 src/components/FlatList/index.web.js delete mode 100644 src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js new file mode 100644 index 000000000000..357db1535e6b --- /dev/null +++ b/src/components/FlatList/MVCPFlatList.js @@ -0,0 +1,190 @@ +/* eslint-disable es/no-optional-chaining, es/no-nullish-coalescing-operators, react/prop-types */ + +import React from 'react'; +import {FlatList} from 'react-native'; + +function mergeRefs(...args) { + return function forwardRef(node) { + args.forEach((ref) => { + if (ref == null) { + return; + } + if (typeof ref === 'function') { + ref(node); + return; + } + if (typeof ref === 'object') { + // eslint-disable-next-line no-param-reassign + ref.current = node; + return; + } + console.error(`mergeRefs cannot handle Refs of type boolean, number or string, received ref ${String(ref)}`); + }); + }; +} + +function useMergeRefs(...args) { + return React.useMemo( + () => mergeRefs(...args), + // eslint-disable-next-line + [...args], + ); +} + +const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizontal, inverted, onScroll, ...props}, forwardedRef) => { + const {minIndexForVisible: mvcpMinIndexForVisible, autoscrollToTopThreshold: mvcpAutoscrollToTopThreshold} = maintainVisibleContentPosition ?? {}; + const scrollRef = React.useRef(null); + const prevFirstVisibleOffsetRef = React.useRef(null); + const firstVisibleViewRef = React.useRef(null); + const mutationObserverRef = React.useRef(null); + const lastScrollOffsetRef = React.useRef(0); + + const getScrollOffset = React.useCallback(() => { + if (scrollRef.current == null) { + return 0; + } + return horizontal ? scrollRef.current.getScrollableNode().scrollLeft : scrollRef.current.getScrollableNode().scrollTop; + }, [horizontal]); + + const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode().childNodes[0], []); + + const scrollToOffset = React.useCallback( + (offset, animated) => { + const behavior = animated ? 'smooth' : 'instant'; + scrollRef.current?.getScrollableNode().scroll(horizontal ? {left: offset, behavior} : {top: offset, behavior}); + }, + [horizontal], + ); + + const prepareForMaintainVisibleContentPosition = React.useCallback(() => { + if (mvcpMinIndexForVisible == null) { + return; + } + + const contentView = getContentView(); + if (contentView == null) { + return; + } + + const scrollOffset = getScrollOffset(); + + for (let i = mvcpMinIndexForVisible; i < contentView.childNodes.length; i++) { + const subview = contentView.childNodes[i]; + const subviewOffset = horizontal ? subview.offsetLeft : subview.offsetTop; + if (subviewOffset > scrollOffset || i === contentView.childNodes.length - 1) { + prevFirstVisibleOffsetRef.current = subviewOffset; + firstVisibleViewRef.current = subview; + break; + } + } + }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal]); + + const adjustForMaintainVisibleContentPosition = React.useCallback(() => { + if (mvcpMinIndexForVisible == null) { + return; + } + + const firstVisibleView = firstVisibleViewRef.current; + const prevFirstVisibleOffset = prevFirstVisibleOffsetRef.current; + if (firstVisibleView == null || prevFirstVisibleOffset == null) { + return; + } + + const firstVisibleViewOffset = horizontal ? firstVisibleView.offsetLeft : firstVisibleView.offsetTop; + const delta = firstVisibleViewOffset - prevFirstVisibleOffset; + if (Math.abs(delta) > 0.5) { + const scrollOffset = getScrollOffset(); + prevFirstVisibleOffsetRef.current = firstVisibleViewOffset; + scrollToOffset(scrollOffset + delta, false); + if (mvcpAutoscrollToTopThreshold != null && scrollOffset <= mvcpAutoscrollToTopThreshold) { + scrollToOffset(0, true); + } + } + }, [getScrollOffset, scrollToOffset, mvcpMinIndexForVisible, mvcpAutoscrollToTopThreshold, horizontal]); + + const setupMutationObserver = React.useCallback(() => { + const contentView = getContentView(); + if (contentView == null) { + return; + } + + mutationObserverRef.current?.disconnect(); + + const mutationObserver = new MutationObserver(() => { + // Chrome adjusts scroll position when elements are added at the top of the + // view. We want to have the same behavior as react-native / Safari so we + // reset the scroll position to the last value we got from an event. + const lastScrollOffset = lastScrollOffsetRef.current; + const scrollOffset = getScrollOffset(); + if (lastScrollOffset !== scrollOffset) { + scrollToOffset(lastScrollOffset, false); + } + + // This needs to execute after scroll events are dispatched, but + // in the same tick to avoid flickering. rAF provides the right timing. + requestAnimationFrame(() => { + adjustForMaintainVisibleContentPosition(); + }); + }); + mutationObserver.observe(contentView, { + attributes: true, + childList: true, + subtree: true, + }); + + mutationObserverRef.current = mutationObserver; + }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); + + React.useEffect(() => { + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]); + + const setMergedRef = useMergeRefs(scrollRef, forwardedRef); + + const onRef = React.useCallback( + (newRef) => { + // Make sure to only call refs and re-attach listeners if the node changed. + if (newRef == null || newRef === scrollRef.current) { + return; + } + + setMergedRef(newRef); + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }, + [prepareForMaintainVisibleContentPosition, setMergedRef, setupMutationObserver], + ); + + React.useEffect(() => { + const mutationObserver = mutationObserverRef.current; + return () => { + mutationObserver?.disconnect(); + }; + }, []); + + const onScrollInternal = React.useCallback( + (ev) => { + lastScrollOffsetRef.current = getScrollOffset(); + + prepareForMaintainVisibleContentPosition(); + + onScroll?.(ev); + }, + [getScrollOffset, prepareForMaintainVisibleContentPosition, onScroll], + ); + + return ( + + ); +}); + +export default MVCPFlatList; diff --git a/src/components/FlatList/index.web.js b/src/components/FlatList/index.web.js new file mode 100644 index 000000000000..7299776db9bc --- /dev/null +++ b/src/components/FlatList/index.web.js @@ -0,0 +1,3 @@ +import MVCPFlatList from './MVCPFlatList'; + +export default MVCPFlatList; diff --git a/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js b/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js deleted file mode 100644 index f0139a4ec39c..000000000000 --- a/src/components/InvertedFlatList/MVCPScrollView/MVCPScrollView.js +++ /dev/null @@ -1,128 +0,0 @@ -import React, {forwardRef, useEffect, useRef} from 'react'; -import {ScrollView, StyleSheet} from 'react-native'; -import PropTypes from 'prop-types'; - - -const MVCPScrollView = forwardRef(({maintainVisibleContentPosition, horizontal, ...props}, ref) => { - const scrollViewRef = useRef(null); - const prevFirstVisibleOffset = useRef(null); - const firstVisibleView = useRef(null); - const mutationObserver = useRef(null); - - const getContentView = () => scrollViewRef.current?.childNodes[0]; - - const prepareForMaintainVisibleContentPosition = () => { - if (maintainVisibleContentPosition == null || scrollViewRef.current == null) { - return; - } - - const contentView = getContentView(); - const minIdx = maintainVisibleContentPosition.minIndexForVisible; - for (let ii = minIdx; ii < contentView.childNodes.length; ii++) { - const subview = contentView.childNodes[ii]; - const hasNewView = horizontal ? subview.offsetLeft > scrollViewRef.current.scrollLeft : subview.offsetTop > scrollViewRef.current.scrollTop; - if (hasNewView || ii === contentView.childNodes.length - 1) { - prevFirstVisibleOffset.current = horizontal ? subview.offsetLeft : subview.offsetTop; - firstVisibleView.current = subview; - break; - } - } - }; - const scrollEventListener = useRef(() => { - prepareForMaintainVisibleContentPosition(); - }); - - const adjustForMaintainVisibleContentPosition = () => { - if (maintainVisibleContentPosition == null || scrollViewRef.current == null || firstVisibleView.current == null || prevFirstVisibleOffset.current == null) { - return; - } - - const autoscrollThreshold = maintainVisibleContentPosition.autoscrollToTopThreshold; - if (horizontal) { - const deltaX = firstVisibleView.current.offsetLeft - prevFirstVisibleOffset.current; - if (Math.abs(deltaX) > 0.5) { - const x = scrollViewRef.current.scrollLeft; - prevFirstVisibleOffset.current = firstVisibleView.current.offsetLeft; - scrollViewRef.current.scrollTo({x: x + deltaX, animated: false}); - if (autoscrollThreshold != null && x <= autoscrollThreshold) { - scrollViewRef.current.scrollTo({x: 0, animated: true}); - } - } - } else { - const deltaY = firstVisibleView.current.offsetTop - prevFirstVisibleOffset.current; - if (Math.abs(deltaY) > 0.5) { - const y = scrollViewRef.current.scrollTop; - prevFirstVisibleOffset.current = firstVisibleView.current.offsetTop; - scrollViewRef.current.scrollTo({y: y + deltaY, animated: false}); - if (autoscrollThreshold != null && y <= autoscrollThreshold) { - scrollViewRef.current.scrollTo({y: 0, animated: true}); - } - } - } - }; - - if (mutationObserver.current == null) { - mutationObserver.current = new MutationObserver(() => { - // This needs to execute after scroll events are dispatched, but - // in the same tick to avoid flickering. rAF provides the right timing. - requestAnimationFrame(adjustForMaintainVisibleContentPosition); - }); - } - - const onRef = (newRef) => { - scrollViewRef.current = newRef; - if (typeof ref === 'function') { - ref(newRef); - } else { - // eslint-disable-next-line no-param-reassign - ref.current = newRef; - } - prepareForMaintainVisibleContentPosition(); - mutationObserver.current.disconnect(); - mutationObserver.current.observe(getContentView(), { - attributes: true, - childList: true, - subtree: true, - }); - newRef.removeEventListener('scroll', scrollEventListener.current); - newRef.addEventListener('scroll', scrollEventListener.current); - }; - - useEffect(() => { - const currentObserver = mutationObserver.current; - const currentScrollEventListener = scrollEventListener.current; - return () => { - currentObserver.disconnect(); - scrollViewRef.current.removeEventListener('scroll', currentScrollEventListener); - }; - }, []); - - return ( - - ); -}); - -const styles = StyleSheet.create({ - inverted: { - transform: [{ scaleY: -1 }], - }, -}); - - -MVCPScrollView.propTypes = { - maintainVisibleContentPosition: PropTypes.shape({ - minIndexForVisible: PropTypes.number.isRequired, - autoscrollToTopThreshold: PropTypes.number, - }), - horizontal: PropTypes.bool, -}; - -export default MVCPScrollView; diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js index e7f6c14f52d8..564db6296c9b 100644 --- a/src/components/InvertedFlatList/index.js +++ b/src/components/InvertedFlatList/index.js @@ -2,7 +2,6 @@ import React, {forwardRef, useEffect, useRef} from 'react'; import PropTypes from 'prop-types'; import {DeviceEventEmitter, FlatList, StyleSheet} from 'react-native'; import _ from 'underscore'; -import MVCPScrollView from './MVCPScrollView/MVCPScrollView'; import BaseInvertedFlatList from './BaseInvertedFlatList'; import styles from '../../styles/styles'; import CONST from '../../CONST'; @@ -122,10 +121,6 @@ function InvertedFlatList(props) { // We need to keep batch size to one to workaround a bug in react-native-web. // This can be removed once https://github.com/Expensify/App/pull/24482 is merged. maxToRenderPerBatch={1} - - // We need to use our own scroll component to workaround a maintainVisibleContentPosition for web - // eslint-disable-next-line react/jsx-props-no-spreading - renderScrollComponent={(_props) => } /> ); } From 69272012a274d2c350b165573ca2d24bc29f85cd Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 23 Oct 2023 11:14:29 +0200 Subject: [PATCH 06/10] WIP testing maintainVisibleContentPosition --- src/components/InvertedFlatList/index.js | 325 ++++++++++++++--------- 1 file changed, 194 insertions(+), 131 deletions(-) diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js index 564db6296c9b..f2414d577222 100644 --- a/src/components/InvertedFlatList/index.js +++ b/src/components/InvertedFlatList/index.js @@ -1,140 +1,203 @@ -import React, {forwardRef, useEffect, useRef} from 'react'; +import React, {forwardRef, useEffect, useRef, useState} from 'react'; import PropTypes from 'prop-types'; -import {DeviceEventEmitter, FlatList, StyleSheet} from 'react-native'; +import {DeviceEventEmitter, StyleSheet, View, Text, Button} from 'react-native'; import _ from 'underscore'; import BaseInvertedFlatList from './BaseInvertedFlatList'; -import styles from '../../styles/styles'; +// import styles from '../../styles/styles'; import CONST from '../../CONST'; +import FlatList from '../FlatList/index.web'; + +// const propTypes = { +// /** Passed via forwardRef so we can access the FlatList ref */ +// innerRef: PropTypes.shape({ +// current: PropTypes.instanceOf(FlatList), +// }).isRequired, + +// /** Any additional styles to apply */ +// // eslint-disable-next-line react/forbid-prop-types +// contentContainerStyle: PropTypes.any, + +// /** Same as for FlatList */ +// onScroll: PropTypes.func, +// }; + +// // This is adapted from https://codesandbox.io/s/react-native-dsyse +// // It's a HACK alert since FlatList has inverted scrolling on web +// function InvertedFlatList(props) { +// const {innerRef, contentContainerStyle} = props; +// const listRef = React.createRef(); + +// const lastScrollEvent = useRef(null); +// const scrollEndTimeout = useRef(null); +// const updateInProgress = useRef(false); +// const eventHandler = useRef(null); + +// useEffect(() => { +// if (!_.isFunction(innerRef)) { +// // eslint-disable-next-line no-param-reassign +// innerRef.current = listRef.current; +// } else { +// innerRef(listRef); +// } + +// return () => { +// if (scrollEndTimeout.current) { +// clearTimeout(scrollEndTimeout.current); +// } + +// if (eventHandler.current) { +// eventHandler.current.remove(); +// } +// }; +// }, [innerRef, listRef]); + +// /** +// * Emits when the scrolling is in progress. Also, +// * invokes the onScroll callback function from props. +// * +// * @param {Event} event - The onScroll event from the FlatList +// */ +// const onScroll = (event) => { +// props.onScroll(event); + +// if (!updateInProgress.current) { +// updateInProgress.current = true; +// eventHandler.current = DeviceEventEmitter.emit(CONST.EVENTS.SCROLLING, true); +// } +// }; + +// /** +// * Emits when the scrolling has ended. +// */ +// const onScrollEnd = () => { +// eventHandler.current = DeviceEventEmitter.emit(CONST.EVENTS.SCROLLING, false); +// updateInProgress.current = false; +// }; + +// /** +// * Decides whether the scrolling has ended or not. If it has ended, +// * then it calls the onScrollEnd function. Otherwise, it calls the +// * onScroll function and pass the event to it. +// * +// * This is a temporary work around, since react-native-web doesn't +// * support onScrollBeginDrag and onScrollEndDrag props for FlatList. +// * More info: +// * https://github.com/necolas/react-native-web/pull/1305 +// * +// * This workaround is taken from below and refactored to fit our needs: +// * https://github.com/necolas/react-native-web/issues/1021#issuecomment-984151185 +// * +// * @param {Event} event - The onScroll event from the FlatList +// */ +// const handleScroll = (event) => { +// onScroll(event); +// const timestamp = Date.now(); + +// if (scrollEndTimeout.current) { +// clearTimeout(scrollEndTimeout.current); +// } + +// if (lastScrollEvent.current) { +// scrollEndTimeout.current = setTimeout(() => { +// if (lastScrollEvent.current !== timestamp) { +// return; +// } +// // Scroll has ended +// lastScrollEvent.current = null; +// onScrollEnd(); +// }, 250); +// } + +// lastScrollEvent.current = timestamp; +// }; + +// return ( +// +// ); +// } + +// InvertedFlatList.propTypes = propTypes; +// InvertedFlatList.defaultProps = { +// contentContainerStyle: {}, +// onScroll: () => {}, +// }; + +// export default forwardRef((props, ref) => ( +// +// )); + + + +function ReportScreen() { + const [data, setData] = useState(generatePosts(15)); + + const loadNewerChats = () => { + const lastId = data[0].id - 1; + setData([...generatePosts(5, lastId - 4), ...data]); + }; + + const renderItem = ({ item }) => ; + const keyExtractor = (item) => item.id.toString(); + + return ( + <> + +