Skip to content
This repository was archived by the owner on May 5, 2023. It is now read-only.

Commit 535c41c

Browse files
asgvardAntonio Salvati
authored andcommitted
Added support for native mode (#13)
* Added support for native mode * Small fix
1 parent 77cd070 commit 535c41c

File tree

7 files changed

+209
-48
lines changed

7 files changed

+209
-48
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
9+
## [2.3.0]
10+
### Added
11+
- Added support for Native environment. Now if the service is initialized with `nativeMode` flag, it will skip creating window event listeners, measuring coordinates and other web-only features. It will still continue to register all focusable components and update `focused` flag on them.
12+
- Added new method `stealFocus` to `withFocusable` hoc. It works exactly the same as `setFocus` apart from that it doesn't care about arguments passed to this method. This is useful when binding it to a callback that passed some params back that you don't care about.
13+
14+
## [2.2.1]
815
### Changed
916
- Improved the main navigation algorithm. Instead of calculating distance between center of the borders between 2 items in the direction of navigation, the new algorithm now prioritises the distance by the main coordinate and then takes into account the distance by the secondary coordinate. Inspired by this [algorithm](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox_OS_for_TV/TV_remote_control_navigation#Algorithm_design)
1017

README.md

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,30 @@ const MenuFocusable = withFocusable({
137137
})(Menu);
138138
```
139139

140+
### Using in Native environment
141+
Since in native environment the focus is controlled by the native engine, we can only "sync" with it by setting focus on the component itself when it gets focused.
142+
Native navigation system automatically converts all `Touchable` component to focusable components and enhances them with the callbacks such as `onFocus` and `onBlur`.
143+
Read more here: [React Native on TVs](https://facebook.github.io/react-native/docs/building-for-apple-tv).
144+
145+
```jsx
146+
import {withFocusable} from '@noriginmedia/react-spatial-navigation';
147+
148+
const Component = ({focused, stealFocus}) => (<View>
149+
<View style={focused ? styles.focusedStyle : styles.defaultStyle} />
150+
<TouchableOpacity
151+
onFocus={stealFocus}
152+
/>
153+
</View>);
154+
155+
const FocusableComponent = withFocusable()(Component);
156+
```
157+
140158
# API
141159

142160
## Top level
143161
### `initNavigation`: function
144162
Function that needs to be called to enable Spatial Navigation system and bind key event listeners.
145-
Accepts [initConfig](#initConfig) as a param.
163+
Accepts [Initialization Config](#initialization-config) as a param.
146164

147165
```jsx
148166
initNavigation({
@@ -151,19 +169,35 @@ initNavigation({
151169
})
152170
```
153171

154-
## Initialization Config
155-
### `debug`: boolean
172+
#### Initialization Config
173+
##### `debug`: boolean
156174
Enable console debugging
157175

158176
* **false (default)**
159177
* **true**
160178

161-
### `visualDebug`: boolean
179+
##### `visualDebug`: boolean
162180
Enable visual debugging (all layouts, reference points and siblings refernce points are printed on canvases)
163181

164182
* **false (default)**
165183
* **true**
166184

185+
##### `nativeMode`: boolean
186+
Enable Native mode. It will block certain web-only functionality such as:
187+
- adding window key listeners
188+
- measuring DOM layout
189+
- `onBecameFocused` callback doesn't return coordinates
190+
- coordinates calculations when navigating
191+
- down-tree propagation
192+
- last focused child
193+
- preferred focus key
194+
195+
Native mode should be only used to keep the tree of focusable components and to sync the `focused` flag to enable styling for focused components.
196+
In Native mode you can only `stealFocus` to some component to flag it as `focused`, normal `setFocus` method is blocked because it will not propagate to native layer.
197+
198+
* **false (default)**
199+
* **true**
200+
167201
### `setKeyMap`: function
168202
Function to set custom key codes.
169203
```jsx
@@ -182,14 +216,14 @@ Main HOC wrapper function. Accepts [config](#config) as a param.
182216
const FocusableComponent = withFocusable({...})(Component);
183217
```
184218

185-
## Config
186-
### `trackChildren`: boolean
219+
#### Config
220+
##### `trackChildren`: boolean
187221
Determine whether to track when any child component is focused. Wrapped component can rely on `hasFocusedChild` prop when this mode is enabled. Otherwise `hasFocusedChild` will be always `false`.
188222

189223
* **false (default)** - Disabled by default because it causes unnecessary render call when `hasFocusedChild` changes
190224
* **true**
191225

192-
### `forgetLastFocusedChild`: boolean
226+
##### `forgetLastFocusedChild`: boolean
193227
Determine whether this component should not remember the last focused child components. By default when focus goes away from the component and then it gets focused again, it will focus the last focused child. This functionality is enabled by default.
194228

195229
* **false (default)**
@@ -263,13 +297,26 @@ Whether component is currently focused. It is only `true` if this exact componen
263297
This prop indicates that the component currently has some focused child on any depth of the focusable tree.
264298

265299
### `setFocus`: function
266-
This method sets the focus to another component (when focus key is passed as param) or steals the focus to itself (when used w/o params). It is also possible to set focus to a non-existent component, and it will be automatically picked up when component with that focus key will get mounted. This preemptive setting of the focus might be useful when rendering lists of data. You can assign focus key with the item index and set it to e.g. first item, then as soon as it will be rendered, that item will get focused.
300+
This method sets the focus to another component (when focus key is passed as param) or steals the focus to itself (when used w/o params). It is also possible to set focus to a non-existent component, and it will be automatically picked up when component with that focus key will get mounted.
301+
This preemptive setting of the focus might be useful when rendering lists of data.
302+
You can assign focus key with the item index and set it to e.g. first item, then as soon as it will be rendered, that item will get focused.
303+
In Native mode this method is ignored (`noop`).
267304

268305
```jsx
269306
setFocus(); // set focus to self
270307
setFocus('SOME_COMPONENT'); // set focus to another component if you know its focus key
271308
```
272309

310+
### `stealFocus`: function
311+
This method works exactly like `setFocus`, but it always sets focus to current component no matter which params you pass in.
312+
This is the only way to set focus in Native mode.
313+
314+
```jsx
315+
<TouchableOpacity
316+
onFocus={stealFocus}
317+
/>
318+
```
319+
273320
### `pauseSpatialNavigation`: function
274321
This function pauses key listeners. Useful when you need to temporary disable navigation. (e.g. when player controls are hidden during video playback and you want to bind the keys to show controls again).
275322

@@ -302,21 +349,19 @@ Source code is in `src/App.js`
302349

303350
### `spatialNavigation` Service
304351
* New components are added in `addFocusable`and removed in `removeFocusable`
305-
* Main function to change focus is `setFocus`. First it decides next focus key (`getNextFocusKey`), then set focus to the new component (`setCurrentFocusedKey`), then the service updates all components that has focused child and finally updates layout (coordinates and dimensions) for all focusable component.
352+
* Main function to change focus in web environment is `setFocus`. First it decides next focus key (`getNextFocusKey`), then set focus to the new component (`setCurrentFocusedKey`), then the service updates all components that has focused child and finally updates layout (coordinates and dimensions) for all focusable component.
306353
* `getNextFocusKey` is used to determine the good candidate to focus when you call `setFocus`. This method will either return the target focus key for the component you are trying to focus, or go down by the focusable tree and select the best child component to focus. This function is recoursive and going down by the focusable tree.
307354
* `smartNavigate` is similar to the previous one, but is called in response to a key press event. It tries to focus the best sibling candidate in the direction of key press, or delegates this task to a focusable parent, that will do the same attempt for its sibling and so on.
355+
* In Native environment the only way to set focus is `stealFocus`. This service mostly works as a "sync" between native navigation system and JS to apply `focused` state and keep the tree structure of focusable components. All the layout and coordinates measurement features are disabled because native engine takes care of it.
308356

309357
## Contributing
310358
Please follow the [Contribution Guide](https://github.com/NoriginMedia/react-spatial-navigation/blob/master/CONTRIBUTING.md)
311359

312360
# TODOs
313-
- [x] Get rid of `propagateFocus`, because it is used in 99% of the times when component has children
314361
- [ ] Unit tests
315-
- [x] Implement more advanced coordination calculation [algorithm](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox_OS_for_TV/TV_remote_control_navigation#Algorithm_design).
316362
- [ ] Refactor with React Hooks instead of recompose.
317-
- [ ] Implement HOC for react-native tvOS and AndroidTV components.
363+
- [x] Native environment support
318364
- [ ] Add custom navigation logic per component. I.e. possibility to override default decision making algorithm and decide where to navigate next based on direction.
319-
- [x] Add "preferable focused component" feature for components with children. By default it's first element, but it is useful to customize this behaviour.
320365
- [ ] Implement mouse support. On some TV devices (or in the Browser) it is possible to use mouse-like input, e.g. magic remote in LG TVs. This system should support switching between "key" and "pointer" modes and apply "focused" state accordingly.
321366

322367
---

dist/spatialNavigation.js

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ var SpatialNavigation = function () {
330330
this.parentsHavingFocusedChild = [];
331331

332332
this.enabled = false;
333+
this.nativeMode = false;
333334

334335
/**
335336
* Flag used to block key events from this service
@@ -360,15 +361,22 @@ var SpatialNavigation = function () {
360361
_ref$debug = _ref.debug,
361362
debug = _ref$debug === undefined ? false : _ref$debug,
362363
_ref$visualDebug = _ref.visualDebug,
363-
visualDebug = _ref$visualDebug === undefined ? false : _ref$visualDebug;
364+
visualDebug = _ref$visualDebug === undefined ? false : _ref$visualDebug,
365+
_ref$nativeMode = _ref.nativeMode,
366+
nativeMode = _ref$nativeMode === undefined ? false : _ref$nativeMode;
364367

365368
if (!this.enabled) {
366369
this.enabled = true;
367-
this.bindEventHandlers();
370+
this.nativeMode = nativeMode;
371+
368372
this.debug = debug;
369-
if (visualDebug) {
370-
this.visualDebugger = new _visualDebugger2.default();
371-
this.startDrawLayouts();
373+
374+
if (!this.nativeMode) {
375+
this.bindEventHandlers();
376+
if (visualDebug) {
377+
this.visualDebugger = new _visualDebugger2.default();
378+
this.startDrawLayouts();
379+
}
372380
}
373381
}
374382
}
@@ -394,6 +402,7 @@ var SpatialNavigation = function () {
394402
value: function destroy() {
395403
if (this.enabled) {
396404
this.enabled = false;
405+
this.nativeMode = false;
397406
this.focusKey = null;
398407
this.parentsHavingFocusedChild = [];
399408
this.focusableComponents = {};
@@ -576,7 +585,7 @@ var SpatialNavigation = function () {
576585
/**
577586
* Security check, if component doesn't exist, stay on the same focusKey
578587
*/
579-
if (!targetComponent) {
588+
if (!targetComponent || this.nativeMode) {
580589
return targetFocusKey;
581590
}
582591

@@ -673,6 +682,10 @@ var SpatialNavigation = function () {
673682
}
674683
};
675684

685+
if (this.nativeMode) {
686+
return;
687+
}
688+
676689
this.updateLayout(focusKey);
677690

678691
/**
@@ -703,6 +716,10 @@ var SpatialNavigation = function () {
703716
*/
704717
parentComponent && parentComponent.lastFocusedChildKey === focusKey && (parentComponent.lastFocusedChildKey = null);
705718

719+
if (this.nativeMode) {
720+
return;
721+
}
722+
706723
/**
707724
* If the component was also focused at this time, focus another one
708725
*/
@@ -831,13 +848,20 @@ var SpatialNavigation = function () {
831848

832849
this.setCurrentFocusedKey(newFocusKey);
833850
this.updateParentsWithFocusedChild(newFocusKey);
834-
this.updateAllLayouts();
851+
852+
if (!this.nativeMode) {
853+
this.updateAllLayouts();
854+
}
835855
}
836856
}, {
837857
key: 'updateAllLayouts',
838858
value: function updateAllLayouts() {
839859
var _this5 = this;
840860

861+
if (this.nativeMode) {
862+
return;
863+
}
864+
841865
(0, _forOwn2.default)(this.focusableComponents, function (component, focusKey) {
842866
_this5.updateLayout(focusKey);
843867
});
@@ -847,7 +871,7 @@ var SpatialNavigation = function () {
847871
value: function updateLayout(focusKey) {
848872
var component = this.focusableComponents[focusKey];
849873

850-
if (!component) {
874+
if (!component || this.nativeMode) {
851875
return;
852876
}
853877

@@ -871,6 +895,10 @@ var SpatialNavigation = function () {
871895
var node = _ref4.node,
872896
preferredChildFocusKey = _ref4.preferredChildFocusKey;
873897

898+
if (this.nativeMode) {
899+
return;
900+
}
901+
874902
var component = this.focusableComponents[focusKey];
875903

876904
if (component) {
@@ -881,6 +909,11 @@ var SpatialNavigation = function () {
881909
}
882910
}
883911
}
912+
}, {
913+
key: 'isNativeMode',
914+
value: function isNativeMode() {
915+
return this.nativeMode;
916+
}
884917
}]);
885918

886919
return SpatialNavigation;

dist/withFocusable.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,18 @@ var withFocusable = function withFocusable() {
8989

9090
return {
9191
realFocusKey: realFocusKey,
92-
setFocus: _spatialNavigation2.default.setFocus.bind(null, realFocusKey),
92+
93+
/**
94+
* This method is used to imperatively set focus to a component.
95+
* It is blocked in the Native mode because the native engine decides what to focus by itself.
96+
*/
97+
setFocus: _spatialNavigation2.default.isNativeMode() ? _noop2.default : _spatialNavigation2.default.setFocus.bind(null, realFocusKey),
98+
99+
/**
100+
* In Native mode this is the only way to mark component as focused.
101+
* This method always steals focus onto current component no matter which arguments are passed in.
102+
*/
103+
stealFocus: _spatialNavigation2.default.setFocus.bind(null, realFocusKey, realFocusKey),
93104
focused: false,
94105
hasFocusedChild: false,
95106
parentFocusKey: parentFocusKey || _spatialNavigation.ROOT_FOCUS_KEY
@@ -163,7 +174,7 @@ var withFocusable = function withFocusable() {
163174
trackChildren = _props.trackChildren;
164175

165176

166-
var node = (0, _reactDom.findDOMNode)(this);
177+
var node = _spatialNavigation2.default.isNativeMode() ? null : (0, _reactDom.findDOMNode)(this);
167178

168179
_spatialNavigation2.default.addFocusable({
169180
focusKey: focusKey,
@@ -186,7 +197,7 @@ var withFocusable = function withFocusable() {
186197
preferredChildFocusKey = _props2.preferredChildFocusKey;
187198

188199

189-
var node = (0, _reactDom.findDOMNode)(this);
200+
var node = _spatialNavigation2.default.isNativeMode() ? null : (0, _reactDom.findDOMNode)(this);
190201

191202
_spatialNavigation2.default.updateFocusable(focusKey, {
192203
node: node,

0 commit comments

Comments
 (0)