Add initial impl, scroll context
This commit is contained in:
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
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 {atoms as a} from '#/alf'
|
||||||
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 * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
import {List, ListScrollProvider, useListScrollHandler} from '#/components/List'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
export function StorybookLists() {
|
export function StorybookLists() {
|
||||||
return (
|
return (
|
||||||
@@ -20,12 +17,43 @@ export function StorybookLists() {
|
|||||||
<Layout.Header.Slot />
|
<Layout.Header.Slot />
|
||||||
</Layout.Header.Outer>
|
</Layout.Header.Outer>
|
||||||
<Layout.Content keyboardShouldPersistTaps="handled">
|
<Layout.Content keyboardShouldPersistTaps="handled">
|
||||||
<StorybookInner />
|
<Inner />
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function StorybookInner() {
|
const items = Array.from({length: 100}).map((_, i) => ({
|
||||||
return null
|
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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user