Skip to content

Commit aeb548e

Browse files
alxlrJulienIzzPierre Poupin
authored
chore(example): reset to beginning of the list when pressing back (#85)
* feat(lists): expose currently focused item index in ref Co-authored-by: JulienIzz <JulienIzz@users.noreply.github.com> * chore(example): add example back button usage for lists * chore(example): add missing forwardRef * chore(example): add missing useEffect * chore(example): more GoBackConfiguration so that each page has its own Otherwise we can't catch hardware back press properly --------- Co-authored-by: JulienIzz <JulienIzz@users.noreply.github.com> Co-authored-by: JulienIzz <julien.izzillo@gmail.com> Co-authored-by: Pierre Poupin <pierrep@bam.tech>
1 parent 4d55b25 commit aeb548e

19 files changed

Lines changed: 229 additions & 121 deletions

packages/example/App.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ThemeProvider } from '@emotion/react';
22
import { NavigationContainer } from '@react-navigation/native';
33
import { useWindowDimensions } from 'react-native';
4-
import { GoBackConfiguration } from './src/components/GoBackConfiguration';
54
import { theme } from './src/design-system/theme/theme';
65
import { Home } from './src/pages/Home';
76
import { ProgramGridPage } from './src/pages/ProgramGridPage';
@@ -78,8 +77,6 @@ function App(): JSX.Element {
7877
<NavigationContainer>
7978
<ThemeProvider theme={theme}>
8079
<SpatialNavigationDeviceTypeProvider>
81-
<GoBackConfiguration />
82-
8380
<Container width={width} height={height}>
8481
<Stack.Navigator
8582
screenOptions={{
Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
11
import { useNavigation } from '@react-navigation/native';
2-
import { useEffect } from 'react';
3-
import RemoteControlManager from './remote-control/RemoteControlManager';
42
import { SupportedKeys } from './remote-control/SupportedKeys';
3+
import { useKey } from '../hooks/useKey';
4+
import { useCallback, useEffect } from 'react';
5+
import { BackHandler } from 'react-native';
56

67
export const GoBackConfiguration = () => {
78
const navigation = useNavigation();
89

910
useEffect(() => {
10-
const remoteControlListener = (pressedKey: SupportedKeys) => {
11-
if (pressedKey !== SupportedKeys.Back) return;
11+
const event = BackHandler.addEventListener('hardwareBackPress', () => {
12+
return true;
13+
});
14+
15+
return () => {
16+
event.remove();
17+
};
18+
}, []);
19+
20+
const goBackOnBackPress = useCallback(
21+
(pressedKey: SupportedKeys) => {
22+
if (!navigation.isFocused) {
23+
return false;
24+
}
25+
if (pressedKey !== SupportedKeys.Back) return false;
1226
if (navigation.canGoBack()) {
1327
navigation.goBack();
28+
return true;
1429
}
15-
};
16-
RemoteControlManager.addKeydownListener(remoteControlListener);
30+
return false;
31+
},
32+
[navigation],
33+
);
1734

18-
return () => RemoteControlManager.removeKeydownListener(remoteControlListener);
19-
}, [navigation]);
35+
useKey(SupportedKeys.Back, goBackOnBackPress);
2036

2137
return <></>;
2238
};

packages/example/src/components/Page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ReactNode, useCallback, useEffect } from 'react';
44
import { SpatialNavigationRoot, useLockSpatialNavigation } from 'react-tv-space-navigation';
55
import { useMenuContext } from './Menu/MenuContext';
66
import { Keyboard } from 'react-native';
7+
import { GoBackConfiguration } from './GoBackConfiguration';
78

89
type Props = { children: ReactNode };
910

@@ -51,6 +52,7 @@ export const Page = ({ children }: Props) => {
5152
isActive={isActive}
5253
onDirectionHandledWithoutMovement={onDirectionHandledWithoutMovement}
5354
>
55+
<GoBackConfiguration />
5456
<SpatialNavigationKeyboardLocker />
5557
{children}
5658
</SpatialNavigationRoot>

packages/example/src/components/configureRemoteControl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ SpatialNavigation.configureRemoteControl({
1515

1616
const remoteControlListener = (keyEvent: SupportedKeys) => {
1717
callback(mapping[keyEvent]);
18+
return false;
1819
};
1920

2021
return RemoteControlManager.addKeydownListener(remoteControlListener);

packages/example/src/components/modals/SpatialNavigationOverlay/useLockOverlay.tsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { useEffect } from 'react';
1+
import { useCallback, useEffect } from 'react';
22
import { useLockSpatialNavigation } from '../../../../../lib/src/spatial-navigation/context/LockSpatialNavigationContext';
3-
import { EventArg, useNavigation } from '@react-navigation/native';
3+
import { useKey } from '../../../hooks/useKey';
4+
import { SupportedKeys } from '../../remote-control/SupportedKeys';
45

56
interface UseLockProps {
67
isModalVisible: boolean;
@@ -27,17 +28,12 @@ const useLockParentSpatialNavigator = (isModalVisible: boolean) => {
2728
};
2829

2930
const usePreventNavigationGoBack = (isModalVisible: boolean, hideModal: () => void) => {
30-
const navigation = useNavigation();
31-
useEffect(() => {
31+
const hideModalListener = useCallback(() => {
3232
if (isModalVisible) {
33-
const navigationListener = (e: EventArg<'beforeRemove', true>) => {
34-
e.preventDefault();
35-
hideModal();
36-
};
37-
navigation.addListener('beforeRemove', navigationListener);
38-
return () => {
39-
navigation.removeListener('beforeRemove', navigationListener);
40-
};
33+
hideModal();
34+
return true;
4135
}
42-
}, [navigation, isModalVisible, hideModal]);
36+
return false;
37+
}, [isModalVisible, hideModal]);
38+
useKey(SupportedKeys.Back, hideModalListener);
4339
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* This event emitter is a minimal reimplementation of `mitt` with the support of stoppable event propagation
3+
*/
4+
5+
export type EventType = string | symbol;
6+
7+
// An event handler can take an optional event argument
8+
// and should return a boolean indicating whether or not to stop event propagation
9+
export type Handler<T = unknown> = (event: T) => boolean;
10+
11+
// An array of all currently registered event handlers for a type
12+
export type EventHandlerList<T = unknown> = Array<Handler<T>>;
13+
14+
// A map of event types and their corresponding event handlers.
15+
export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
16+
keyof Events,
17+
EventHandlerList<Events[keyof Events]>
18+
>;
19+
20+
export default class CustomEventEmitter<Events extends Record<EventType, unknown>> {
21+
private handlers: EventHandlerMap<Events> = new Map();
22+
23+
on = <Key extends keyof Events>(eventType: Key, handler: Handler<Events[keyof Events]>) => {
24+
const eventTypeHandlers = this.handlers.get(eventType);
25+
if (!Array.isArray(eventTypeHandlers)) this.handlers.set(eventType, [handler]);
26+
else eventTypeHandlers.push(handler);
27+
};
28+
29+
off = <Key extends keyof Events>(eventType: Key, handler?: Handler<Events[keyof Events]>) => {
30+
this.handlers.set(
31+
eventType,
32+
this.handlers.get(eventType).filter((h) => h !== handler),
33+
);
34+
};
35+
36+
emit = <Key extends keyof Events>(eventType: Key, evt?: Events[Key]) => {
37+
const eventTypeHandlers = this.handlers.get(eventType);
38+
for (let index = eventTypeHandlers.length - 1; index >= 0; index--) {
39+
const handler = eventTypeHandlers[index];
40+
if (handler(evt)) {
41+
return;
42+
}
43+
}
44+
};
45+
}

packages/example/src/components/remote-control/RemoteControlManager.android.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import mitt from 'mitt';
21
import { SupportedKeys } from './SupportedKeys';
32
import KeyEvent from 'react-native-keyevent';
43
import { RemoteControlManagerInterface } from './RemoteControlManager.interface';
4+
import CustomEventEmitter from './CustomEventEmitter';
55

66
class RemoteControlManager implements RemoteControlManagerInterface {
77
constructor() {
88
KeyEvent.onKeyDownListener(this.handleKeyDown);
99
}
1010

11-
private eventEmitter = mitt<{ keyDown: SupportedKeys }>();
11+
private eventEmitter = new CustomEventEmitter<{ keyDown: SupportedKeys }>();
1212

1313
private handleKeyDown = (keyEvent: { keyCode: number }) => {
1414
const mappedKey = {
@@ -19,6 +19,7 @@ class RemoteControlManager implements RemoteControlManagerInterface {
1919
66: SupportedKeys.Enter,
2020
23: SupportedKeys.Enter,
2121
67: SupportedKeys.Back,
22+
4: SupportedKeys.Back,
2223
}[keyEvent.keyCode];
2324

2425
if (!mappedKey) {
@@ -28,12 +29,12 @@ class RemoteControlManager implements RemoteControlManagerInterface {
2829
this.eventEmitter.emit('keyDown', mappedKey);
2930
};
3031

31-
addKeydownListener = (listener: (event: SupportedKeys) => void) => {
32+
addKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
3233
this.eventEmitter.on('keyDown', listener);
3334
return listener;
3435
};
3536

36-
removeKeydownListener = (listener: (event: SupportedKeys) => void) => {
37+
removeKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
3738
this.eventEmitter.off('keyDown', listener);
3839
};
3940

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { SupportedKeys } from './SupportedKeys';
22

33
export interface RemoteControlManagerInterface {
4-
addKeydownListener: (listener: (event: SupportedKeys) => void) => void;
5-
removeKeydownListener: (listener: (event: SupportedKeys) => void) => void;
4+
addKeydownListener: (listener: (event: SupportedKeys) => boolean) => void;
5+
removeKeydownListener: (listener: (event: SupportedKeys) => boolean) => void;
66
emitKeyDown: (key: SupportedKeys) => void;
77
}

packages/example/src/components/remote-control/RemoteControlManager.ios.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import mitt from 'mitt';
21
import { SupportedKeys } from './SupportedKeys';
32
import { HWEvent, TVEventHandler } from 'react-native';
43
import { RemoteControlManagerInterface } from './RemoteControlManager.interface';
4+
import CustomEventEmitter from './CustomEventEmitter';
55

66
class RemoteControlManager implements RemoteControlManagerInterface {
77
constructor() {
88
TVEventHandler.addListener(this.handleKeyDown);
99
}
1010

11-
private eventEmitter = mitt<{ keyDown: SupportedKeys }>();
11+
private eventEmitter = new CustomEventEmitter<{ keyDown: SupportedKeys }>();
1212

1313
private handleKeyDown = (evt: HWEvent) => {
1414
if (!evt) return;
@@ -28,12 +28,12 @@ class RemoteControlManager implements RemoteControlManagerInterface {
2828
this.eventEmitter.emit('keyDown', mappedKey);
2929
};
3030

31-
addKeydownListener = (listener: (event: SupportedKeys) => void) => {
31+
addKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
3232
this.eventEmitter.on('keyDown', listener);
3333
return listener;
3434
};
3535

36-
removeKeydownListener = (listener: (event: SupportedKeys) => void) => {
36+
removeKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
3737
this.eventEmitter.off('keyDown', listener);
3838
};
3939

packages/example/src/components/remote-control/RemoteControlManager.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import mitt from 'mitt';
21
import { SupportedKeys } from './SupportedKeys';
32
import { RemoteControlManagerInterface } from './RemoteControlManager.interface';
3+
import CustomEventEmitter from './CustomEventEmitter';
44

55
class RemoteControlManager implements RemoteControlManagerInterface {
66
constructor() {
77
window.addEventListener('keydown', this.handleKeyDown);
88
}
99

10-
private eventEmitter = mitt<{ keyDown: SupportedKeys }>();
10+
private eventEmitter = new CustomEventEmitter<{ keyDown: SupportedKeys }>();
1111

1212
private handleKeyDown = (event: KeyboardEvent) => {
1313
const mappedKey = {
@@ -26,12 +26,12 @@ class RemoteControlManager implements RemoteControlManagerInterface {
2626
this.eventEmitter.emit('keyDown', mappedKey);
2727
};
2828

29-
addKeydownListener = (listener: (event: SupportedKeys) => void) => {
29+
addKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
3030
this.eventEmitter.on('keyDown', listener);
3131
return listener;
3232
};
3333

34-
removeKeydownListener = (listener: (event: SupportedKeys) => void) => {
34+
removeKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
3535
this.eventEmitter.off('keyDown', listener);
3636
};
3737

0 commit comments

Comments
 (0)