Better Focus Management For React Native Components Using You.i Engine One

Tom Lachapelle
Tom Lachapelle

When developing with Facebook React Native, you will notice that the API skews heavily towards touch as its main target platforms are iOS and Android mobile devices. In general, Facebook RN does a good job giving the mobile application developer a full toolset for handling touch behaviours and customizing them to fit the developers desired user workflows. When it comes to TV platforms, also known as 10-foot or living room platforms, focus states are much more important and React Native doesn’t have the same APIs and extensibility with focus management as it has with touch. This is where You.i Engine One can help.

You.i Engine One And Focus Management

You.i Engine One is a cross-platform rendering engine that supports building applications using React Native. When describing the user interface and interactions, an application developer can choose to either use You.i’s Adobe After Effects workflow or use JSX and Javascript. Since the AE workflow already has a lot of built-in capabilities with respect to focus, I am going to examine how we might enhance the Facebook RN workflow (JSX & Javascript) by leveraging the power of You.i Engine One through native modules and its event system.

You.i Engine One supports iOS, Android, and most of the popular TV platforms. When dealing with 10-foot (non-touch) platforms, focus is crucial to the user experience when giving and receiving feedback to the end-user. Some Facebook RN Components do not have onFocus and onBlur handlers and currently, the developer needs to wrap these into other RN Components in order to get this functionality. This can add complexity to Components that have many children, such as FlatLists, where it is hard to manually keep track if any of your list items have focus and to trigger onBlur mechanics when you detect that none of them do.

A simple way to add onFocus and onBlur handlers to any RN Component is by using the EventEmitterModule included with You.i Engine One. To access this functionality, you will need to create a native module connecting your React Native code to You.i Engine One’s C++ layer.

Leveraging the Event Emitter

Let’s now take a look at an example on how to extend your component to include Focus handling. For this example, we will leverage two event systems within You.i Engine One. The first is the EventEmitterModule, which sends events from the C++ layer to your RN Application and the other is the internal Signals and Slots system built into the Engine and used for decoupled communication.

To send events from your C++ native module within You.i Engine One and listen for them in your Javascript RN application, simply have your native module’s C++ class inherit from the EventEmitterModule and use the EmitEvent() method from within this class. In this example, I have added convenient subscribe and unsubscribe methods exported to the JS layer that will hook up your C++ events (Signals) to the event emitter.

#ifndef _YOUIREACT_VANILLA_REF_WITH_FOCUS_MODULE_H
#define _YOUIREACT_VANILLA_REF_WITH_FOCUS_MODULE_H

#include <youireact/modules/EventEmitter.h>
#include <youireact/NativeModule.h>

class YI_RN_MODULE(VanillaRefWithFocusModule, yi::reactEventEmitterModule)
{
public:
    VanillaRefWithFocusModule();

    YI_RN_EXPORT_NAME(VanillaRefWithFocusModule);
    
    YI_RN_EXPORT_METHOD(subscribeToFocusEvents)(uint64_t tag);
    YI_RN_EXPORT_METHOD(unSubscribeFromFocusEvents)(uint64_t tag);

private:
    void DescendantGainedFocus();
    void DescendantLostFocus();
};

#endif

Header for module to add focus mechanics to a single ref

With our module now inheriting from the EventEmitterModule, we must specify in the constructor the events that you are going to support within the class using the SetSupportedEvents() method. This method takes in a vector of strings representing the descriptors we want to use to catch the events in the application’s javascript layer. For this example, we use a descriptor called FOCUS_IN_REF_EVENT. Now, we can make use of the EmitEvent() method at any time using this descriptor string and passing through any arguments the application may need to process this event. In this case, the module emits an event every time the RN composition, mapped through the ref tag to the Engine CYISceneView class, gains or loses focus. We will also pass in the event type as ‘onFocus’ or ‘onBlur’ for processing on the application side.

#include "VanillaRefWithFocusModule.h"

#include <view/YiSceneView.h>
#include <youireact/ReactBridge.h>
#include <youireact/ShadowTree.h>
#include <youireact/NativeModuleRegistry.h>

#define LOG_TAG "VanillaRefWithFocusModule"

static const std::string EVENT_FOCUS_IN_REF = "FOCUS_IN_REF_EVENT";

YI_RN_REGISTER_MODULE(VanillaRefWithFocusModule);

YI_RN_INSTANTIATE_MODULE(VanillaRefWithFocusModule, EventEmitterModule);

VanillaRefWithFocusModule::VanillaRefWithFocusModule()
{
    SetSupportedEvents
    ({
        EVENT_FOCUS_IN_REF
    });
}

YI_RN_DEFINE_EXPORT_METHOD(VanillaRefWithFocusModule, subscribeToFocusEvents)(uint64_t tag)
{
    YI_LOGD(LOG_TAG, "subscribe method called");
    auto &shadowRegistry = GetBridge().GetShadowTree().GetShadowRegistry();
    auto pComponent = shadowRegistry.Get(tag);
    YI_ASSERT(pComponent, LOG_TAG, "Shadow view with tag %" PRIu64 " not found in ShadowRegistry", tag);
    auto pCounterpart = pComponent->GetCounterpart();
    YI_ASSERT(pCounterpart, LOG_TAG, "Not counterpart found");
    CYISceneView *pSceneView = dynamic_cast<CYISceneView*>(pCounterpart);
    YI_ASSERT(pSceneView, LOG_TAG, "Expected a CYISceneView");

    // set up the focus signals
    if (pSceneView)
    {
        pSceneView->DescendantGainedFocus.Connect(*this, &VanillaRefWithFocusModule::DescendantGainedFocus);
        pSceneView->DescendantLostFocus.Connect(*this, &VanillaRefWithFocusModule::DescendantLostFocus);
    }
}

YI_RN_DEFINE_EXPORT_METHOD(VanillaRefWithFocusModule, unSubscribeFromFocusEvents)(uint64_t tag)
{
    YI_LOGD(LOG_TAG, "unsubscribe method called");
    auto &shadowRegistry = GetBridge().GetShadowTree().GetShadowRegistry();
    auto pComponent = shadowRegistry.Get(tag);
    YI_ASSERT(pComponent, LOG_TAG, "Shadow view with tag %" PRIu64 " not found in ShadowRegistry", tag);
    auto pCounterpart = pComponent->GetCounterpart();
    YI_ASSERT(pCounterpart, LOG_TAG, "Not counterpart found");
    CYISceneView *pSceneView = dynamic_cast<CYISceneView*>(pCounterpart);
    YI_ASSERT(pSceneView, LOG_TAG, "Expected a CYISceneView");

    // clean up focus signals
    if (pSceneView)
    {
        pSceneView->DescendantGainedFocus.Disconnect(*this, &VanillaRefWithFocusModule::DescendantGainedFocus);
        pSceneView->DescendantLostFocus.Disconnect(*this, &VanillaRefWithFocusModule::DescendantLostFocus);
    }
}

// implement signal handlers
void VanillaRefWithFocusModule::DescendantGainedFocus()
{
    EmitEvent(EVENT_FOCUS_IN_REF, folly::dynamic::object("type", "onFocus"));
}

void VanillaRefWithFocusModule::DescendantLostFocus(uint64_t tag)
{
    EmitEvent(EVENT_FOCUS_IN_REF, folly::dynamic::object("type", "onBlur"));
}

Native module implementation to add focus mechanics to a single ref

Using Signals To Catch Focus Events within You.i Engine One

In the example above, I glossed over the code that informs this module that a focus event has occurred within You.i Engine One. To do this, we must connect to the Signal that gets fired from the underlying CYISceneView object. All components within the React Native application derive from the CYISceneView class when using the Engine’s RN solution. To do this, use the connect() method of the Signal we want to listen for and pass in a pointer to the slot, in our example — *this (i.e, the object that will handle the Signal) as well as a reference to the Signal handling function — &VanillaRefWithFocusModule::DescendantGainedFocus. This function now calls our EmitEvent() method that is sent and handled within the Javascript layer of our RN application. If you would like to read more on Signals, I would recommend you read through the API in our developer portal and learn more about this powerful and integral part of You.i Engine One.

How To Use The Module And Events in Javascript

For this example, I’ve created a simple React Native application that has a title and a custom component that wraps a FlatList of images. When we focus on the title or the FlatList, the container gets an orange border to give a visual cue where the focus is.

Note: the app’s title element is implemented like most typical RN components. It catches focus by wrapping it within a touchable or other RN component that has support for focus. The extended FlatList component, FlatListWithFocus, uses our native module to do the same thing without adding a wrapper composition.

import React, { Component } from 'react';
import {
  AppRegistry,
  FocusManager,
  StyleSheet,
  Text,
  TouchableHighlight,
  View,
} from '@youi/react-native-youi';
import FlatListWithFocus from './FlatListWithFocus';
import ListItem from './ListItem';
import MockData from './MockData';

export default class YiReactApp extends Component {
  constructor(props) {
    super(props);

    this.titleRef = React.createRef();

    this.state = {
      activeTitleFocus: true,
      activeFocus: false,
    };
  }

  componentDidMount() {
    if (this.titleRef) { 
      FocusManager.focus(this.titleRef);
    }
  }

  render() {
    const activeStyle = {
      borderWidth: 5,
      borderColor: 'orange',
    };

    return (
      <View style={styles.mainContainer}>
        <View style={styles.statusInfoContainer}>
          <TouchableHighlight 
            style={ [styles.statusInfoContainer, this.state.activeTitleFocus ? activeStyle : null] }
            onFocus={this.onTitleFocus}
            onBlur={this.onTitleBlur}
            ref={ (ref) => { this.titleRef = ref }}
          >
            <Text style={{padding: 10}}>
                Extended FlatList Using You.i View Focus Management
            </Text>
          </TouchableHighlight>
        </View>
        <View style={styles.flatlistContainer}>
          <View style={this.state.activeFocus ? activeStyle : null} >
            <FlatListWithFocus
              horizontal={true}
              data={MockData}
              renderItem={this.renderItem}
              onFocus={this.onFocus}
              onBlur={this.onBlur}
            />
          </View>
        </View>
      </View>
    );
  }

  renderItem = (item, index) => {
    return (
      <ListItem 
        width={160} 
        height={120}
        title={item.item.title} 
        imageUrl={item.item.imageUrl} 
      />
    );
  }

  onTitleFocus = () => {
    if (!this.state.activeTitleFocus) {
      this.setState({ activeTitleFocus: true });
    }
  }

  onTitleBlur = () => {
    if (this.state.activeTitleFocus) {
      this.setState({ activeTitleFocus: false });
    }
  }

  onFocus = () => {
    if (!this.state.activeFocus) {
      this.setState({ activeFocus: true });
    }
  }

  onBlur = () => {
    if (this.state.activeFocus) {
      this.setState({ activeFocus: false });
    }
  }
}

const styles = StyleSheet.create({
  mainContainer: {
    flex: 1, 
    display: 'flex', 
    flexDirection: 'colum', 
    backgroundColor: '#E6E6E6'
  },
  statusInfoContainer: {
    padding: 10,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: 'lightgrey'
  },
  flatlistContainer: {
    paddingTop: 20,
    justifyContent: 'space-evenly',
    display: 'flex',
    flexDirection: 'column',
  }
});

AppRegistry.registerComponent("YiReactApp", () => YiReactApp);

index.ios.js for our RN example application

Now we will get into the FlatListWithFocus Component to see how we can make use of this event system in our Javascript code to extend the FlatList to have onFocus and onBlur mechanics.

import React, { PureComponent } from 'react';
import {
  NativeModules,
  NativeEventEmitter,
  findNodeHandle
} from 'react-native';
import { FlatList } from "@youi/react-native-youi";

const RefWithFocusEmitter = new NativeEventEmitter(NativeModules.VanillaRefWithFocusModule);
const RefWithFocusModule = NativeModules.VanillaRefWithFocusModule;

const ON_FOCUS_EVENT = 'onFocus';
const ON_BLUR_EVENT = 'onBlur';

export default class FlatListWithFocus extends PureComponent {
  constructor(props) {
    super(props);

    this.ListRef = React.createRef();

    this.focusEventSubscription = RefWithFocusEmitter.addListener("FOCUS_IN_REF_EVENT", payload => {
      switch (payload.type) {
        case ON_FOCUS_EVENT:
            this.props.onFocus();
          break;
        case ON_BLUR_EVENT:
          this.props.onBlur();
          break;
        default:
          break;
      }
    });
  }

  componentDidMount() {
    if (this.ListRef && RefWithFocusModule) {
      RefWithFocusModule.subscribeToFocusEvents(findNodeHandle(this.ListRef));
    }
  }

  componentWillUnmount() {
    if (this.ListRef && RefWithFocusModule) {
      RefWithFocusModule.unSubscribeFromFocusEvents(findNodeHandle(this.ListRef));
    }
    if (this.focusEventSubscription) {
      this.RefWithFocusEmitter.remove();
    }
  }

  render() {
    return (
      <FlatList 
        {...this.props}
        ref={ (ref) => {this.ListRef = ref} }
      />
    )
  }
}

FlatListWithFocus Component that extends FlatList but with built-in onFocus and onBlur mechanics

To do this we have to create a new NativeEventEmitter passing in our native module that implements the EventEmitter on the native side. We also want a reference to the module itself in order to use the subscribe and unsubscribe methods exported from it. We then need to setup the listener within the Javascript code using RefWithFocusEmitter.addListener() (inherited through NativeEventEmitter) by passing in the event descriptor we are listening for and the function we want to use in order to handle the event. This example lends itself to subscription so we will add subscribe and unsubscribe methods so the internal module code will only fire the FOCUS_IN_VIEW_EVENT when this Composition, denoted by its ref handle, receives focus and blur related signals from inside the Engine native code. We must also clean up the emitter once the component unmounts using remove() method of the subscription object passed back from the addListener() method.

By handling the onFocus and onBlur types of events described in the payload, we can call the appropriate onFocus and onBlur methods passed in as props to the component. As you can see in the code, once we pass through the props to the underlying FlatList, that is all that is needed.

focus management-onFocus-onBlur-single-flatlist

Single FlatList extended to use onFocus/onBlur mechanics

But What Happens When You Add Another Composition?

Now, what happens if we add another <FlatListWithFocus>? As you can see the issues appear right away.

focus management-onFocus-onBlur-extended-flatlist

Using two FlatLists extended to use onFocus/onBlur mechanics

With our current solution, you are essentially subscribing to all events for any ref tag that the Signal has been connected to. What we need is for the javascript code to filter out events that are not associated with its ref. There are a few steps to take to make this happen. First, we need to add a way for the EventEmitter method in the native module to pass the ref tag the Javascript code can filter on. Unfortunately, we can’t change the signal handler function signature as the signature is defined in the Engine. Luckily, the Signal architecture allows you to provide lambda functions (thanks C++11) as signal handlers when you connect to the Signal. The only real downside to this is that you don’t have a function reference that you can pass into the Disconnect () Signal method. Again, You.i Engine One architecture allows for this scenario as the Connect() method will pass back a CYISignalConnectionID object that can then be used as an argument to the Disconnect() method to properly clean up when you no longer need to receive the Signal. Now, all we need to do is use the original signal handler function in the lambda, making sure to pass in the ref tag (don’t forget to update these method signatures in .cpp and header files) to send when the event is emitted.

Note: for this example, I am simply storing the CYISignalConnectionID objects in a map keyed with the ref tag. Feel free to chose the implementation that best fits your use case.

YI_RN_DEFINE_EXPORT_METHOD(VanillaRefWithFocusModule, subscribeToFocusEvents)(uint64_t tag)
{
    YI_LOGD(LOG_TAG, "subscribe method called");
    auto &shadowRegistry = GetBridge().GetShadowTree().GetShadowRegistry();
    auto pComponent = shadowRegistry.Get(tag);
    YI_ASSERT(pComponent, LOG_TAG, "Shadow view with tag %" PRIu64 " not found in ShadowRegistry", tag);
    auto pCounterpart = pComponent->GetCounterpart();
    YI_ASSERT(pCounterpart, LOG_TAG, "Not counterpart found");
    CYISceneView *pSceneView = dynamic_cast<CYISceneView*>(pCounterpart);
    YI_ASSERT(pSceneView, LOG_TAG, "Expected a CYISceneView");

    // set up the focus signals
    if (pSceneView)
    {
        CYISignalConnectionID gainedFocusID = pSceneView->DescendantGainedFocus.Connect(*this, [this, tag]{ DescendantGainedFocus(tag); });
        CYISignalConnectionID lostFocusID = pSceneView->DescendantLostFocus.Connect(*this, [this, tag]{ DescendantLostFocus(tag); });
    
        m_gainedFocusMap.insert(std::pair<uint64_t, CYISignalConnectionID>(tag, gainedFocusID));
        m_lostFocusMap.insert(std::pair<uint64_t, CYISignalConnectionID>(tag, lostFocusID));
    }
}

YI_RN_DEFINE_EXPORT_METHOD(VanillaRefWithFocusModule, unSubscribeFromFocusEvents)(uint64_t tag)
{
    YI_LOGD(LOG_TAG, "unsubscribe method called");
    auto &shadowRegistry = GetBridge().GetShadowTree().GetShadowRegistry();
    auto pComponent = shadowRegistry.Get(tag);
    YI_ASSERT(pComponent, LOG_TAG, "Shadow view with tag %" PRIu64 " not found in ShadowRegistry", tag);
    auto pCounterpart = pComponent->GetCounterpart();
    YI_ASSERT(pCounterpart, LOG_TAG, "Not counterpart found");
    CYISceneView *pSceneView = dynamic_cast<CYISceneView*>(pCounterpart);
    YI_ASSERT(pSceneView, LOG_TAG, "Expected a CYISceneView");

    // clean up focus signals
    if (pSceneView)
    {
        pSceneView->DescendantGainedFocus.Disconnect(m_gainedFocusMap[tag]);
        pSceneView->DescendantLostFocus.Disconnect(m_lostFocusMap[tag]);
        
        m_gainedFocusMap.erase(tag);
        m_lostFocusMap.erase(tag);
    }
}

// implement signal handlers
void VanillaRefWithFocusModule::DescendantGainedFocus(uint64_t tag)
{
    EmitEvent(EVENT_FOCUS_IN_REF, folly::dynamic::object("type", "onFocus")("ref", tag));
}

void VanillaRefWithFocusModule::DescendantLostFocus(uint64_t tag)
{
    EmitEvent(EVENT_FOCUS_IN_REF, folly::dynamic::object("type", "onBlur")("ref", tag));
}

Updates to Native Module to take multiple composition refs into account

Now, all we have to do is update our event listener function in the FlatListWithFocus component so it filters on its own ref and only triggers the onFocus and onBlur mechanics when the ref tag passed in the payload of the event matches its own.

this.focusEventSubscription = RefWithFocusEmitter.addListener("FOCUS_IN_REF_EVENT", payload => {
  if (payload.ref === findNodeHandle(this.ListRef)) {
    switch (payload.type) {
      case ON_FOCUS_EVENT:
          this.props.onFocus();
        break;
      case ON_BLUR_EVENT:
        this.props.onBlur();
        break;
      default:
        break;
    }
  }
});

Update to DeviceEventEmitter listener to filter on ref tag code snippet in FlatListWithFocus.js

Now we can add as many Components as we want and each one will only handle the focus events emitted from the underlying CYISceneView that it is associated with.

focus management-onFocus-onBlur-extended-flatlist-2

Properly handling multiple Components with extended onFocus onBlur mechanics

Wrap Up

I hope this example demonstrated the advantages that an application developer achieves by using the built-in tools available in You.i Engine One. You can extend your React Native component, through use of a Native Module, to add extra focus management capabilities advantage of You.i Engine One’s Signals & Slots API and built-in Event Emitter Module. This simple technique can be used to extend your React Native application in ways that makes sense for applications written with more than just handheld mobile devices in mind.

Here’s something similar we think you’ll enjoy.