diff --git a/src/components/List/index.tsx b/src/components/List/index.tsx index 52333b4d64..ae696b3df4 100644 --- a/src/components/List/index.tsx +++ b/src/components/List/index.tsx @@ -5,7 +5,7 @@ import Animated, { useAnimatedScrollHandler, } from 'react-native-reanimated' -import {atoms as a, web} from '#/alf' +import {atoms as a, useTheme, web} from '#/alf' import {useListScrollContext} from '#/components/List/ListScrollProvider' export { @@ -16,16 +16,31 @@ export { /** * Cleaned up FlatList without some problematic props. * - * - `contentOffset` - Doesn't work, use padding on `contentContainerStyle` instead. + * - `contentOffset` - Use `headerOffset` or `footerOffset` */ type ListProps = Omit, '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 } export const List = forwardRef(function List( props: ListProps, ref: React.Ref>, ) { + const t = useTheme() const scrollHandlers = useListScrollContext() const onScroll = useAnimatedScrollHandler({ onScroll(e, ctx) { @@ -71,7 +86,7 @@ export const List = forwardRef(function List( }, [props.onItemSeen]) return ( - + ( * @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, + }} + /** + * iOS-only, should match `scrollIndicatorInsets` + * @see https://reactnative.dev/docs/scrollview#contentinset-ios + */ + contentInset={{ + top: props.headerOffset ?? 0, + bottom: props.footerOffset ?? 0, + }} + /** + * Native only. On web, we use padding on `style` instead. + */ + contentOffset={ + props.headerOffset ? {x: 0, y: props.headerOffset * -1} : undefined + } {...(props as FlatListPropsWithLayout)} 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), + web([ + a.h_full, + { + paddingTop: props.headerOffset, + paddingBottom: props.footerOffset, + }, + ]), ]} onScroll={onScroll} /> diff --git a/src/view/screens/StorybookLists/index.tsx b/src/view/screens/StorybookLists/index.tsx index bd5cfbc22e..7a4385cf56 100644 --- a/src/view/screens/StorybookLists/index.tsx +++ b/src/view/screens/StorybookLists/index.tsx @@ -46,6 +46,8 @@ export function Inner() { data={items} + headerOffset={100} + footerOffset={100} renderItem={({item}) => ( {item.title}