Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Native - On Device UI #1413

Merged
merged 2 commits into from
Jul 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions app/react-native/src/preview/components/OnDeviceUI/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<View style={style.main}>
<View style={style.leftPanel}>
<StoryListView stories={stories} events={events} />
</View>
<View style={style.rightPanel}>
<View style={style.preview}>
<StoryView url={url} events={events} />
</View>
</View>
</View>
);
}

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,
};
28 changes: 28 additions & 0 deletions app/react-native/src/preview/components/OnDeviceUI/style.js
Original file line number Diff line number Diff line change
@@ -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,
},
};
127 changes: 127 additions & 0 deletions app/react-native/src/preview/components/StoryListView/index.js
Original file line number Diff line number Diff line change
@@ -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 }) => (
<View key={title} style={style.header}>
<Text style={[style.headerText, selected && style.headerTextSelected]}>
{title}
</Text>
</View>
);

SectionHeader.propTypes = {
title: PropTypes.string.isRequired,
selected: PropTypes.bool.isRequired,
};

const ListItem = ({ title, selected, onPress }) => (
<TouchableOpacity
key={title}
style={style.item}
onPress={onPress}
>
<Text style={[style.itemText, selected && style.itemTextSelected]}>
{title}
</Text>
</TouchableOpacity>
);

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 (
<SectionList
style={style.list}
renderItem={({ item }) => (
<ListItem
title={item.name}
selected={item.kind === this.state.selectedKind && item.name === this.state.selectedStory}
onPress={() => this.changeStory(item.kind, item.name)}
/>
)}
renderSectionHeader={({ section }) => (
<SectionHeader
title={section.title}
selected={section.title === this.state.selectedKind}
/>
)}
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,
};
26 changes: 26 additions & 0 deletions app/react-native/src/preview/components/StoryListView/style.js
Original file line number Diff line number Diff line change
@@ -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',
},
};
8 changes: 7 additions & 1 deletion app/react-native/src/preview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 <StoryView url={webUrl} events={this._events} />;
return (params.onDeviceUI) ? (
<OnDeviceUI stories={this._stories} events={this._events} url={webUrl} />
) : (
<StoryView url={webUrl} events={this._events} />
);
};
}

Expand Down
7 changes: 6 additions & 1 deletion app/react-native/src/preview/story_store.js
Original file line number Diff line number Diff line change
@@ -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 = {};
}

Expand All @@ -21,6 +24,8 @@ export default class StoryStore {
index: count,
fn,
};

this.emit('storyAdded', kind, name, fn);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how removeStory works/becomes called, but do we want to add an emitter for that as well for our StoryListView to subscribe to?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hadn't spotted the remove method. I can't think of a situation when that would be used...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither can I - I'm not familiar enough with that part; cc @ndelangen

}

getStoryKinds() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we probably don't need this right now, but since all the other -tvOS libs are included, we should keep this so that one day if they're needed, there's no confusion why one was missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a change which Xcode made when I changed the example to be universal rather than iPhone only. Looking at it there are two targets, one for the phone and another for tvOS. The tvOS one includes libRCTAnimation.a but there is no such framework as libRCTAnimation-tvOS.a listed in Xcode. I'm guessing that this was a change in React Native and the example project was using out of date references (at a complete guess).

I think for now we should go with what Xcode has changed it to to avoid commit noise in the future. It can always be fixed if there is an issue if/when tvOS support is added to Storybook.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation! Sounds good 👍

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 */; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -419,7 +419,7 @@
isa = PBXGroup;
children = (
5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */,
5E9157351DD0AC6500FF2AA8 /* libRCTAnimation-tvOS.a */,
5E9157351DD0AC6500FF2AA8 /* libRCTAnimation.a */,
);
name = Products;
sourceTree = "<group>";
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -1006,6 +1006,7 @@
"-lc++",
);
PRODUCT_NAME = ReactNativeVanilla;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
Expand All @@ -1023,6 +1024,7 @@
"-lc++",
);
PRODUCT_NAME = ReactNativeVanilla;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
Expand Down
2 changes: 1 addition & 1 deletion examples/react-native-vanilla/storybook/storybook.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
() =>
Expand Down