Snap the header scroll on the web

This commit is contained in:
Dan Abramov
2023-12-23 18:16:44 +00:00
parent d9c44c373a
commit e582713e23
3 changed files with 64 additions and 19 deletions
+18 -5
View File
@@ -1,17 +1,28 @@
import React, {createContext, useContext, useMemo} from 'react'
import {ScrollHandlers} from 'react-native-reanimated'
import {ScrollHandler} from 'react-native-reanimated'
import {NativeScrollEvent} from 'react-native'
const ScrollContext = createContext<ScrollHandlers<any>>({
type ScrollHandlers = {
onBeginDrag: undefined | ScrollHandler
onEndDrag: undefined | ScrollHandler<any>
onScroll: undefined | ScrollHandler<any>
onScrollEndWeb:
| undefined
| ((e: Pick<NativeScrollEvent, 'contentOffset'>) => void) // Web-only.
}
const ScrollContext = createContext<ScrollHandlers>({
onBeginDrag: undefined,
onEndDrag: undefined,
onScroll: undefined,
onScrollEndWeb: undefined,
})
export function useScrollHandlers(): ScrollHandlers<any> {
export function useScrollHandlers(): ScrollHandlers {
return useContext(ScrollContext)
}
type ProviderProps = {children: React.ReactNode} & ScrollHandlers<any>
type ProviderProps = {children: React.ReactNode} & ScrollHandlers
// Note: this completely *overrides* the parent handlers.
// It's up to you to compose them with the parent ones via useScrollHandlers() if needed.
@@ -20,14 +31,16 @@ export function ScrollProvider({
onBeginDrag,
onEndDrag,
onScroll,
onScrollEndWeb,
}: ProviderProps) {
const handlers = useMemo(
() => ({
onBeginDrag,
onEndDrag,
onScroll,
onScrollEndWeb,
}),
[onBeginDrag, onEndDrag, onScroll],
[onBeginDrag, onEndDrag, onScroll, onScrollEndWeb],
)
return (
<ScrollContext.Provider value={handlers}>{children}</ScrollContext.Provider>
+17 -3
View File
@@ -109,7 +109,7 @@ function ListImpl<ItemT>(
const containerRef = useRef(null)
useResizeObserver(containerRef, onContentSizeChange)
// --- onScroll ---
// --- onScroll & onScrollEndWeb ---
const [isInsideVisibleTree, setIsInsideVisibleTree] = React.useState(false)
const handleWindowScroll = useNonReactiveCallback(() => {
if (isInsideVisibleTree) {
@@ -124,6 +124,16 @@ function ListImpl<ItemT>(
)
}
})
const handleWindowScrollEnd = useNonReactiveCallback(() => {
if (isInsideVisibleTree) {
contextScrollHandlers.onScrollEndWeb?.({
contentOffset: {
x: window.scrollX,
y: window.scrollY,
},
})
}
})
React.useEffect(() => {
if (!isInsideVisibleTree) {
// Prevents hidden tabs from firing scroll events.
@@ -131,8 +141,12 @@ function ListImpl<ItemT>(
return
}
window.addEventListener('scroll', handleWindowScroll)
return () => window.removeEventListener('scroll', handleWindowScroll)
}, [isInsideVisibleTree, handleWindowScroll])
window.addEventListener('scrollend', handleWindowScrollEnd)
return () => {
window.removeEventListener('scroll', handleWindowScroll)
window.removeEventListener('scrollend', handleWindowScrollEnd)
}
}, [isInsideVisibleTree, handleWindowScroll, handleWindowScrollEnd])
// --- onScrolledDownChange ---
const isScrolledDown = useRef(false)
+29 -11
View File
@@ -28,6 +28,21 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
}
})
const snapToClosestMode = React.useCallback(
(scrollY: number) => {
startDragOffset.value = null
startMode.value = null
if (scrollY < headerHeight.value / 2) {
// If we're close to the top, show the shell.
setMode(false)
} else {
// Snap to whichever state is the closest.
setMode(Math.round(mode.value) === 1)
}
},
[startDragOffset, startMode, headerHeight, mode, setMode],
)
const onBeginDrag = useCallback(
(e: NativeScrollEvent) => {
'worklet'
@@ -40,17 +55,19 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
const onEndDrag = useCallback(
(e: NativeScrollEvent) => {
'worklet'
startDragOffset.value = null
startMode.value = null
if (e.contentOffset.y < headerHeight.value / 2) {
// If we're close to the top, show the shell.
setMode(false)
} else {
// Snap to whichever state is the closest.
setMode(Math.round(mode.value) === 1)
}
snapToClosestMode(e.contentOffset.y)
},
[startDragOffset, startMode, setMode, mode, headerHeight],
[snapToClosestMode],
)
// On the web, we don't get the begin/end drag events,
// but we still need to expand or collapse the header.
const onScrollEndWeb = useCallback(
(e: Pick<NativeScrollEvent, 'contentOffset'>) => {
'worklet'
snapToClosestMode(e.contentOffset.y)
},
[snapToClosestMode],
)
const onScroll = useCallback(
@@ -100,7 +117,8 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
<ScrollProvider
onBeginDrag={onBeginDrag}
onEndDrag={onEndDrag}
onScroll={onScroll}>
onScroll={onScroll}
onScrollEndWeb={onScrollEndWeb}>
{children}
</ScrollProvider>
)