Skip to content

Commit 1e9d68d

Browse files
author
Pierre Poupin
committed
Merge pull request #116 from bamlab/fix-error-when-recreating-node
fix(core): fix error when recreating node
2 parents 016404e + 2292ce8 commit 1e9d68d

8 files changed

Lines changed: 166 additions & 6 deletions

File tree

packages/example/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { NonVirtualizedGridPage } from './src/pages/NonVirtualizedGridPage';
1717
import { GridWithLongNodesPage } from './src/pages/GridWithLongNodesPage';
1818
import { useTVPanEvent } from './src/components/PanEvent/useTVPanEvent';
1919
import { SpatialNavigationDeviceTypeProvider } from '../lib/src/spatial-navigation/context/DeviceContext';
20+
import { ListWithVariableSize } from './src/pages/ListWithVariableSize';
2021

2122
const Stack = createNativeStackNavigator<RootStackParamList>();
2223

@@ -27,6 +28,7 @@ export type RootTabParamList = {
2728
ProgramGridPage: undefined;
2829
NonVirtualizedGridPage: undefined;
2930
GridWithLongNodesPage: undefined;
31+
ListWithVariableSize: undefined;
3032
};
3133

3234
export type RootStackParamList = {
@@ -54,6 +56,7 @@ const TabNavigator = () => {
5456
<Tab.Screen name="ProgramGridPage" component={ProgramGridPage} />
5557
<Tab.Screen name="NonVirtualizedGridPage" component={NonVirtualizedGridPage} />
5658
<Tab.Screen name="GridWithLongNodesPage" component={GridWithLongNodesPage} />
59+
<Tab.Screen name="ListWithVariableSize" component={ListWithVariableSize} />
5760
</Tab.Navigator>
5861
</MenuProvider>
5962
);

packages/example/src/components/Menu/Menu.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const menuItems: Record<keyof RootTabParamList, MenuItems> = {
5858
label: 'Grid with long nodes',
5959
icon: 'LayoutDashboard',
6060
},
61+
ListWithVariableSize: { label: 'List with variable size', icon: 'LayoutDashboard' },
6162
};
6263

6364
export const Menu = ({ state, navigation }: BottomTabBarProps) => {

packages/example/src/modules/program/view/ProgramList.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ export const ProgramList = ({
2525
orientation,
2626
containerStyle,
2727
listRef,
28+
data,
2829
}: {
2930
orientation?: 'vertical' | 'horizontal';
3031
containerStyle?: object;
3132
listRef: MutableRefObject<SpatialNavigationVirtualizedListRef>;
33+
data?: ProgramInfo[];
3234
}) => {
3335
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
3436

@@ -52,7 +54,7 @@ export const ProgramList = ({
5254
<Container isActive={isActive} style={containerStyle}>
5355
<SpatialNavigationVirtualizedList
5456
orientation={orientation}
55-
data={programInfos}
57+
data={data ?? programInfos}
5658
renderItem={renderItem}
5759
itemSize={theme.sizes.program.portrait.width + 30}
5860
numberOfRenderedItems={WINDOW_SIZE}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ThemeProvider } from '@emotion/react';
2+
import { render } from '@testing-library/react-native';
3+
import { theme } from '../design-system/theme/theme';
4+
import { ListWithVariableSize } from './ListWithVariableSize';
5+
import { NavigationContainer } from '@react-navigation/native';
6+
import '../components/tests/helpers/configureTestRemoteControl';
7+
import testRemoteControlManager from '../components/tests/helpers/testRemoteControlManager';
8+
9+
jest.mock('../modules/program/infra/programInfos', () => ({
10+
getPrograms: () => {
11+
return jest.requireActual('../modules/program/infra/programInfos').programInfos;
12+
},
13+
}));
14+
15+
const renderWithProviders = (page: JSX.Element) => {
16+
return render(
17+
<NavigationContainer>
18+
<ThemeProvider theme={theme}>{page}</ThemeProvider>
19+
</NavigationContainer>,
20+
);
21+
};
22+
describe('ListWithVariableSize', () => {
23+
it('node is still focusable after being removed', () => {
24+
const screen = renderWithProviders(<ListWithVariableSize />);
25+
26+
// Go to last programd and focus it
27+
testRemoteControlManager.handleRight();
28+
testRemoteControlManager.handleRight();
29+
testRemoteControlManager.handleRight();
30+
expect(screen.getByLabelText('Program 4')).toBeSelected();
31+
32+
// Go back to first position
33+
testRemoteControlManager.handleLeft();
34+
testRemoteControlManager.handleLeft();
35+
testRemoteControlManager.handleLeft();
36+
37+
// Remove last item
38+
testRemoteControlManager.handleDown();
39+
testRemoteControlManager.handleDown();
40+
testRemoteControlManager.handleEnter();
41+
42+
expect(screen.queryByLabelText('Program 4')).not.toBeOnTheScreen();
43+
44+
// Add back last item
45+
testRemoteControlManager.handleUp();
46+
testRemoteControlManager.handleEnter();
47+
expect(screen.getByLabelText('Program 4')).toBeOnTheScreen();
48+
49+
// Focus last item
50+
testRemoteControlManager.handleUp();
51+
testRemoteControlManager.handleRight();
52+
testRemoteControlManager.handleRight();
53+
testRemoteControlManager.handleRight();
54+
55+
expect(screen.getByLabelText('Program 4')).toBeSelected();
56+
});
57+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import styled from '@emotion/native';
2+
import { Page } from '../components/Page';
3+
import { getPrograms } from '../modules/program/infra/programInfos';
4+
import { SpatialNavigationView } from '../../../lib/src/spatial-navigation/components/View';
5+
import { scaledPixels } from '../design-system/helpers/scaledPixels';
6+
import { DefaultFocus } from '../../../lib/src/spatial-navigation/context/DefaultFocusContext';
7+
import { SpatialNavigationNode } from '../../../lib/src/spatial-navigation/components/Node';
8+
import { Spacer } from '../design-system/components/Spacer';
9+
import { Button } from '../design-system/components/Button';
10+
import { useState } from 'react';
11+
import { ProgramList } from '../modules/program/view/ProgramList';
12+
import { useTheme } from '@emotion/react';
13+
14+
const ROW_PADDING = scaledPixels(70);
15+
16+
const MAX = 1000;
17+
18+
export const ListWithVariableSize = () => {
19+
const theme = useTheme();
20+
const [programsBase, setProgramsBase] = useState(getPrograms(MAX));
21+
22+
const [numberOfPrograms, setNumberOfPrograms] = useState(4);
23+
24+
const addItem = () => {
25+
setNumberOfPrograms((prev) => {
26+
if (prev === MAX) return prev;
27+
28+
return prev + 1;
29+
});
30+
};
31+
32+
const removeItem = () => {
33+
setNumberOfPrograms((prev) => {
34+
if (prev === 0) return prev;
35+
return prev - 1;
36+
});
37+
};
38+
39+
const shuffleItems = () => {
40+
setProgramsBase((prev) => [...prev].sort(() => Math.random() - 0.5));
41+
};
42+
43+
const programs = programsBase.slice(0, numberOfPrograms);
44+
45+
return (
46+
<Page>
47+
<DefaultFocus>
48+
<Container>
49+
<SpatialNavigationNode orientation="horizontal">
50+
<ListContainer>
51+
<ProgramList
52+
data={programs}
53+
listRef={null}
54+
containerStyle={{ height: theme.sizes.program.portrait.height + ROW_PADDING }}
55+
/>
56+
</ListContainer>
57+
</SpatialNavigationNode>
58+
<Spacer gap="$6" />
59+
<SpatialNavigationView direction="vertical">
60+
<Button label="Add item" onSelect={addItem} />
61+
<Button label="Remove item" onSelect={removeItem} />
62+
<Button label="Shuffle items" onSelect={shuffleItems} />
63+
</SpatialNavigationView>
64+
</Container>
65+
</DefaultFocus>
66+
</Page>
67+
);
68+
};
69+
70+
const Container = styled.View({
71+
flex: 1,
72+
padding: scaledPixels(30),
73+
});
74+
75+
const ListContainer = styled.View(({ theme }) => ({
76+
flexDirection: 'row',
77+
gap: theme.spacings.$4,
78+
padding: theme.spacings.$4,
79+
}));

packages/lib/src/spatial-navigation/components/virtualizedList/SpatialNavigationVirtualizedListWithVirtualNodes.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ const useRegisterInitialAndUnregisterFinalVirtualNodes = <T,>({
4848
const useUpdateRegistration = <T,>({
4949
allItems,
5050
registerNthVirtualNode,
51+
unregisterNthVirtualNode,
5152
}: {
5253
allItems: Array<T>;
5354
registerNthVirtualNode: (index: number) => void;
55+
unregisterNthVirtualNode: (index: number) => void;
5456
}) => {
5557
const previousAllItems = useRef<Array<T>>();
5658

@@ -63,6 +65,7 @@ const useUpdateRegistration = <T,>({
6365
currentItems: allItems,
6466
previousItems: previousAllItemsList,
6567
addVirtualNode: registerNthVirtualNode,
68+
removeVirtualNode: unregisterNthVirtualNode,
6669
});
6770
}
6871
previousAllItems.current = allItems;
@@ -109,7 +112,7 @@ const useRegisterVirtualNodes = <T extends ItemWithIndex>({
109112
unregisterNthVirtualNode,
110113
});
111114

112-
useUpdateRegistration({ allItems, registerNthVirtualNode });
115+
useUpdateRegistration({ allItems, registerNthVirtualNode, unregisterNthVirtualNode });
113116

114117
return { getNthVirtualNodeID };
115118
};

packages/lib/src/spatial-navigation/components/virtualizedList/helpers/updateVirtualNodeRegistration.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ describe('updateVirtualNodeRegistration', () => {
1111
previousItems,
1212
currentItems,
1313
addVirtualNode: mockAddNode,
14+
removeVirtualNode: jest.fn(),
1415
});
1516

1617
expect(mockAddNode).toHaveBeenCalledTimes(2);
@@ -24,6 +25,7 @@ describe('updateVirtualNodeRegistration', () => {
2425
previousItems,
2526
currentItems,
2627
addVirtualNode: mockAddNode,
28+
removeVirtualNode: jest.fn(),
2729
});
2830

2931
expect(mockAddNode).not.toHaveBeenCalled();

packages/lib/src/spatial-navigation/components/virtualizedList/helpers/updateVirtualNodeRegistration.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ const registerNewNode = <T>({
1515
});
1616
};
1717

18+
const unregisterOldNode = <T>({
19+
currentItems,
20+
previousItems,
21+
removeVirtualNode,
22+
}: {
23+
currentItems: Array<T>;
24+
previousItems: Array<T>;
25+
removeVirtualNode: (index: number) => void;
26+
}) => {
27+
for (let index = previousItems.length - 1; index > currentItems.length - 1; index--) {
28+
removeVirtualNode(index);
29+
}
30+
};
31+
1832
/**
1933
* This function aims to compare 2 arrays of items : currentItems and previousItems and :
2034
* - addVirtualNode for every item from currentItems that weren't in previousItems
@@ -26,17 +40,16 @@ export const updateVirtualNodeRegistration = <T>({
2640
currentItems,
2741
previousItems,
2842
addVirtualNode,
43+
removeVirtualNode,
2944
}: {
3045
currentItems: Array<T>;
3146
previousItems: Array<T>;
3247
addVirtualNode: (index: number) => void;
48+
removeVirtualNode: (index: number) => void;
3349
}) => {
3450
// Step 1 : addVirtualNode for every item from currentItems that weren't in previousItems
3551
registerNewNode({ currentItems, previousItems, addVirtualNode });
3652

3753
// Step 2 : removeVirtualNode for every from previousItems that aren't there anymore in currentItems
38-
// TODO
39-
40-
// Step 3 : re-order all the items
41-
// TODO
54+
unregisterOldNode({ currentItems, previousItems, removeVirtualNode });
4255
};

0 commit comments

Comments
 (0)