Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20a5ac8cd2 | |||
| e8f830aad7 | |||
| 94d9357b69 | |||
| 391a1d4be3 | |||
| e44a022226 | |||
| 4b19e3794e | |||
| e7a4686bc4 | |||
| b71575a3f0 | |||
| 2b10180d74 | |||
| 9a73611f73 | |||
| 2b6cff298a | |||
| afa6d67c23 | |||
| d53c0e548f | |||
| ca5d4bff0c | |||
| a8b78588fb | |||
| 0098d617ee | |||
| 661d20d176 | |||
| c85ce941c3 | |||
| 2ad93a5895 | |||
| ffac99ff80 |
@@ -65,6 +65,7 @@ import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
|
||||
import {ProfileScreen} from '#/view/screens/Profile'
|
||||
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
|
||||
import {Storybook} from '#/view/screens/Storybook'
|
||||
import {StorybookLists} from '#/view/screens/StorybookLists'
|
||||
import {SupportScreen} from '#/view/screens/Support'
|
||||
import {TermsOfServiceScreen} from '#/view/screens/TermsOfService'
|
||||
import {BottomBar} from '#/view/shell/bottom-bar/BottomBar'
|
||||
@@ -305,6 +306,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
getComponent={() => Storybook}
|
||||
options={{title: title(msg`Storybook`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="StorybookLists"
|
||||
getComponent={() => StorybookLists}
|
||||
options={{title: title(msg`Storybook Lists`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="DebugMod"
|
||||
getComponent={() => DebugModScreen}
|
||||
|
||||
@@ -23,6 +23,11 @@ import {
|
||||
import {useDialogContext} from '#/components/Dialog'
|
||||
import {CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET} from '#/components/Layout/const'
|
||||
import {ScrollbarOffsetContext} from '#/components/Layout/context'
|
||||
import {
|
||||
List as BaseList,
|
||||
type ListItem,
|
||||
type ListProps,
|
||||
} from '#/components/List'
|
||||
|
||||
export * from '#/components/Layout/const'
|
||||
export * as Header from '#/components/Layout/Header'
|
||||
@@ -223,3 +228,15 @@ const WebCenterBorders = memo(function LayoutWebCenterBorders() {
|
||||
/>
|
||||
) : null
|
||||
})
|
||||
|
||||
export function List<Item extends ListItem>(props: ListProps<Item>) {
|
||||
return (
|
||||
<BaseList<Item>
|
||||
{...props}
|
||||
renderItem={item => {
|
||||
return <Center>{props.renderItem!(item)}</Center>
|
||||
}}
|
||||
style={[a.h_full_vh, props.style]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,235 @@
|
||||
import {forwardRef, useMemo} from 'react'
|
||||
import {
|
||||
type FlatList,
|
||||
type FlatListProps,
|
||||
RefreshControl,
|
||||
type ViewToken,
|
||||
} from 'react-native'
|
||||
import Animated, {
|
||||
type FlatListPropsWithLayout,
|
||||
runOnJS,
|
||||
useAnimatedScrollHandler,
|
||||
useSharedValue,
|
||||
} from 'react-native-reanimated'
|
||||
import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video'
|
||||
|
||||
import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||
import {isIOS, isNative} from '#/platform/detection'
|
||||
import {useLightbox} from '#/state/lightbox'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useListScrollContext} from '#/components/List/ListScrollProvider'
|
||||
|
||||
export {
|
||||
ListScrollProvider,
|
||||
useListScrollHandler,
|
||||
} from '#/components/List/ListScrollProvider'
|
||||
|
||||
export type ListRef<Item extends {key: string}> =
|
||||
React.MutableRefObject<FlatList<Item> | null>
|
||||
|
||||
export type ListItem = {key: string}
|
||||
|
||||
export type ListProps<Item extends ListItem> = Omit<
|
||||
FlatListProps<Item>,
|
||||
| 'onScroll'
|
||||
| 'onScrollBeginDrag'
|
||||
| 'onScrollEndDrag'
|
||||
| 'onMomentumScrollBegin'
|
||||
| 'onMomentumScrollEnd'
|
||||
| 'refreshControl'
|
||||
| 'contentOffset'
|
||||
> & {
|
||||
/**
|
||||
* @deprecated use `ListScrollProvider` handler instead
|
||||
*/
|
||||
onScroll?: FlatListProps<Item>['onScroll']
|
||||
/**
|
||||
* @deprecated use `ListScrollProvider` handler instead
|
||||
*/
|
||||
onScrollBeginDrag?: FlatListProps<Item>['onScrollBeginDrag']
|
||||
/**
|
||||
* @deprecated use `ListScrollProvider` handler instead
|
||||
*/
|
||||
onScrollEndDrag?: FlatListProps<Item>['onScrollEndDrag']
|
||||
/**
|
||||
* @deprecated use `ListScrollProvider` handler instead
|
||||
*/
|
||||
onMomentumScrollBegin?: FlatListProps<Item>['onMomentumScrollBegin']
|
||||
/**
|
||||
* @deprecated use `ListScrollProvider` handler instead
|
||||
*/
|
||||
onMomentumScrollEnd?: FlatListProps<Item>['onMomentumScrollEnd']
|
||||
/**
|
||||
* @deprecated pass `refreshing` and `onRefresh` instead to enable
|
||||
*/
|
||||
refreshControl?: FlatListProps<Item>['refreshControl']
|
||||
/**
|
||||
* @deprecated use `headerOffset` instead
|
||||
*/
|
||||
contentOffset?: FlatListProps<Item>['contentOffset']
|
||||
/**
|
||||
* Wrapper around `onViewableItemsChanged` that calls back with individual
|
||||
* items IF they `item.isViewable` is true.
|
||||
*/
|
||||
onItemSeen?: (item: Item) => void
|
||||
/**
|
||||
* Sugar for adding padding to the top of the list to accommodate fixed
|
||||
* headers. Also applies insets to the scroll indicators.
|
||||
*/
|
||||
headerOffset?: number
|
||||
/**
|
||||
* Sugar for adding padding to the bottom of the list to accommodate fixed
|
||||
* footers. Also applies insets to the scroll indicators.
|
||||
*/
|
||||
footerOffset?: number
|
||||
/**
|
||||
* Configures the point at which `onScrolledDownChange` is called.
|
||||
*/
|
||||
didScrollDownThreshold?: number
|
||||
onScrolledDownChange?: (isScrolledDown: boolean) => void
|
||||
}
|
||||
|
||||
export const List = forwardRef(function List<Item extends ListItem>(
|
||||
{...props}: ListProps<Item>,
|
||||
ref: React.Ref<FlatList<Item>>,
|
||||
) {
|
||||
const t = useTheme()
|
||||
const {activeLightbox} = useLightbox()
|
||||
const debounce400 = useDedupe(400)
|
||||
const isScrolledDown = useSharedValue(false)
|
||||
const scrollHandlers = useListScrollContext()
|
||||
const onScroll = useAnimatedScrollHandler({
|
||||
onScroll(e, ctx) {
|
||||
scrollHandlers.onScroll?.(e, ctx)
|
||||
|
||||
const didScrollDown =
|
||||
e.contentOffset.y > (props.didScrollDownThreshold ?? 200)
|
||||
if (isScrolledDown.get() !== didScrollDown) {
|
||||
isScrolledDown.set(didScrollDown)
|
||||
if (props.onScrolledDownChange) {
|
||||
runOnJS(props.onScrolledDownChange)(didScrollDown)
|
||||
}
|
||||
}
|
||||
|
||||
if (isIOS) runOnJS(debounce400)(updateActiveVideoViewAsync)
|
||||
},
|
||||
onBeginDrag(e, ctx) {
|
||||
scrollHandlers.onScrollBeginDrag?.(e, ctx)
|
||||
},
|
||||
onEndDrag(e, ctx) {
|
||||
scrollHandlers.onScrollEndDrag?.(e, ctx)
|
||||
if (isNative) runOnJS(updateActiveVideoViewAsync)()
|
||||
},
|
||||
/*
|
||||
* Note: adding onMomentumBegin here makes simulator scroll lag on Android.
|
||||
* So either don't add it, or figure out why. - sfn
|
||||
* TODO
|
||||
*/
|
||||
onMomentumBegin(e, ctx) {
|
||||
scrollHandlers.onMomentumScrollBegin?.(e, ctx)
|
||||
},
|
||||
onMomentumEnd(e, ctx) {
|
||||
scrollHandlers.onMomentumScrollEnd?.(e, ctx)
|
||||
if (isNative) runOnJS(updateActiveVideoViewAsync)()
|
||||
},
|
||||
})
|
||||
|
||||
const [onViewableItemsChanged, viewabilityConfig] = useMemo(() => {
|
||||
const onItemSeen = props.onItemSeen
|
||||
if (!onItemSeen) return [undefined, undefined]
|
||||
return [
|
||||
(info: {
|
||||
viewableItems: Array<ViewToken<Item>>
|
||||
changed: Array<ViewToken<Item>>
|
||||
}) => {
|
||||
for (const item of info.changed) {
|
||||
if (item.isViewable) {
|
||||
onItemSeen(item.item)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
itemVisiblePercentThreshold: 40,
|
||||
minimumViewTime: 0.5e3,
|
||||
},
|
||||
]
|
||||
}, [props.onItemSeen])
|
||||
|
||||
let refreshControl
|
||||
if (props.refreshing !== undefined || props.onRefresh !== undefined) {
|
||||
refreshControl = (
|
||||
<RefreshControl
|
||||
key={t.atoms.text.color}
|
||||
refreshing={props.refreshing ?? false}
|
||||
onRefresh={props.onRefresh ?? undefined}
|
||||
tintColor={t.atoms.text.color}
|
||||
titleColor={t.atoms.text.color}
|
||||
progressViewOffset={props.progressViewOffset ?? props.headerOffset}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Web only, provides a handle on the resulting `List` DOM element, which we
|
||||
* then use to apply sticky styles to individual list item wrappers.
|
||||
*/
|
||||
const dataSet = props.stickyHeaderIndices?.reduce((ds, i) => {
|
||||
return {...ds, [`sticky-header-index-${i}`]: 1}
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.FlatList
|
||||
// @ts-ignore
|
||||
dataSet={dataSet}
|
||||
ref={ref}
|
||||
keyExtractor={props.keyExtractor || (i => i.key)}
|
||||
viewabilityConfig={viewabilityConfig}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
/**
|
||||
* iOS automatically adds in the safe area to the scroll indicator
|
||||
* insets, even though the overwhelming majority of our ScrollViews do
|
||||
* not stretch from edge to edge.
|
||||
* @see https://github.com/bluesky-social/social-app/pull/7131
|
||||
*/
|
||||
automaticallyAdjustsScrollIndicatorInsets={false}
|
||||
/**
|
||||
* For better UX, we default to true, but it can be disabled if needed.
|
||||
* @see https://github.com/bluesky-social/social-app/pull/8529
|
||||
*/
|
||||
showsVerticalScrollIndicator
|
||||
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
|
||||
scrollIndicatorInsets={{
|
||||
top: props.headerOffset ?? 0,
|
||||
bottom: props.footerOffset ?? 0,
|
||||
/**
|
||||
* May fix a bug where the scroll indicator is in the middle of the screen
|
||||
* @see https://github.com/facebook/react-native/issues/26610
|
||||
*/
|
||||
right: 1,
|
||||
}}
|
||||
/**
|
||||
* Native only. On web, we use padding on `style` instead.
|
||||
*/
|
||||
contentOffset={
|
||||
props.headerOffset ? {x: 0, y: props.headerOffset * -1} : undefined
|
||||
}
|
||||
scrollsToTop={!activeLightbox}
|
||||
refreshControl={refreshControl}
|
||||
{...(props as FlatListPropsWithLayout<Item>)}
|
||||
style={[
|
||||
a.h_full,
|
||||
{
|
||||
paddingTop: props.headerOffset,
|
||||
paddingBottom: props.footerOffset,
|
||||
transform: 'unset',
|
||||
},
|
||||
props.style,
|
||||
]}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}) as <Item extends {key: string}>(
|
||||
props: ListProps<Item> & {ref?: React.Ref<FlatList<Item>>},
|
||||
) => React.ReactElement
|
||||
@@ -34,6 +34,7 @@ export type CommonNavigatorParams = {
|
||||
ProfileLabelerLikedBy: {name: string}
|
||||
Debug: undefined
|
||||
DebugMod: undefined
|
||||
StorybookLists: undefined
|
||||
SharedPreferencesTester: undefined
|
||||
Log: undefined
|
||||
Support: undefined
|
||||
|
||||
@@ -39,6 +39,7 @@ export const router = new Router<AllNavigatableRoutes>({
|
||||
// debug
|
||||
Debug: '/sys/debug',
|
||||
DebugMod: '/sys/debug-mod',
|
||||
StorybookLists: '/sys/debug-lists',
|
||||
Log: '/sys/log',
|
||||
// settings
|
||||
LanguageSettings: '/settings/language',
|
||||
|
||||
@@ -389,3 +389,25 @@ input[type='range'][orient='vertical']::-moz-range-thumb {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Sticky headers for `List` component. Add more indices as needed.
|
||||
*
|
||||
* Note — :nth-child is 1-indexed
|
||||
*/
|
||||
*[data-sticky-header-index-0] > div > div:nth-child(1),
|
||||
*[data-sticky-header-index-1] > div > div:nth-child(2),
|
||||
*[data-sticky-header-index-2] > div > div:nth-child(3),
|
||||
*[data-sticky-header-index-3] > div > div:nth-child(4),
|
||||
*[data-sticky-header-index-4] > div > div:nth-child(5),
|
||||
*[data-sticky-header-index-5] > div > div:nth-child(6),
|
||||
*[data-sticky-header-index-6] > div > div:nth-child(7),
|
||||
*[data-sticky-header-index-7] > div > div:nth-child(8),
|
||||
*[data-sticky-header-index-8] > div > div:nth-child(9),
|
||||
*[data-sticky-header-index-9] > div > div:nth-child(10) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ import {View} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
@@ -17,6 +19,7 @@ import {ButtonIcon} from '#/components/Button'
|
||||
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Link} from '#/components/Link'
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
export function HomeHeaderLayoutMobile({
|
||||
children,
|
||||
@@ -30,6 +33,7 @@ export function HomeHeaderLayoutMobile({
|
||||
const headerMinimalShellTransform = useMinimalShellHeaderTransform()
|
||||
const {hasSession} = useSession()
|
||||
const playHaptic = useHaptics()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
@@ -57,7 +61,11 @@ export function HomeHeaderLayoutMobile({
|
||||
targetScale={0.9}
|
||||
onPress={() => {
|
||||
playHaptic('Light')
|
||||
emitSoftReset()
|
||||
if (IS_DEV) {
|
||||
navigation.navigate('Debug')
|
||||
} else {
|
||||
emitSoftReset()
|
||||
}
|
||||
}}>
|
||||
<Logo width={30} />
|
||||
</PressableScale>
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type FlatList, View} from 'react-native'
|
||||
|
||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||
import {List, type ListMethods} from '#/view/com/util/List'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {List, ListScrollProvider} from '#/components/List'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function ListContained() {
|
||||
const [animated, setAnimated] = React.useState(false)
|
||||
const ref = React.useRef<ListMethods>(null)
|
||||
const ref = React.useRef<FlatList>(null)
|
||||
|
||||
const data = React.useMemo(() => {
|
||||
return Array.from({length: 100}, (_, i) => ({
|
||||
id: i,
|
||||
key: i + '',
|
||||
text: `Message ${i}`,
|
||||
}))
|
||||
}, [])
|
||||
@@ -21,7 +20,7 @@ export function ListContained() {
|
||||
return (
|
||||
<>
|
||||
<View style={{width: '100%', height: 300}}>
|
||||
<ScrollProvider
|
||||
<ListScrollProvider
|
||||
onScroll={e => {
|
||||
'worklet'
|
||||
console.log(
|
||||
@@ -33,6 +32,7 @@ export function ListContained() {
|
||||
)
|
||||
}}>
|
||||
<List
|
||||
ref={ref}
|
||||
data={data}
|
||||
renderItem={item => {
|
||||
return (
|
||||
@@ -46,8 +46,8 @@ export function ListContained() {
|
||||
</View>
|
||||
)
|
||||
}}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
disableFullWindowScroll={true}
|
||||
keyExtractor={item => item.key.toString()}
|
||||
// disableFullWindowScroll={true}
|
||||
style={{flex: 1}}
|
||||
onStartReached={() => {
|
||||
console.log('Start Reached')
|
||||
@@ -56,10 +56,9 @@ export function ListContained() {
|
||||
console.log('End Reached (threshold of 2)')
|
||||
}}
|
||||
onEndReachedThreshold={2}
|
||||
ref={ref}
|
||||
disableVirtualization={true}
|
||||
/>
|
||||
</ScrollProvider>
|
||||
</ListScrollProvider>
|
||||
</View>
|
||||
|
||||
<View style={{flexDirection: 'row', gap: 10, alignItems: 'center'}}>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 {Link} from '#/components/Link'
|
||||
import {Admonitions} from './Admonitions'
|
||||
import {Breakpoints} from './Breakpoints'
|
||||
import {Buttons} from './Buttons'
|
||||
@@ -96,6 +97,14 @@ function StorybookInner() {
|
||||
<ButtonText>Open Shared Prefs Tester</ButtonText>
|
||||
</Button>
|
||||
|
||||
<Link
|
||||
to="/sys/debug-lists"
|
||||
label="Lists debugger"
|
||||
size="small"
|
||||
color="primary_subtle">
|
||||
<ButtonText>Lists debugger</ButtonText>
|
||||
</Link>
|
||||
|
||||
<ThemeProvider theme="light">
|
||||
<Theming />
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import {View} from 'react-native'
|
||||
import {runOnJS} from 'react-native-reanimated'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {ListScrollProvider, useListScrollHandler} from '#/components/List'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function StorybookLists() {
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Inner />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
type Item =
|
||||
| {
|
||||
key: string
|
||||
type: 'item'
|
||||
title: string
|
||||
}
|
||||
| {
|
||||
key: string
|
||||
type: 'header'
|
||||
title: string
|
||||
}
|
||||
| {
|
||||
key: string
|
||||
type: 'spacer'
|
||||
}
|
||||
|
||||
const log = (msg: any) => console.log(msg)
|
||||
|
||||
export function Inner() {
|
||||
const t = useTheme()
|
||||
const onScrollWorklet = useListScrollHandler(e => {
|
||||
'worklet'
|
||||
runOnJS(log)(`scroll ${e.contentOffset.y}`)
|
||||
}, [])
|
||||
|
||||
const items: Item[] = Array.from({length: 1000}).map((_, i) => ({
|
||||
key: `item-${i + 1}`,
|
||||
type: 'item' as const,
|
||||
title: `Item ${i + 1}`,
|
||||
}))
|
||||
|
||||
items.unshift({
|
||||
key: 'header',
|
||||
type: 'header' as const,
|
||||
title: 'Header',
|
||||
})
|
||||
|
||||
items.unshift({
|
||||
key: 'spacer',
|
||||
type: 'spacer' as const,
|
||||
})
|
||||
|
||||
return (
|
||||
<ListScrollProvider onScroll={onScrollWorklet}>
|
||||
<Layout.List<Item>
|
||||
windowSize={9}
|
||||
maxToRenderPerBatch={5}
|
||||
data={items}
|
||||
stickyHeaderIndices={[1]}
|
||||
renderItem={({item, index}) => {
|
||||
if (item.type === 'header') {
|
||||
return (
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>Storybook</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
)
|
||||
}
|
||||
if (item.type === 'spacer') {
|
||||
return <View style={[t.atoms.bg_contrast_50, {height: 100}]} />
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.px_md,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{height: 100},
|
||||
index % 2 === 0 ? t.atoms.bg_contrast_25 : t.atoms.bg,
|
||||
]}>
|
||||
<Text>{item.title}</Text>
|
||||
</View>
|
||||
)
|
||||
}}
|
||||
onScrolledDownChange={scrolledDown => {
|
||||
console.log(`Scrolled down: ${scrolledDown}`)
|
||||
}}
|
||||
/>
|
||||
</ListScrollProvider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user