Skip to content

Commit 86679eb

Browse files
committed
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
1 parent fbc665f commit 86679eb

8 files changed

Lines changed: 243 additions & 8 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React, { PropTypes } from 'react';
2+
import { View } from 'react-native';
3+
import style from './style';
4+
import StoryListView from '../StoryListView';
5+
import StoryView from '../StoryView';
6+
7+
export default function OnDeviceUI(props) {
8+
const {
9+
stories,
10+
events,
11+
url
12+
} = props;
13+
14+
return (
15+
<View style={style.main}>
16+
<View style={style.leftPanel}>
17+
<StoryListView stories={stories} events={events} />
18+
</View>
19+
<View style={style.rightPanel}>
20+
<View style={style.preview}>
21+
<StoryView url={url} events={events} />
22+
</View>
23+
</View>
24+
</View>
25+
);
26+
}
27+
28+
OnDeviceUI.propTypes = {
29+
stories: PropTypes.shape({
30+
dumpStoryBook: PropTypes.func.isRequired,
31+
on: PropTypes.func.isRequired,
32+
emit: PropTypes.func.isRequired,
33+
removeListener: PropTypes.func.isRequired,
34+
}).isRequired,
35+
events: PropTypes.shape({
36+
on: PropTypes.func.isRequired,
37+
emit: PropTypes.func.isRequired,
38+
removeListener: PropTypes.func.isRequired,
39+
}).isRequired,
40+
url: PropTypes.string.isRequired,
41+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { StyleSheet } from 'react-native';
2+
3+
export default {
4+
main: {
5+
flex: 1,
6+
flexDirection: 'row',
7+
paddingTop: 20,
8+
backgroundColor: 'rgba(247, 247, 247, 1)',
9+
},
10+
leftPanel: {
11+
flex: 1,
12+
maxWidth: 250,
13+
paddingHorizontal: 8,
14+
paddingBottom: 8,
15+
},
16+
rightPanel: {
17+
flex: 1,
18+
backgroundColor: 'rgba(255, 255, 255, 1)',
19+
borderWidth: StyleSheet.hairlineWidth,
20+
borderColor: 'rgba(236, 236, 236, 1)',
21+
borderRadius: 4,
22+
marginBottom: 8,
23+
marginHorizontal: 8,
24+
},
25+
preview: {
26+
...StyleSheet.absoluteFillObject,
27+
},
28+
};
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import React, { Component, PropTypes } from 'react';
2+
import { SectionList, View, Text, TouchableOpacity } from 'react-native';
3+
import style from './style';
4+
5+
const SectionHeader = ({ title, selected }) => (
6+
<View key={title} style={style.header}>
7+
<Text style={[style.headerText, selected && style.headerTextSelected]}>
8+
{title}
9+
</Text>
10+
</View>
11+
);
12+
13+
SectionHeader.propTypes = {
14+
title: PropTypes.string.isRequired,
15+
selected: PropTypes.bool.isRequired,
16+
};
17+
18+
const ListItem = ({ title, selected, onPress }) => (
19+
<TouchableOpacity
20+
key={title}
21+
style={style.item}
22+
onPress={onPress}
23+
>
24+
<Text style={[style.itemText, selected && style.itemTextSelected]}>
25+
{title}
26+
</Text>
27+
</TouchableOpacity>
28+
);
29+
30+
ListItem.propTypes = {
31+
title: PropTypes.string.isRequired,
32+
onPress: PropTypes.func.isRequired,
33+
selected: PropTypes.bool.isRequired,
34+
};
35+
36+
export default class StoryListView extends Component {
37+
constructor(props, ...args) {
38+
super(props, ...args);
39+
this.state = {
40+
sections: [],
41+
selectedKind: null,
42+
selectedStory: null,
43+
};
44+
45+
this.storyAddedHandler = this.handleStoryAdded.bind(this);
46+
this.storyChangedHandler = this.handleStoryChanged.bind(this);
47+
this.changeStoryHandler = this.changeStory.bind(this);
48+
49+
this.props.stories.on('storyAdded', this.storyAddedHandler);
50+
this.props.events.on('story', this.storyChangedHandler);
51+
}
52+
53+
componentDidMount() {
54+
this.handleStoryAdded();
55+
}
56+
57+
componentWillUnmount() {
58+
this.props.stories.removeListener('storyAdded', this.storiesHandler);
59+
this.props.events.removeListener('story', this.storyChangedHandler);
60+
}
61+
62+
handleStoryAdded() {
63+
if (this.props.stories) {
64+
const data = this.props.stories.dumpStoryBook();
65+
this.setState({
66+
sections: data.map((section) => ({
67+
key: section.kind,
68+
title: section.kind,
69+
data: section.stories.map((story) => ({
70+
key: story,
71+
kind: section.kind,
72+
name: story
73+
}))
74+
}))
75+
});
76+
}
77+
}
78+
79+
handleStoryChanged(storyFn, selection) {
80+
const { kind, story } = selection;
81+
this.setState({
82+
selectedKind: kind,
83+
selectedStory: story
84+
});
85+
}
86+
87+
changeStory(kind, story) {
88+
this.props.events.emit('setCurrentStory', { kind, story });
89+
}
90+
91+
render() {
92+
return (
93+
<SectionList
94+
style={style.list}
95+
renderItem={({ item }) => (
96+
<ListItem
97+
title={item.name}
98+
selected={item.kind === this.state.selectedKind && item.name === this.state.selectedStory}
99+
onPress={() => this.changeStory(item.kind, item.name)}
100+
/>
101+
)}
102+
renderSectionHeader={({ section }) => (
103+
<SectionHeader
104+
title={section.title}
105+
selected={section.title === this.state.selectedKind}
106+
/>
107+
)}
108+
sections={this.state.sections}
109+
stickySectionHeadersEnabled={false}
110+
/>
111+
);
112+
}
113+
}
114+
115+
StoryListView.propTypes = {
116+
stories: PropTypes.shape({
117+
dumpStoryBook: PropTypes.func.isRequired,
118+
on: PropTypes.func.isRequired,
119+
emit: PropTypes.func.isRequired,
120+
removeListener: PropTypes.func.isRequired,
121+
}).isRequired,
122+
events: PropTypes.shape({
123+
on: PropTypes.func.isRequired,
124+
emit: PropTypes.func.isRequired,
125+
removeListener: PropTypes.func.isRequired,
126+
}).isRequired,
127+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export default {
2+
list: {
3+
flex: 1,
4+
maxWidth: 250,
5+
},
6+
header: {
7+
paddingTop: 24,
8+
paddingBottom: 4,
9+
},
10+
headerText: {
11+
fontSize: 16,
12+
},
13+
headerTextSelected: {
14+
fontWeight: 'bold',
15+
},
16+
item: {
17+
paddingVertical: 4,
18+
paddingHorizontal: 16,
19+
},
20+
itemText: {
21+
fontSize: 14,
22+
},
23+
itemTextSelected: {
24+
fontWeight: 'bold',
25+
},
26+
};

app/react-native/src/preview/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import createChannel from '@storybook/channel-websocket';
66
import { EventEmitter } from 'events';
77
import StoryStore from './story_store';
88
import StoryKindApi from './story_kind';
9+
import OnDeviceUI from './components/OnDeviceUI';
910
import StoryView from './components/StoryView';
1011

1112
export default class Preview {
@@ -70,11 +71,16 @@ export default class Preview {
7071
}
7172
channel.on('getStories', () => this._sendSetStories());
7273
channel.on('setCurrentStory', d => this._selectStory(d));
74+
this._events.on('setCurrentStory', d => this._selectStory(d));
7375
this._sendSetStories();
7476
this._sendGetCurrentStory();
7577

7678
// finally return the preview component
77-
return <StoryView url={webUrl} events={this._events} />;
79+
return (params.onDeviceUI) ? (
80+
<OnDeviceUI stories={this._stories} events={this._events} url={webUrl} />
81+
) : (
82+
<StoryView url={webUrl} events={this._events} />
83+
);
7884
};
7985
}
8086

app/react-native/src/preview/story_store.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/* eslint no-underscore-dangle: 0 */
2+
import { EventEmitter } from 'events';
3+
24
let count = 0;
35

4-
export default class StoryStore {
6+
export default class StoryStore extends EventEmitter {
57
constructor() {
8+
super();
69
this._data = {};
710
}
811

@@ -21,6 +24,8 @@ export default class StoryStore {
2124
index: count,
2225
fn,
2326
};
27+
28+
this.emit('storyAdded', kind, name, fn);
2429
}
2530

2631
getStoryKinds() {

examples/react-native-vanilla/ios/ReactNativeVanilla.xcodeproj/project.pbxproj

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
2D02E4BC1E0B4A80006451C7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
2626
2D02E4BD1E0B4A84006451C7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
2727
2D02E4BF1E0B4AB3006451C7 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
28-
2D02E4C21E0B4AEC006451C7 /* libRCTAnimation-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157351DD0AC6500FF2AA8 /* libRCTAnimation-tvOS.a */; };
28+
2D02E4C21E0B4AEC006451C7 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157351DD0AC6500FF2AA8 /* libRCTAnimation.a */; };
2929
2D02E4C31E0B4AEC006451C7 /* libRCTImage-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DAD3E841DF850E9000B6D8A /* libRCTImage-tvOS.a */; };
3030
2D02E4C41E0B4AEC006451C7 /* libRCTLinking-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DAD3E881DF850E9000B6D8A /* libRCTLinking-tvOS.a */; };
3131
2D02E4C51E0B4AEC006451C7 /* libRCTNetwork-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DAD3E8C1DF850E9000B6D8A /* libRCTNetwork-tvOS.a */; };
@@ -289,7 +289,7 @@
289289
buildActionMask = 2147483647;
290290
files = (
291291
2D02E4C91E0B4AEC006451C7 /* libReact.a in Frameworks */,
292-
2D02E4C21E0B4AEC006451C7 /* libRCTAnimation-tvOS.a in Frameworks */,
292+
2D02E4C21E0B4AEC006451C7 /* libRCTAnimation.a in Frameworks */,
293293
2D02E4C31E0B4AEC006451C7 /* libRCTImage-tvOS.a in Frameworks */,
294294
2D02E4C41E0B4AEC006451C7 /* libRCTLinking-tvOS.a in Frameworks */,
295295
2D02E4C51E0B4AEC006451C7 /* libRCTNetwork-tvOS.a in Frameworks */,
@@ -419,7 +419,7 @@
419419
isa = PBXGroup;
420420
children = (
421421
5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */,
422-
5E9157351DD0AC6500FF2AA8 /* libRCTAnimation-tvOS.a */,
422+
5E9157351DD0AC6500FF2AA8 /* libRCTAnimation.a */,
423423
);
424424
name = Products;
425425
sourceTree = "<group>";
@@ -804,10 +804,10 @@
804804
remoteRef = 5E9157321DD0AC6500FF2AA8 /* PBXContainerItemProxy */;
805805
sourceTree = BUILT_PRODUCTS_DIR;
806806
};
807-
5E9157351DD0AC6500FF2AA8 /* libRCTAnimation-tvOS.a */ = {
807+
5E9157351DD0AC6500FF2AA8 /* libRCTAnimation.a */ = {
808808
isa = PBXReferenceProxy;
809809
fileType = archive.ar;
810-
path = "libRCTAnimation-tvOS.a";
810+
path = libRCTAnimation.a;
811811
remoteRef = 5E9157341DD0AC6500FF2AA8 /* PBXContainerItemProxy */;
812812
sourceTree = BUILT_PRODUCTS_DIR;
813813
};
@@ -1006,6 +1006,7 @@
10061006
"-lc++",
10071007
);
10081008
PRODUCT_NAME = ReactNativeVanilla;
1009+
TARGETED_DEVICE_FAMILY = "1,2";
10091010
VERSIONING_SYSTEM = "apple-generic";
10101011
};
10111012
name = Debug;
@@ -1023,6 +1024,7 @@
10231024
"-lc++",
10241025
);
10251026
PRODUCT_NAME = ReactNativeVanilla;
1027+
TARGETED_DEVICE_FAMILY = "1,2";
10261028
VERSIONING_SYSTEM = "apple-generic";
10271029
};
10281030
name = Release;

examples/react-native-vanilla/storybook/storybook.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ configure(() => {
88
require('./stories');
99
}, module);
1010

11-
const StorybookUI = getStorybookUI({ port: 7007, host: 'localhost' });
11+
const StorybookUI = getStorybookUI({ port: 7007, host: 'localhost', onDeviceUI: true });
1212

1313
setTimeout(
1414
() =>

0 commit comments

Comments
 (0)