mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-31 05:03:21 +08:00
Add different header and panels layout for mobile devices:
* add separate Header component * add isMobileDevice helper * initially place addonPanel at the bottom if storybook is open on mobile device * add first part of tests
This commit is contained in:
parent
a03b6a604f
commit
ce32102063
@ -8,14 +8,14 @@ setOptions({
|
||||
name: 'CRA Kitchen Sink',
|
||||
url: 'https://github.com/storybooks/storybook/tree/master/examples/cra-kitchen-sink',
|
||||
goFullScreen: false,
|
||||
showStoriesPanel: true,
|
||||
// showStoriesPanel: true,
|
||||
showAddonsPanel: true,
|
||||
showSearchBox: false,
|
||||
addonPanelInRight: true,
|
||||
sortStoriesByKind: false,
|
||||
hierarchySeparator: /\./,
|
||||
hierarchyRootSeparator: /\|/,
|
||||
});
|
||||
}, 'shit');
|
||||
|
||||
// deprecated usage of infoAddon
|
||||
setAddon(infoAddon);
|
||||
|
@ -1,11 +1,16 @@
|
||||
import actions from './actions';
|
||||
import checkIfMobileDevice from '../ui/libs/is_mobile_device';
|
||||
|
||||
const { userAgent } = global.window.navigator;
|
||||
const isMobileDevice = checkIfMobileDevice(userAgent);
|
||||
|
||||
export default {
|
||||
actions,
|
||||
defaultState: {
|
||||
isMobileDevice,
|
||||
shortcutOptions: {
|
||||
goFullScreen: false,
|
||||
showStoriesPanel: true,
|
||||
showStoriesPanel: !isMobileDevice,
|
||||
showAddonPanel: true,
|
||||
showSearchBox: false,
|
||||
addonPanelInRight: false,
|
||||
|
@ -0,0 +1,30 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Storyshots ui/stories/Header simple 1`] = `
|
||||
<div
|
||||
style="background:#F7F7F7;margin-bottom:10px;display:flex"
|
||||
>
|
||||
<button
|
||||
style="text-transform:uppercase;font-size:12px;font-weight:bolder;color:rgb(130, 130, 130);border:1px solid rgb(193, 193, 193);text-align:center;border-radius:2px;cursor:pointer;display:inlineBlock;padding:0;margin:0 0 0 5px;background-color:inherit;outline:0;width:30px;flex-shrink:0"
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
<a
|
||||
href="http://google.com"
|
||||
rel="noopener noreferrer"
|
||||
style="text-decoration:none;flex-grow:1;display:flex;align-items:center;justify-content:center;border:1px solid rgb(193, 193, 193);border-radius:2px"
|
||||
target="_blank"
|
||||
>
|
||||
<h3
|
||||
style="font-family:-apple-system, \\".SFNSText-Regular\\", \\"San Francisco\\", BlinkMacSystemFont, \\"Segoe UI\\", \\"Roboto\\", \\"Oxygen\\", \\"Ubuntu\\", \\"Cantarell\\", \\"Fira Sans\\", \\"Droid Sans\\", \\"Helvetica Neue\\", \\"Lucida Grande\\", \\"Arial\\", sans-serif;color:#828282;-webkit-font-smoothing:antialiased;text-transform:uppercase;letter-spacing:1.5px;font-size:12px;font-weight:bolder;text-align:center;cursor:pointer;padding:5px;margin:0;overflow:hidden"
|
||||
>
|
||||
name
|
||||
</h3>
|
||||
</a>
|
||||
<button
|
||||
style="text-transform:uppercase;font-size:12px;font-weight:bolder;color:rgb(130, 130, 130);border:1px solid rgb(193, 193, 193);text-align:center;border-radius:2px;cursor:pointer;display:inlineBlock;padding:0;margin:0 0 0 5px;background-color:inherit;outline:0;width:30px;flex-shrink:0"
|
||||
>
|
||||
⌘
|
||||
</button>
|
||||
</div>
|
||||
`;
|
@ -1,12 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { baseFonts } from '@storybook/components';
|
||||
// import isMobileDevice from '../libs/is_mobile_device';
|
||||
|
||||
const wrapperStyle = {
|
||||
background: '#F7F7F7',
|
||||
marginBottom: 10,
|
||||
const wrapperStyle = isMobileDevice => ({
|
||||
background: isMobileDevice ? 'none' : '#F7F7F7',
|
||||
margin: isMobileDevice ? '10px 0' : '0 0 10px',
|
||||
display: 'flex',
|
||||
};
|
||||
});
|
||||
|
||||
const headingStyle = {
|
||||
...baseFonts,
|
||||
@ -22,7 +23,7 @@ const headingStyle = {
|
||||
overflow: 'hidden',
|
||||
};
|
||||
|
||||
const shortcutIconStyle = {
|
||||
const iconStyle = isMobileDevice => ({
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bolder',
|
||||
@ -33,29 +34,39 @@ const shortcutIconStyle = {
|
||||
cursor: 'pointer',
|
||||
display: 'inlineBlock',
|
||||
padding: 0,
|
||||
margin: '0 0 0 5px',
|
||||
margin: isMobileDevice ? '0 15px' : '0 0 0 5px',
|
||||
backgroundColor: 'inherit',
|
||||
outline: 0,
|
||||
width: 30,
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
const burgerIconStyle = {
|
||||
...iconStyle(true),
|
||||
paddingBottom: 2,
|
||||
};
|
||||
|
||||
const linkStyle = {
|
||||
const linkStyle = isMobileDevice => ({
|
||||
textDecoration: 'none',
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '1px solid rgb(193, 193, 193)',
|
||||
border: isMobileDevice ? 'none' : '1px solid rgb(193, 193, 193)',
|
||||
borderRadius: 2,
|
||||
};
|
||||
});
|
||||
|
||||
const Header = ({ openShortcutsHelp, name, url }) => (
|
||||
<div style={wrapperStyle}>
|
||||
<a style={linkStyle} href={url} target="_blank" rel="noopener noreferrer">
|
||||
const Header = ({ openShortcutsHelp, onBurgerButtonClick, name, url, isMobileDevice }) => (
|
||||
<div style={wrapperStyle(isMobileDevice)}>
|
||||
{isMobileDevice && (
|
||||
<button style={burgerIconStyle} onClick={onBurgerButtonClick}>
|
||||
☰
|
||||
</button>
|
||||
)}
|
||||
<a style={linkStyle(isMobileDevice)} href={url} target="_blank" rel="noopener noreferrer">
|
||||
<h3 style={headingStyle}>{name}</h3>
|
||||
</a>
|
||||
<button style={shortcutIconStyle} onClick={openShortcutsHelp}>
|
||||
<button style={iconStyle(isMobileDevice)} onClick={openShortcutsHelp}>
|
||||
⌘
|
||||
</button>
|
||||
</div>
|
||||
@ -63,14 +74,18 @@ const Header = ({ openShortcutsHelp, name, url }) => (
|
||||
|
||||
Header.defaultProps = {
|
||||
openShortcutsHelp: null,
|
||||
onBurgerButtonClick: null,
|
||||
name: '',
|
||||
url: '',
|
||||
isMobileDevice: false,
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
openShortcutsHelp: PropTypes.func,
|
||||
onBurgerButtonClick: PropTypes.func,
|
||||
name: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
isMobileDevice: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Header;
|
@ -98,7 +98,11 @@ const defaultSizes = {
|
||||
},
|
||||
};
|
||||
|
||||
const saveSizes = sizes => {
|
||||
const saveSizes = (sizes, isMobileDevice) => {
|
||||
if (isMobileDevice) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem('panelSizes', JSON.stringify(sizes));
|
||||
return true;
|
||||
@ -107,7 +111,11 @@ const saveSizes = sizes => {
|
||||
}
|
||||
};
|
||||
|
||||
const getSavedSizes = sizes => {
|
||||
const getSavedSizes = (sizes, isMobileDevice) => {
|
||||
if (isMobileDevice) {
|
||||
return sizes;
|
||||
}
|
||||
|
||||
try {
|
||||
const panelSizes = localStorage.getItem('panelSizes');
|
||||
if (panelSizes) {
|
||||
@ -125,7 +133,7 @@ class Layout extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.layerSizes = getSavedSizes(defaultSizes);
|
||||
this.layerSizes = getSavedSizes(defaultSizes, props.isMobileDevice);
|
||||
|
||||
this.state = {
|
||||
previewPanelDimensions: {
|
||||
@ -159,7 +167,7 @@ class Layout extends React.Component {
|
||||
onResize(pane, mode) {
|
||||
return size => {
|
||||
this.layerSizes[pane][mode] = size;
|
||||
saveSizes(this.layerSizes);
|
||||
saveSizes(this.layerSizes, this.props.isMobileDevice);
|
||||
|
||||
const { clientWidth, clientHeight } = this.previewPanelRef;
|
||||
|
||||
@ -177,13 +185,15 @@ class Layout extends React.Component {
|
||||
goFullScreen,
|
||||
showStoriesPanel,
|
||||
showAddonPanel,
|
||||
addonPanelInRight,
|
||||
addonPanel,
|
||||
storiesPanel,
|
||||
preview,
|
||||
isMobileDevice,
|
||||
} = this.props;
|
||||
const { previewPanelDimensions } = this.state;
|
||||
|
||||
const addonPanelInRight = isMobileDevice ? 0 : this.props.addonPanelInRight;
|
||||
|
||||
const storiesPanelOnTop = false;
|
||||
|
||||
let previewStyle = normalPreviewStyle;
|
||||
@ -192,7 +202,7 @@ class Layout extends React.Component {
|
||||
previewStyle = fullScreenPreviewStyle;
|
||||
}
|
||||
|
||||
const sizes = getSavedSizes(this.layerSizes);
|
||||
const sizes = getSavedSizes(this.layerSizes, isMobileDevice);
|
||||
|
||||
const storiesPanelDefaultSize = !storiesPanelOnTop
|
||||
? sizes.storiesPanel.left
|
||||
@ -209,7 +219,7 @@ class Layout extends React.Component {
|
||||
<SplitPane
|
||||
split={storiesSplit}
|
||||
allowResize={showStoriesPanel}
|
||||
minSize={150}
|
||||
minSize={isMobileDevice ? 0 : 150}
|
||||
maxSize={-400}
|
||||
size={showStoriesPanel ? storiesPanelDefaultSize : 1}
|
||||
defaultSize={storiesPanelDefaultSize}
|
||||
@ -271,6 +281,7 @@ Layout.propTypes = {
|
||||
preview: PropTypes.func.isRequired,
|
||||
addonPanel: PropTypes.func.isRequired,
|
||||
addonPanelInRight: PropTypes.bool.isRequired,
|
||||
isMobileDevice: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
@ -1,9 +1,10 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import pick from 'lodash.pick';
|
||||
import Header from './header';
|
||||
import Header from '../../containers/header';
|
||||
import Stories from './stories_tree';
|
||||
import TextFilter from './text_filter';
|
||||
import isMobileDevice from '../../libs/is_mobile_device';
|
||||
|
||||
const scrollStyle = {
|
||||
height: 'calc(100vh - 105px)',
|
||||
@ -46,11 +47,11 @@ class StoriesPanel extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name, onStoryFilter, openShortcutsHelp, storyFilter, url } = this.props;
|
||||
const { onStoryFilter, storyFilter } = this.props;
|
||||
|
||||
return (
|
||||
<div style={mainStyle}>
|
||||
<Header name={name} url={url} openShortcutsHelp={openShortcutsHelp} />
|
||||
{!isMobileDevice && <Header />}
|
||||
<TextFilter
|
||||
text={storyFilter}
|
||||
onClear={() => onStoryFilter('')}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import StoriesPanel from './index';
|
||||
import Header from './header';
|
||||
import Header from '../header';
|
||||
import TextFilter from './text_filter';
|
||||
import Stories from './stories_tree';
|
||||
import { createHierarchies } from '../../libs/hierarchy';
|
||||
|
38
lib/ui/src/modules/ui/containers/header.js
Executable file
38
lib/ui/src/modules/ui/containers/header.js
Executable file
@ -0,0 +1,38 @@
|
||||
import pick from 'lodash.pick';
|
||||
import Header from '../components/header';
|
||||
import genPoddaLoader from '../libs/gen_podda_loader';
|
||||
import compose from '../../../compose';
|
||||
|
||||
export const headerMapper = (state, props, { actions }) => {
|
||||
const currentOptions = pick(
|
||||
state.shortcutOptions,
|
||||
'showStoriesPanel',
|
||||
'showAddonPanel',
|
||||
'goFullScreen',
|
||||
'addonPanelInRight'
|
||||
);
|
||||
|
||||
const actionMap = actions();
|
||||
const { uiOptions, isMobileDevice } = state;
|
||||
const { name, url } = uiOptions;
|
||||
|
||||
const handleBurgerButtonClick = () => {
|
||||
actionMap.shortcuts.setOptions({
|
||||
showStoriesPanel: !currentOptions.showStoriesPanel,
|
||||
});
|
||||
};
|
||||
|
||||
const addonPanelInRight = isMobileDevice ? false : currentOptions.addonPanelInRight;
|
||||
|
||||
return {
|
||||
name,
|
||||
url,
|
||||
...currentOptions,
|
||||
openShortcutsHelp: actionMap.ui.toggleShortcutsHelp,
|
||||
onBurgerButtonClick: handleBurgerButtonClick,
|
||||
isMobileDevice,
|
||||
addonPanelInRight,
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(genPoddaLoader(headerMapper))(Header);
|
39
lib/ui/src/modules/ui/containers/header.test.js
Executable file
39
lib/ui/src/modules/ui/containers/header.test.js
Executable file
@ -0,0 +1,39 @@
|
||||
import { headerMapper } from './header';
|
||||
|
||||
describe('manager.ui.containers.header', () => {
|
||||
describe('headerMapper', () => {
|
||||
test('should give correct data', () => {
|
||||
const uiOptions = {
|
||||
name: 'foo',
|
||||
url: 'bar',
|
||||
};
|
||||
const shortcutOptions = {
|
||||
showStoriesPanel: 'aa',
|
||||
showAddonPanel: 'bb',
|
||||
goFullScreen: 'cc',
|
||||
addonPanelInRight: true,
|
||||
};
|
||||
const toggleShortcutsHelp = () => 'toggleShortcutsHelp';
|
||||
const props = {};
|
||||
const env = {
|
||||
actions: () => ({
|
||||
ui: {
|
||||
toggleShortcutsHelp,
|
||||
},
|
||||
}),
|
||||
};
|
||||
const state = {
|
||||
uiOptions,
|
||||
shortcutOptions,
|
||||
};
|
||||
|
||||
const result = headerMapper(state, props, env);
|
||||
|
||||
expect(result.showStoriesPanel).toEqual('aa');
|
||||
expect(result.showAddonPanel).toEqual('bb');
|
||||
expect(result.goFullScreen).toEqual('cc');
|
||||
expect(result.addonPanelInRight).toEqual('lalala');
|
||||
expect(result.openShortcutsHelp).toBe(toggleShortcutsHelp);
|
||||
});
|
||||
});
|
||||
});
|
@ -2,8 +2,21 @@ import pick from 'lodash.pick';
|
||||
import Layout from '../components/layout';
|
||||
import genPoddaLoader from '../libs/gen_podda_loader';
|
||||
import compose from '../../../compose';
|
||||
import isMobileDevice from '../libs/is_mobile_device';
|
||||
|
||||
export const mapper = ({ shortcutOptions }) =>
|
||||
pick(shortcutOptions, 'showStoriesPanel', 'showAddonPanel', 'goFullScreen', 'addonPanelInRight');
|
||||
export const shortcutMapper = state => {
|
||||
const currentOptions = pick(
|
||||
state.shortcutOptions,
|
||||
'showStoriesPanel',
|
||||
'showAddonPanel',
|
||||
'goFullScreen',
|
||||
'addonPanelInRight'
|
||||
);
|
||||
|
||||
export default compose(genPoddaLoader(mapper))(Layout);
|
||||
return {
|
||||
...currentOptions,
|
||||
isMobileDevice,
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(genPoddaLoader(shortcutMapper))(Layout);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { mapper } from './layout';
|
||||
import { shortcutMapper } from './layout';
|
||||
|
||||
describe('manager.ui.containers.layout', () => {
|
||||
describe('mapper', () => {
|
||||
describe('shortcutMapper', () => {
|
||||
test('should give correct data', () => {
|
||||
const state = {
|
||||
shortcutOptions: {
|
||||
@ -10,7 +10,7 @@ describe('manager.ui.containers.layout', () => {
|
||||
goFullScreen: 'cc',
|
||||
},
|
||||
};
|
||||
const data = mapper(state);
|
||||
const data = shortcutMapper(state);
|
||||
|
||||
expect(data).toEqual(state.shortcutOptions);
|
||||
});
|
||||
|
@ -15,8 +15,6 @@ export const mapper = (state, props, { actions }) => {
|
||||
|
||||
const { stories, selectedKind, selectedStory, uiOptions, storyFilter } = state;
|
||||
const {
|
||||
name,
|
||||
url,
|
||||
sortStoriesByKind,
|
||||
hierarchySeparator,
|
||||
hierarchyRootSeparator,
|
||||
@ -52,10 +50,7 @@ export const mapper = (state, props, { actions }) => {
|
||||
storyFilter,
|
||||
onStoryFilter: actionMap.ui.setStoryFilter,
|
||||
|
||||
openShortcutsHelp: actionMap.ui.toggleShortcutsHelp,
|
||||
sidebarAnimations,
|
||||
name,
|
||||
url,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -12,7 +12,6 @@ describe('manager.ui.containers.stories_panel', () => {
|
||||
url: 'bar',
|
||||
};
|
||||
const selectStory = () => 'selectStory';
|
||||
const toggleShortcutsHelp = () => 'toggleShortcutsHelp';
|
||||
const setStoryFilter = () => 'setStoryFilter';
|
||||
const props = {};
|
||||
const env = {
|
||||
@ -21,7 +20,6 @@ describe('manager.ui.containers.stories_panel', () => {
|
||||
selectStory,
|
||||
},
|
||||
ui: {
|
||||
toggleShortcutsHelp,
|
||||
setStoryFilter,
|
||||
},
|
||||
}),
|
||||
@ -57,7 +55,6 @@ describe('manager.ui.containers.stories_panel', () => {
|
||||
expect(result.storyFilter).toBe(null);
|
||||
expect(result.onSelectStory).toBe(selectStory);
|
||||
expect(result.onStoryFilter).toBe(setStoryFilter);
|
||||
expect(result.openShortcutsHelp).toBe(toggleShortcutsHelp);
|
||||
});
|
||||
|
||||
test('should filter stories according to the given filter', () => {
|
||||
|
7
lib/ui/src/modules/ui/libs/is_mobile_device.js
Normal file
7
lib/ui/src/modules/ui/libs/is_mobile_device.js
Normal file
@ -0,0 +1,7 @@
|
||||
export default userAgent => {
|
||||
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
28
lib/ui/src/modules/ui/libs/is_mobile_device.test.js
Normal file
28
lib/ui/src/modules/ui/libs/is_mobile_device.test.js
Normal file
@ -0,0 +1,28 @@
|
||||
import isMobileDevice from './is_mobile_device';
|
||||
|
||||
describe('manager.ui.libs.is_mobile_device', () => {
|
||||
it('should detect if storybook is open on mobile device', () => {
|
||||
// chrome
|
||||
expect(
|
||||
isMobileDevice(
|
||||
'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19'
|
||||
)
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isMobileDevice('Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0')
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isMobileDevice(
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36'
|
||||
)
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isMobileDevice(
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6'
|
||||
)
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
@ -6,11 +6,12 @@ import StoriesPanel from './containers/stories_panel';
|
||||
import AddonPanel from './containers/addon_panel';
|
||||
import ShortcutsHelp from './containers/shortcuts_help';
|
||||
import SearchBox from './containers/search_box';
|
||||
import Header from './containers/header';
|
||||
|
||||
export default function(injectDeps, { clientStore, provider, domNode }) {
|
||||
const state = clientStore.getAll();
|
||||
// generate preview
|
||||
const Preview = () => {
|
||||
const state = clientStore.getAll();
|
||||
const preview = provider.renderPreview(state.selectedKind, state.selectedStory);
|
||||
return preview;
|
||||
};
|
||||
@ -20,6 +21,7 @@ export default function(injectDeps, { clientStore, provider, domNode }) {
|
||||
|
||||
const root = (
|
||||
<div>
|
||||
{state.isMobileDevice && <Header />}
|
||||
<Layout
|
||||
storiesPanel={() => <StoriesPanel />}
|
||||
preview={() => <Preview />}
|
||||
|
Loading…
x
Reference in New Issue
Block a user