FIX tool addons, no longer needing a direct reference to the iframe

- using global styles instead, which is marginally better
This commit is contained in:
Norbert de Langen 2019-02-01 12:44:32 +01:00
parent 8fc746a491
commit a86e25f040
3 changed files with 131 additions and 138 deletions

View File

@ -1,26 +1,25 @@
import { document } from 'global';
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import memoize from 'memoizerific'; import memoize from 'memoizerific';
import { logger } from '@storybook/client-logger';
import { SET_STORIES } from '@storybook/core-events'; import { SET_STORIES } from '@storybook/core-events';
import { Global } from '@storybook/theming';
import { Popout, Item, Icons, Icon, IconButton, Title, Detail, List } from '@storybook/components'; import { Popout, Item, Icons, Icon, IconButton, Title, Detail, List } from '@storybook/components';
import * as S from './components'; import * as S from './components';
import { PARAM_KEY } from './constants'; import { PARAM_KEY } from './constants';
const getIframe = () => document.getElementById('storybook-preview-background'); const iframeId = 'storybook-preview-background';
const getState = (props, state) => { const getState = memoize(10)((props, state) => {
const data = props.api.getCurrentStoryData(); const data = props.api.getCurrentStoryData();
const list = data && data.parameters && data.parameters[PARAM_KEY]; const list = data && data.parameters && data.parameters[PARAM_KEY];
return list && list.length return list && list.length
? list.reduce( ? list.reduce(
(acc, { name, value, default: isSelected }) => { (acc, { name, value, default: isSelected }) => {
acc.backgrounds.push({ name, value }); acc.items.push({ name, value });
if (isSelected && state.selected !== 'transparent') { if (isSelected && state.selected !== 'transparent') {
if (!list.find(i => i.value === state.selected)) { if (!list.find(i => i.value === state.selected)) {
@ -30,23 +29,14 @@ const getState = (props, state) => {
return acc; return acc;
}, },
{ {
backgrounds: [], items: [],
selected: state.selected, selected: state.selected,
} }
) )
: { : {
backgrounds: [], items: [],
selected: 'transparent', selected: 'transparent',
}; };
};
const apply = memoize(1)((value, iframe) => {
if (iframe) {
// eslint-disable-next-line no-param-reassign
iframe.style.background = value;
} else {
logger.error('Cannot find Storybook iframe');
}
}); });
export default class BackgroundTool extends Component { export default class BackgroundTool extends Component {
@ -54,69 +44,81 @@ export default class BackgroundTool extends Component {
super(props); super(props);
this.state = { this.state = {
backgrounds: [], items: [],
selected: 'transparent', selected: 'transparent',
}; };
this.listener = () => {
this.setState({ selected: null });
};
} }
componentDidMount() { componentDidMount() {
const { api } = this.props; const { api } = this.props;
api.on(SET_STORIES, this.listener);
}
api.on(SET_STORIES, () => { componentWillUnmount() {
const { state, props } = this; const { api } = this.props;
this.setState(getState(props, state)); api.off(SET_STORIES, this.listener);
});
} }
change = selected => { change = selected => {
this.setState({ selected }, this.apply); this.setState({ selected });
}; };
render() { render() {
const { backgrounds, selected } = getState(this.props, this.state); const { items, selected } = getState(this.props, this.state);
const iframe = getIframe();
apply(selected, iframe); return items.length ? (
<Fragment>
<Global
styles={{
[`#${iframeId}`]: {
background: selected,
},
}}
/>
return backgrounds.length ? ( <Popout key="backgrounds">
<Popout key="backgrounds"> <IconButton key="background" title="Backgrounds">
<IconButton key="background" title="Backgrounds"> <Icons icon="photo" />
<Icons icon="photo" /> </IconButton>
</IconButton> {({ hide }) => (
{({ hide }) => ( <List>
<List> {selected !== 'transparent' ? (
{selected !== 'transparent' ? ( <Fragment>
<Fragment> <Item
key="clear"
onClick={() => {
hide();
this.change('transparent');
}}
>
<Icon type="undo" />
<Title>Clear</Title>
<Detail>transparent</Detail>
</Item>
</Fragment>
) : null}
{items.map(({ name, value }) => (
<Item <Item
key="clear" key={name}
onClick={() => { onClick={() => {
hide(); hide();
this.change('transparent'); this.change(value);
}} }}
> >
<Icon type="undo" /> <Icon type={<S.ColorIcon background={value} />} />
<Title>Clear</Title> <Title>{name}</Title>
<Detail>transparent</Detail> <Detail>{value}</Detail>
</Item> </Item>
</Fragment> ))}
) : null} </List>
)}
{backgrounds.map(({ name, value }) => ( </Popout>
<Item </Fragment>
key={name}
onClick={() => {
hide();
this.change(value);
}}
>
<Icon type={<S.ColorIcon background={value} />} />
<Title>{name}</Title>
<Detail>{value}</Detail>
</Item>
))}
</List>
)}
</Popout>
) : null; ) : null;
} }
} }

View File

@ -1,121 +1,120 @@
import { document } from 'global';
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import memoize from 'memoizerific'; import memoize from 'memoizerific';
import { Global, css } from '@storybook/theming'; import { Global } from '@storybook/theming';
import { Popout, Item, Icons, Icon, IconButton, Title, List } from '@storybook/components'; import { Popout, Item, Icons, Icon, IconButton, Title, List } from '@storybook/components';
import { STORY_CHANGED } from '@storybook/core-events'; import { SET_STORIES } from '@storybook/core-events';
import { logger } from '@storybook/client-logger';
import { PARAM_KEY } from './constants'; import { PARAM_KEY } from './constants';
const toList = memoize(50)(viewports => Object.entries(viewports)); const toList = memoize(50)(items =>
const getIframe = memoize(1)(() => document.getElementById('storybook-preview-iframe')); items ? Object.entries(items).map(([id, value]) => ({ ...value, id })) : []
const iframeClass = 'storybook-preview-iframe-viewport'; );
const iframeId = 'storybook-preview-background';
const getState = memoize(10)((props, state) => {
const data = props.api.getCurrentStoryData();
const list = toList(data && data.parameters && data.parameters[PARAM_KEY]);
return list && list.length
? list.reduce(
(acc, { name, styles: value, id }) => {
acc.items.push({ name, value, id });
if (state.selected !== 'responsive') {
if (!list.find(i => i.id === state.selected)) {
acc.selected = id;
}
}
return acc;
},
{
isRotated: state.isRotated,
items: [],
selected: state.selected,
}
)
: {
isRotated: false,
items: [],
selected: 'responsive',
};
});
const flip = ({ width, height }) => ({ height: width, widht: height });
export default class ViewportTool extends Component { export default class ViewportTool extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
viewports: {},
selected: undefined,
isRotated: false, isRotated: false,
items: [],
selected: 'responsive',
};
this.listener = () => {
this.setState({});
}; };
} }
componentDidMount() { componentDidMount() {
const { api } = this.props; const { api } = this.props;
api.on(STORY_CHANGED, this.onStoryChange); api.on(SET_STORIES, this.listener);
} }
componentWillUnmount() { componentWillUnmount() {
const { api } = this.props; const { api } = this.props;
api.off(STORY_CHANGED, this.onStoryChange); api.off(SET_STORIES, this.listener);
} }
onStoryChange = id => { change = selected => {
const { api } = this.props; this.setState({ selected });
const viewports = api.getParameters(id, PARAM_KEY);
if (viewports) {
this.setState({ viewports });
}
};
change = key => {
this.setState({ selected: key }, () => {
this.apply();
});
}; };
rotate = () => { rotate = () => {
const { isRotated } = this.state; const { isRotated } = this.state;
this.setState({ isRotated: !isRotated }, () => { this.setState({ isRotated: !isRotated });
this.apply();
});
};
apply = () => {
const iframe = getIframe();
const { isRotated, selected, viewports } = this.state;
if (iframe) {
if (selected) {
const {
styles: { width: a, height: b },
} = viewports[selected];
iframe.style.width = isRotated ? b : a;
iframe.style.height = isRotated ? a : b;
if (!iframe.classList.item(iframeClass)) {
iframe.classList.add(iframeClass);
}
} else {
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.classList.remove(iframeClass);
}
} else {
logger.error('Cannot find Storybook iframe');
}
}; };
render() { render() {
const { viewports, selected } = this.state; const { items, selected, isRotated } = getState(this.props, this.state);
const item = items.find(i => i.id === selected);
const list = toList(viewports); if (!items.length) {
if (!list.length) {
return null; return null;
} }
return ( return (
<Fragment> <Fragment>
<Global {item && item.value ? (
styles={css({ <Global
[`.${iframeClass}`]: { styles={{
border: '10px solid black', [`#${iframeId}`]: {
borderRadius: 4, border: '10px solid black',
margin: 10, borderRadius: 4,
}, margin: 10,
})}
/> ...(isRotated ? flip(item.value || {}) : item.value || {}),
},
}}
/>
) : null}
<Popout key="viewports"> <Popout key="viewports">
<IconButton key="viewport" title="Change Viewport"> <IconButton key="viewport" title="Change Viewport">
<Icons icon="grow" /> <Icons icon="grow" />
</IconButton> </IconButton>
{({ hide }) => ( {({ hide }) => (
<List> <List>
{selected !== undefined ? ( {selected !== 'responsive' ? (
<Fragment> <Fragment>
<Item <Item
key="reset" key="reset"
onClick={() => { onClick={() => {
hide(); hide();
this.change(undefined); this.change('responsive');
}} }}
> >
<Icon type="undo" /> <Icon type="undo" />
@ -134,12 +133,12 @@ export default class ViewportTool extends Component {
</Fragment> </Fragment>
) : null} ) : null}
{list.map(([key, { name, type }]) => ( {items.map(({ id, name, type }) => (
<Item <Item
key={key} key={id}
onClick={() => { onClick={() => {
hide(); hide();
this.change(key); this.change(id);
}} }}
> >
<Icon type={type} /> <Icon type={type} />
@ -155,14 +154,7 @@ export default class ViewportTool extends Component {
} }
ViewportTool.propTypes = { ViewportTool.propTypes = {
channel: PropTypes.shape({
on: PropTypes.func,
emit: PropTypes.func,
removeListener: PropTypes.func,
}).isRequired,
api: PropTypes.shape({ api: PropTypes.shape({
on: PropTypes.func, on: PropTypes.func,
getQueryParam: PropTypes.func,
setQueryParams: PropTypes.func,
}).isRequired, }).isRequired,
}; };

View File

@ -6,11 +6,10 @@ import { ADDON_ID } from './constants';
import Tool from './Tool'; import Tool from './Tool';
addons.register(ADDON_ID, api => { addons.register(ADDON_ID, api => {
const channel = addons.getChannel();
addons.add(ADDON_ID, { addons.add(ADDON_ID, {
type: types.TOOL,
title: 'viewport / media-queries', title: 'viewport / media-queries',
type: types.TOOL,
match: ({ viewMode }) => viewMode === 'story', match: ({ viewMode }) => viewMode === 'story',
render: () => <Tool channel={channel} api={api} />, render: () => <Tool api={api} />,
}); });
}); });