Add initial impl, scroll context

This commit is contained in:
Eric Bailey
2025-08-27 09:52:39 -05:00
parent 2ad93a5895
commit c85ce941c3
3 changed files with 176 additions and 10 deletions
@@ -0,0 +1,72 @@
import {createContext, useCallback, useContext, useMemo} from 'react'
import {type NativeScrollEvent} from 'react-native'
import {type ScrollHandlers} from 'react-native-reanimated'
export type NormalizedScrollHandlers = {
/**
* Web + Native — needs to be a `worklet` on native, but can be either on web
*/
onScroll?: ScrollHandlers<any>['onScroll']
/**
* Native only
*/
onScrollBeginDrag?: ScrollHandlers<any>['onBeginDrag']
/**
* Native only
*/
onScrollEndDrag?: ScrollHandlers<any>['onEndDrag']
/**
* Native only
*/
onMomentumScrollBegin?: ScrollHandlers<any>['onMomentumBegin']
/**
* Native only
*/
onMomentumScrollEnd?: ScrollHandlers<any>['onMomentumEnd']
}
const ListScrollContext = createContext<NormalizedScrollHandlers>({
onScroll: undefined,
onScrollBeginDrag: undefined,
onScrollEndDrag: undefined,
onMomentumScrollBegin: undefined,
onMomentumScrollEnd: undefined,
})
ListScrollContext.displayName = 'ListScrollContext'
export function ListScrollProvider({
children,
onScrollBeginDrag,
onScrollEndDrag,
onScroll,
onMomentumScrollBegin,
onMomentumScrollEnd,
}: {children: React.ReactNode} & NormalizedScrollHandlers) {
const handlers = useMemo(
() => ({
onScroll,
onScrollBeginDrag: onScrollBeginDrag,
onScrollEndDrag: onScrollEndDrag,
onMomentumScrollBegin: onMomentumScrollBegin,
onMomentumScrollEnd: onMomentumScrollEnd,
}),
[
onScrollBeginDrag,
onScrollEndDrag,
onScroll,
onMomentumScrollBegin,
onMomentumScrollEnd,
],
)
return (
<ListScrollContext.Provider value={handlers}>
{children}
</ListScrollContext.Provider>
)
}
export function useListScrollContext(): NormalizedScrollHandlers {
return useContext(ListScrollContext)
}
export const useListScrollHandler = useCallback<(e: NativeScrollEvent) => void>
+66
View File
@@ -0,0 +1,66 @@
import {forwardRef} from 'react'
import {type FlatList, type FlatListProps} from 'react-native'
import Animated, {
type FlatListPropsWithLayout,
useAnimatedScrollHandler,
} from 'react-native-reanimated'
import {atoms as a, web} from '#/alf'
import {useListScrollContext} from '#/components/List/ListScrollProvider'
export {
ListScrollProvider,
useListScrollHandler,
} from '#/components/List/ListScrollProvider'
/**
* Cleaned up FlatList without some problematic props.
*
* - `contentOffset` - Doesn't work, use padding on `contentContainerStyle` instead.
*/
type ListProps<Item> = Omit<FlatListProps<Item>, 'contentOffset'>
export const List = forwardRef(function List<Item>(
props: ListProps<Item>,
ref: React.Ref<FlatList<Item>>,
) {
const scrollHandlers = useListScrollContext()
const onScroll = useAnimatedScrollHandler({
onScroll(e, ctx) {
scrollHandlers.onScroll?.(e, ctx)
},
onBeginDrag(e, ctx) {
scrollHandlers.onScrollBeginDrag?.(e, ctx)
},
onEndDrag(e, ctx) {
scrollHandlers.onScrollEndDrag?.(e, ctx)
},
onMomentumBegin(e, ctx) {
scrollHandlers.onMomentumScrollBegin?.(e, ctx)
},
/*
* Note: adding onMomentumBegin here makes simulator scroll lag on Android.
* So either don't add it, or figure out why. - sfn
*/
onMomentumEnd(e, ctx) {
scrollHandlers.onMomentumScrollEnd?.(e, ctx)
},
})
return (
<Animated.FlatList<Item>
ref={ref}
{...(props as FlatListPropsWithLayout<Item>)}
style={[
/*
* On web, the List should always fill its container, otherwise
* `onScroll` will not work due to the entire page scrolling.
*/
web(a.h_full),
]}
onScroll={onScroll}
/>
)
}) as <Item>(
props: ListProps<Item> & {ref?: React.Ref<FlatList<Item>>},
) => React.ReactElement
+38 -10
View File
@@ -1,13 +1,10 @@
import React from 'react'
import {View} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import {runOnJS} from 'react-native-reanimated'
import {type NavigationProp} from '#/lib/routes/types'
import {useSetThemePrefs} from '#/state/shell'
import {ListContained} from '#/view/screens/Storybook/ListContained'
import {atoms as a, ThemeProvider} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {atoms as a} from '#/alf'
import * as Layout from '#/components/Layout'
import {List, ListScrollProvider, useListScrollHandler} from '#/components/List'
import {Text} from '#/components/Typography'
export function StorybookLists() {
return (
@@ -20,12 +17,43 @@ export function StorybookLists() {
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content keyboardShouldPersistTaps="handled">
<StorybookInner />
<Inner />
</Layout.Content>
</Layout.Screen>
)
}
function StorybookInner() {
return null
const items = Array.from({length: 100}).map((_, i) => ({
key: `item-${i + 1}`,
title: `Item ${i + 1}`,
}))
type Item = {
key: string
title: string
}
const log = (msg: any) => console.log(msg)
export function Inner() {
const onScrollWorklet = useListScrollHandler(e => {
'worklet'
runOnJS(log)(`Scroll Y: ${e.contentOffset.y}`)
}, [])
return (
<View style={[a.h_full_vh]}>
<ListScrollProvider onScroll={onScrollWorklet}>
<List<Item>
data={items}
renderItem={({item}) => (
<View style={[a.p_md, a.border_b]}>
<Text>{item.title}</Text>
</View>
)}
style={[a.debug]}
/>
</ListScrollProvider>
</View>
)
}