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

Building for TV Devices #1566

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
14 changes: 12 additions & 2 deletions packages/react-native-web/src/exports/BackHandler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,19 @@ function emptyFunction() {}

const BackHandler = {
exitApp: emptyFunction,
addEventListener() {
/**
* Listen to "hardwareBackPress" event
*
* @param event
* @param callback
* @returns {{remove: remove}}
*/
addEventListener(event: string, callback: Function) {
document.addEventListener(event, callback);
return {
remove: emptyFunction
remove: () => {
document.removeEventListener(event, callback);
}
};
},
removeEventListener: emptyFunction
Expand Down
3 changes: 3 additions & 0 deletions packages/react-native-web/src/exports/Platform/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const Platform = {
return true;
}
return false;
},
get isTV(): boolean {
return process.env.REACT_APP_IS_TV === 'true';
}
};

Expand Down
100 changes: 99 additions & 1 deletion packages/react-native-web/src/exports/TVEventHandler/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,99 @@
export default {};
class TVEventHandler {
constructor() {
this.component = null;
this.callback = null;
this.onHWKeyEvent = this.onHWKeyEvent.bind(this);
}

enable(component, callback) {
this.component = component;
this.callback = callback;
document.addEventListener('onHWKeyEvent', this.onHWKeyEvent);
}

disable() {
document.removeEventListener('onHWKeyEvent', this.onHWKeyEvent);
this.component = null;
this.callback = null;
}

onHWKeyEvent(event) {
if (this.callback) {
if (event && event.detail) {
const tvEvent = event.detail.tvEvent;
if (tvEvent) {
this.callback(this.component, tvEvent);
}
}
}
}

static dispatchEvent(tvEvent) {
// Dispatch tvEvent through onHWKeyEvent
// eslint-disable-next-line no-undef
const hwKeyEvent = new CustomEvent('onHWKeyEvent', {
detail: { tvEvent: tvEvent }
});
document.dispatchEvent(hwKeyEvent);
}

static getTVEvent(event) {
// create tv event
const tvEvent = {
eventKeyAction: -1,
eventType: '',
tag: ''
};
// Key Event
if (event.type === 'keydown' || event.type === 'keyup') {
// get event type
switch (event.key) {
case 'Enter':
tvEvent.eventType = 'select';
break;
case 'ArrowUp':
tvEvent.eventType = 'up';
break;
case 'ArrowRight':
tvEvent.eventType = 'right';
break;
case 'ArrowDown':
tvEvent.eventType = 'down';
break;
case 'ArrowLeft':
tvEvent.eventType = 'left';
break;
case 'MediaPlayPause':
tvEvent.eventType = 'playPause';
break;
case 'MediaRewind':
tvEvent.eventType = 'rewind';
break;
case 'MediaFastForward':
tvEvent.eventType = 'fastForward';
break;
case 'Menu':
tvEvent.eventType = 'menu';
break;
default:
tvEvent.eventType = '';
}
if (event.type === 'keydown') {
tvEvent.eventKeyAction = 0;
} else if (event.type === 'keyup') {
tvEvent.eventKeyAction = 1;
}
}
// Focus / Blur event
else if (event.type === 'focus' || event.type === 'blur') {
tvEvent.eventType = event.type;
}
// Get tag from id attribute
if (event.target && event.target.id) {
tvEvent.tag = event.target.id;
}
return tvEvent;
}
}

export default TVEventHandler;
7 changes: 5 additions & 2 deletions packages/react-native-web/src/exports/TextInput/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import usePlatformMethods from '../../modules/usePlatformMethods';
import useResponderEvents from '../../modules/useResponderEvents';
import StyleSheet from '../StyleSheet';
import TextInputState from '../../modules/TextInputState';
import Platform from '../Platform';

/**
* Determines whether a 'selection' prop differs from a node's existing
Expand Down Expand Up @@ -244,8 +245,10 @@ const TextInput = forwardRef<TextInputProps, *>((props, forwardedRef) => {
}

function handleKeyDown(e) {
// Prevent key events bubbling (see #612)
e.stopPropagation();
if (!Platform.isTV) {
// Prevent key events bubbling (see #612)
e.stopPropagation();
}

const blurOnSubmitDefault = !multiline;
const shouldBlurOnSubmit = blurOnSubmit == null ? blurOnSubmitDefault : blurOnSubmit;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as React from 'react';
import { useCallback, useMemo, useState, useRef } from 'react';
import useMergeRefs from '../../modules/useMergeRefs';
import usePressEvents from '../../modules/usePressEvents';
import useTVEvents from '../../modules/useTVEvents';
import StyleSheet from '../StyleSheet';
import View from '../View';

Expand All @@ -30,7 +31,13 @@ type Props = $ReadOnly<{|
onShowUnderlay?: ?() => void,
style?: ViewStyle,
testOnly_pressed?: ?boolean,
underlayColor?: ?ColorValue
underlayColor?: ?ColorValue,
hasTVPreferredFocus?: ?boolean,
nextFocusDown?: ?any,
nextFocusForward?: ?any,
nextFocusLeft?: ?any,
nextFocusRight?: ?any,
nextFocusUp?: ?any
|}>;

type ExtraStyles = $ReadOnly<{|
Expand Down Expand Up @@ -78,6 +85,14 @@ function TouchableHighlight(props: Props, forwardedRef): React.Node {
delayLongPress,
disabled,
focusable,
hasTVPreferredFocus,
nextFocusDown,
nextFocusForward,
nextFocusLeft,
nextFocusRight,
nextFocusUp,
onFocus,
onBlur,
onHideUnderlay,
onLongPress,
onPress,
Expand Down Expand Up @@ -159,12 +174,40 @@ function TouchableHighlight(props: Props, forwardedRef): React.Node {

const pressEventHandlers = usePressEvents(hostRef, pressConfig);

const tvConfig = useMemo(
() => ({
hasTVPreferredFocus,
nextFocusDown,
nextFocusForward,
nextFocusLeft,
nextFocusRight,
nextFocusUp,
onPress,
onFocus,
onBlur
}),
[
hasTVPreferredFocus,
nextFocusDown,
nextFocusForward,
nextFocusLeft,
nextFocusRight,
nextFocusUp,
onPress,
onFocus,
onBlur
]
);

const tvEventHandlers = useTVEvents(hostRef, tvConfig);

const child = React.Children.only(children);

return (
<View
{...rest}
{...pressEventHandlers}
{...tvEventHandlers}
accessibilityDisabled={disabled}
focusable={!disabled && focusable !== false}
ref={setRef}
Expand Down
138 changes: 138 additions & 0 deletions packages/react-native-web/src/modules/useTVEvents/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

'use strict';

import * as React from 'react';
import { useEffect } from 'react';
import View from '../../exports/View';
import UIManager from '../../exports/UIManager';
import Platform from '../../exports/Platform';
import TVEventHandler from '../../exports/TVEventHandler';

type Event = any;

export type TVResponderConfig = $ReadOnly<{|
hasTVPreferredFocus?: ?boolean,
nextFocusDown?: ?any,
nextFocusForward?: ?any,
nextFocusLeft?: ?any,
nextFocusRight?: ?any,
nextFocusUp?: ?any,
onPress?: ?(event: Event) => void,
onFocus?: ?(event: Event) => void,
onBlur?: ?(event: Event) => void
|}>;

export default function useTVEvents(
hostRef: React.ElementRef<typeof View>,
config: TVResponderConfig
) {
useEffect(() => {
if (Platform.isTV && config.hasTVPreferredFocus) {
if (hostRef.current) {
UIManager.focus(hostRef.current);
}
}
}, [config.hasTVPreferredFocus, hostRef]);

function onKeyEvent(event: Event) {
const { type, key } = event;
// Get tvEvent
const tvEvent = TVEventHandler.getTVEvent(event);
// Dispatch 'select' tvEvent to component
if (tvEvent.eventType === 'select') {
if (config.onPress) {
config.onPress(tvEvent);
}
}
// Dispatch tvEvent to all listeners
TVEventHandler.dispatchEvent(tvEvent);
// Handle next focus
let nextElement = null;
// Check nextFocus properties set using : nextFocus*={findNodeHandle(ref.current)}
if (config.nextFocusUp && key === 'ArrowUp') {
nextElement = config.nextFocusUp;
} else if (config.nextFocusRight && key === 'ArrowRight') {
nextElement = config.nextFocusRight;
} else if (config.nextFocusDown && key === 'ArrowDown') {
nextElement = config.nextFocusDown;
} else if (config.nextFocusLeft && key === 'ArrowLeft') {
nextElement = config.nextFocusLeft;
} else if (config.nextFocusForward && key === 'ArrowRight') {
nextElement = config.nextFocusForward;
}
if (nextElement) {
// Focus if element is focusable
UIManager.focus(nextElement);
// Stop event propagation
event.stopPropagation();
}
// Check nextFocus properties set using : ref.current.setNativeProps({nextFocus*: nativeID}
let nextFocusID = '';
// Check nextFocus* properties
if (hostRef.current.hasAttribute('nextFocusUp') && key === 'ArrowUp') {
nextFocusID = hostRef.current.getAttribute('nextFocusUp');
} else if (hostRef.current.hasAttribute('nextFocusRight') && key === 'ArrowRight') {
nextFocusID = hostRef.current.getAttribute('nextFocusRight');
} else if (hostRef.current.hasAttribute('nextFocusDown') && key === 'ArrowDown') {
nextFocusID = hostRef.current.getAttribute('nextFocusDown');
} else if (hostRef.current.hasAttribute('nextFocusLeft') && key === 'ArrowLeft') {
nextFocusID = hostRef.current.getAttribute('nextFocusLeft');
} else if (hostRef.current.hasAttribute('nextFocusForward') && key === 'ArrowRight') {
nextFocusID = hostRef.current.getAttribute('nextFocusForward');
}
if (nextFocusID && nextFocusID !== '') {
// Get DOM element
nextElement = document.getElementById(nextFocusID);
if (nextElement) {
// Focus is element if focusable
UIManager.focus(nextElement);
// Stop event propagation
event.stopPropagation();
}
}
// Trigger Hardware Back Press for Back/Escape event keys
if (type === 'keydown' && (key === 'Back' || key === 'Escape')) {
// eslint-disable-next-line no-undef
const hwKeyEvent = new CustomEvent('hardwareBackPress', {});
document.dispatchEvent(hwKeyEvent);
}
}

const tvEventHandlers = Platform.isTV
? {
onFocus: (event: Event) => {
// Get tvEvent
const tvEvent = TVEventHandler.getTVEvent(event);
// Dispatch tvEvent to component
if (config.onFocus) {
config.onFocus(tvEvent);
}
// Dispatch tvEvent to all listeners
TVEventHandler.dispatchEvent(tvEvent);
},
onBlur: (event: Event) => {
// Get tvEvent
const tvEvent = TVEventHandler.getTVEvent(event);
// Dispatch tvEvent to component
if (config.onBlur) {
config.onBlur(tvEvent);
}
// Dispatch tvEvent to all listeners
TVEventHandler.dispatchEvent(tvEvent);
},
onKeyDown: onKeyEvent,
onKeyUp: onKeyEvent
}
: {};

return tvEventHandlers;
}