Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/example back on list item #85

Merged
merged 5 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions packages/example/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ThemeProvider } from '@emotion/react';
import { NavigationContainer } from '@react-navigation/native';
import { useWindowDimensions } from 'react-native';
import { GoBackConfiguration } from './src/components/GoBackConfiguration';
import { theme } from './src/design-system/theme/theme';
import { Home } from './src/pages/Home';
import { ProgramGridPage } from './src/pages/ProgramGridPage';
Expand Down Expand Up @@ -78,8 +77,6 @@ function App(): JSX.Element {
<NavigationContainer>
<ThemeProvider theme={theme}>
<SpatialNavigationDeviceTypeProvider>
<GoBackConfiguration />

<Container width={width} height={height}>
<Stack.Navigator
screenOptions={{
Expand Down
32 changes: 24 additions & 8 deletions packages/example/src/components/GoBackConfiguration.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
import { useNavigation } from '@react-navigation/native';
import { useEffect } from 'react';
import RemoteControlManager from './remote-control/RemoteControlManager';
import { SupportedKeys } from './remote-control/SupportedKeys';
import { useKey } from '../hooks/useKey';
import { useCallback, useEffect } from 'react';
import { BackHandler } from 'react-native';

export const GoBackConfiguration = () => {
const navigation = useNavigation();

useEffect(() => {
const remoteControlListener = (pressedKey: SupportedKeys) => {
if (pressedKey !== SupportedKeys.Back) return;
const event = BackHandler.addEventListener('hardwareBackPress', () => {
return true;
});

return () => {
event.remove();
};
}, []);

const goBackOnBackPress = useCallback(
(pressedKey: SupportedKeys) => {
if (!navigation.isFocused) {
return false;
}
if (pressedKey !== SupportedKeys.Back) return false;
if (navigation.canGoBack()) {
navigation.goBack();
return true;
}
};
RemoteControlManager.addKeydownListener(remoteControlListener);
return false;
},
[navigation],
);

return () => RemoteControlManager.removeKeydownListener(remoteControlListener);
}, [navigation]);
useKey(SupportedKeys.Back, goBackOnBackPress);

return <></>;
};
2 changes: 2 additions & 0 deletions packages/example/src/components/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ReactNode, useCallback, useEffect } from 'react';
import { SpatialNavigationRoot, useLockSpatialNavigation } from 'react-tv-space-navigation';
import { useMenuContext } from './Menu/MenuContext';
import { Keyboard } from 'react-native';
import { GoBackConfiguration } from './GoBackConfiguration';

type Props = { children: ReactNode };

Expand Down Expand Up @@ -51,6 +52,7 @@ export const Page = ({ children }: Props) => {
isActive={isActive}
onDirectionHandledWithoutMovement={onDirectionHandledWithoutMovement}
>
<GoBackConfiguration />
<SpatialNavigationKeyboardLocker />
{children}
</SpatialNavigationRoot>
Expand Down
1 change: 1 addition & 0 deletions packages/example/src/components/configureRemoteControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ SpatialNavigation.configureRemoteControl({

const remoteControlListener = (keyEvent: SupportedKeys) => {
callback(mapping[keyEvent]);
return false;
};

return RemoteControlManager.addKeydownListener(remoteControlListener);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { useLockSpatialNavigation } from '../../../../../lib/src/spatial-navigation/context/LockSpatialNavigationContext';
import { EventArg, useNavigation } from '@react-navigation/native';
import { useKey } from '../../../hooks/useKey';
import { SupportedKeys } from '../../remote-control/SupportedKeys';

interface UseLockProps {
isModalVisible: boolean;
Expand All @@ -27,17 +28,12 @@ const useLockParentSpatialNavigator = (isModalVisible: boolean) => {
};

const usePreventNavigationGoBack = (isModalVisible: boolean, hideModal: () => void) => {
const navigation = useNavigation();
useEffect(() => {
const hideModalListener = useCallback(() => {
if (isModalVisible) {
const navigationListener = (e: EventArg<'beforeRemove', true>) => {
e.preventDefault();
hideModal();
};
navigation.addListener('beforeRemove', navigationListener);
return () => {
navigation.removeListener('beforeRemove', navigationListener);
};
hideModal();
return true;
}
}, [navigation, isModalVisible, hideModal]);
return false;
}, [isModalVisible, hideModal]);
useKey(SupportedKeys.Back, hideModalListener);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* This event emitter is a minimal reimplementation of `mitt` with the support of stoppable event propagation
*/

export type EventType = string | symbol;

// An event handler can take an optional event argument
// and should return a boolean indicating whether or not to stop event propagation
export type Handler<T = unknown> = (event: T) => boolean;

// An array of all currently registered event handlers for a type
export type EventHandlerList<T = unknown> = Array<Handler<T>>;

// A map of event types and their corresponding event handlers.
export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
keyof Events,
EventHandlerList<Events[keyof Events]>
>;

export default class CustomEventEmitter<Events extends Record<EventType, unknown>> {
private handlers: EventHandlerMap<Events> = new Map();

on = <Key extends keyof Events>(eventType: Key, handler: Handler<Events[keyof Events]>) => {
const eventTypeHandlers = this.handlers.get(eventType);
if (!Array.isArray(eventTypeHandlers)) this.handlers.set(eventType, [handler]);
else eventTypeHandlers.push(handler);
};

off = <Key extends keyof Events>(eventType: Key, handler?: Handler<Events[keyof Events]>) => {
this.handlers.set(
eventType,
this.handlers.get(eventType).filter((h) => h !== handler),
);
};

emit = <Key extends keyof Events>(eventType: Key, evt?: Events[Key]) => {
const eventTypeHandlers = this.handlers.get(eventType);
for (let index = eventTypeHandlers.length - 1; index >= 0; index--) {
const handler = eventTypeHandlers[index];
if (handler(evt)) {
return;
}
}
};
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import mitt from 'mitt';
import { SupportedKeys } from './SupportedKeys';
import KeyEvent from 'react-native-keyevent';
import { RemoteControlManagerInterface } from './RemoteControlManager.interface';
import CustomEventEmitter from './CustomEventEmitter';

class RemoteControlManager implements RemoteControlManagerInterface {
constructor() {
KeyEvent.onKeyDownListener(this.handleKeyDown);
}

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

private handleKeyDown = (keyEvent: { keyCode: number }) => {
const mappedKey = {
Expand All @@ -19,6 +19,7 @@ class RemoteControlManager implements RemoteControlManagerInterface {
66: SupportedKeys.Enter,
23: SupportedKeys.Enter,
67: SupportedKeys.Back,
4: SupportedKeys.Back,
}[keyEvent.keyCode];

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

addKeydownListener = (listener: (event: SupportedKeys) => void) => {
addKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
this.eventEmitter.on('keyDown', listener);
return listener;
};

removeKeydownListener = (listener: (event: SupportedKeys) => void) => {
removeKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
this.eventEmitter.off('keyDown', listener);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SupportedKeys } from './SupportedKeys';

export interface RemoteControlManagerInterface {
addKeydownListener: (listener: (event: SupportedKeys) => void) => void;
removeKeydownListener: (listener: (event: SupportedKeys) => void) => void;
addKeydownListener: (listener: (event: SupportedKeys) => boolean) => void;
removeKeydownListener: (listener: (event: SupportedKeys) => boolean) => void;
emitKeyDown: (key: SupportedKeys) => void;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import mitt from 'mitt';
import { SupportedKeys } from './SupportedKeys';
import { HWEvent, TVEventHandler } from 'react-native';
import { RemoteControlManagerInterface } from './RemoteControlManager.interface';
import CustomEventEmitter from './CustomEventEmitter';

class RemoteControlManager implements RemoteControlManagerInterface {
constructor() {
TVEventHandler.addListener(this.handleKeyDown);
}

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

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

addKeydownListener = (listener: (event: SupportedKeys) => void) => {
addKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
this.eventEmitter.on('keyDown', listener);
return listener;
};

removeKeydownListener = (listener: (event: SupportedKeys) => void) => {
removeKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
this.eventEmitter.off('keyDown', listener);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import mitt from 'mitt';
import { SupportedKeys } from './SupportedKeys';
import { RemoteControlManagerInterface } from './RemoteControlManager.interface';
import CustomEventEmitter from './CustomEventEmitter';

class RemoteControlManager implements RemoteControlManagerInterface {
constructor() {
window.addEventListener('keydown', this.handleKeyDown);
}

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

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

addKeydownListener = (listener: (event: SupportedKeys) => void) => {
addKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
this.eventEmitter.on('keyDown', listener);
return listener;
};

removeKeydownListener = (listener: (event: SupportedKeys) => void) => {
removeKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
this.eventEmitter.off('keyDown', listener);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ SpatialNavigation.configureRemoteControl({

const remoteControlListener = (keyEvent: SupportedKeys) => {
callback(mapping[keyEvent]);
return false;
};

return TestRemoteControlManager.addKeydownListener(remoteControlListener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ class TestRemoteControlManager {
act(() => jest.runAllTimers());
};

addKeydownListener = (listener: (event: SupportedKeys) => void) => {
addKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
this.eventEmitter.on('keyDown', listener);
return listener;
};

removeKeydownListener = (listener: (event: SupportedKeys) => void) => {
removeKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
this.eventEmitter.off('keyDown', listener);
};
}
Expand Down
19 changes: 19 additions & 0 deletions packages/example/src/hooks/useKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect } from 'react';
import RemoteControlManager from '../components/remote-control/RemoteControlManager';
import { SupportedKeys } from '../components/remote-control/SupportedKeys';

/**
* A convenient hook to listen to a key and react to it
*
* @example useKey(SupportedKeys.Back, () => { console.log('pressed back!') })
*/
export const useKey = (key: SupportedKeys, callback: (pressedKey: SupportedKeys) => boolean) => {
pierpo marked this conversation as resolved.
Show resolved Hide resolved
useEffect(() => {
const remoteControlListener = (actualKey: SupportedKeys) => {
if (actualKey !== key) return;
return callback(key);
};
RemoteControlManager.addKeydownListener(remoteControlListener);
return () => RemoteControlManager.removeKeydownListener(remoteControlListener);
}, [key, callback]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ describe('ProgramList', () => {
jest.spyOn(programInfos, 'getPrograms').mockReturnValue(programsFixture);

it('renders the list with every items', () => {
const screen = renderWithProviders(<ProgramList />);
const screen = renderWithProviders(<ProgramList isActive={true} />);

screen.getByLabelText('Program 1');
screen.getByLabelText('Program 2');
});

it('renders the list and focus elements accordingly with inputs', () => {
const screen = renderWithProviders(<ProgramList />);
const screen = renderWithProviders(<ProgramList isActive={true} />);

const program1 = screen.getByLabelText('Program 1');
expect(program1).toBeSelected();
Expand Down
Loading
Loading