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, children,
tabBarPosition = 'top', tabBarPosition = 'top',
initialPage = 0, initialPage = 0,
headerOnlyHeight = 0,
renderTabBar, renderTabBar,
onPageSelected, onPageSelected,
onPageSelecting, onPageSelecting,
@@ -27,6 +28,7 @@ export const Pager = React.forwardRef(function PagerImpl(
ref, ref,
) { ) {
const [selectedPage, setSelectedPage] = React.useState(initialPage) const [selectedPage, setSelectedPage] = React.useState(initialPage)
const scrollYs = React.useRef([])
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
setPage: (index: number) => setSelectedPage(index), setPage: (index: number) => setSelectedPage(index),
@@ -34,11 +36,25 @@ export const Pager = React.forwardRef(function PagerImpl(
const onTabBarSelect = React.useCallback( const onTabBarSelect = React.useCallback(
(index: number) => { (index: number) => {
const scrollY = window.scrollY
scrollYs.current[selectedPage] = scrollY
setSelectedPage(index) setSelectedPage(index)
onPageSelected?.(index) onPageSelected?.(index)
onPageSelecting?.(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 ( return (
+25 -2
View File
@@ -39,6 +39,17 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
ref, ref,
) { ) {
const [currentPage, setCurrentPage] = React.useState(0) 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( const renderTabBar = React.useCallback(
(props: RenderTabBarFnProps) => { (props: RenderTabBarFnProps) => {
@@ -48,12 +59,20 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
renderHeader={renderHeader} renderHeader={renderHeader}
currentPage={currentPage} currentPage={currentPage}
onCurrentPageSelected={onCurrentPageSelected} onCurrentPageSelected={onCurrentPageSelected}
onHeaderOnlyLayout={onHeaderOnlyLayout}
onSelect={props.onSelect} onSelect={props.onSelect}
testID={testID} testID={testID}
/> />
) )
}, },
[items, renderHeader, currentPage, onCurrentPageSelected, testID], [
items,
renderHeader,
currentPage,
onCurrentPageSelected,
onHeaderOnlyLayout,
testID,
],
) )
const onPageSelectedInner = React.useCallback( const onPageSelectedInner = React.useCallback(
@@ -76,6 +95,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
onPageSelected={onPageSelectedInner} onPageSelected={onPageSelectedInner}
onPageSelecting={onPageSelecting} onPageSelecting={onPageSelecting}
renderTabBar={renderTabBar} renderTabBar={renderTabBar}
headerOnlyHeight={headerOnlyHeight}
tabBarPosition="top"> tabBarPosition="top">
{toArray(children) {toArray(children)
.filter(Boolean) .filter(Boolean)
@@ -97,6 +117,7 @@ let PagerTabBar = ({
testID, testID,
renderHeader, renderHeader,
onCurrentPageSelected, onCurrentPageSelected,
onHeaderOnlyLayout,
onSelect, onSelect,
}: { }: {
currentPage: number currentPage: number
@@ -110,7 +131,9 @@ let PagerTabBar = ({
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
return ( return (
<> <>
<View style={[!isMobile && styles.headerContainerDesktop, pal.border]}> <View
style={[!isMobile && styles.headerContainerDesktop, pal.border]}
onLayout={onHeaderOnlyLayout}>
{renderHeader?.()} {renderHeader?.()}
</View> </View>
<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 {ScrollProvider} from '#/lib/ScrollContext'
import {NativeScrollEvent} from 'react-native' import {NativeScrollEvent} from 'react-native'
import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
@@ -18,6 +19,15 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
const startDragOffset = useSharedValue<number | null>(null) const startDragOffset = useSharedValue<number | null>(null)
const startMode = useSharedValue<number | null>(null) const startMode = useSharedValue<number | null>(null)
useEffect(() => {
if (isWeb) {
return listenToForcedWindowScroll(() => {
startDragOffset.value = null
startMode.value = null
})
}
})
const onBeginDrag = useCallback( const onBeginDrag = useCallback(
(e: NativeScrollEvent) => { (e: NativeScrollEvent) => {
'worklet' 'worklet'
@@ -95,3 +105,26 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
</ScrollProvider> </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)
}
}