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;