-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
Viewport Addon #1753
Viewport Addon #1753
Changes from 27 commits
a0cc1c9
b47bc5b
3eb9cc5
c25a42f
0a8cf2a
82f413d
34f3136
c6efb68
7b6e1af
f0121f5
9de1008
5233d5b
ea4a605
2366e9b
d4feca3
5b910c7
8852d63
df6a566
c6f68ba
420a5ac
640491a
e8e6384
d622f82
9695408
ea0b055
48723a8
9464b0d
badc57f
ea3d9f2
220d9a9
2d878da
bfdc4a3
d51d0c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ node_modules | |
*.log | ||
.idea | ||
.vscode | ||
*.sw* | ||
npm-shrinkwrap.json | ||
dist | ||
.tern-port | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# Storybook Viewport Addon | ||
|
||
Storybook Viewport Addon allows your stories to be displayed in different sizes and layouts in [Storybook](https://storybookjs.org). This helps build responsive components inside of Storybook. | ||
|
||
This addon works with Storybook for: [React](https://github.com/storybooks/storybook/tree/master/app/react) and [Vue](https://github.com/storybooks/storybook/tree/master/app/vue). | ||
|
||
![Screenshot](https://github.com/storybooks/storybook/blob/master/docs/viewport.png) | ||
|
||
## Installation | ||
|
||
Install the following npm module: | ||
|
||
npm i --save-dev @storybook/addon-viewport | ||
|
||
or with yarn: | ||
|
||
yarn add -D @storybook/addon-viewport | ||
|
||
## Basic Usage | ||
|
||
Simply import the Storybook Viewport Addon in the `addon.js` file in your `.storybook` directory. | ||
|
||
```js | ||
import '@storybook/addon-viewport/register' | ||
``` | ||
|
||
This will register the Viewport Addon to Storybook and will show up in the action area. | ||
|
||
## FAQ | ||
|
||
#### How do I add a new device? | ||
|
||
Unfortunately, this is currently not supported. | ||
|
||
#### How can I make a custom screen size? | ||
|
||
Unfortunately, this is currently not supported. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const manager = require('./dist/manager'); | ||
|
||
manager.init(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"name": "@storybook/addon-viewport", | ||
"version": "3.2.0", | ||
"description": "Storybook addon to change the viewport size to mobile", | ||
"main": "dist/index.js", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file is broken (see #1753 (comment)) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #2589 fixes that by removing the |
||
"keywords": [ | ||
"storybook" | ||
], | ||
"scripts": { | ||
"prepublish": "node ../../scripts/prepublish.js" | ||
}, | ||
"license": "MIT", | ||
"dependencies": { | ||
"@storybook/components": "^3.2.7", | ||
"global": "^4.3.2", | ||
"prop-types": "^15.5.10" | ||
}, | ||
"peerDependencies": { | ||
"@storybook/addons": "^3.2.0", | ||
"react": "*" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// NOTE: loading addons using this file is deprecated! | ||
// please use manager.js and preview.js files instead | ||
require('./manager'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ndelangen @mnmtanish tangential to this PR but do you know when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's also weird that we actually recommend importing this file in addon's documentation |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import React, { Component } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { baseFonts } from '@storybook/components'; | ||
import { document } from 'global'; | ||
|
||
import { viewports, defaultViewport, resetViewport } from './viewportInfo'; | ||
import { SelectViewport } from './SelectViewport'; | ||
import { RotateViewport } from './RotateViewport'; | ||
|
||
import * as styles from './styles'; | ||
|
||
const storybookIframe = 'storybook-preview-iframe'; | ||
const containerStyles = { | ||
padding: 15, | ||
width: '100%', | ||
boxSizing: 'border-box', | ||
...baseFonts, | ||
}; | ||
|
||
export class Panel extends Component { | ||
static propTypes = { | ||
channel: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types | ||
}; | ||
|
||
constructor(props, context) { | ||
super(props, context); | ||
this.state = { | ||
viewport: defaultViewport, | ||
isLandscape: false, | ||
}; | ||
|
||
this.props.channel.on('addon:viewport:update', this.changeViewport); | ||
} | ||
|
||
componentDidMount() { | ||
this.iframe = document.getElementById(storybookIframe); | ||
} | ||
|
||
componentWillUnmount() { | ||
this.props.channel.removeListener('addon:viewport:update', this.changeViewport); | ||
} | ||
|
||
iframe = undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer to set this in the constructor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it a general suggestion not to use property initializers? Why? Maybe we should setup a eslint rule then? Note that it's already stage 3: https://github.com/tc39/proposal-class-fields |
||
|
||
changeViewport = viewport => { | ||
const { viewport: previousViewport } = this.state; | ||
|
||
if (previousViewport !== viewport) { | ||
this.setState( | ||
{ | ||
viewport, | ||
isLandscape: false, | ||
}, | ||
this.updateIframe | ||
); | ||
} | ||
}; | ||
|
||
toggleLandscape = () => { | ||
const { isLandscape } = this.state; | ||
|
||
this.setState({ isLandscape: !isLandscape }, this.updateIframe); | ||
}; | ||
|
||
updateIframe = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. arrow-functions cannot have a this binding, why are they object methods then? If they are utility functions that are unrelated to the instance, we should hoist them to the top of the module. Or possibly even be moved into their own module. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function obviously uses There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ndelangen are you saying to instead have this be a normal class function than in the constructor do fwiw, this is a fairly common pattern as @Hypnosphi pointed out, but I think it's more important to be consistent through the codebase. |
||
const { viewport: viewportKey, isLandscape } = this.state; | ||
const viewport = viewports[viewportKey] || resetViewport; | ||
|
||
if (!this.iframe) { | ||
throw new Error('Cannot find Storybook iframe'); | ||
} | ||
|
||
Object.keys(viewport.styles).forEach(prop => { | ||
this.iframe.style[prop] = viewport.styles[prop]; | ||
}); | ||
|
||
if (isLandscape) { | ||
this.iframe.style.height = viewport.styles.width; | ||
this.iframe.style.width = viewport.styles.height; | ||
} | ||
}; | ||
|
||
render() { | ||
const { isLandscape, viewport } = this.state; | ||
|
||
const disableDefault = viewport === defaultViewport; | ||
const disabledStyles = disableDefault ? styles.disabled : {}; | ||
|
||
const buttonStyles = { | ||
...styles.button, | ||
...disabledStyles, | ||
marginTop: 30, | ||
padding: 20, | ||
}; | ||
|
||
return ( | ||
<div style={containerStyles}> | ||
<SelectViewport | ||
activeViewport={viewport} | ||
onChange={e => this.changeViewport(e.target.value)} | ||
/> | ||
|
||
<RotateViewport | ||
onClick={this.toggleLandscape} | ||
disabled={disableDefault} | ||
active={isLandscape} | ||
/> | ||
|
||
<button | ||
style={buttonStyles} | ||
onClick={() => this.changeViewport(defaultViewport)} | ||
disabled={disableDefault} | ||
> | ||
Reset Viewport | ||
</button> | ||
</div> | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import * as styles from './styles'; | ||
|
||
export function RotateViewport({ active, ...props }) { | ||
const disabledStyles = props.disabled ? styles.disabled : {}; | ||
const actionStyles = { | ||
...styles.action, | ||
...disabledStyles, | ||
}; | ||
return ( | ||
<div style={styles.row}> | ||
<label style={styles.label}>Rotate</label> | ||
<button {...props} style={actionStyles}> | ||
{active ? 'Vertical' : 'Landscape'} | ||
</button> | ||
</div> | ||
); | ||
} | ||
|
||
RotateViewport.propTypes = { | ||
disabled: PropTypes.bool, | ||
onClick: PropTypes.func.isRequired, | ||
active: PropTypes.bool, | ||
}; | ||
|
||
RotateViewport.defaultProps = { | ||
disabled: true, | ||
active: false, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
import { viewports, defaultViewport } from './viewportInfo'; | ||
import * as styles from './styles'; | ||
|
||
export function SelectViewport({ activeViewport, onChange }) { | ||
return ( | ||
<div style={styles.row}> | ||
<label style={styles.label}>Device</label> | ||
<select style={styles.action} value={activeViewport} onChange={onChange}> | ||
<option value={defaultViewport}>Default</option> | ||
{Object.keys(viewports).map(key => | ||
<option value={key} key={key}> | ||
{viewports[key].name} | ||
</option> | ||
)} | ||
</select> | ||
</div> | ||
); | ||
} | ||
|
||
SelectViewport.propTypes = { | ||
onChange: PropTypes.func.isRequired, | ||
activeViewport: PropTypes.string.isRequired, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
export const row = { | ||
width: '100%', | ||
display: 'flex', | ||
marginBottom: 15, | ||
}; | ||
|
||
export const label = { | ||
width: 80, | ||
marginRight: 15, | ||
}; | ||
|
||
const actionColor = 'rgb(247, 247, 247)'; | ||
|
||
export const button = { | ||
color: 'rgb(85, 85, 85)', | ||
width: '100%', | ||
border: `1px solid ${actionColor}`, | ||
backgroundColor: actionColor, | ||
borderRadius: 3, | ||
}; | ||
|
||
export const disabled = { | ||
opacity: '0.5', | ||
cursor: 'not-allowed', | ||
}; | ||
|
||
export const action = { | ||
...button, | ||
height: 30, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, great work!