diff --git a/.babelrc b/.babelrc
index a7d8829..e4e5340 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,3 +1,3 @@
{
- "presets": [ "env", "react", "stage-2" ],
+ "presets": [ "env", "react", "stage-2", "flow" ],
}
diff --git a/package.json b/package.json
index 2547119..39f7fcb 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"devDependencies": {
"babel-jest": "21.0.2",
"babel-plugin-external-helpers": "^6.22.0",
+ "babel-preset-flow": "^6.23.0",
"babel-preset-stage-2": "^6.24.1",
"chalk": "^2.4.1",
"flow-bin": "^0.76.0",
diff --git a/packages/example-movie-list/.babelrc b/packages/example-movie-list/.babelrc
deleted file mode 100644
index 63c3455..0000000
--- a/packages/example-movie-list/.babelrc
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "presets":[
- "env", "react"
- ]
-}
diff --git a/packages/example-movie-list/package.json b/packages/example-movie-list/package.json
index 4ad0a18..d8f62f0 100644
--- a/packages/example-movie-list/package.json
+++ b/packages/example-movie-list/package.json
@@ -15,6 +15,7 @@
"babel-loader": "^7.1.2",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.6.1",
+ "babel-preset-flow": "^6.23.0",
"babel-preset-react": "^6.24.1",
"uglifyjs-webpack-plugin": "^1.0.1",
"webpack": "^3.8.1",
diff --git a/packages/example-movie-list/src/App.js b/packages/example-movie-list/src/App.js
index 5e85c43..0dc5800 100644
--- a/packages/example-movie-list/src/App.js
+++ b/packages/example-movie-list/src/App.js
@@ -1,5 +1,14 @@
import React from 'react';
-import {render, Text, ListView, View, Image, StyleSheet} from 'react-ape';
+import {
+ render,
+ Text,
+ ListView,
+ View,
+ Image,
+ StyleSheet,
+ withFocus,
+ withNavigation
+} from 'react-ape';
const styles = StyleSheet.create({
heading: {
@@ -8,7 +17,7 @@ const styles = StyleSheet.create({
color: 'white',
fontFamily: 'Arial',
fontWeight: 'bold',
- fontSize: 29,
+ fontSize: 29
},
time: {
top: 62,
@@ -16,11 +25,11 @@ const styles = StyleSheet.create({
color: 'red',
fontFamily: 'Arial',
fontWeight: 'bold',
- fontSize: 25,
+ fontSize: 25
},
logo: {
top: 10,
- left: 30,
+ left: 30
},
infoAboutRenderer: {
top: 590,
@@ -28,33 +37,59 @@ const styles = StyleSheet.create({
fontFamily: 'Arial',
fontWeight: 'bold',
color: 'lightblue',
- fontSize: 23,
+ fontSize: 23
},
list: {
top: 100,
left: 0,
backgroundColor: '#303030',
width: 2000,
- height: 400,
- },
+ height: 400
+ }
});
+class Item extends React.Component {
+ render() {
+ const { idx, data } = this.props;
+ return (
+ {
+ console.log(data);
+ }}
+ >
+
+
+ {data.name}
+
+
+ );
+ }
+}
+
+const FocusableItem = withFocus(Item);
+
class App extends React.Component {
constructor() {
- super();
+ super(...arguments);
this.posters = [
- {name: 'Narcos', src: 'posters/narcos.jpg'},
- {name: 'Daredevil', src: 'posters/daredevil.jpg'},
- {name: 'Stranger Things', src: 'posters/stranger-things.jpg'},
- {name: 'Narcos', src: 'posters/narcos.jpg'},
- {name: 'Daredevil', src: 'posters/daredevil.jpg'},
- {name: 'Stranger Things', src: 'posters/stranger-things.jpg'},
- {name: 'Narcos', src: 'posters/narcos.jpg'},
- {name: 'Daredevil', src: 'posters/daredevil.jpg'},
- {name: 'Stranger Things', src: 'posters/stranger-things.jpg'},
+ { name: 'Narcos', src: 'posters/narcos.jpg' },
+ { name: 'Daredevil', src: 'posters/daredevil.jpg' },
+ { name: 'Stranger Things', src: 'posters/stranger-things.jpg' },
+ { name: 'Narcos', src: 'posters/narcos.jpg' },
+ { name: 'Daredevil', src: 'posters/daredevil.jpg' },
+ { name: 'Stranger Things', src: 'posters/stranger-things.jpg' },
+ { name: 'Narcos', src: 'posters/narcos.jpg' },
+ { name: 'Daredevil', src: 'posters/daredevil.jpg' },
+ { name: 'Stranger Things', src: 'posters/stranger-things.jpg' }
];
this.state = {
- time: new Date().toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1'),
+ time: new Date().toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1')
};
}
@@ -63,29 +98,19 @@ class App extends React.Component {
const time = new Date()
.toTimeString()
.replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1');
- this.setState({time});
+ this.setState({ time });
}, 100);
}
renderPostersList() {
const renderRow = (data, idx) => (
- {
- console.log(data);
- }}>
-
-
- {data.name}
-
-
+
);
-
return (
, document.getElementById('root'));
+const NavigationApp = withNavigation(App);
+
+render(, document.getElementById('root'));
diff --git a/packages/example-movie-list/webpack.config.babel.js b/packages/example-movie-list/webpack.config.babel.js
index d9e00a5..85aa4eb 100644
--- a/packages/example-movie-list/webpack.config.babel.js
+++ b/packages/example-movie-list/webpack.config.babel.js
@@ -22,10 +22,9 @@ const config = {
module: {
rules: [
{
- test: /\.(js|jsx)$/,
+ test: /\.jsx?$/,
exclude: /node_modules/,
- use: ['babel-loader'],
- include: sourcePath,
+ use: { loader: 'babel-loader' }
},
],
},
@@ -43,15 +42,13 @@ if (process.env.NODE_ENV === 'production') {
warnings: false,
},
},
- })
- );
- config.plugins.push(
+ }),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
- })
+ }),
+ new webpack.optimize.ModuleConcatenationPlugin(),
+ new webpack.HashedModuleIdsPlugin(),
);
- config.plugins.push(new webpack.optimize.ModuleConcatenationPlugin());
- config.plugins.push(new webpack.HashedModuleIdsPlugin());
}
module.exports = config;
diff --git a/packages/react-ape/modules/navigation/FocusPathContext.js b/packages/react-ape/modules/navigation/FocusPathContext.js
new file mode 100644
index 0000000..0534406
--- /dev/null
+++ b/packages/react-ape/modules/navigation/FocusPathContext.js
@@ -0,0 +1,3 @@
+import React from 'react';
+
+export const FocusPathContext = React.createContext();
diff --git a/packages/react-ape/modules/navigation/withFocus.js b/packages/react-ape/modules/navigation/withFocus.js
new file mode 100644
index 0000000..ecb3fd5
--- /dev/null
+++ b/packages/react-ape/modules/navigation/withFocus.js
@@ -0,0 +1,55 @@
+/**
+ * @flow
+ */
+
+import * as React from 'react';
+import { getComponentDisplayName } from '../utils';
+import { FocusPathContext } from './FocusPathContext';
+
+type RequiredProps = {
+ focusKey: string
+};
+
+// See why we do `| void`
+// https://flow.org/en/docs/react/hoc/#toc-injecting-props-with-a-higher-order-component
+type InjectedProps = {
+ focused: boolean | void
+};
+
+/**
+ * Allows the WrappedComponent to be focusable and provides the `focused`
+ * property to it.
+ * The resulting component should provide a `focusKey` property.
+ */
+export function withFocus(
+ WrappedComponent: React.ComponentType
+): React.ComponentType<$Diff> {
+ return class extends React.Component {
+ static WrappedComponent = WrappedComponent;
+ static displayName = `withFocus(${getComponentDisplayName(
+ WrappedComponent
+ )})`;
+
+ renderWithFocusPath = focusPath => {
+ // TODO: I need to listen to a global and observable focusPath that will
+ // define if this component should be focused or not (the value of focused)
+ const { focusKey } = this.props;
+ return (
+
+
+
+ );
+ };
+
+ render() {
+ return (
+
+ {this.renderWithFocusPath}
+
+ );
+ }
+ };
+}
diff --git a/packages/react-ape/modules/navigation/withNavigation.js b/packages/react-ape/modules/navigation/withNavigation.js
new file mode 100644
index 0000000..277cdb1
--- /dev/null
+++ b/packages/react-ape/modules/navigation/withNavigation.js
@@ -0,0 +1,46 @@
+/**
+ * @flow
+ */
+
+import * as React from 'react';
+import { getComponentDisplayName, unsafeCreateUniqueId } from '../utils';
+import { FocusPathContext } from './FocusPathContext';
+
+type RequiredProps = {};
+
+// See why we do `| void`
+// https://flow.org/en/docs/react/hoc/#toc-injecting-props-with-a-higher-order-component
+type InjectedProps = {
+ focusPath: string | void
+};
+
+/**
+ * Adds `focusPath` to the context so we can create the `focusPath`
+ * from the focusable elements inside the WrappedComponent.
+ * Should be used to wrap the root component of the application.
+ */
+export function withNavigation(
+ WrappedComponent: React.ComponentType
+): React.ComponentType<$Diff> {
+ return class extends React.Component {
+ static WrappedComponent = WrappedComponent;
+ static displayName = `withNavigation(${getComponentDisplayName(
+ WrappedComponent
+ )})`;
+
+ rootFocusPath: string;
+
+ constructor() {
+ super(...arguments);
+ this.rootFocusPath = `root-${unsafeCreateUniqueId()}`;
+ }
+
+ render() {
+ return (
+
+
+
+ );
+ }
+ };
+}
diff --git a/packages/react-ape/modules/utils.js b/packages/react-ape/modules/utils.js
new file mode 100644
index 0000000..2720f1e
--- /dev/null
+++ b/packages/react-ape/modules/utils.js
@@ -0,0 +1,18 @@
+/**
+ * @flow
+ */
+
+import type { ComponentType } from 'react';
+
+export function getComponentDisplayName(Comp: ComponentType): string {
+ return Comp.displayName || Comp.name || 'Component';
+}
+
+/**
+ * Returns a relatively "guaranteed" unique id.
+ * It's still not 100% guaranteed, that's why we added the "unsafe" prefix on
+ * this function.
+ */
+export function unsafeCreateUniqueId(): string {
+ return ((Math.random() * 10e18) + Date.now()).toString(36);
+}
diff --git a/packages/react-ape/package.json b/packages/react-ape/package.json
index 0b5562f..9e31b28 100644
--- a/packages/react-ape/package.json
+++ b/packages/react-ape/package.json
@@ -2,7 +2,10 @@
"name": "react-ape",
"version": "0.0.6",
"description": "React Renderer to build interfaces using canvas/WebGL",
- "main": "index.js",
+ "main": "reactApeEntry.js",
+ "scripts": {
+ "flow": "flow"
+ },
"peerDependencies": {
"react": "^16.4.1"
},
diff --git a/packages/react-ape/reactApeEntry.js b/packages/react-ape/reactApeEntry.js
index a8205d5..be9b150 100644
--- a/packages/react-ape/reactApeEntry.js
+++ b/packages/react-ape/reactApeEntry.js
@@ -6,19 +6,13 @@
*
*/
-import ReactApeRenderer from './renderer/reactApeRenderer';
-import StyleSheetModule from './modules/StyleSheet';
-
-import ListViewComponent from './renderer/components/ListView';
-export const ListView = ListViewComponent;
+export { View, Image, Text } from './renderer/constants';
+export { default as ListView } from './renderer/components/ListView';
+export { default as StyleSheet } from './modules/StyleSheet';
+export { withFocus, withNavigation } from './modules/navigation';
+import ReactApeRenderer from './renderer/reactApeRenderer';
export const render = ReactApeRenderer.render;
// export const unmountComponentAtNode = ReactTVRenderer.unmountComponentAtNode;
-export const Image = 'Image';
-export const View = 'View';
-export const Text = 'Text';
-
-export const StyleSheet = StyleSheetModule;
-
export default ReactApeRenderer;
diff --git a/packages/react-ape/renderer/reactApeComponentTree.js b/packages/react-ape/renderer/reactApeComponentTree.js
index ccd2eac..f540df6 100644
--- a/packages/react-ape/renderer/reactApeComponentTree.js
+++ b/packages/react-ape/renderer/reactApeComponentTree.js
@@ -6,9 +6,9 @@
*
*/
-const randomKey = Math.random()
- .toString(36)
- .slice(2);
+import { unsafeCreateUniqueId } from '../modules/utils';
+
+const randomKey = unsafeCreateUniqueId();
const internalInstanceKey = '__reactInternalInstance$' + randomKey;
const internalEventHandlersKey = '__reactEventHandlers$' + randomKey;
diff --git a/packages/react-ape/renderer/reactApeRenderer.js b/packages/react-ape/renderer/reactApeRenderer.js
index b3b2911..fb19535 100644
--- a/packages/react-ape/renderer/reactApeRenderer.js
+++ b/packages/react-ape/renderer/reactApeRenderer.js
@@ -10,12 +10,6 @@ import reconciler from 'react-reconciler';
import reactApeComponent from './reactApeComponent';
import { precacheFiberNode, updateFiberProps } from './reactApeComponentTree';
-export type CanvasComponentContext = {
- _renderQueueForUpdate: Array,
- type: 'canvas',
- ctx: CanvasRenderingContext2D
-};
-
function scaleDPI(canvas, context, customWidth, customHeight) {
const devicePixelRatio = window.devicePixelRatio || 1;