Scroll restore for tabs

This commit is contained in:
Dan Abramov
2023-12-19 02:52:29 +00:00
parent 634b76dc3d
commit 4bd43e4fbf
3 changed files with 76 additions and 4 deletions
+17 -1
View File
@@ -20,6 +20,7 @@ export const Pager = React.forwardRef(function PagerImpl(
children,
tabBarPosition = 'top',
initialPage = 0,
headerOnlyHeight = 0,
renderTabBar,
onPageSelected,
onPageSelecting,
@@ -27,6 +28,7 @@ export const Pager = React.forwardRef(function PagerImpl(
ref,
) {
const [selectedPage, setSelectedPage] = React.useState(initialPage)
const scrollYs = React.useRef([])
React.useImperativeHandle(ref, () => ({
setPage: (index: number) => setSelectedPage(index),
@@ -34,11 +36,25 @@ export const Pager = React.forwardRef(function PagerImpl(
const onTabBarSelect = React.useCallback(
(index: number) => {
const scrollY = window.scrollY
scrollYs.current[selectedPage] = scrollY
setSelectedPage(index)
onPageSelected?.(index)
onPageSelecting?.(index)
if (scrollY >= headerOnlyHeight) {
window.scrollTo(
0,
Math.max(headerOnlyHeight, scrollYs.current[index] ?? 0),
)
}
},
[setSelectedPage, onPageSelected, onPageSelecting],
[
selectedPage,
setSelectedPage,
onPageSelected,
onPageSelecting,
headerOnlyHeight,
],
)
return (
+25 -2
View File
@@ -39,6 +39,17 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
ref,
) {
const [currentPage, setCurrentPage] = React.useState(0)
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
const onHeaderOnlyLayout = React.useCallback(
(evt: LayoutChangeEvent) => {
const height = evt.nativeEvent.layout.height
if (height > 0) {
setHeaderOnlyHeight(Math.round(height))
}
},
[setHeaderOnlyHeight],
)
const renderTabBar = React.useCallback(
(props: RenderTabBarFnProps) => {
@@ -48,12 +59,20 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
renderHeader={renderHeader}
currentPage={currentPage}
onCurrentPageSelected={onCurrentPageSelected}
onHeaderOnlyLayout={onHeaderOnlyLayout}
onSelect={props.onSelect}
testID={testID}
/>
)
},
[items, renderHeader, currentPage, onCurrentPageSelected, testID],
[
items,
renderHeader,
currentPage,
onCurrentPageSelected,
onHeaderOnlyLayout,
testID,
],
)
const onPageSelectedInner = React.useCallback(
@@ -76,6 +95,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
onPageSelected={onPageSelectedInner}
onPageSelecting={onPageSelecting}
renderTabBar={renderTabBar}
headerOnlyHeight={headerOnlyHeight}
tabBarPosition="top">
{toArray(children)
.filter(Boolean)
@@ -97,6 +117,7 @@ let PagerTabBar = ({
testID,
renderHeader,
onCurrentPageSelected,
onHeaderOnlyLayout,
onSelect,
}: {
currentPage: number
@@ -110,7 +131,9 @@ let PagerTabBar = ({
const {isMobile} = useWebMediaQueries()
return (
<>
<View style={[!isMobile && styles.headerContainerDesktop, pal.border]}>
<View
style={[!isMobile && styles.headerContainerDesktop, pal.border]}
onLayout={onHeaderOnlyLayout}>
{renderHeader?.()}
</View>
<View
+34 -1
View File
@@ -1,4 +1,5 @@
import React, {useCallback} from 'react'
import React, {useCallback, useEffect} from 'react'
import EventEmitter from 'eventemitter3'
import {ScrollProvider} from '#/lib/ScrollContext'
import {NativeScrollEvent} from 'react-native'
import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
@@ -18,6 +19,15 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
const startDragOffset = useSharedValue<number | null>(null)
const startMode = useSharedValue<number | null>(null)
useEffect(() => {
if (isWeb) {
return listenToForcedWindowScroll(() => {
startDragOffset.value = null
startMode.value = null
})
}
})
const onBeginDrag = useCallback(
(e: NativeScrollEvent) => {
'worklet'
@@ -95,3 +105,26 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
</ScrollProvider>
)
}
const emitter = new EventEmitter()
if (isWeb) {
const originalScroll = window.scroll
window.scroll = function () {
emitter.emit('forced-scroll')
return originalScroll.apply(this, arguments as any)
}
const originalScrollTo = window.scrollTo
window.scrollTo = function () {
emitter.emit('forced-scroll')
return originalScrollTo.apply(this, arguments as any)
}
}
function listenToForcedWindowScroll(listener: () => void) {
emitter.addListener('forced-scroll', listener)
return () => {
emitter.removeListener('forced-scroll', listener)
}
}