Skip to content

Commit 9e00878

Browse files
committed
Handle TV devices
Add support for : - TVEventHandler events - Platform.isTV - hasTVPreferredFocus property - nextFocusUp, nextFocusRight, nextFocusDown, nextFocusLeft properties - back event from remote control
1 parent 16c0109 commit 9e00878

File tree

7 files changed

+271
-19
lines changed

7 files changed

+271
-19
lines changed

packages/react-native-web/src/exports/BackHandler/index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ function emptyFunction() {}
1212

1313
const BackHandler = {
1414
exitApp: emptyFunction,
15-
addEventListener() {
15+
addEventListener(event, callback) {
16+
document.addEventListener(event, callback);
1617
return {
17-
remove: emptyFunction
18+
remove: () => {
19+
document.removeEventListener(event, callback);
20+
}
1821
};
1922
},
2023
removeEventListener: emptyFunction

packages/react-native-web/src/exports/Platform/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const Platform = {
1616
return true;
1717
}
1818
return false;
19+
},
20+
get isTV(): boolean {
21+
return process.env.REACT_APP_IS_TV === "true";
1922
}
2023
};
2124

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,102 @@
1-
export default {};
1+
class TVEventHandler {
2+
3+
constructor() {
4+
this.component = null;
5+
this.callback = null;
6+
}
7+
8+
enable(component, callback) {
9+
this.component = component;
10+
this.callback = callback;
11+
document.addEventListener(
12+
'onHWKeyEvent',
13+
this.onHWKeyEvent.bind(this)
14+
);
15+
}
16+
17+
disable() {
18+
document.removeEventListener(
19+
'onHWKeyEvent',
20+
this.onHWKeyEvent
21+
);
22+
}
23+
24+
onHWKeyEvent(event) {
25+
if (this.callback) {
26+
if (event && event.detail) {
27+
const tvEvent = event.detail.tvEvent;
28+
if(tvEvent) {
29+
this.callback(this.component, tvEvent);
30+
}
31+
}
32+
}
33+
}
34+
35+
static dispatchEvent(tvEvent) {
36+
// Dispatch tvEvent through onHWKeyEvent
37+
const hwKeyEvent = new CustomEvent("onHWKeyEvent", {
38+
detail: {tvEvent: tvEvent},
39+
});
40+
document.dispatchEvent(hwKeyEvent);
41+
}
42+
43+
static getTVEvent(event) {
44+
// create tv event
45+
let tvEvent = {
46+
eventKeyAction: -1,
47+
eventType: '',
48+
tag: ''
49+
};
50+
// Key Event
51+
if (event.type === 'keydown' || event.type === 'keyup') {
52+
// get event type
53+
switch (event.key) {
54+
case 'Enter':
55+
tvEvent.eventType = 'select';
56+
break;
57+
case 'ArrowUp':
58+
tvEvent.eventType = 'up';
59+
break;
60+
case 'ArrowRight':
61+
tvEvent.eventType = 'right';
62+
break;
63+
case 'ArrowDown':
64+
tvEvent.eventType = 'down';
65+
break;
66+
case 'ArrowLeft':
67+
tvEvent.eventType = 'left';
68+
break;
69+
case 'MediaPlayPause':
70+
tvEvent.eventType = 'playPause';
71+
break;
72+
case 'MediaRewind':
73+
tvEvent.eventType = 'rewind';
74+
break;
75+
case 'MediaFastForward':
76+
tvEvent.eventType = 'fastForward';
77+
break;
78+
case 'Menu':
79+
tvEvent.eventType = 'menu';
80+
break;
81+
}
82+
if (event.type === 'keydown') {
83+
tvEvent.eventKeyAction = 0;
84+
}
85+
else if (event.type === 'keyup') {
86+
tvEvent.eventKeyAction = 1;
87+
}
88+
}
89+
// Focus / Blur event
90+
else if (event.type === 'focus' || event.type === 'blur') {
91+
tvEvent.eventType = event.type;
92+
}
93+
// Get tag from id attribute
94+
if (event.target && event.target.id) {
95+
tvEvent.tag = event.target.id;
96+
}
97+
return tvEvent;
98+
}
99+
100+
}
101+
102+
export default TVEventHandler;

packages/react-native-web/src/exports/TextInput/index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import findNodeHandle from '../findNodeHandle';
2020
import React from 'react';
2121
import StyleSheet from '../StyleSheet';
2222
import TextInputState from '../../modules/TextInputState';
23+
import Platform from '../Platform';
2324

2425
const isAndroid = canUseDOM && /Android/i.test(navigator && navigator.userAgent);
2526
const emptyObject = {};
@@ -241,8 +242,10 @@ class TextInput extends React.Component<TextInputProps> {
241242
};
242243

243244
_handleKeyDown = e => {
244-
// Prevent key events bubbling (see #612)
245-
e.stopPropagation();
245+
if(!Platform.isTV) {
246+
// Prevent key events bubbling (see #612)
247+
e.stopPropagation();
248+
}
246249

247250
// Backspace, Escape, Tab, Cmd+Enter, and Arrow keys only fire 'keydown'
248251
// DOM events

packages/react-native-web/src/exports/Touchable/index.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import Position from './Position';
1818
import React from 'react';
1919
import UIManager from '../UIManager';
2020
import View from '../View';
21+
import Platform from "../Platform";
22+
import TVEventHandler from "../TVEventHandler";
2123

2224
type Event = Object;
2325
type PressEvent = Object;
@@ -380,6 +382,7 @@ const TouchableMixin = {
380382
};
381383
this._touchableNode.addEventListener('blur', this._touchableBlurListener);
382384
}
385+
383386
},
384387

385388
/**
@@ -403,7 +406,8 @@ const TouchableMixin = {
403406
*/
404407
touchableGetInitialState: function() {
405408
return {
406-
touchable: { touchState: undefined, responderID: null }
409+
touchable: { touchState: undefined, responderID: null },
410+
focused: false
407411
};
408412
},
409413

@@ -566,6 +570,7 @@ const TouchableMixin = {
566570
* using `Touchable.Mixin.withoutDefaultFocusAndBlur`.
567571
*/
568572
touchableHandleFocus: function(e: Event) {
573+
this.state.focused = true;
569574
this.props.onFocus && this.props.onFocus(e);
570575
},
571576

@@ -578,6 +583,7 @@ const TouchableMixin = {
578583
* `Touchable.Mixin.withoutDefaultFocusAndBlur`.
579584
*/
580585
touchableHandleBlur: function(e: Event) {
586+
this.state.focused = false;
581587
this.props.onBlur && this.props.onBlur(e);
582588
},
583589

@@ -873,6 +879,48 @@ const TouchableMixin = {
873879
// delays and longPress)
874880
touchableHandleKeyEvent: function(e: Event) {
875881
const { type, key } = e;
882+
if(Platform.isTV) {
883+
// Get tvEvent
884+
const tvEvent = TVEventHandler.getTVEvent(e);
885+
// Dispatch 'select' tvEvent to component
886+
if(tvEvent.eventType === 'select') {
887+
this.touchableHandlePress(tvEvent);
888+
}
889+
// Dispatch tvEvent to all listeners
890+
TVEventHandler.dispatchEvent(tvEvent);
891+
// Handle next focus
892+
if(this._touchableNode) {
893+
let nextFocusID = '';
894+
// Check nextFocus* properties
895+
if(this._touchableNode.hasAttribute("nextFocusUp") && key === 'ArrowUp') {
896+
nextFocusID = this._touchableNode.getAttribute("nextFocusUp");
897+
}
898+
else if(this._touchableNode.hasAttribute("nextFocusRight") && key === 'ArrowRight') {
899+
nextFocusID = this._touchableNode.getAttribute("nextFocusRight");
900+
}
901+
else if(this._touchableNode.hasAttribute("nextFocusDown") && key === 'ArrowDown') {
902+
nextFocusID = this._touchableNode.getAttribute("nextFocusDown");
903+
}
904+
else if(this._touchableNode.hasAttribute("nextFocusLeft") && key === 'ArrowLeft') {
905+
nextFocusID = this._touchableNode.getAttribute("nextFocusLeft");
906+
}
907+
if(nextFocusID && nextFocusID !== '') {
908+
// Get DOM element
909+
const element = document.getElementById(nextFocusID);
910+
if(element && element.tabIndex >= 0) {
911+
// Force focus
912+
element.focus();
913+
// Stop event propagation
914+
e.stopPropagation();
915+
}
916+
}
917+
}
918+
// Trigger Hardware Back Press for Back/Escape event keys
919+
if(type === 'keydown' && (key === 'Back' || key === 'Escape')) {
920+
const hwKeyEvent = new CustomEvent("hardwareBackPress", {});
921+
document.dispatchEvent(hwKeyEvent);
922+
}
923+
}
876924
if (key === 'Enter' || key === ' ') {
877925
if (type === 'keydown') {
878926
if (!this._isTouchableKeyboardActive) {

packages/react-native-web/src/exports/TouchableHighlight/index.js

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ import type { Props as TouchableWithoutFeedbackProps } from '../TouchableWithout
1414
import applyNativeMethods from '../../modules/applyNativeMethods';
1515
import createReactClass from 'create-react-class';
1616
import ensurePositiveDelayProps from '../Touchable/ensurePositiveDelayProps';
17+
import findNodeHandle from '../../exports/findNodeHandle';
1718
import * as React from 'react';
1819
import StyleSheet from '../StyleSheet';
1920
import Touchable from '../Touchable';
2021
import View from '../View';
22+
import UIManager from '../UIManager';
23+
import Platform from '../Platform';
24+
import TVEventHandler from "../TVEventHandler";
2125

2226
type Event = Object;
2327
type PressEvent = Object;
@@ -169,6 +173,10 @@ const TouchableHighlight = ((createReactClass({
169173
componentDidMount: function() {
170174
this._isMounted = true;
171175
ensurePositiveDelayProps(this.props);
176+
// Focus component
177+
if(Platform.isTV && this.props.hasTVPreferredFocus === true) {
178+
UIManager.focus(findNodeHandle(this));
179+
}
172180
},
173181

174182
componentWillUnmount: function() {
@@ -187,6 +195,15 @@ const TouchableHighlight = ((createReactClass({
187195
},
188196
*/
189197

198+
/**
199+
* Set focus to current element
200+
*/
201+
setTVPreferredFocus(hasTVPreferredFocus) {
202+
if(Platform.isTV && hasTVPreferredFocus === true) {
203+
UIManager.focus(findNodeHandle(this));
204+
}
205+
},
206+
190207
/**
191208
* `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
192209
* defined on your component.
@@ -206,11 +223,37 @@ const TouchableHighlight = ((createReactClass({
206223
},
207224

208225
touchableHandleFocus: function(e: Event) {
209-
this.props.onFocus && this.props.onFocus(e);
226+
if(Platform.isTV) {
227+
this.state.focused = true;
228+
// Keep underlay visible
229+
this._showUnderlay();
230+
// Get tvEvent
231+
const tvEvent = TVEventHandler.getTVEvent(e);
232+
// Dispatch tvEvent to component
233+
this.props.onFocus && this.props.onFocus(tvEvent);
234+
// Dispatch tvEvent to all listeners
235+
TVEventHandler.dispatchEvent(tvEvent);
236+
}
237+
else {
238+
this.props.onFocus && this.props.onFocus(e);
239+
}
210240
},
211241

212242
touchableHandleBlur: function(e: Event) {
213-
this.props.onBlur && this.props.onBlur(e);
243+
if(Platform.isTV) {
244+
this.state.focused = false;
245+
// Hide underlay
246+
this._hideUnderlay();
247+
// Get tvEvent
248+
const tvEvent = TVEventHandler.getTVEvent(e);
249+
// Dispatch tvEvent to component
250+
this.props.onBlur && this.props.onBlur(tvEvent);
251+
// Dispatch tvEvent to all listeners
252+
TVEventHandler.dispatchEvent(tvEvent);
253+
}
254+
else {
255+
this.props.onBlur && this.props.onBlur(e);
256+
}
214257
},
215258

216259
touchableHandlePress: function(e: PressEvent) {
@@ -262,6 +305,9 @@ const TouchableHighlight = ((createReactClass({
262305
_hideUnderlay: function() {
263306
clearTimeout(this._hideTimeout);
264307
this._hideTimeout = null;
308+
if(Platform.isTV && this.state.focused) {
309+
return;
310+
}
265311
if (this.props.testOnly_pressed) {
266312
return;
267313
}
@@ -298,7 +344,9 @@ const TouchableHighlight = ((createReactClass({
298344
onKeyDown={this.touchableHandleKeyEvent}
299345
//isTVSelectable={true}
300346
//tvParallaxProperties={this.props.tvParallaxProperties}
301-
//hasTVPreferredFocus={this.props.hasTVPreferredFocus}
347+
hasTVPreferredFocus={this.props.hasTVPreferredFocus}
348+
onFocus={this.touchableHandleFocus}
349+
onBlur={this.touchableHandleBlur}
302350
//nextFocusDown={this.props.nextFocusDown}
303351
//nextFocusForward={this.props.nextFocusForward}
304352
//nextFocusLeft={this.props.nextFocusLeft}

0 commit comments

Comments
 (0)