From 86679ebe7c09c7957058f1867fca68ae3cfef62f Mon Sep 17 00:00:00 2001 From: Matt Oakes Date: Wed, 5 Jul 2017 12:45:00 +0100 Subject: [PATCH] MVP version of the on device React Native UI This is a combination of 10 commits. - Add a basic implementation of the native UI for the storybook - Fix the lint errors - Add a param to turn the native UI on and off using the same package - Correctly set the width of the story list - Added a size selector for the different sized iPhone screens - Show the selected story in bold - Add some styling to the panels - Removed the canvas size switcher - Fix lint errors - Shortened the long render item and header lines --- .../preview/components/OnDeviceUI/index.js | 41 ++++++ .../preview/components/OnDeviceUI/style.js | 28 ++++ .../preview/components/StoryListView/index.js | 127 ++++++++++++++++++ .../preview/components/StoryListView/style.js | 26 ++++ app/react-native/src/preview/index.js | 8 +- app/react-native/src/preview/story_store.js | 7 +- .../project.pbxproj | 12 +- .../storybook/storybook.js | 2 +- 8 files changed, 243 insertions(+), 8 deletions(-) create mode 100644 app/react-native/src/preview/components/OnDeviceUI/index.js create mode 100644 app/react-native/src/preview/components/OnDeviceUI/style.js create mode 100644 app/react-native/src/preview/components/StoryListView/index.js create mode 100644 app/react-native/src/preview/components/StoryListView/style.js diff --git a/app/react-native/src/preview/components/OnDeviceUI/index.js b/app/react-native/src/preview/components/OnDeviceUI/index.js new file mode 100644 index 000000000000..c939414dd5ca --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/index.js @@ -0,0 +1,41 @@ +import React, { PropTypes } from 'react'; +import { View } from 'react-native'; +import style from './style'; +import StoryListView from '../StoryListView'; +import StoryView from '../StoryView'; + +export default function OnDeviceUI(props) { + const { + stories, + events, + url + } = props; + + return ( + + + + + + + + + + + ); +} + +OnDeviceUI.propTypes = { + stories: PropTypes.shape({ + dumpStoryBook: PropTypes.func.isRequired, + on: PropTypes.func.isRequired, + emit: PropTypes.func.isRequired, + removeListener: PropTypes.func.isRequired, + }).isRequired, + events: PropTypes.shape({ + on: PropTypes.func.isRequired, + emit: PropTypes.func.isRequired, + removeListener: PropTypes.func.isRequired, + }).isRequired, + url: PropTypes.string.isRequired, +}; diff --git a/app/react-native/src/preview/components/OnDeviceUI/style.js b/app/react-native/src/preview/components/OnDeviceUI/style.js new file mode 100644 index 000000000000..819d4cd6449c --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/style.js @@ -0,0 +1,28 @@ +import { StyleSheet } from 'react-native'; + +export default { + main: { + flex: 1, + flexDirection: 'row', + paddingTop: 20, + backgroundColor: 'rgba(247, 247, 247, 1)', + }, + leftPanel: { + flex: 1, + maxWidth: 250, + paddingHorizontal: 8, + paddingBottom: 8, + }, + rightPanel: { + flex: 1, + backgroundColor: 'rgba(255, 255, 255, 1)', + borderWidth: StyleSheet.hairlineWidth, + borderColor: 'rgba(236, 236, 236, 1)', + borderRadius: 4, + marginBottom: 8, + marginHorizontal: 8, + }, + preview: { + ...StyleSheet.absoluteFillObject, + }, +}; diff --git a/app/react-native/src/preview/components/StoryListView/index.js b/app/react-native/src/preview/components/StoryListView/index.js new file mode 100644 index 000000000000..8ea27dd5fe01 --- /dev/null +++ b/app/react-native/src/preview/components/StoryListView/index.js @@ -0,0 +1,127 @@ +import React, { Component, PropTypes } from 'react'; +import { SectionList, View, Text, TouchableOpacity } from 'react-native'; +import style from './style'; + +const SectionHeader = ({ title, selected }) => ( + + + {title} + + +); + +SectionHeader.propTypes = { + title: PropTypes.string.isRequired, + selected: PropTypes.bool.isRequired, +}; + +const ListItem = ({ title, selected, onPress }) => ( + + + {title} + + +); + +ListItem.propTypes = { + title: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired, + selected: PropTypes.bool.isRequired, +}; + +export default class StoryListView extends Component { + constructor(props, ...args) { + super(props, ...args); + this.state = { + sections: [], + selectedKind: null, + selectedStory: null, + }; + + this.storyAddedHandler = this.handleStoryAdded.bind(this); + this.storyChangedHandler = this.handleStoryChanged.bind(this); + this.changeStoryHandler = this.changeStory.bind(this); + + this.props.stories.on('storyAdded', this.storyAddedHandler); + this.props.events.on('story', this.storyChangedHandler); + } + + componentDidMount() { + this.handleStoryAdded(); + } + + componentWillUnmount() { + this.props.stories.removeListener('storyAdded', this.storiesHandler); + this.props.events.removeListener('story', this.storyChangedHandler); + } + + handleStoryAdded() { + if (this.props.stories) { + const data = this.props.stories.dumpStoryBook(); + this.setState({ + sections: data.map((section) => ({ + key: section.kind, + title: section.kind, + data: section.stories.map((story) => ({ + key: story, + kind: section.kind, + name: story + })) + })) + }); + } + } + + handleStoryChanged(storyFn, selection) { + const { kind, story } = selection; + this.setState({ + selectedKind: kind, + selectedStory: story + }); + } + + changeStory(kind, story) { + this.props.events.emit('setCurrentStory', { kind, story }); + } + + render() { + return ( + ( + this.changeStory(item.kind, item.name)} + /> + )} + renderSectionHeader={({ section }) => ( + + )} + sections={this.state.sections} + stickySectionHeadersEnabled={false} + /> + ); + } +} + +StoryListView.propTypes = { + stories: PropTypes.shape({ + dumpStoryBook: PropTypes.func.isRequired, + on: PropTypes.func.isRequired, + emit: PropTypes.func.isRequired, + removeListener: PropTypes.func.isRequired, + }).isRequired, + events: PropTypes.shape({ + on: PropTypes.func.isRequired, + emit: PropTypes.func.isRequired, + removeListener: PropTypes.func.isRequired, + }).isRequired, +}; diff --git a/app/react-native/src/preview/components/StoryListView/style.js b/app/react-native/src/preview/components/StoryListView/style.js new file mode 100644 index 000000000000..00b79d358308 --- /dev/null +++ b/app/react-native/src/preview/components/StoryListView/style.js @@ -0,0 +1,26 @@ +export default { + list: { + flex: 1, + maxWidth: 250, + }, + header: { + paddingTop: 24, + paddingBottom: 4, + }, + headerText: { + fontSize: 16, + }, + headerTextSelected: { + fontWeight: 'bold', + }, + item: { + paddingVertical: 4, + paddingHorizontal: 16, + }, + itemText: { + fontSize: 14, + }, + itemTextSelected: { + fontWeight: 'bold', + }, +}; diff --git a/app/react-native/src/preview/index.js b/app/react-native/src/preview/index.js index c5d253887083..52e521cd1320 100644 --- a/app/react-native/src/preview/index.js +++ b/app/react-native/src/preview/index.js @@ -6,6 +6,7 @@ import createChannel from '@storybook/channel-websocket'; import { EventEmitter } from 'events'; import StoryStore from './story_store'; import StoryKindApi from './story_kind'; +import OnDeviceUI from './components/OnDeviceUI'; import StoryView from './components/StoryView'; export default class Preview { @@ -70,11 +71,16 @@ export default class Preview { } channel.on('getStories', () => this._sendSetStories()); channel.on('setCurrentStory', d => this._selectStory(d)); + this._events.on('setCurrentStory', d => this._selectStory(d)); this._sendSetStories(); this._sendGetCurrentStory(); // finally return the preview component - return ; + return (params.onDeviceUI) ? ( + + ) : ( + + ); }; } diff --git a/app/react-native/src/preview/story_store.js b/app/react-native/src/preview/story_store.js index 86021172b09f..07e91876b125 100644 --- a/app/react-native/src/preview/story_store.js +++ b/app/react-native/src/preview/story_store.js @@ -1,8 +1,11 @@ /* eslint no-underscore-dangle: 0 */ +import { EventEmitter } from 'events'; + let count = 0; -export default class StoryStore { +export default class StoryStore extends EventEmitter { constructor() { + super(); this._data = {}; } @@ -21,6 +24,8 @@ export default class StoryStore { index: count, fn, }; + + this.emit('storyAdded', kind, name, fn); } getStoryKinds() { diff --git a/examples/react-native-vanilla/ios/ReactNativeVanilla.xcodeproj/project.pbxproj b/examples/react-native-vanilla/ios/ReactNativeVanilla.xcodeproj/project.pbxproj index 758386b95aba..b6fe386869b9 100644 --- a/examples/react-native-vanilla/ios/ReactNativeVanilla.xcodeproj/project.pbxproj +++ b/examples/react-native-vanilla/ios/ReactNativeVanilla.xcodeproj/project.pbxproj @@ -25,7 +25,7 @@ 2D02E4BC1E0B4A80006451C7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 2D02E4BD1E0B4A84006451C7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 2D02E4BF1E0B4AB3006451C7 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 2D02E4C21E0B4AEC006451C7 /* libRCTAnimation-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157351DD0AC6500FF2AA8 /* libRCTAnimation-tvOS.a */; }; + 2D02E4C21E0B4AEC006451C7 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157351DD0AC6500FF2AA8 /* libRCTAnimation.a */; }; 2D02E4C31E0B4AEC006451C7 /* libRCTImage-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DAD3E841DF850E9000B6D8A /* libRCTImage-tvOS.a */; }; 2D02E4C41E0B4AEC006451C7 /* libRCTLinking-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DAD3E881DF850E9000B6D8A /* libRCTLinking-tvOS.a */; }; 2D02E4C51E0B4AEC006451C7 /* libRCTNetwork-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DAD3E8C1DF850E9000B6D8A /* libRCTNetwork-tvOS.a */; }; @@ -289,7 +289,7 @@ buildActionMask = 2147483647; files = ( 2D02E4C91E0B4AEC006451C7 /* libReact.a in Frameworks */, - 2D02E4C21E0B4AEC006451C7 /* libRCTAnimation-tvOS.a in Frameworks */, + 2D02E4C21E0B4AEC006451C7 /* libRCTAnimation.a in Frameworks */, 2D02E4C31E0B4AEC006451C7 /* libRCTImage-tvOS.a in Frameworks */, 2D02E4C41E0B4AEC006451C7 /* libRCTLinking-tvOS.a in Frameworks */, 2D02E4C51E0B4AEC006451C7 /* libRCTNetwork-tvOS.a in Frameworks */, @@ -419,7 +419,7 @@ isa = PBXGroup; children = ( 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */, - 5E9157351DD0AC6500FF2AA8 /* libRCTAnimation-tvOS.a */, + 5E9157351DD0AC6500FF2AA8 /* libRCTAnimation.a */, ); name = Products; sourceTree = ""; @@ -804,10 +804,10 @@ remoteRef = 5E9157321DD0AC6500FF2AA8 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 5E9157351DD0AC6500FF2AA8 /* libRCTAnimation-tvOS.a */ = { + 5E9157351DD0AC6500FF2AA8 /* libRCTAnimation.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; - path = "libRCTAnimation-tvOS.a"; + path = libRCTAnimation.a; remoteRef = 5E9157341DD0AC6500FF2AA8 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1006,6 +1006,7 @@ "-lc++", ); PRODUCT_NAME = ReactNativeVanilla; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -1023,6 +1024,7 @@ "-lc++", ); PRODUCT_NAME = ReactNativeVanilla; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/examples/react-native-vanilla/storybook/storybook.js b/examples/react-native-vanilla/storybook/storybook.js index 9af39a2ee00c..d25d21f9d9c7 100644 --- a/examples/react-native-vanilla/storybook/storybook.js +++ b/examples/react-native-vanilla/storybook/storybook.js @@ -8,7 +8,7 @@ configure(() => { require('./stories'); }, module); -const StorybookUI = getStorybookUI({ port: 7007, host: 'localhost' }); +const StorybookUI = getStorybookUI({ port: 7007, host: 'localhost', onDeviceUI: true }); setTimeout( () =>