Skip to content

Commit 5c4913e

Browse files
authored
Merge pull request #156 from bamlab/feat/on-long-press
Feat: Add onLongSelect
2 parents 4534522 + f5dc7e6 commit 5c4913e

16 files changed

Lines changed: 482 additions & 15 deletions

File tree

docs/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ The `SpatialNavigationNode` component receives the following props:
5151
| `onFocus` | `function` | `undefined` | Callback function to be called when the node gains focus. |
5252
| `onBlur` | `function` | `undefined` | Callback function to be called when the node loses focus. |
5353
| `onSelect` | `function` | `undefined` | Callback function to be called when the node is selected. |
54+
| `onLongSelect` | `function` | `onSelect` | Callback function to be called when the node is selected with long key press. |
5455
| `onActive` | `function` | `undefined` | Callback function to be called when the node is made active by either itself or one of its descendants gaining focus. |
5556
| `onInactive` | `function` | `undefined` | Callback function to be called when the node was active and due to an updated focus, is no longer active. |
5657
| `orientation` | `'vertical' \| 'horizontal` | `'vertical'` | Determines the orientation of the node. |

packages/example/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import './src/components/configureRemoteControl';
12
import { ThemeProvider } from '@emotion/react';
23
import { NavigationContainer } from '@react-navigation/native';
34
import { useWindowDimensions } from 'react-native';

packages/example/babel.jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ module.exports = {
2222
},
2323
],
2424
['@babel/plugin-proposal-class-properties', { loose: false }],
25+
'react-native-reanimated/plugin',
2526
],
2627
};

packages/example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"react": "18.2.0",
3131
"react-native": "npm:react-native-tvos@0.74.1-0",
3232
"react-native-keyevent": "^0.3.2",
33+
"react-native-reanimated": "~3.10.1",
3334
"react-native-safe-area-context": "4.10.1",
3435
"react-native-screens": "3.31.1",
3536
"react-native-svg": "15.2.0",

packages/example/src/components/configureRemoteControl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ SpatialNavigation.configureRemoteControl({
1010
[SupportedKeys.Up]: Directions.UP,
1111
[SupportedKeys.Down]: Directions.DOWN,
1212
[SupportedKeys.Enter]: Directions.ENTER,
13+
[SupportedKeys.LongEnter]: Directions.LONG_ENTER,
1314
[SupportedKeys.Back]: null,
1415
};
1516

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,26 @@ import KeyEvent from 'react-native-keyevent';
33
import { RemoteControlManagerInterface } from './RemoteControlManager.interface';
44
import CustomEventEmitter from './CustomEventEmitter';
55

6+
const LONG_PRESS_DURATION = 500;
7+
68
class RemoteControlManager implements RemoteControlManagerInterface {
79
constructor() {
810
KeyEvent.onKeyDownListener(this.handleKeyDown);
11+
KeyEvent.onKeyUpListener(this.handleKeyUp);
912
}
1013

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

16+
private isEnterKeyDownPressed = false;
17+
private longEnterTimeout: NodeJS.Timeout | null = null;
18+
19+
private handleLongEnter = () => {
20+
this.longEnterTimeout = setTimeout(() => {
21+
this.eventEmitter.emit('keyDown', SupportedKeys.LongEnter);
22+
this.longEnterTimeout = null;
23+
}, LONG_PRESS_DURATION);
24+
};
25+
1326
private handleKeyDown = (keyEvent: { keyCode: number }) => {
1427
const mappedKey = {
1528
21: SupportedKeys.Left,
@@ -26,9 +39,36 @@ class RemoteControlManager implements RemoteControlManagerInterface {
2639
return;
2740
}
2841

42+
if (mappedKey === SupportedKeys.Enter) {
43+
if (!this.isEnterKeyDownPressed) {
44+
this.isEnterKeyDownPressed = true;
45+
this.handleLongEnter();
46+
}
47+
return;
48+
}
49+
2950
this.eventEmitter.emit('keyDown', mappedKey);
3051
};
3152

53+
private handleKeyUp = (keyEvent: { keyCode: number }) => {
54+
const mappedKey = {
55+
66: SupportedKeys.Enter,
56+
23: SupportedKeys.Enter,
57+
}[keyEvent.keyCode];
58+
59+
if (!mappedKey) {
60+
return;
61+
}
62+
63+
if (mappedKey === SupportedKeys.Enter) {
64+
this.isEnterKeyDownPressed = false;
65+
if (this.longEnterTimeout) {
66+
clearTimeout(this.longEnterTimeout);
67+
this.eventEmitter.emit('keyDown', mappedKey);
68+
}
69+
}
70+
};
71+
3272
addKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
3373
this.eventEmitter.on('keyDown', listener);
3474
return listener;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@ class RemoteControlManager implements RemoteControlManagerInterface {
1919
up: SupportedKeys.Up,
2020
down: SupportedKeys.Down,
2121
select: SupportedKeys.Enter,
22+
longSelect: SupportedKeys.LongEnter,
2223
}[evt.eventType];
2324

2425
if (!mappedKey) {
2526
return;
2627
}
2728

29+
// We only want to handle keydown for long select to avoid triggering the event twice
30+
if (mappedKey === SupportedKeys.LongEnter && evt.eventKeyAction === 1) {
31+
return;
32+
}
33+
2834
this.eventEmitter.emit('keyDown', mappedKey);
2935
};
3036

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,26 @@ import { SupportedKeys } from './SupportedKeys';
22
import { RemoteControlManagerInterface } from './RemoteControlManager.interface';
33
import CustomEventEmitter from './CustomEventEmitter';
44

5+
const LONG_PRESS_DURATION = 500;
6+
57
class RemoteControlManager implements RemoteControlManagerInterface {
68
constructor() {
79
window.addEventListener('keydown', this.handleKeyDown);
10+
window.addEventListener('keyup', this.handleKeyUp);
811
}
912

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

15+
private isEnterKeyDown = false;
16+
private longEnterTimeout: NodeJS.Timeout | null = null;
17+
18+
private handleLongEnter = () => {
19+
this.longEnterTimeout = setTimeout(() => {
20+
this.eventEmitter.emit('keyDown', SupportedKeys.LongEnter);
21+
this.longEnterTimeout = null;
22+
}, LONG_PRESS_DURATION);
23+
};
24+
1225
private handleKeyDown = (event: KeyboardEvent) => {
1326
const mappedKey = {
1427
ArrowRight: SupportedKeys.Right,
@@ -23,9 +36,35 @@ class RemoteControlManager implements RemoteControlManagerInterface {
2336
return;
2437
}
2538

39+
if (mappedKey === SupportedKeys.Enter) {
40+
if (!this.isEnterKeyDown) {
41+
this.isEnterKeyDown = true;
42+
this.handleLongEnter();
43+
}
44+
return;
45+
}
46+
2647
this.eventEmitter.emit('keyDown', mappedKey);
2748
};
2849

50+
private handleKeyUp = (event: KeyboardEvent) => {
51+
const mappedKey = {
52+
Enter: SupportedKeys.Enter,
53+
}[event.code];
54+
55+
if (!mappedKey) {
56+
return;
57+
}
58+
59+
if (mappedKey === SupportedKeys.Enter) {
60+
this.isEnterKeyDown = false;
61+
if (this.longEnterTimeout) {
62+
clearTimeout(this.longEnterTimeout);
63+
this.eventEmitter.emit('keyDown', mappedKey);
64+
}
65+
}
66+
};
67+
2968
addKeydownListener = (listener: (event: SupportedKeys) => boolean) => {
3069
this.eventEmitter.on('keyDown', listener);
3170
return listener;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export enum SupportedKeys {
44
Left = 'Left',
55
Right = 'Right',
66
Enter = 'Enter',
7+
LongEnter = 'LongEnter',
78
Back = 'Back',
89
}

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

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Program } from './Program';
55
import { forwardRef } from 'react';
66
import { SpatialNavigationNodeRef } from '../../../../../lib/src/spatial-navigation/types/SpatialNavigationNodeRef';
77

8+
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
9+
810
type Props = {
911
programInfo: ProgramInfo;
1012
onSelect?: () => void;
@@ -15,20 +17,35 @@ type Props = {
1517

1618
export const ProgramNode = forwardRef<SpatialNavigationNodeRef, Props>(
1719
({ programInfo, onSelect, indexRange, label, variant }: Props, ref) => {
20+
const rotationZ = useSharedValue(0);
21+
22+
const rotate360 = () => {
23+
rotationZ.value = withTiming(rotationZ.value + 360);
24+
};
25+
26+
const animatedStyle = useAnimatedStyle(() => {
27+
return {
28+
transform: [{ rotateZ: `${rotationZ.value}deg` }],
29+
};
30+
});
31+
1832
return (
1933
<SpatialNavigationFocusableView
2034
onSelect={onSelect}
35+
onLongSelect={rotate360}
2136
indexRange={indexRange}
2237
viewProps={{ accessibilityLabel: programInfo.title }}
2338
ref={ref}
2439
>
2540
{({ isFocused, isRootActive }) => (
26-
<Program
27-
isFocused={isFocused && isRootActive}
28-
programInfo={programInfo}
29-
label={label}
30-
variant={variant}
31-
/>
41+
<Animated.View style={animatedStyle}>
42+
<Program
43+
isFocused={isFocused && isRootActive}
44+
programInfo={programInfo}
45+
label={label}
46+
variant={variant}
47+
/>
48+
</Animated.View>
3249
)}
3350
</SpatialNavigationFocusableView>
3451
);

0 commit comments

Comments
 (0)