Skip to content

Commit

Permalink
[onDeviceUI] Add ability to render addons in React Native (#4381)
Browse files Browse the repository at this point in the history
* Reducing the size of the ui.

* Addon scrollview with addon names

* Updated style, shows addons list with loaded addons.

* Updated status bar, so content would not be drawn below it on android.

* Added some addons to example.

* Moved out of state

* Fixed event on android.

* Temporary disabled yellow box.

* Adding two options for storybook ui:

isUIOpen - whether to initially show top bar - default true.
isStoryMenuOpen - should show story menu open by default - default false.

* Creating rn-addons.js file inside generators.

* Removed separate addon store.

* Using active prop.

* Rewrote addons wrapper without using modal.
We cannot use modal since addons expect to be rendered all the time.
So now we display the wrapper as position absolute and make it 0 0 size when it is not visible.

* Fixing yellow box warnings.

* Addon window by default is scrollable.

* Removed yellow box disabling, cleared up example index.

* Reverted accidentally deleted line.

* Updated dependencies.

* Dependency bump

* Updated UI. Removed modal, displaying menus as panels appearing from sides.

* Clearing up the style a little bit.

* Temporary disabling some examples

* Updating the readmes.

* Updating the readme.

* Added notes addon that support rn

* Updated addon documentation regarding setTimeout.

* Fixed proptypes issue, properly set initial tab from the props.

* Reduced the amount of rerenders, if you use onDeviceUI it only listens to story change events once instead of twice.

* Adding more addons.

* OnDeviceUI is now set to true by default.

* Updated the notes addon so it renders markdown properly.

* Fixes endless cycle when initially rendering when both onDeviceUI and server are enabled.

* Selection prop is not required ( it is not set when not using onDeviceUI).

* Renamed rn-notes to ondevice-notes addon.

* Added option to use channels as async.

* Using async channel if onDeviceUI is set to true.

* Updated notes documentation/tests.

* Adding backgrounds addon.

* Updated notes readme.

* Adding ondevice knobs addon.

* Updated example

* Reverted accidental merge issue.

* Updating documentation.

* Fixed knobs entry file.

* Updating documentation.

* Updating documentation.

* Removed packager completely.

* Updated cli.

* Added missing dependency.

* Websocket doesn't throw red screen on connection fail anymore.

* Takes children from props instead out of state..

* Fixed bug where selecting story didn't actually select it.

* Removed ondeviceUI in example.

* If it fails to connect it selects initial story.

* Knobs are properly reset on change.

* Proper import in example

* Reverts to localhost if no host is defined.

* Ui doesnt jump when hiding bottom bar, should handle keyboard correctly on ios.

* Updated background addon to unregister on unmount.

* Properly handles animations on android.

* Creates channel as soon as getStorybookUI is called instead of during the render.

* Displays message if no addons are loaded.

* setOptions called without timeout.

* Sets initial story if connection to websocket server fail.

* Updating style.

* Removed margin bottom.

* Added swiping on the nav bar, touching preview maximizes it.

* Fixed keyboard aware view.

* Fixed background panel so it doesn't lose color immediately.

* Uses preview width for panels.

* Moving class inside preview.

* All react native installations receive same template.

* Removed react_native fixture.

* Fixing lint.

* Lint fix.

* Improving performance dramatically.

* Adding on device addons.

* Reverted back fixtures change.

* Reverting file change

* Updating readmes.

* Updated readme about server.

* Reverted yarn.lock

* Fixing propTypes.

* Splitting out onDeviceUI/index to smaller components.

* Splitting up onDeviceUI.

* Removed unused dependency.

* Properly uses whole screen for preview.

* Updated visibility button.

* Few code review fixes

Updated readme,
Renamed handlers.

* Fixed where width is taken from.
  • Loading branch information
Gongreg committed Oct 12, 2018
1 parent 3f6dce7 commit e413ec2
Show file tree
Hide file tree
Showing 34 changed files with 1,047 additions and 288 deletions.
40 changes: 21 additions & 19 deletions ADDONS_SUPPORT.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
## Addon / Framework Support Table

| | [React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)| [Mithril](app/mithril)| [HTML](app/html)| [Marko](app/marko)| [Svelte](app/svelte)| [Riot](app/riot)| [Ember](app/ember)|
| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|
|[a11y](addons/a11y) |+| |+|+|+|+|+|+| | |+|
|[actions](addons/actions) |+|+|+|+|+|+|+|+|+|+|+|
|[backgrounds](addons/backgrounds)|+| |+|+|+|+|+|+|+|+|+|
|[centered](addons/centered) |+| |+|+| |+|+| |+| |+|
|[events](addons/events) |+| |+|+|+|+|+|+| | |+|
|[graphql](addons/graphql) |+| | | | | | | | | | |
|[info](addons/info) |+| | | | | | | | | | |
|[jest](addons/jest) |+| | |+| | |+| | | | |
|[knobs](addons/knobs) |+|+|+|+|+|+|+|+|+|+|+|
|[links](addons/links) |+|+|+|+|+|+|+| |+|+|+|
|[notes](addons/notes) |+| |+|+|+|+|+| |+|+|+|
|[options](addons/options) |+|+|+|+|+|+|+| |+|+|+|
|[storyshots](addons/storyshots) |+|+|+|+| | |+| |+|+| |
|[storysource](addons/storysource)|+| |+|+|+|+|+|+|+|+|+|
|[viewport](addons/viewport) |+| |+|+|+|+|+|+|+|+|+|
## Addon / Framework Support Table

| | [React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)| [Mithril](app/mithril)| [HTML](app/html)| [Marko](app/marko)| [Svelte](app/svelte)| [Riot](app/riot)| [Ember](app/ember)|
| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|
|[a11y](addons/a11y) |+| |+|+|+|+|+|+| | |+|
|[actions](addons/actions) |+|+|+|+|+|+|+|+|+|+|+|
|[backgrounds](addons/backgrounds)|+|*|+|+|+|+|+|+|+|+|+|
|[centered](addons/centered) |+| |+|+| |+|+| |+| |+|
|[events](addons/events) |+| |+|+|+|+|+|+| | |+|
|[graphql](addons/graphql) |+| | | | | | | | | | |
|[info](addons/info) |+| | | | | | | | | | |
|[jest](addons/jest) |+| | |+| | |+| | | | |
|[knobs](addons/knobs) |+|+*|+|+|+|+|+|+|+|+|+|
|[links](addons/links) |+|+|+|+|+|+|+| |+|+|+|
|[notes](addons/notes) |+|+*|+|+|+|+|+| |+|+|+|
|[options](addons/options) |+|+|+|+|+|+|+| |+|+|+|
|[storyshots](addons/storyshots) |+|+|+|+| | |+| |+|+| |
|[storysource](addons/storysource)|+| |+|+|+|+|+|+|+|+|+|
|[viewport](addons/viewport) |+| |+|+|+|+|+|+|+|+|+|

`*` - React Native on device addon (addons/onDevice-\<name>)
50 changes: 50 additions & 0 deletions app/react-native/docs/addons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Addons

Storybook supports addons. You can read more about them [here](https://storybook.js.org/addons/introduction/)

There is one big difference in React Native is that it has two types of addons: Addons that work in the browser
and addons that work on the app itself (on device addons).

## Browser addons
Browser addons are default addons to storybook. You create a file called addons.js inside storybook and it is
automatically added inside your browser.

## On device addons
On device addons are addons that are displayed in your app in addons panel.
To use them you have to create a file called `rn-addons.js` next to your storybook entry.
Because React Native does not dynamically resolve imports, you also have to manually import them.
Example:
**storybook/index.js**
```
import { getStorybookUI, configure } from '@storybook/react-native';
import './rn-addons';
// import stories
configure(() => {
require($PATH_TO_STORIES);
}, module);
const StorybookUI = getStorybookUI();
export default StorybookUI;
**storybook/rn-addons.js**
```
import '@storybook/addon-ondevice-knobs/register';
import '@storybook/addon-ondevice-notes/register';
...
```
This step is done automatically when you install Storybook for the first time and also described in [Manual Setup](https://github.com/storybooks/storybook/blob/master/app/react-native/docs/manual-setup.md)
## Compatibility
Addon compatibilty can be found [here](https://github.com/storybooks/storybook/blob/master/ADDONS_SUPPORT.md)
## Performance of on device addons
Because on device addons are inside the app, they are also rerendered on every change. This can reduce performance a lot.
## Writing the on device addons
On device addons use same addon store and api as web addons. The only difference in api is that you don't have `api` prop
and have to rely on channel for everything.
The main difference between browser and app addons is that the render has to be supported by React Native (View, Text).
For more info about writing addons read [writing addons](https://storybook.js.org/addons/writing-addons/) section in
storybook documentation.
55 changes: 40 additions & 15 deletions app/react-native/docs/manual-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,38 @@ First, install the `@storybook/react-native` module
npm install @storybook/react-native
```

Create a new directory called `storybook` in your project root and create an entry file (index.ios.js or index.android.js) as given below. (Don't forget to replace "MyApplicationName" with your app name).
Create a new directory called `storybook` in your project root and create an entry file (index.js) as given below.
(Don't forget to replace "MyApplicationName" with your app name).

**storybook/index.js**
```js
import { AppRegistry } from 'react-native';
import { getStorybookUI, configure } from '@storybook/react-native';
import './addons';
import './rn-addons';

// import your stories
configure(function() {
// import stories
configure(() => {
// eslint-disable-next-line global-require
require('./stories');
}, module);

const StorybookUI = getStorybookUI({
port: 7007,
host: 'localhost',
});
AppRegistry.registerComponent('MyApplicationName', () => StorybookUI);
const StorybookUIRoot = getStorybookUI();

AppRegistry.registerComponent('MyApplicationName', () => StorybookUIRoot);
export default StorybookUIRoot;
```

Create a file named `addons.js` file in `storybook` directory to use addons. Here is a list of default addons:
Create a file called `rn-addons.js`
In this file you can import on device addons.

```js
import '@storybook/addon-actions';
import '@storybook/addon-links';
**storybook/rn-addons.js**
```
import '@storybook/addon-ondevice-knobs/register';
import '@storybook/addon-ondevice-notes/register';
...
```


Then write your first story in the `stories` directory like this:

```js
Expand All @@ -58,12 +64,31 @@ storiesOf('CenteredView')
));
```

Then add following NPM script into your `package.json` file:
Finally replace your app entry with
```js
import './storybook';
```
If you cannot replace your entry point just make sure that the component exported from `./storybook` is displayed
somewhere in your app. `StorybookUI` is simply a RN `View` component that can be embedded anywhere in your
RN application, e.g. on a tab or within an admin screen.

## Server support

If you want to support having a storybook server running add following NPM script into your `package.json` file:

```json
{
"scripts": {
"storybook": "storybook start -p 7007"
"storybook": "storybook start"
}
}
```

If you want to have addons inside browser, create a file named `addons.js` file in `storybook`. Here is a list of default addons:

**storybook/addons.js**
```js
import '@storybook/addon-actions';
import '@storybook/addon-links';
```

24 changes: 24 additions & 0 deletions app/react-native/docs/server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Storybook server
The default usage of React Native Storybook till version 4 involved starting Storybook server.
Starting from v4 we do not expect user to start the server since in most cases it is not really necessary.

In case you still want to run Storybook server simply call `npm run storybook` or `npx storybook start`.

## Benefits of storybook server

* ### Websockets connection
The main benefit you get from running storybook server is that your app will be listening for websockets connection.
That means that you can create your own tools that integrate with your storybook app.

* ### IDE Plugins
Having server running allows you to control your storybook view from inside web page or your ide.

There is a plugin for [JetBrains IDEs](https://plugins.jetbrains.com/plugin/9910-storybook) and there is one
for [VS Code](https://github.com/orta/vscode-react-native-storybooks).


* ### Web addons
There are Storybook addons that work with React Native but do not have on device implementations.



4 changes: 2 additions & 2 deletions app/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
},
"dependencies": {
"@storybook/addons": "4.0.0-alpha.24",
"@storybook/channels": "4.0.0-alpha.24",
"@storybook/channel-websocket": "4.0.0-alpha.24",
"@storybook/core": "4.0.0-alpha.24",
"@storybook/core-events": "4.0.0-alpha.24",
Expand Down Expand Up @@ -57,8 +58,7 @@
"prop-types": "^15.6.2",
"raw-loader": "^0.5.1",
"react-dev-utils": "6.0.4",
"react-native-compat": "^1.0.0",
"react-native-iphone-x-helper": "^1.2.0",
"react-native-swipe-gestures": "^1.0.2",
"shelljs": "^0.8.2",
"universal-dotenv": "^1.9.1",
"url-parse": "^1.4.3",
Expand Down
8 changes: 6 additions & 2 deletions app/react-native/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ You can pass these parameters to getStorybookUI call in your storybook entry poi

```
{
onDeviceUI: Boolean (false)
-- display stories list on the device
onDeviceUI: Boolean (true)
-- display navigator and addons on the device
disableWebsockets: Boolean (false)
-- allows to display stories without running storybook server. Should be used with onDeviceUI
secured: Boolean (false)
Expand All @@ -116,6 +116,10 @@ You can pass these parameters to getStorybookUI call in your storybook entry poi
-- port to use
query: String ("")
-- additional query string to pass to websockets
isUIHidden: Boolean (false)
-- should the ui be closed initialy.
tabOpen: Number (0)
-- which tab should be open. -1 Navigator, 0 Preview, 1 Addons
}
```

Expand Down
6 changes: 3 additions & 3 deletions app/react-native/src/bin/storybook-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import program from 'commander';
import Server from '../server';

program
.option('-h, --host <host>', 'host to listen on')
.option('-p, --port <port>', 'port to listen on')
.option('-h, --host <host>', 'host to listen on', 'localhost')
.option('-p, --port <port>', 'port to listen on', 7007)
.option('-s, --secured', 'whether server is running on https')
.option('-c, --config-dir [dir-name]', 'storybook config directory')
.option('-e, --environment [environment]', 'DEVELOPMENT/PRODUCTION environment for webpack')
Expand All @@ -33,7 +33,7 @@ server.listen(...listenAddr, err => {
if (err) {
throw err;
}
const address = `http://${program.host || 'localhost'}:${program.port}/`;
const address = `http://${program.host}:${program.port}/`;
console.info(`\nReact Native Storybook started on => ${address}\n`);
if (program.smokeTest) {
process.exit(0);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Platform, Keyboard, Dimensions, View } from 'react-native';

import style from './style';

// Android changes screen size when keyboard opens.
// To avoid issues we use absolute positioned element with predefined screen size
export default class AbsolutePositionedKeyboardAwareView extends PureComponent {
componentWillMount() {
this.keyboardDidShowListener = Keyboard.addListener(
'keyboardDidShow',
this.keyboardDidShowHandler
);
this.keyboardDidHideListener = Keyboard.addListener(
'keyboardDidHide',
this.keyboardDidHideHandler
);
Dimensions.addEventListener('change', this.removeKeyboardOnOrientationChange);
}

componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
Dimensions.removeEventListener('change', this.removeKeyboardOnOrientationChange);
}

keyboardDidShowHandler = e => {
if (Platform.OS === 'android') {
const { previewWidth } = this.props;
// There is bug in RN android that keyboardDidShow event is called simply when you go from portrait to landscape.
// To make sure that this is keyboard event we check screen width
if (previewWidth === e.endCoordinates.width) {
this.keyboardOpen = true;
}
}
};

// When rotating screen from portrait to landscape with keyboard open on android it calls keyboardDidShow, but doesn't call
// keyboardDidHide. To avoid issues we set keyboardOpen to false immediately on keyboardChange.
removeKeyboardOnOrientationChange = () => {
if (Platform.OS === 'android') {
this.keyboardOpen = false;
}
};

keyboardDidHideHandler = () => {
if (this.keyboardOpen) {
this.keyboardOpen = false;
}
};

onLayoutHandler = ({ nativeEvent }) => {
if (!this.keyboardOpen) {
const { width, height } = nativeEvent.layout;
const { onLayout } = this.props;

onLayout({
previewHeight: height,
previewWidth: width,
});
}
};

render() {
const { children, previewWidth, previewHeight } = this.props;

return (
<View style={style.flex} onLayout={this.onLayoutHandler}>
<View
style={
previewWidth === 0
? style.flex
: { position: 'absolute', width: previewWidth, height: previewHeight }
}
>
{children}
</View>
</View>
);
}
}

AbsolutePositionedKeyboardAwareView.propTypes = {
children: PropTypes.node.isRequired,
previewWidth: PropTypes.number.isRequired,
previewHeight: PropTypes.number.isRequired,
onLayout: PropTypes.func.isRequired,
};

0 comments on commit e413ec2

Please sign in to comment.