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

viewport-addon: Add support for adding new viewports/devices #3098

Closed
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
2 changes: 1 addition & 1 deletion addons/viewport/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"storybook"
],
"license": "MIT",
"main": "register.js",
"main": "preview.js",
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
Expand Down
6 changes: 6 additions & 0 deletions addons/viewport/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const preview = require('./dist/preview');

exports.addViewports = preview.addViewports;
exports.setViewports = preview.setViewports;

preview.init();
19 changes: 0 additions & 19 deletions addons/viewport/src/components/tests/viewportInfo.spec.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@ import PropTypes from 'prop-types';
import { baseFonts } from '@storybook/components';
import { document } from 'global';

import { viewports, defaultViewport, resetViewport } from './viewportInfo';
import {
initialViewports,
defaultViewport,
resetViewport,
applyDefaultStyles,
} from './viewportInfo';
import { SelectViewport } from './SelectViewport';
import { RotateViewport } from './RotateViewport';
import {
ADD_VIEWPORTS_EVENT_ID,
SET_VIEWPORTS_EVENT_ID,
UPDATE_VIEWPORT_EVENT_ID,
} from '../../shared';

import * as styles from './styles';

Expand All @@ -17,6 +27,17 @@ const containerStyles = {
...baseFonts,
};

const transformViewports = transformer => viewports =>
Object.keys(viewports).reduce(
(all, key) => ({
...all,
[key]: transformer(viewports[key]),
}),
{}
);

const viewportsTransformer = transformViewports(applyDefaultStyles);

export class Panel extends Component {
static propTypes = {
channel: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
Expand All @@ -26,20 +47,42 @@ export class Panel extends Component {
super(props, context);
this.state = {
viewport: defaultViewport,
viewports: initialViewports,
isLandscape: false,
};

this.props.channel.on('addon:viewport:update', this.changeViewport);
}

componentDidMount() {
this.iframe = document.getElementById(storybookIframe);

this.props.channel.on(UPDATE_VIEWPORT_EVENT_ID, this.changeViewport);
this.props.channel.on(ADD_VIEWPORTS_EVENT_ID, this.addViewports);
this.props.channel.on(SET_VIEWPORTS_EVENT_ID, this.setViewports);
}

componentWillUnmount() {
this.props.channel.removeListener('addon:viewport:update', this.changeViewport);
this.props.channel.removeListener(UPDATE_VIEWPORT_EVENT_ID, this.changeViewport);
this.props.channel.removeListener(ADD_VIEWPORTS_EVENT_ID, this.addViewports);
this.props.channel.removeListener(SET_VIEWPORTS_EVENT_ID, this.setViewports);
}

setViewports = viewports => {
const newViewports = viewportsTransformer(viewports);

this.setState({ viewports: newViewports });
};

addViewports = viewports => {
const newViewports = viewportsTransformer(viewports);

this.setState({
viewports: {
...initialViewports,
...newViewports,
},
});
};

iframe = undefined;

changeViewport = viewport => {
Expand All @@ -63,7 +106,7 @@ export class Panel extends Component {
};

updateIframe = () => {
const { viewport: viewportKey, isLandscape } = this.state;
const { viewports, viewport: viewportKey, isLandscape } = this.state;
const viewport = viewports[viewportKey] || resetViewport;

if (!this.iframe) {
Expand All @@ -81,7 +124,7 @@ export class Panel extends Component {
};

render() {
const { isLandscape, viewport } = this.state;
const { isLandscape, viewport, viewports } = this.state;

const disableDefault = viewport === defaultViewport;
const disabledStyles = disableDefault ? styles.disabled : {};
Expand All @@ -96,6 +139,8 @@ export class Panel extends Component {
return (
<div style={containerStyles}>
<SelectViewport
viewports={viewports}
defaultViewport={defaultViewport}
activeViewport={viewport}
onChange={e => this.changeViewport(e.target.value)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';

import { viewports, defaultViewport } from './viewportInfo';
import * as styles from './styles';

export function SelectViewport({ activeViewport, onChange }) {
export function SelectViewport({ viewports, defaultViewport, activeViewport, onChange }) {
return (
<div style={styles.row}>
<label htmlFor="device" style={styles.label}>
Expand All @@ -25,4 +24,6 @@ export function SelectViewport({ activeViewport, onChange }) {
SelectViewport.propTypes = {
onChange: PropTypes.func.isRequired,
activeViewport: PropTypes.string.isRequired,
viewports: PropTypes.shape({}).isRequired,
defaultViewport: PropTypes.string.isRequired,
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { shallow } from 'enzyme';
import { document } from 'global';

import { Panel } from '../Panel';
import { viewports, defaultViewport, resetViewport } from '../viewportInfo';
import { initialViewports, defaultViewport, resetViewport } from '../viewportInfo';
import * as styles from '../styles';

describe('Viewport/Panel', () => {
Expand All @@ -24,16 +24,10 @@ describe('Viewport/Panel', () => {
it('creates the initial state', () => {
expect(subject.instance().state).toEqual({
viewport: defaultViewport,
viewports: initialViewports,
isLandscape: false,
});
});

it('listens on the channel', () => {
expect(props.channel.on).toHaveBeenCalledWith(
'addon:viewport:update',
subject.instance().changeViewport
);
});
});

describe('componentDidMount', () => {
Expand All @@ -53,19 +47,54 @@ describe('Viewport/Panel', () => {
it('gets the iframe', () => {
expect(subject.instance().iframe).toEqual('iframe');
});

it('listens on `update` channel', () => {
expect(props.channel.on).toHaveBeenCalledWith(
'addon:viewport:update',
subject.instance().changeViewport
);
});

it('listens on the `setViewports` topic', () => {
expect(props.channel.on).toHaveBeenCalledWith(
'addon:viewport:setViewports',
subject.instance().setViewports
);
});

it('listens on the `addViewports` topic', () => {
expect(props.channel.on).toHaveBeenCalledWith(
'addon:viewport:addViewports',
subject.instance().addViewports
);
});
});

describe('componentWillUnmount', () => {
beforeEach(() => {
subject.instance().componentWillUnmount();
});

it('removes the channel listener', () => {
it('removes `update` channel listener', () => {
expect(props.channel.removeListener).toHaveBeenCalledWith(
'addon:viewport:update',
subject.instance().changeViewport
);
});

it('removes `setViewports` channel listener', () => {
expect(props.channel.removeListener).toHaveBeenCalledWith(
'addon:viewport:setViewports',
subject.instance().setViewports
);
});

it('removes `addViewports` channel listener', () => {
expect(props.channel.removeListener).toHaveBeenCalledWith(
'addon:viewport:addViewports',
subject.instance().addViewports
);
});
});

describe('changeViewport', () => {
Expand All @@ -76,13 +105,13 @@ describe('Viewport/Panel', () => {

describe('new viewport', () => {
beforeEach(() => {
subject.instance().changeViewport(viewports[0]);
subject.instance().changeViewport(initialViewports[0]);
});

it('sets the state with the new information', () => {
expect(subject.instance().setState).toHaveBeenCalledWith(
{
viewport: viewports[0],
viewport: initialViewports[0],
isLandscape: false,
},
subject.instance().updateIframe
Expand Down Expand Up @@ -204,6 +233,22 @@ describe('Viewport/Panel', () => {
);
});

it('passes the defaultViewport', () => {
expect(select.props()).toEqual(
expect.objectContaining({
defaultViewport,
})
);
});

it('passes the initialViewports', () => {
expect(select.props()).toEqual(
expect.objectContaining({
viewports: initialViewports,
})
);
});

it('onChange it updates the viewport', () => {
const e = { target: { value: 'iphone6' } };
select.simulate('change', e);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { SelectViewport } from '../SelectViewport';
import { viewports, defaultViewport } from '../viewportInfo';
import { initialViewports, defaultViewport } from '../viewportInfo';
import * as styles from '../styles';

describe('Viewport/SelectViewport', () => {
Expand All @@ -12,6 +12,8 @@ describe('Viewport/SelectViewport', () => {
props = {
onChange: jest.fn(),
activeViewport: defaultViewport,
viewports: initialViewports,
defaultViewport,
};

subject = shallow(<SelectViewport {...props} />);
Expand All @@ -35,17 +37,17 @@ describe('Viewport/SelectViewport', () => {
});

describe('dynamic options', () => {
const viewportKeys = Object.keys(viewports);
const viewportKeys = Object.keys(initialViewports);
it('has at least 1 option', () => {
expect(viewportKeys.length).toBeGreaterThan(0);
});

viewportKeys.forEach(key => {
let option;

it(`renders an option for ${viewports[key].name}`, () => {
it(`renders an option for ${initialViewports[key].name}`, () => {
option = subject.find(`[value="${key}"]`);
expect(option.text()).toEqual(viewports[key].name);
expect(option.text()).toEqual(initialViewports[key].name);
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { initialViewports, resetViewport, configuredStyles, applyStyles } from '../viewportInfo';

describe('Viewport/constants', () => {
describe('initialViewports', () => {
it('includes the default styles on every custom viewport', () => {
const keys = Object.keys(initialViewports);

keys.forEach(key => {
expect(initialViewports[key].styles).toEqual(expect.objectContaining(configuredStyles));
});
});
});

describe('resetViewport', () => {
it('does not include the styles for a responsive iframe', () => {
expect(resetViewport).not.toEqual(expect.objectContaining(configuredStyles));
});
});

describe('applyStyles', () => {
it('creates a new viewport with all given styles applied', () => {
const viewport = {
styles: {
width: '50px',
},
};
const styles = {
foo: 'bar',
john: 'doe',
};
const newViewport = applyStyles(viewport, styles);

expect(newViewport.styles).toEqual(
expect.objectContaining({
width: '50px',
foo: 'bar',
john: 'doe',
})
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,23 @@ export const resetViewport = {
},
};

export const viewports = {
export function applyStyles(viewport, styles) {
const mixedStyles = {
...viewport.styles,
...styles,
};

return {
...viewport,
styles: mixedStyles,
};
}

export function applyDefaultStyles(viewport) {
return applyStyles(viewport, configuredStyles);
}

export const initialViewports = {
iphone5: {
name: 'iPhone 5',
styles: {
Expand Down
Loading