Skip to content

Commit d87a081

Browse files
committed
Added new FlashList component as a drop-in replacement for @shopify/flash-list
Also updated ScrollView and VirtualizedList to work properly with the new clipping changes in lightning
1 parent 5f26ca9 commit d87a081

File tree

26 files changed

+1168
-1686
lines changed

26 files changed

+1168
-1686
lines changed

.changeset/ready-wasps-brush.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@plexinc/react-native-lightning-components": patch
3+
"@plexinc/react-native-lightning-example": patch
4+
"@plexinc/react-native-lightning": patch
5+
"@plexinc/react-lightning-storybook": patch
6+
---
7+
8+
Updated ScrollView and VirtualizedListView packages, and added a new FlashList implementation for RN

apps/react-native-lightning-example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@plexinc/react-lightning-plugin-flexbox": "workspace:*",
2828
"@plexinc/react-lightning-plugin-reanimated": "workspace:*",
2929
"@plexinc/react-native-lightning": "workspace:*",
30+
"@plexinc/react-native-lightning-components": "workspace:*",
3031
"@react-navigation/native": "7.0.14",
3132
"@react-navigation/stack": "7.1.1",
3233
"@shopify/flash-list": "1.7.3",
@@ -38,7 +39,6 @@
3839
},
3940
"devDependencies": {
4041
"@babel/core": "7.26.9",
41-
"@babel/runtime": "7.26.9",
4242
"@plexinc/vite-plugin-msdf-fontgen": "workspace:*",
4343
"@plexinc/vite-plugin-react-native-lightning": "workspace:*",
4444
"@plexinc/vite-plugin-react-reanimated-lightning": "workspace:*",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { focusable } from '@plexinc/react-lightning';
2+
import { View } from '@plexinc/react-native-lightning';
3+
import { type ReactNode, useEffect } from 'react';
4+
import { type ColorValue, Text } from 'react-native';
5+
6+
export type ScrollItemProps = {
7+
children: ReactNode;
8+
index: number;
9+
horizontal?: boolean;
10+
color: ColorValue;
11+
onFocused: (index: number) => void;
12+
};
13+
14+
const ScrollItem = focusable<ScrollItemProps, View>(
15+
({ color, index, horizontal, focused, children, onFocused }, ref) => {
16+
useEffect(() => {
17+
if (focused) {
18+
onFocused(index);
19+
}
20+
}, [index, focused, onFocused]);
21+
22+
return (
23+
<View
24+
ref={ref}
25+
style={{
26+
width: horizontal ? 75 : 200,
27+
height: horizontal ? 200 : 75,
28+
borderWidth: focused ? 0 : 1,
29+
borderStyle: 'solid',
30+
borderColor: color,
31+
backgroundColor: focused ? color : 'transparent',
32+
display: 'flex' as const,
33+
justifyContent: 'center' as const,
34+
alignItems: 'center' as const,
35+
borderRadius: 4,
36+
}}
37+
>
38+
<Text
39+
style={{
40+
color: focused ? 'black' : color,
41+
transform: `rotate(${horizontal ? '-90deg' : '0deg'})`,
42+
}}
43+
>
44+
{children}
45+
</Text>
46+
</View>
47+
);
48+
},
49+
);
50+
51+
export default ScrollItem;

apps/react-native-lightning-example/src/index.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ComponentTest } from './pages/ComponentTest';
1717
import { FlashListTest } from './pages/FlashListTest';
1818
import { LayoutTest } from './pages/LayoutTest';
1919
import { LibraryTest } from './pages/LibraryTest';
20+
import { VirtualizedListTest } from './pages/VirtualizedListTest';
2021

2122
const Stack = createStackNavigator();
2223

@@ -26,6 +27,7 @@ const screens = {
2627
Library: 'library',
2728
Components: 'components',
2829
NestedLayouts: 'nestedLayouts',
30+
VirtualizedList: 'virtualizedList',
2931
FlashList: 'flashList',
3032
};
3133

@@ -73,6 +75,11 @@ const MainApp = () => {
7375
color={'rgba(55, 55, 22, 1)'}
7476
onPress={() => nav.navigate('Components')}
7577
/>
78+
<Button
79+
title="VirtualizedList"
80+
color={'rgba(55, 55, 22, 1)'}
81+
onPress={() => nav.navigate('VirtualizedList')}
82+
/>
7683
<Button
7784
title="FlashList"
7885
color={'rgba(55, 55, 22, 1)'}
@@ -95,6 +102,10 @@ const MainApp = () => {
95102
<Stack.Screen name="Library" component={LibraryTest} />
96103
<Stack.Screen name="Components" component={ComponentTest} />
97104
<Stack.Screen name="FlashList" component={FlashListTest} />
105+
<Stack.Screen
106+
name="VirtualizedList"
107+
component={VirtualizedListTest}
108+
/>
98109
</Stack.Navigator>
99110
</Column>
100111
</Row>

apps/react-native-lightning-example/src/pages/ComponentTest.tsx

Lines changed: 14 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,34 @@
1+
import type { LightningElement } from '@plexinc/react-lightning';
12
import { Column, Row } from '@plexinc/react-lightning-components';
3+
import { ScrollView } from '@plexinc/react-native-lightning';
4+
import { type RefObject, useCallback, useRef } from 'react';
25
import {
36
ActivityIndicator,
47
Button,
5-
FlatList,
68
Image,
7-
ScrollView,
89
Text,
910
TouchableOpacity,
1011
TouchableWithoutFeedback,
1112
View,
12-
VirtualizedList,
1313
} from 'react-native';
1414

15-
type DataItem = { key: string };
16-
17-
const flatlistData: DataItem[] = [
18-
{ key: 'a' },
19-
{ key: 'b' },
20-
{ key: 'c' },
21-
{ key: 'd' },
22-
{ key: 'e' },
23-
{ key: 'f' },
24-
{ key: 'g' },
25-
{ key: 'h' },
26-
{ key: 'i' },
27-
{ key: 'j' },
28-
{ key: 'k' },
29-
{ key: 'l' },
30-
{ key: 'm' },
31-
{ key: 'n' },
32-
{ key: 'o' },
33-
{ key: 'p' },
34-
{ key: 'q' },
35-
{ key: 'r' },
36-
{ key: 's' },
37-
{ key: 't' },
38-
{ key: 'u' },
39-
{ key: 'v' },
40-
{ key: 'w' },
41-
{ key: 'x' },
42-
{ key: 'y' },
43-
{ key: 'z' },
44-
];
15+
const ComponentTest = () => {
16+
const ref = useRef<ScrollView>();
4517

46-
// biome-ignore lint/style/noNonNullAssertion: We can assume data is not empty
47-
const getItem = (data: DataItem[], index: number) => data[index]!;
48-
const renderItem = ({ item }: { item: DataItem }) => (
49-
<Button title={`Test ${item.key}`} color="#123456" />
50-
);
18+
const handleChildFocused = useCallback((child: LightningElement) => {
19+
ref.current?.scrollToElement(child);
20+
}, []);
5121

52-
const ComponentTest = () => {
5322
return (
5423
<Row focusable style={{ gap: 30 }}>
5524
<Column focusable style={{ width: 700 }}>
5625
<Text>ScrollView</Text>
57-
<ScrollView style={{ width: 700, height: 400 }}>
26+
<ScrollView
27+
ref={ref as RefObject<ScrollView>}
28+
onChildFocused={handleChildFocused}
29+
snapToAlignment="center"
30+
style={{ width: 700, height: 400 }}
31+
>
5832
<Button title="This is the top" color="blue" />
5933
<Text
6034
style={{
@@ -129,38 +103,6 @@ const ComponentTest = () => {
129103
<Button title="This is the bottom" color="blue" />
130104
</ScrollView>
131105
</Column>
132-
<Column focusable>
133-
<Text>VirtualizedList</Text>
134-
<View style={{ backgroundColor: 'red', width: 240, top: 50 }}>
135-
<VirtualizedList
136-
data={flatlistData}
137-
style={{ width: 200, height: 400, left: 20 }}
138-
initialScrollIndex={5}
139-
initialNumToRender={10}
140-
getItem={getItem}
141-
getItemCount={() => flatlistData.length}
142-
getItemLayout={(_data, index) => ({
143-
length: 40,
144-
offset: 40 * index,
145-
index,
146-
})}
147-
renderItem={renderItem}
148-
/>
149-
</View>
150-
</Column>
151-
<Column focusable>
152-
<Text>FlatList</Text>
153-
<FlatList
154-
data={flatlistData}
155-
style={{ width: 200, height: 400 }}
156-
getItemLayout={(_data, index) => ({
157-
length: 40,
158-
offset: 40 * index,
159-
index,
160-
})}
161-
renderItem={renderItem}
162-
/>
163-
</Column>
164106
</Row>
165107
);
166108
};
Lines changed: 66 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,73 @@
1-
import { Column } from '@plexinc/react-lightning-components';
2-
import { FocusGroup } from '@plexinc/react-native-lightning';
3-
import { FlashList } from '@shopify/flash-list';
4-
import { Text } from 'react-native';
1+
import Row from '@plexinc/react-lightning-components/layout/Row';
2+
import FlashList from '@plexinc/react-native-lightning-components/lists/FlashList';
3+
import { useCallback, useRef } from 'react';
4+
import { View } from 'react-native';
5+
import ScrollItem from '../components/ScrollItem';
56

6-
type Item = { title: string };
7+
export const FlashListTest = () => {
8+
const buttons = new Array(50).fill(null).map((_, i) => `Flash Button ${i}`);
9+
const verticalRef = useRef<FlashList<string>>(null);
10+
const horizontalRef = useRef<FlashList<string>>(null);
711

8-
const DATA: Item[] = [
9-
{
10-
title: 'First Item',
11-
},
12-
{
13-
title: 'Second Item',
14-
},
15-
{
16-
title: 'Third Item',
17-
},
18-
];
12+
const handleVerticalFocus = useCallback((index: number) => {
13+
verticalRef.current?.scrollToIndex({ index, viewPosition: 0.5 });
14+
}, []);
1915

20-
const Item = ({ item }: { item: Item }) => (
21-
<FocusGroup
22-
style={{
23-
color: 0xffb400ff,
24-
width: 200,
25-
height: 200,
26-
}}
27-
>
28-
<Text>Testing: {item.title}</Text>
29-
</FocusGroup>
30-
);
16+
const handleHorizontalFocus = useCallback((index: number) => {
17+
horizontalRef.current?.scrollToIndex({ index, viewPosition: 0.5 });
18+
}, []);
3119

32-
export const FlashListTest = () => {
3320
return (
34-
<Column focusable style={{ flex: 1 }}>
35-
<FlashList
36-
data={DATA}
37-
renderItem={({ item }) => <Item item={item} />}
38-
estimatedItemSize={200}
39-
showsHorizontalScrollIndicator={false}
40-
showsVerticalScrollIndicator={false}
41-
/>
42-
</Column>
21+
<Row key="ListContainer">
22+
<View
23+
style={{
24+
width: 300,
25+
height: 800,
26+
}}
27+
>
28+
<FlashList
29+
ref={verticalRef}
30+
data={buttons}
31+
renderItem={({ index, item }) => (
32+
<ScrollItem
33+
color="rgb(79, 175, 175)"
34+
index={index}
35+
onFocused={handleVerticalFocus}
36+
>
37+
{item}
38+
</ScrollItem>
39+
)}
40+
estimatedItemSize={75}
41+
drawDistance={75}
42+
snapToAlignment="center"
43+
/>
44+
</View>
45+
46+
<View
47+
style={{
48+
width: 600,
49+
height: 200,
50+
}}
51+
>
52+
<FlashList
53+
ref={horizontalRef}
54+
data={buttons}
55+
horizontal
56+
renderItem={({ index, item }) => (
57+
<ScrollItem
58+
horizontal
59+
color="rgb(175, 175, 79)"
60+
index={index}
61+
onFocused={handleHorizontalFocus}
62+
>
63+
{item}
64+
</ScrollItem>
65+
)}
66+
estimatedItemSize={75}
67+
drawDistance={75}
68+
snapToAlignment="center"
69+
/>
70+
</View>
71+
</Row>
4372
);
4473
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createRef, useCallback } from 'react';
2+
import { VirtualizedList } from 'react-native';
3+
import ScrollItem from '../components/ScrollItem';
4+
5+
const getItem = (_data: string[], index: number) => `Button ${index}`;
6+
7+
export const VirtualizedListTest = () => {
8+
const ref = createRef<VirtualizedList<string>>();
9+
10+
const handleFocus = useCallback(
11+
(index: number) => {
12+
ref.current?.scrollToIndex({ index, viewPosition: 0.5 });
13+
},
14+
[ref.current],
15+
);
16+
17+
return (
18+
<VirtualizedList
19+
ref={ref}
20+
removeClippedSubviews={true}
21+
snapToAlignment="center"
22+
initialNumToRender={20}
23+
getItem={getItem}
24+
getItemCount={() => 5000}
25+
getItemLayout={(_, index) => ({
26+
index,
27+
length: 75,
28+
offset: index * 75,
29+
})}
30+
keyExtractor={(item) => item}
31+
windowSize={2}
32+
renderItem={({ index, item }) => (
33+
<ScrollItem
34+
color="rgb(79, 175, 175)"
35+
index={index}
36+
onFocused={handleFocus}
37+
>
38+
{item}
39+
</ScrollItem>
40+
)}
41+
/>
42+
);
43+
};

0 commit comments

Comments
 (0)