Checkpoint external sticky header
This commit is contained in:
+101
-54
@@ -1,8 +1,9 @@
|
|||||||
import {forwardRef, useMemo} from 'react'
|
import {forwardRef, useMemo, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
type FlatList,
|
type FlatList,
|
||||||
type FlatListProps,
|
type FlatListProps,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
|
View,
|
||||||
type ViewToken,
|
type ViewToken,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
@@ -14,9 +15,9 @@ import Animated, {
|
|||||||
import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video'
|
import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video'
|
||||||
|
|
||||||
import {useDedupe} from '#/lib/hooks/useDedupe'
|
import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||||
import {isIOS, isNative} from '#/platform/detection'
|
import {isIOS, isNative, isWeb} from '#/platform/detection'
|
||||||
import {useLightbox} from '#/state/lightbox'
|
import {useLightbox} from '#/state/lightbox'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, platform, useTheme} from '#/alf'
|
||||||
import {useListScrollContext} from '#/components/List/ListScrollProvider'
|
import {useListScrollContext} from '#/components/List/ListScrollProvider'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -85,10 +86,19 @@ type ListProps<Item extends {key: string}> = Omit<
|
|||||||
*/
|
*/
|
||||||
didScrollDownThreshold?: number
|
didScrollDownThreshold?: number
|
||||||
onScrolledDownChange?: (isScrolledDown: boolean) => void
|
onScrolledDownChange?: (isScrolledDown: boolean) => void
|
||||||
|
|
||||||
|
StickyHeaderComponent?: FlatListProps<Item>['ListHeaderComponent']
|
||||||
|
stickyHeaderOffset?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const List = forwardRef(function List<Item extends {key: string}>(
|
export const List = forwardRef(function List<Item extends {key: string}>(
|
||||||
props: ListProps<Item>,
|
{
|
||||||
|
stickyHeaderIndices,
|
||||||
|
ListHeaderComponent,
|
||||||
|
StickyHeaderComponent,
|
||||||
|
stickyHeaderOffset,
|
||||||
|
...props
|
||||||
|
}: ListProps<Item>,
|
||||||
ref: React.Ref<FlatList<Item>>,
|
ref: React.Ref<FlatList<Item>>,
|
||||||
) {
|
) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -167,58 +177,95 @@ export const List = forwardRef(function List<Item extends {key: string}>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const header = useMemo(() => {
|
||||||
<Animated.FlatList
|
if (isWeb) return {}
|
||||||
ref={ref}
|
|
||||||
keyExtractor={props.keyExtractor || (i => i.key)}
|
if (StickyHeaderComponent) {
|
||||||
viewabilityConfig={viewabilityConfig}
|
if (ListHeaderComponent) {
|
||||||
onViewableItemsChanged={onViewableItemsChanged}
|
throw new Error(
|
||||||
/**
|
`List: cannot use both StickyHeaderComponent and ListHeaderComponent`,
|
||||||
* 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}
|
return {
|
||||||
{...(props as FlatListPropsWithLayout<Item>)}
|
ListHeaderComponent: platform({
|
||||||
style={[
|
default: StickyHeaderComponent,
|
||||||
{
|
// web: () => (
|
||||||
paddingTop: props.headerOffset,
|
// <View style={[a.fixed]}>
|
||||||
paddingBottom: props.footerOffset,
|
// <StickyHeaderComponent />
|
||||||
},
|
// </View>
|
||||||
web([
|
// )
|
||||||
/*
|
}),
|
||||||
* On web, the List should always fill its container, otherwise
|
stickyHeaderIndices: [0],
|
||||||
* `onScroll` will not work due to the entire page scrolling.
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {ListHeaderComponent, stickyHeaderIndices}
|
||||||
|
}, [ListHeaderComponent, StickyHeaderComponent, stickyHeaderIndices])
|
||||||
|
|
||||||
|
const [stickyHeaderHeight, setStickyHeaderHeight] =
|
||||||
|
useState(stickyHeaderOffset)
|
||||||
|
|
||||||
|
const headerOffset = isWeb ? stickyHeaderHeight : (props.headerOffset ?? 0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isWeb && StickyHeaderComponent && (
|
||||||
|
<View
|
||||||
|
style={[a.fixed, a.w_full, a.z_10]}
|
||||||
|
onLayout={e => {
|
||||||
|
setStickyHeaderHeight(e.nativeEvent.layout.height)
|
||||||
|
}}>
|
||||||
|
<StickyHeaderComponent />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Animated.FlatList
|
||||||
|
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: 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
|
||||||
*/
|
*/
|
||||||
a.h_full,
|
right: 1,
|
||||||
]),
|
}}
|
||||||
]}
|
/**
|
||||||
onScroll={onScroll}
|
* Native only. On web, we use padding on `style` instead.
|
||||||
/>
|
*/
|
||||||
|
contentOffset={headerOffset ? {x: 0, y: headerOffset * -1} : undefined}
|
||||||
|
scrollsToTop={!activeLightbox}
|
||||||
|
refreshControl={refreshControl}
|
||||||
|
{...(props as FlatListPropsWithLayout<Item>)}
|
||||||
|
{...header}
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
paddingTop: headerOffset,
|
||||||
|
paddingBottom: props.footerOffset,
|
||||||
|
transform: 'unset',
|
||||||
|
},
|
||||||
|
props.style,
|
||||||
|
]}
|
||||||
|
onScroll={onScroll}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}) as <Item extends {key: string}>(
|
}) as <Item extends {key: string}>(
|
||||||
props: ListProps<Item> & {ref?: React.Ref<FlatList<Item>>},
|
props: ListProps<Item> & {ref?: React.Ref<FlatList<Item>>},
|
||||||
|
|||||||
@@ -1,30 +1,18 @@
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {runOnJS} from 'react-native-reanimated'
|
import {runOnJS} from 'react-native-reanimated'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
|
||||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||||
import {List as OldList} from '#/view/com/util/List'
|
import {List as OldList} from '#/view/com/util/List'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
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 {List, ListScrollProvider, useListScrollHandler} from '#/components/List'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
export function StorybookLists() {
|
export function StorybookLists() {
|
||||||
return (
|
return <Inner />
|
||||||
<Layout.Screen>
|
|
||||||
<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>
|
|
||||||
<Layout.Content keyboardShouldPersistTaps="handled">
|
|
||||||
<Inner />
|
|
||||||
</Layout.Content>
|
|
||||||
</Layout.Screen>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = Array.from({length: 100}).map((_, i) => ({
|
const items = Array.from({length: 100}).map((_, i) => ({
|
||||||
@@ -39,7 +27,21 @@ type Item = {
|
|||||||
|
|
||||||
const log = (msg: any) => console.log(msg)
|
const log = (msg: any) => console.log(msg)
|
||||||
|
|
||||||
|
function 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function Inner() {
|
export function Inner() {
|
||||||
|
const t = useTheme()
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
const [old, setOld] = useState<boolean>(false)
|
const [old, setOld] = useState<boolean>(false)
|
||||||
const onScrollWorklet = useListScrollHandler(e => {
|
const onScrollWorklet = useListScrollHandler(e => {
|
||||||
'worklet'
|
'worklet'
|
||||||
@@ -47,7 +49,7 @@ export function Inner() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.h_full_vh]}>
|
<View style={[]}>
|
||||||
<View
|
<View
|
||||||
style={[a.fixed, a.w_full, a.py_sm, a.px_xl, a.align_center, a.z_10]}>
|
style={[a.fixed, a.w_full, a.py_sm, a.px_xl, a.align_center, a.z_10]}>
|
||||||
<Button
|
<Button
|
||||||
@@ -65,15 +67,25 @@ export function Inner() {
|
|||||||
<ListScrollProvider onScroll={onScrollWorklet}>
|
<ListScrollProvider onScroll={onScrollWorklet}>
|
||||||
<List<Item>
|
<List<Item>
|
||||||
data={items}
|
data={items}
|
||||||
headerOffset={100}
|
headerOffset={insets.top}
|
||||||
footerOffset={100}
|
stickyHeaderOffset={52}
|
||||||
|
StickyHeaderComponent={Header}
|
||||||
onScrolledDownChange={scrolledDown => {
|
onScrolledDownChange={scrolledDown => {
|
||||||
console.log(`Scrolled down: ${scrolledDown}`)
|
console.log(`Scrolled down: ${scrolledDown}`)
|
||||||
}}
|
}}
|
||||||
renderItem={({item}) => (
|
renderItem={({item, index}) => (
|
||||||
<View style={[a.p_md, a.border_b]}>
|
<Layout.Center>
|
||||||
<Text>{item.title}</Text>
|
<View
|
||||||
</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>
|
||||||
|
</Layout.Center>
|
||||||
)}
|
)}
|
||||||
style={[a.debug]}
|
style={[a.debug]}
|
||||||
/>
|
/>
|
||||||
@@ -93,6 +105,7 @@ export function Inner() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
style={[a.debug]}
|
style={[a.debug]}
|
||||||
|
desktopFixedHeight
|
||||||
/>
|
/>
|
||||||
</ScrollProvider>
|
</ScrollProvider>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user