react-native-tvos: hasTVPreferredFocus props breaks remote events on tvOS 14.2

Description

With the update to tvOS 14.2 we noticed that remote events stop emitting. For example the press of the play/pause button or simply pressing the remote to select something. After further investigation we realised that the use of the prop hasTVPreferredFocus is the culprit.

I have created a sample project that illustrate this https://github.com/darji89/react-native-tvos-remote-bug

React Native version:

System: OS: macOS 10.15.4 CPU: (12) x64 Intel® Core™ i7-9750H CPU @ 2.60GHz Memory: 875.97 MB / 16.00 GB Shell: 3.2.57 - /bin/bash Binaries: Node: 10.18.0 - ~/.nvm/versions/node/v10.18.0/bin/node Yarn: 1.19.1 - /usr/local/bin/yarn npm: 6.13.4 - ~/.nvm/versions/node/v10.18.0/bin/npm Watchman: 4.9.0 - /usr/local/bin/watchman Managers: CocoaPods: 1.8.4 - /usr/local/bin/pod SDKs: iOS SDK: Platforms: iOS 14.2, DriverKit 20.0, macOS 11.0, tvOS 14.2, watchOS 7.1 Android SDK: Not Found IDEs: Android Studio: 3.5 AI-191.8026.42.35.5977832 Xcode: 12.2/12B45b - /usr/bin/xcodebuild Languages: Java: 13.0.1 - /usr/bin/javac Python: 2.7.16 - /usr/bin/python npmPackages: @react-native-community/cli: Not Found react: 16.13.1 => 16.13.1 react-native: Not Found react-native-macos: Not Found react-native-tvos: 0.63.1-4 npmGlobalPackages: react-native: Not Found

Steps To Reproduce

  1. clone this repo: https://github.com/darji89/react-native-tvos-remote-bug
  2. run the app on a tvOS 14.2 device. Alternate pressing the select and the menu button.
  3. In the logs you can see which key is being pressed.
  4. A “press me” button will appear and disappear.
  5. After the button disappear it doesn’t appear back - this is because the button had hasTVPreferredFocus prop
  6. if you remove this prop and restart the app (!) the toggle between the two screen will work.

Expected Results

I should be able to keep toggling screen by alternating between pressing the select and menu button. I can also confirm that this bug is also still present is use the Pressable component instead of TouchableOpacity

Snack, code example, screenshot, or link to a repository:

https://github.com/darji89/react-native-tvos-remote-bug

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 16

Most upvoted comments

@darji89 you want TVMenuControl. Apple requires that back navigation with the menu key go to the home screen when you are at the bottom of your navigation stack. To support this, the RN tvOS code does not automatically add a gesture handler for menu key presses. To get the menu key behavior you want, call TVMenuControl.enableTVMenuKey() when you want to handle menu key presses, and TVMenuControl.disableTVMenuKey() when you want to allow the app to exit normally with the menu key.

TVMenuControl: This module provides methods to enable and disable navigation using the menu key on the TV remote. This is required in order to fix an issue with Apple’s guidelines for menu key navigation (see https://github.com/facebook/react-native/issues/18930). The RNTester app uses this new module to implement correct menu key behavior.

Since the react-native-tvos-controller does take over the gesture handlers normally present in RCTRootView, I’m going to “officially” recommend that for this use case, you not use that package and instead use useTVEventHandler() in your code. It will actually simplify this code.

Here’s your code modified to show the button on ‘select’ and hide it on ‘playPause’. If you really want to use the menu key, you would import TVMenuControl and call TVMenuControl.enableTVMenuKey() first.

I have an issue (#16 ) in the backlog to try to pull in some of the additional functionality of react-native-tvos-controller (particularly the pan gesture handling).

import React, { useState, } from 'react';
import ReactNative, {
  StyleSheet,
  View,
  Text,
  TouchableOpacity,
  useTVEventHandler,
} from 'react-native';

const App: () => React$Node = () => {
  const [toggle, setToggle] = useState(false);
  const myTVEventHandler = (evt: HWFocusEvent) => {
    if (evt.eventType === 'select') {
      setToggle(true);
    }
    if (evt.eventType === 'playPause') {
      setToggle(false);
    }
  };
  useTVEventHandler(myTVEventHandler);
  return (
    <View style={styles.root}>
      {toggle && (
        <TouchableOpacity hasTVPreferredFocus onPress={() => {
          console.log('press');
        }}>
          <Text style={styles.label}>Press me</Text>
        </TouchableOpacity>
      )}
    </View>
  );
};

I also reproduce the issue in the 14.2 simulator. I think the issue is that the underlying Apple API preferredFocusEnvironments will not work if the view isn’t actually rendered as a native view. For a use case like this, a TVFocusGuideView might be better than using hasTVPreferredFocus. I’ll see if I can get that to work.

I ran the sample repo he provided and can confirm I see the issue he describes on my 14.2 simulator.

I ran into this issue last night. I made a quick fix in our project to make sure that a TouchableOpacity with hasTVPreferredFocus=true is always mounted to the screen, even if it requires hiding it.