Skip to content

Commit d6250e2

Browse files
committed
feat: added a scrollable creator hook to allow integrate with third party list libraries
1 parent 248ddd9 commit d6250e2

File tree

10 files changed

+299
-5
lines changed

10 files changed

+299
-5
lines changed

example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@react-navigation/native": "^7.1.9",
1717
"@react-navigation/native-stack": "^7.3.13",
1818
"@react-navigation/stack": "^7.3.2",
19+
"@legendapp/list": "^1.1.4",
1920
"@shopify/flash-list": "1.7.6",
2021
"expo": "53.0.20",
2122
"expo-asset": "~11.1.7",

example/src/screens/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import type { ShowcaseExampleScreenSectionType } from '@gorhom/showcase-template';
22
import { Platform } from 'react-native';
33

4-
const screens: ShowcaseExampleScreenSectionType[] = [];
4+
const screens: Array<object> = [
5+
{
6+
name: '🔥 LegendList',
7+
slug: 'Integrations/LegendList-featured',
8+
title: '🔥 LegendList',
9+
getScreen: () => require('./integrations/legendlist').default,
10+
},
11+
];
512

613
//#region Basic Section
714
const basicSection = {
@@ -168,6 +175,11 @@ if (Platform.OS !== 'web') {
168175
slug: 'Integrations/FlashList',
169176
getScreen: () => require('./integrations/flashlist').default,
170177
},
178+
{
179+
name: 'LegendList',
180+
slug: 'Integrations/LegendList',
181+
getScreen: () => require('./integrations/legendlist').default,
182+
},
171183
],
172184
collapsed: true,
173185
};

example/src/screens/integrations/flashlist/FlashListExample.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import BottomSheet, { BottomSheetFlashList } from '@gorhom/bottom-sheet';
1+
import BottomSheet, {
2+
useBottomSheetScrollableCreator,
3+
} from '@gorhom/bottom-sheet';
4+
import { FlashList, type ListRenderItemInfo } from '@shopify/flash-list';
25
import React, { useCallback, useMemo, useRef, useState } from 'react';
36
import {
47
ActivityIndicator,
5-
type ListRenderItemInfo,
68
StyleSheet,
79
Text,
810
View,
@@ -61,6 +63,7 @@ const FlashListExample = () => {
6163
() => <Footer isLoading={tweets.length !== tweetsData.length} />,
6264
[tweets]
6365
);
66+
const BottomSheetFlashListScrollable = useBottomSheetScrollableCreator();
6467
return (
6568
<View style={styles.container}>
6669
<Button label="Snap To 90%" onPress={() => handleSnapPress(2)} />
@@ -74,7 +77,7 @@ const FlashListExample = () => {
7477
snapPoints={snapPoints}
7578
enableDynamicSizing={false}
7679
>
77-
<BottomSheetFlashList
80+
<FlashList
7881
keyExtractor={keyExtractor}
7982
renderItem={renderItem}
8083
onEndReached={handleOnEndReached}
@@ -84,6 +87,7 @@ const FlashListExample = () => {
8487
ItemSeparatorComponent={Divider}
8588
data={tweets}
8689
viewabilityConfig={viewabilityConfig}
90+
renderScrollComponent={BottomSheetFlashListScrollable}
8791
/>
8892
</BottomSheet>
8993
</View>

example/src/screens/integrations/flashlist/TweetContent.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ const styles = StyleSheet.create({
112112
width: 18,
113113
height: 18,
114114
marginRight: 8,
115-
backgroundColor: 'red',
116115
},
117116
gray: {
118117
color: '#777',
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import BottomSheet, {
2+
useBottomSheetScrollableCreator,
3+
} from '@gorhom/bottom-sheet';
4+
import { LegendList, type LegendListRef } from '@legendapp/list';
5+
import React, { useCallback, useRef } from 'react';
6+
import { StyleSheet, View } from 'react-native';
7+
import { Button } from '../../../components/button';
8+
import renderItem from './renderItem';
9+
10+
export const DRAW_DISTANCE = 200;
11+
export const ESTIMATED_ITEM_LENGTH = 200;
12+
13+
const snapPoints = ['50%', '85%'];
14+
const data = new Array(500).fill(0).map((_, i) => ({
15+
id: i.toString(),
16+
type: 'item',
17+
}));
18+
19+
const keyExtractor = (item: (typeof data)[0]) => `id${item.id}`;
20+
21+
const LegendListExample = () => {
22+
//#region refs
23+
const bottomSheetRef = useRef<BottomSheet>(null);
24+
//#endregion
25+
26+
//#region callbacks
27+
const handleExpandPress = useCallback(() => {
28+
bottomSheetRef.current?.expand();
29+
}, []);
30+
const handleCollapsePress = useCallback(() => {
31+
bottomSheetRef.current?.collapse();
32+
}, []);
33+
const handleClosePress = useCallback(() => {
34+
bottomSheetRef.current?.close();
35+
}, []);
36+
//#endregion
37+
38+
//#region renders
39+
const BottomSheetLegendListScrollable = useBottomSheetScrollableCreator();
40+
//#endregion
41+
42+
return (
43+
<View style={styles.container}>
44+
<Button label="Expand" onPress={handleExpandPress} />
45+
<Button label="Collapse" onPress={handleCollapsePress} />
46+
<Button label="Close" onPress={handleClosePress} />
47+
<BottomSheet
48+
ref={bottomSheetRef}
49+
enableDynamicSizing={false}
50+
snapPoints={snapPoints}
51+
>
52+
<LegendList
53+
style={styles.scrollContainer}
54+
contentContainerStyle={styles.listContainer}
55+
data={data}
56+
renderItem={renderItem}
57+
keyExtractor={keyExtractor}
58+
indicatorStyle="black"
59+
estimatedItemSize={ESTIMATED_ITEM_LENGTH}
60+
drawDistance={DRAW_DISTANCE}
61+
maintainVisibleContentPosition
62+
renderScrollComponent={BottomSheetLegendListScrollable}
63+
recycleItems
64+
/>
65+
</BottomSheet>
66+
</View>
67+
);
68+
};
69+
70+
const styles = StyleSheet.create({
71+
container: {
72+
flex: 1,
73+
padding: 24,
74+
},
75+
scrollContainer: {
76+
flex: 1,
77+
height: 0.1,
78+
},
79+
listContainer: {},
80+
});
81+
82+
export default LegendListExample;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './LegendListExample';
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import type { LegendListRenderItemProps } from '@legendapp/list';
2+
import Faker from 'faker';
3+
import { memo, useRef, useState } from 'react';
4+
import {
5+
Animated,
6+
Image,
7+
Platform,
8+
Pressable,
9+
StyleSheet,
10+
Text,
11+
UIManager,
12+
View,
13+
} from 'react-native';
14+
import { RectButton } from 'react-native-gesture-handler';
15+
16+
export interface Item {
17+
id: string;
18+
}
19+
20+
// Generate random metadata
21+
const randomAvatars = Array.from(
22+
{ length: 20 },
23+
(_, i) => `https://i.pravatar.cc/150?img=${i + 1}`
24+
);
25+
26+
if (Platform.OS === 'android') {
27+
if (UIManager.setLayoutAnimationEnabledExperimental) {
28+
UIManager.setLayoutAnimationEnabledExperimental(true);
29+
}
30+
}
31+
32+
export const ItemCard = memo(({ item }: LegendListRenderItemProps<Item>) => {
33+
const indexForData = item.id.includes('new')
34+
? 100 + +item.id.replace('new', '')
35+
: +item.id;
36+
37+
const randomText = Faker.lorem.sentences(10);
38+
const avatarUrl = randomAvatars[indexForData % randomAvatars.length];
39+
const authorName = Faker.name.firstName();
40+
const timestamp = `${Math.max(1, indexForData % 24)}h ago`;
41+
42+
return (
43+
<View style={styles.itemOuterContainer}>
44+
<View style={styles.itemContainer}>
45+
<View style={styles.headerContainer}>
46+
<Image source={{ uri: avatarUrl }} style={styles.avatar} />
47+
<View style={styles.headerText}>
48+
<Text style={styles.authorName}>
49+
{authorName} {item.id}
50+
</Text>
51+
<Text style={styles.timestamp}>{timestamp}</Text>
52+
</View>
53+
</View>
54+
55+
<Text style={styles.itemTitle}>Item #{item.id}</Text>
56+
<Text style={styles.itemBody}>{randomText}</Text>
57+
<View style={styles.itemFooter}>
58+
<Text style={styles.footerText}>❤️ 42</Text>
59+
<Text style={styles.footerText}>💬 12</Text>
60+
<Text style={styles.footerText}>🔄 8</Text>
61+
</View>
62+
</View>
63+
</View>
64+
);
65+
});
66+
67+
export const renderItem = (props: LegendListRenderItemProps<Item>) => (
68+
<ItemCard {...props} />
69+
);
70+
71+
const styles = StyleSheet.create({
72+
itemOuterContainer: {
73+
padding: 12,
74+
},
75+
itemContainer: {
76+
overflow: 'hidden',
77+
},
78+
titleContainer: {
79+
flexDirection: 'row',
80+
alignItems: 'center',
81+
gap: 8,
82+
},
83+
stepContainer: {
84+
gap: 8,
85+
marginBottom: 8,
86+
},
87+
listContainer: {
88+
paddingHorizontal: 16,
89+
},
90+
itemTitle: {
91+
fontSize: 18,
92+
fontWeight: 'bold',
93+
marginBottom: 8,
94+
color: '#1a1a1a',
95+
},
96+
itemBody: {
97+
fontSize: 14,
98+
color: '#666666',
99+
lineHeight: 20,
100+
// flex: 1,
101+
},
102+
itemFooter: {
103+
flexDirection: 'row',
104+
justifyContent: 'flex-start',
105+
gap: 16,
106+
marginTop: 12,
107+
paddingTop: 12,
108+
borderTopWidth: 1,
109+
borderTopColor: '#f0f0f0',
110+
},
111+
footerText: {
112+
fontSize: 14,
113+
color: '#888888',
114+
},
115+
headerContainer: {
116+
flexDirection: 'row',
117+
alignItems: 'center',
118+
marginBottom: 12,
119+
},
120+
avatar: {
121+
width: 40,
122+
height: 40,
123+
borderRadius: 20,
124+
marginRight: 12,
125+
},
126+
headerText: {
127+
flex: 1,
128+
},
129+
authorName: {
130+
fontSize: 16,
131+
fontWeight: '600',
132+
color: '#1a1a1a',
133+
},
134+
timestamp: {
135+
fontSize: 12,
136+
color: '#888888',
137+
marginTop: 2,
138+
},
139+
});
140+
141+
export default renderItem;

src/components/bottomSheetScrollable/BottomSheetFlashList.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ const BottomSheetFlashListComponent = forwardRef<
4545
if (!FlashList) {
4646
throw 'You need to install FlashList first, `yarn install @shopify/flash-list`';
4747
}
48+
49+
console.warn(
50+
'BottomSheetFlashList is deprecated, please use useBottomSheetScrollableCreator instead.'
51+
);
4852
}, []);
4953

5054
//#region render
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { ReactElement } from 'react';
2+
import {
3+
BottomSheetScrollView,
4+
type BottomSheetScrollableProps,
5+
} from '../components/bottomSheetScrollable';
6+
7+
type BottomSheetScrollableCreatorConfigs = {} & BottomSheetScrollableProps;
8+
9+
/**
10+
* A custom hook that creates a scrollable component for third-party libraries
11+
* like `LegendList` or `FlashList` to integrate the interaction and scrolling
12+
* behaviors with th BottomSheet component.
13+
*
14+
* @param configs - Configuration options for the scrollable creator.
15+
* @param configs.focusHook - This needed when bottom sheet used with multiple scrollables to allow bottom sheet
16+
* detect the current scrollable ref, especially when used with `React Navigation`.
17+
* You will need to provide `useFocusEffect` from `@react-navigation/native`.
18+
* @param configs.scrollEventsHandlersHook - Custom hook to provide scroll events handler, which will allow advance and
19+
* customize handling for scrollables.
20+
* @param configs.enableFooterMarginAdjustment - Adjust the scrollable bottom margin to avoid the animated footer.
21+
*
22+
* @example
23+
* ```tsx
24+
* const BottomSheetLegendListScrollable = useBottomSheetScrollableCreator();
25+
*
26+
* // Usage in JSX
27+
* <LegendList
28+
* renderScrollComponent={BottomSheetLegendListScrollable}
29+
* />
30+
* ```
31+
*/
32+
// biome-ignore lint/suspicious/noExplicitAny: out of my control
33+
export function useBottomSheetScrollableCreator<T = any>({
34+
focusHook,
35+
scrollEventsHandlersHook,
36+
enableFooterMarginAdjustment,
37+
}: BottomSheetScrollableCreatorConfigs = {}): (props: T) => ReactElement<T> {
38+
// @ts-ignore
39+
return ({ data: _, ...props }: T, ref: never) => (
40+
// @ts-ignore
41+
<BottomSheetScrollView
42+
ref={ref}
43+
{...props}
44+
focusHook={focusHook}
45+
scrollEventsHandlersHook={scrollEventsHandlersHook}
46+
enableFooterMarginAdjustment={enableFooterMarginAdjustment}
47+
/>
48+
);
49+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export { useGestureEventsHandlersDefault } from './hooks/useGestureEventsHandler
1717
export { useBottomSheetGestureHandlers } from './hooks/useBottomSheetGestureHandlers';
1818
export { useScrollHandler } from './hooks/useScrollHandler';
1919
export { useScrollableSetter } from './hooks/useScrollableSetter';
20+
export { useBottomSheetScrollableCreator } from './hooks/useBottomSheetScrollableCreator';
2021
//#endregion
2122

2223
//#region components

0 commit comments

Comments
 (0)