mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-20 05:02:37 +08:00
Merge branch 'master' into patch-2
This commit is contained in:
commit
523fae4724
@ -27,7 +27,7 @@ module.exports = {
|
||||
singleQuote: true,
|
||||
},
|
||||
],
|
||||
quotes: [warn, 'single'],
|
||||
quotes: [warn, 'single', { avoidEscape: true }],
|
||||
'class-methods-use-this': ignore,
|
||||
'arrow-parens': [warn, 'as-needed'],
|
||||
'space-before-function-paren': ignore,
|
||||
|
@ -87,6 +87,12 @@ setOptions({
|
||||
* @type {Regex}
|
||||
*/
|
||||
hierarchySeparator: null,
|
||||
|
||||
/**
|
||||
* sidebar tree animations
|
||||
* @type {Boolean}
|
||||
*/
|
||||
sidebarAnimations: true,
|
||||
});
|
||||
|
||||
storybook.configure(() => require('./stories'), module);
|
||||
|
@ -72,6 +72,9 @@ For RN apps:
|
||||
|
||||
Once your app is started, changing the selected story in web browser will update the story displayed within your mobile app.
|
||||
|
||||
If you are using Android and you get the following error after running the app: `'websocket: connection error', 'Failed to connect to localhost/127.0.0.1:7007'`, you have to forward the port 7007 on your device/emulator to port 7007 on your local machine with the following command:
|
||||
`adb reverse tcp:7007 tcp:7007`
|
||||
|
||||
## Using Haul-cli
|
||||
|
||||
[Haul](https://github.com/callstack-io/haul) is an alternative to the react-native packager and has several advantages in that it allows you to define your own loaders, and handles symlinks better.
|
||||
|
@ -23,6 +23,11 @@ squarespace:
|
||||
title: Squarespace
|
||||
description: Component design and development at Squarespace
|
||||
site: http://squarespace.com
|
||||
dbsbank:
|
||||
logo: ./logos/dbsbank.svg
|
||||
title: DBS Bank
|
||||
description: DBS Bank consumer products improves performance and maintainability with Storybook!
|
||||
site: https://www.dbs.com
|
||||
coursera:
|
||||
logo: ./logos/coursera.svg
|
||||
title: Coursera
|
||||
|
5
docs/pages/logos/dbsbank.svg
Normal file
5
docs/pages/logos/dbsbank.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="13.49 12.03 110.47 33.24">
|
||||
<path fill="#FFF" fill-rule="evenodd" d="M19.486 18.034H40.74v21.254H19.486z" clip-rule="evenodd"/>
|
||||
<path fill="#C00" d="M43.652 28.658v-.003c0-2.094.086-3.962 1.64-7.26.452-.955 1.442-2.357-.02-3.98-1.183-1.185-2.538-.986-3.446-.467.52-.908.72-2.263-.467-3.448-1.63-1.462-3.03-.47-3.99-.022-3.29 1.56-5.16 1.646-7.26 1.646-2.1 0-3.97-.086-7.26-1.646-.96-.448-2.36-1.44-3.98.022-1.19 1.186-.99 2.54-.47 3.448-.91-.522-2.26-.718-3.45.466-1.46 1.624-.47 3.026-.02 3.98 1.55 3.3 1.63 5.164 1.63 7.264 0 2.1-.09 3.972-1.64 7.264-.45.956-1.45 2.358.02 3.987 1.18 1.18 2.53.98 3.44.46-.52.9-.72 2.26.47 3.44 1.62 1.46 3.02.47 3.98.01 3.29-1.55 5.16-1.64 7.26-1.64 2.1 0 3.97.09 7.26 1.64.96.45 2.36 1.44 3.98-.02 1.18-1.19.98-2.54.46-3.45.91.52 2.26.72 3.45-.47 1.46-1.63.47-3.04.01-3.99-1.55-3.3-1.64-5.17-1.64-7.27zM40.05 38.62l-7.715-6.474s-1.04-1.017-2.223-1.017c-1.178 0-2.225 1.01-2.225 1.01l-7.71 6.47-.03-.03 6.48-7.72s1.015-1.05 1.015-2.23-1.016-2.22-1.016-2.22l-6.48-7.72.03-.03 7.713 6.47s1.04 1.02 2.22 1.02c1.18 0 2.22-1.02 2.22-1.02l7.71-6.478.02.027-6.47 7.72s-1.02 1.04-1.02 2.22c0 1.18 1.02 2.22 1.02 2.22l6.47 7.71-.02.02z"/>
|
||||
<path fill="#000" d="M121.32 14.57l.275-.002.1 5.052-.307-.004c-.498-2.05-2.475-3.995-6.436-4.2-4.686-.244-6.715 2.547-6.736 5.183-.027 3.51 2.707 4.23 6.76 5.22 1.914.46 8.994 1.39 8.428 9.35-.394 5.56-5.19 8.2-11.984 7.99 0 0-2.82-.1-6.766-1.19-.738-.21-.922.04-1.225.58l-.32.01.01-5.45.31.02c.16.55.2 1.57 1.19 2.45.74.64 2.44 2.22 6.19 2.26 3.68.04 6.74-1.52 7.15-5.64.11-1.12-.02-3.66-2.5-4.83-1.95-.91-7.34-1.51-9.96-3.8 0 0-3.1-2.16-2.61-6.2.69-5.7 5.45-7.13 9.72-7.22 0 0 3.83-.03 6.87.76 0 0 .75.2 1.38-.037.24-.083.36-.24.43-.34zM94.597 27.693c6.947 1.724 7.367 6.738 7.266 8.146-.31 6.27-5.64 6.99-7.78 6.99H77.7l.017-.32c1.19-.16 2.082-.82 2.082-2.9l.1-21.62c.02-2.35-.6-3.06-1.9-3.22l-.03-.3h12.4c2.27 0 9.11-.46 9.7 5.84.46 5.03-5.07 7.14-5.48 7.36zm1.824 8.186c.13-6.09-4.34-7.71-8.96-7.96-.09-.01-.05-.24.01-.25 1.51-.06 7.49-.75 7.24-6.46-.24-5.56-4.46-5.68-6-5.7-1.26-.02-1.53-.02-2.06.01-.89.03-1.12.1-1.11 1.07 0 .12-.2 10.01-.31 16.97-.06 3.78-.07 6.7-.07 6.7.02.68-.07 1.39 1.64 1.47 1.76.08 4.04.15 5.62-.18 1.46-.32 3.94-1.37 4.03-5.68zm-32.65-21.4c5.9-.03 13.67 4.02 13.65 14.13-.02 8.79-6.37 14.21-12.24 14.21H50.29l.017-.32c.847-.14 1.776-.67 1.93-1.23.497-2.12.39-21.51.08-24.7-.09-1-.872-1.64-1.84-1.8l-.02-.31H63.78zm2.7 26.51c2.64-.95 5.92-5.46 5.38-12.57-.52-6.88-3.14-12.27-10.12-12.78 0 0-1.32-.1-2.43-.11-1.18-.01-1.44-.05-1.53 1.21-.16 2.37-.2 20.85-.05 23.53.03.36.07 1.24 1.59 1.39 2.78.27 5.08.05 7.17-.7z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
@ -1,2 +1,3 @@
|
||||
import '@storybook/addon-actions/register';
|
||||
import '@storybook/addon-options/register';
|
||||
import '@storybook/addon-links/register';
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { configure } from '@storybook/react';
|
||||
import { setOptions } from '@storybook/addon-options';
|
||||
|
||||
setOptions({
|
||||
sidebarAnimations: false,
|
||||
});
|
||||
|
||||
function loadStories() {
|
||||
require('../src/stories');
|
||||
|
@ -8,7 +8,7 @@ export const features = {
|
||||
ESCAPE: 5,
|
||||
NEXT_STORY: 6,
|
||||
PREV_STORY: 7,
|
||||
SEARCH: 8,
|
||||
SHOW_SEARCH: 8,
|
||||
DOWN_PANEL_IN_RIGHT: 9,
|
||||
};
|
||||
|
||||
@ -54,7 +54,7 @@ export default function handle(e) {
|
||||
return features.PREV_STORY;
|
||||
case keycode('P'):
|
||||
e.preventDefault();
|
||||
return features.SEARCH;
|
||||
return features.SHOW_SEARCH;
|
||||
case keycode('J'):
|
||||
e.preventDefault();
|
||||
return features.DOWN_PANEL_IN_RIGHT;
|
||||
|
@ -9,6 +9,7 @@ export default {
|
||||
url: 'https://github.com/storybooks/storybook',
|
||||
sortStoriesByKind: false,
|
||||
hierarchySeparator: '/',
|
||||
sidebarAnimations: true,
|
||||
},
|
||||
},
|
||||
load({ clientStore, provider }, _actions) {
|
||||
|
@ -10,8 +10,8 @@ export function keyEventToOptions(currentOptions, event) {
|
||||
return { showDownPanel: !currentOptions.showDownPanel };
|
||||
case features.LEFT_PANEL:
|
||||
return { showLeftPanel: !currentOptions.showLeftPanel };
|
||||
case features.SEARCH:
|
||||
return { showSearchBox: !currentOptions.showSearchBox };
|
||||
case features.SHOW_SEARCH:
|
||||
return { showSearchBox: true };
|
||||
case features.DOWN_PANEL_IN_RIGHT:
|
||||
return { downPanelInRight: !currentOptions.downPanelInRight };
|
||||
default:
|
||||
|
@ -21,6 +21,7 @@ const storyProps = [
|
||||
'selectedHierarchy',
|
||||
'selectedStory',
|
||||
'onSelectStory',
|
||||
'sidebarAnimations',
|
||||
];
|
||||
|
||||
const LeftPanel = props =>
|
||||
|
@ -3,11 +3,10 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import deepEqual from 'deep-equal';
|
||||
import treeNodeTypes from './tree_node_type';
|
||||
import createTreeDecorators from './tree_decorators';
|
||||
import treeDecorators from './tree_decorators';
|
||||
import treeStyle from './tree_style';
|
||||
|
||||
const namespaceSeparator = '@';
|
||||
const keyCodeEnter = 13;
|
||||
|
||||
function createNodeKey({ namespaces, type }) {
|
||||
return [...namespaces, [type]].join(namespaceSeparator);
|
||||
@ -39,14 +38,12 @@ class Stories extends React.Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.onToggle = this.onToggle.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
|
||||
const { selectedHierarchy } = this.props;
|
||||
|
||||
this.state = {
|
||||
nodes: getSelectedNodes(selectedHierarchy),
|
||||
};
|
||||
this.treeDecorators = createTreeDecorators(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
@ -84,12 +81,6 @@ class Stories extends React.Component {
|
||||
}));
|
||||
}
|
||||
|
||||
onKeyDown(event, node) {
|
||||
if (event.keyCode === keyCodeEnter) {
|
||||
this.onToggle(node, !node.toggled);
|
||||
}
|
||||
}
|
||||
|
||||
fireOnKind(kind) {
|
||||
const { onSelectStory } = this.props;
|
||||
if (onSelectStory) onSelectStory(kind, null);
|
||||
@ -140,7 +131,7 @@ class Stories extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { storiesHierarchy } = this.props;
|
||||
const { storiesHierarchy, sidebarAnimations } = this.props;
|
||||
|
||||
const data = this.mapStoriesHierarchy(storiesHierarchy);
|
||||
data.toggled = true;
|
||||
@ -152,7 +143,8 @@ class Stories extends React.Component {
|
||||
style={treeStyle}
|
||||
data={data}
|
||||
onToggle={this.onToggle}
|
||||
decorators={this.treeDecorators}
|
||||
animations={sidebarAnimations ? undefined : false}
|
||||
decorators={treeDecorators}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -173,6 +165,7 @@ Stories.propTypes = {
|
||||
selectedKind: PropTypes.string.isRequired,
|
||||
selectedStory: PropTypes.string.isRequired,
|
||||
onSelectStory: PropTypes.func,
|
||||
sidebarAnimations: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default Stories;
|
||||
|
@ -1,9 +1,25 @@
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import Stories from './index';
|
||||
import { setContext } from '../../../../../compose';
|
||||
import { createHierarchy } from '../../../libs/hierarchy';
|
||||
|
||||
const leftClick = { button: 0 };
|
||||
|
||||
describe('manager.ui.components.left_panel.stories', () => {
|
||||
beforeEach(() =>
|
||||
setContext({
|
||||
clientStore: {
|
||||
getAll() {
|
||||
return { shortcutOptions: {} };
|
||||
},
|
||||
subscribe() {},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
afterEach(() => setContext(null));
|
||||
|
||||
const data = createHierarchy([
|
||||
{ kind: 'a', stories: ['a1', 'a2'] },
|
||||
{ kind: 'b', stories: ['b1', 'b2'] },
|
||||
@ -65,7 +81,7 @@ describe('manager.ui.components.left_panel.stories', () => {
|
||||
const output = wrap.html();
|
||||
|
||||
expect(output).toMatch(/some/);
|
||||
expect(output).not.toMatch(/name/);
|
||||
expect(output).not.toMatch(/>name</);
|
||||
expect(output).not.toMatch(/item1/);
|
||||
expect(output).not.toMatch(/a1/);
|
||||
expect(output).not.toMatch(/a2/);
|
||||
@ -125,8 +141,8 @@ describe('manager.ui.components.left_panel.stories', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const kind = wrap.find('a').filterWhere(el => el.text() === 'some').last();
|
||||
kind.simulate('click');
|
||||
const kind = wrap.find('[data-name="some"]');
|
||||
kind.simulate('click', leftClick);
|
||||
|
||||
const { nodes } = wrap.state();
|
||||
|
||||
@ -216,8 +232,8 @@ describe('manager.ui.components.left_panel.stories', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const kind = wrap.find('a').filterWhere(el => el.text() === 'a').last();
|
||||
kind.simulate('click');
|
||||
const kind = wrap.find('[data-name="a"]');
|
||||
kind.simulate('click', leftClick);
|
||||
|
||||
expect(onSelectStory).toHaveBeenCalledWith('a', null);
|
||||
});
|
||||
@ -234,7 +250,7 @@ describe('manager.ui.components.left_panel.stories', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const kind = wrap.find('a').filterWhere(el => el.text() === 'a').last();
|
||||
const kind = wrap.find('[data-name="a"]').filterWhere(el => el.text() === 'a').last();
|
||||
kind.simulate('click');
|
||||
|
||||
onSelectStory.mockClear();
|
||||
@ -255,8 +271,8 @@ describe('manager.ui.components.left_panel.stories', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const kind = wrap.find('a').filterWhere(el => el.text() === 'b1').last();
|
||||
kind.simulate('click');
|
||||
const kind = wrap.find('[data-name="b1"]');
|
||||
kind.simulate('click', leftClick);
|
||||
|
||||
expect(onSelectStory).toHaveBeenCalledWith('b', 'b1');
|
||||
});
|
||||
@ -273,13 +289,13 @@ describe('manager.ui.components.left_panel.stories', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
wrap.find('a').filterWhere(el => el.text() === 'another').last().simulate('click');
|
||||
wrap.find('a').filterWhere(el => el.text() === 'space').last().simulate('click');
|
||||
wrap.find('a').filterWhere(el => el.text() === '20').last().simulate('click');
|
||||
wrap.find('[data-name="another"]').simulate('click', leftClick);
|
||||
wrap.find('[data-name="space"]').simulate('click', leftClick);
|
||||
wrap.find('[data-name="20"]').simulate('click', leftClick);
|
||||
|
||||
expect(onSelectStory).toHaveBeenCalledWith('another.space.20', null);
|
||||
|
||||
wrap.find('a').filterWhere(el => el.text() === 'b2').last().simulate('click');
|
||||
wrap.find('[data-name="b2"]').simulate('click', leftClick);
|
||||
|
||||
expect(onSelectStory).toHaveBeenCalledWith('another.space.20', 'b2');
|
||||
});
|
||||
@ -296,23 +312,12 @@ describe('manager.ui.components.left_panel.stories', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
wrap
|
||||
.find('a')
|
||||
.filterWhere(el => el.text() === 'another')
|
||||
.last()
|
||||
.simulate('keyDown', { keyCode: 13 });
|
||||
wrap.find('[data-name="another"]').simulate('keyDown', { keyCode: 13 });
|
||||
|
||||
wrap
|
||||
.find('a')
|
||||
.filterWhere(el => el.text() === 'space')
|
||||
.last()
|
||||
.simulate('keyDown', { keyCode: 13 });
|
||||
wrap.find('[data-name="space"]').simulate('keyDown', { keyCode: 13 });
|
||||
|
||||
wrap
|
||||
.find('a')
|
||||
.filterWhere(el => el.text() === '20')
|
||||
.last()
|
||||
.simulate('keyDown', { keyCode: 13 });
|
||||
// enter press on native link triggers click event
|
||||
wrap.find('[data-name="20"]').simulate('click', leftClick);
|
||||
|
||||
expect(onSelectStory).toHaveBeenCalledWith('another.space.20', null);
|
||||
});
|
||||
|
@ -2,6 +2,11 @@ import { decorators } from 'react-treebeard';
|
||||
import { IoChevronRight } from 'react-icons/lib/io';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import RoutedLink from '../../../containers/routed_link';
|
||||
import MenuItem from '../../menu_item';
|
||||
import treeNodeTypes from './tree_node_type';
|
||||
|
||||
function noop() {}
|
||||
|
||||
function ToggleDecorator({ style }) {
|
||||
const { height, width, arrow } = style;
|
||||
@ -24,85 +29,92 @@ ToggleDecorator.propTypes = {
|
||||
};
|
||||
|
||||
function ContainerDecorator(props) {
|
||||
const { node } = props;
|
||||
const { node, style, onClick } = props;
|
||||
const { container, ...restStyles } = style;
|
||||
|
||||
if (node.root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <decorators.Container {...props} />;
|
||||
let containerStyle = container.reduce((acc, styles) => ({ ...acc, ...styles }), {});
|
||||
const innerContainer = <decorators.Container {...props} style={restStyles} onClick={noop} />;
|
||||
|
||||
if (node.type !== treeNodeTypes.STORY) {
|
||||
return (
|
||||
<MenuItem style={containerStyle} onClick={onClick} data-name={node.name}>
|
||||
{innerContainer}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
const overrideParams = {
|
||||
selectedKind: node.kind,
|
||||
selectedStory: node.story,
|
||||
};
|
||||
|
||||
containerStyle = {
|
||||
...style.nativeLink,
|
||||
...containerStyle,
|
||||
};
|
||||
|
||||
return (
|
||||
<RoutedLink
|
||||
overrideParams={overrideParams}
|
||||
style={containerStyle}
|
||||
onClick={onClick}
|
||||
data-name={node.name}
|
||||
>
|
||||
{innerContainer}
|
||||
</RoutedLink>
|
||||
);
|
||||
}
|
||||
|
||||
ContainerDecorator.propTypes = {
|
||||
style: PropTypes.shape({
|
||||
container: PropTypes.array.isRequired,
|
||||
}).isRequired,
|
||||
node: PropTypes.shape({
|
||||
root: PropTypes.bool,
|
||||
type: PropTypes.oneOf([treeNodeTypes.NAMESPACE, treeNodeTypes.COMPONENT, treeNodeTypes.STORY])
|
||||
.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
kind: PropTypes.string,
|
||||
story: PropTypes.string,
|
||||
}).isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function HeaderDecorator(props) {
|
||||
const { style, node } = props;
|
||||
|
||||
let newStyle = style;
|
||||
|
||||
if (node.type === treeNodeTypes.STORY) {
|
||||
newStyle = {
|
||||
...style,
|
||||
title: {
|
||||
...style.title,
|
||||
...style.storyTitle,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return <decorators.Header {...props} style={newStyle} />;
|
||||
}
|
||||
|
||||
HeaderDecorator.propTypes = {
|
||||
style: PropTypes.shape({
|
||||
title: PropTypes.object.isRequired,
|
||||
base: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
node: PropTypes.shape({
|
||||
type: PropTypes.oneOf([treeNodeTypes.NAMESPACE, treeNodeTypes.COMPONENT, treeNodeTypes.STORY]),
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
function createHeaderDecoratorScope(parent) {
|
||||
class HeaderDecorator extends React.Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
const { onKeyDown } = parent;
|
||||
const { node } = this.props;
|
||||
|
||||
onKeyDown(event, node);
|
||||
}
|
||||
|
||||
// Prevent focusing on mousedown
|
||||
onMouseDown(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { style, node } = this.props;
|
||||
|
||||
const newStyleTitle = {
|
||||
...style.title,
|
||||
};
|
||||
|
||||
if (!node.children || !node.children.length) {
|
||||
newStyleTitle.fontSize = '13px';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style.base}
|
||||
role="menuitem"
|
||||
tabIndex="0"
|
||||
onKeyDown={this.onKeyDown}
|
||||
onMouseDown={this.onMouseDown}
|
||||
>
|
||||
<a style={newStyleTitle}>
|
||||
{node.name}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HeaderDecorator.propTypes = {
|
||||
style: PropTypes.shape({
|
||||
title: PropTypes.object.isRequired,
|
||||
base: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
node: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
return HeaderDecorator;
|
||||
}
|
||||
|
||||
export default function(parent) {
|
||||
return {
|
||||
...decorators,
|
||||
Header: createHeaderDecoratorScope(parent),
|
||||
Container: ContainerDecorator,
|
||||
Toggle: ToggleDecorator,
|
||||
};
|
||||
}
|
||||
export default {
|
||||
...decorators,
|
||||
Header: HeaderDecorator,
|
||||
Container: ContainerDecorator,
|
||||
Toggle: ToggleDecorator,
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ export default {
|
||||
base: {
|
||||
listStyle: 'none',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
padding: '5px',
|
||||
fontFamily: baseFonts.fontFamily,
|
||||
fontSize: '15px',
|
||||
minWidth: '200px',
|
||||
@ -20,12 +20,19 @@ export default {
|
||||
link: {
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
padding: '0px 5px',
|
||||
display: 'block',
|
||||
zIndex: 1,
|
||||
},
|
||||
activeLink: {
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#EEE',
|
||||
zIndex: 0,
|
||||
},
|
||||
nativeLink: {
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
toggle: {
|
||||
base: {
|
||||
@ -67,6 +74,9 @@ export default {
|
||||
lineHeight: '24px',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
storyTitle: {
|
||||
fontSize: '13px',
|
||||
},
|
||||
},
|
||||
subtree: {
|
||||
paddingLeft: '19px',
|
||||
|
43
lib/ui/src/modules/ui/components/menu_item.js
Normal file
43
lib/ui/src/modules/ui/components/menu_item.js
Normal file
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const keyCodeEnter = 13;
|
||||
|
||||
export default class MenuItem extends React.Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
}
|
||||
|
||||
// Prevent focusing on mousedown
|
||||
onMouseDown(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
if (e.keyCode === keyCodeEnter) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, ...restProps } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="menuitem"
|
||||
tabIndex="0"
|
||||
onKeyDown={this.onKeyDown}
|
||||
onMouseDown={this.onMouseDown}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
59
lib/ui/src/modules/ui/components/menu_item.test.js
Normal file
59
lib/ui/src/modules/ui/components/menu_item.test.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import MenuItem from './menu_item';
|
||||
|
||||
const keyCodeEnter = 13;
|
||||
|
||||
describe('manager.ui.components.menu_item', () => {
|
||||
describe('render', () => {
|
||||
test('should use "a" tag', () => {
|
||||
const wrap = shallow(<MenuItem title="title">Content</MenuItem>);
|
||||
|
||||
expect(
|
||||
wrap.matchesElement(
|
||||
<div role="menuitem" tabIndex="0" title="title">
|
||||
Content
|
||||
</div>
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
let onClick;
|
||||
let wrap;
|
||||
|
||||
beforeEach(() => {
|
||||
onClick = jest.fn();
|
||||
wrap = shallow(<MenuItem onClick={onClick} />);
|
||||
});
|
||||
|
||||
test('should call onClick on a click', () => {
|
||||
wrap.simulate('click');
|
||||
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should call onClick on enter key', () => {
|
||||
const e = { keyCode: keyCodeEnter };
|
||||
wrap.simulate('keyDown', e);
|
||||
|
||||
expect(onClick).toHaveBeenCalledWith(e);
|
||||
});
|
||||
|
||||
test("shouldn't call onClick on other keys", () => {
|
||||
wrap.simulate('keyDown', {});
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should prevent default on mousedown', () => {
|
||||
const e = {
|
||||
preventDefault: jest.fn(),
|
||||
};
|
||||
wrap.simulate('mouseDown', e);
|
||||
|
||||
expect(e.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
46
lib/ui/src/modules/ui/components/routed_link.js
Normal file
46
lib/ui/src/modules/ui/components/routed_link.js
Normal file
@ -0,0 +1,46 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
const LEFT_BUTTON = 0;
|
||||
|
||||
// Cmd/Ctrl/Shift/Alt + Click should trigger default browser behaviour. Same applies to non-left clicks
|
||||
function isPlainLeftClick(e) {
|
||||
return e.button === LEFT_BUTTON && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey;
|
||||
}
|
||||
|
||||
export default class RoutedLink extends React.Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
onClick(e) {
|
||||
if (this.props.onClick && isPlainLeftClick(e)) {
|
||||
e.preventDefault();
|
||||
this.props.onClick(e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onClick, href, children, overrideParams, ...restProps } = this.props;
|
||||
return (
|
||||
<a onClick={this.onClick} href={href} {...restProps}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RoutedLink.defaultProps = {
|
||||
onClick: null,
|
||||
href: '#',
|
||||
children: null,
|
||||
overrideParams: null,
|
||||
};
|
||||
|
||||
RoutedLink.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
href: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
overrideParams: PropTypes.shape({}),
|
||||
};
|
97
lib/ui/src/modules/ui/components/routed_link.test.js
Normal file
97
lib/ui/src/modules/ui/components/routed_link.test.js
Normal file
@ -0,0 +1,97 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import RoutedLink from './routed_link';
|
||||
|
||||
const LEFT_BUTTON = 0;
|
||||
const MIDDLE_BUTTON = 1;
|
||||
const RIGHT_BUTTON = 2;
|
||||
|
||||
describe('manager.ui.components.routed_link', () => {
|
||||
describe('render', () => {
|
||||
test('should use "a" tag', () => {
|
||||
const wrap = shallow(
|
||||
<RoutedLink href="href" title="title">
|
||||
Content
|
||||
</RoutedLink>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrap.matchesElement(
|
||||
<a href="href" title="title">
|
||||
Content
|
||||
</a>
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
let e;
|
||||
let onClick;
|
||||
let wrap;
|
||||
|
||||
beforeEach(() => {
|
||||
e = {
|
||||
button: LEFT_BUTTON,
|
||||
preventDefault: jest.fn(),
|
||||
};
|
||||
onClick = jest.fn();
|
||||
wrap = shallow(<RoutedLink onClick={onClick} />);
|
||||
});
|
||||
|
||||
test('should call onClick on a plain left click', () => {
|
||||
wrap.simulate('click', e);
|
||||
|
||||
expect(onClick).toHaveBeenCalledWith(e);
|
||||
expect(e.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shouldn't call onClick on a middle click", () => {
|
||||
e.button = MIDDLE_BUTTON;
|
||||
wrap.simulate('click', e);
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
expect(e.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shouldn't call onClick on a right click", () => {
|
||||
e.button = RIGHT_BUTTON;
|
||||
wrap.simulate('click', e);
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
expect(e.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shouldn't call onClick on alt+click", () => {
|
||||
e.altKey = true;
|
||||
wrap.simulate('click', e);
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
expect(e.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shouldn't call onClick on ctrl+click", () => {
|
||||
e.ctrlKey = true;
|
||||
wrap.simulate('click', e);
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
expect(e.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shouldn't call onClick on cmd+click / win+click", () => {
|
||||
e.metaKey = true;
|
||||
wrap.simulate('click', e);
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
expect(e.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("shouldn't call onClick on shift+click", () => {
|
||||
e.shiftKey = true;
|
||||
wrap.simulate('click', e);
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
expect(e.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,17 +1,26 @@
|
||||
import { document } from 'global';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ReactModal from 'react-modal';
|
||||
import FuzzySearch from '@storybook/react-fuzzy';
|
||||
|
||||
import { features } from '../../../libs/key_events';
|
||||
import { baseFonts } from './theme';
|
||||
|
||||
const searchBoxStyle = {
|
||||
position: 'absolute',
|
||||
backgroundColor: '#FFF',
|
||||
top: '100px',
|
||||
left: '50%',
|
||||
marginLeft: '-215px',
|
||||
...baseFonts,
|
||||
const modalStyle = {
|
||||
content: {
|
||||
top: '100px',
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
left: '50%',
|
||||
marginLeft: '-215px',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
overflow: 'visible',
|
||||
...baseFonts,
|
||||
},
|
||||
overlay: {
|
||||
background: 'transparent',
|
||||
},
|
||||
};
|
||||
|
||||
const formatStories = stories => {
|
||||
@ -61,11 +70,19 @@ export default class SearchBox extends React.Component {
|
||||
this.fireOnKind = this.fireOnKind.bind(this);
|
||||
}
|
||||
|
||||
// TODO: Remove this if and when https://github.com/reactjs/react-modal/issues/464 resolves
|
||||
componentDidUpdate(prevProps) {
|
||||
// remove current focus on opening to prevent firing 'enter' keyDowns on it when modal closes
|
||||
if (this.props.showSearchBox && !prevProps.showSearchBox && document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
onSelect(selected) {
|
||||
const { handleEvent } = this.props;
|
||||
const { onClose } = this.props;
|
||||
if (selected.type === 'story') this.fireOnStory(selected.value, selected.kind);
|
||||
else this.fireOnKind(selected.value);
|
||||
handleEvent(features.SEARCH);
|
||||
onClose();
|
||||
}
|
||||
|
||||
fireOnKind(kind) {
|
||||
@ -80,16 +97,20 @@ export default class SearchBox extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={searchBoxStyle}>
|
||||
{this.props.showSearchBox &&
|
||||
<FuzzySearch
|
||||
list={formatStories(this.props.stories)}
|
||||
onSelect={this.onSelect}
|
||||
keys={['value', 'type']}
|
||||
resultsTemplate={suggestionTemplate}
|
||||
autoFocus
|
||||
/>}
|
||||
</div>
|
||||
<ReactModal
|
||||
isOpen={this.props.showSearchBox}
|
||||
onRequestClose={this.props.onClose}
|
||||
style={modalStyle}
|
||||
contentLabel="Search"
|
||||
>
|
||||
<FuzzySearch
|
||||
list={formatStories(this.props.stories)}
|
||||
onSelect={this.onSelect}
|
||||
keys={['value', 'type']}
|
||||
resultsTemplate={suggestionTemplate}
|
||||
autoFocus
|
||||
/>
|
||||
</ReactModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -100,5 +121,5 @@ SearchBox.propTypes = {
|
||||
showSearchBox: PropTypes.bool.isRequired,
|
||||
stories: PropTypes.arrayOf(PropTypes.object),
|
||||
onSelectStory: PropTypes.func.isRequired,
|
||||
handleEvent: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
99
lib/ui/src/modules/ui/components/search_box.test.js
Normal file
99
lib/ui/src/modules/ui/components/search_box.test.js
Normal file
@ -0,0 +1,99 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import ReactModal from 'react-modal';
|
||||
import FuzzySearch from '@storybook/react-fuzzy';
|
||||
|
||||
import SearchBox from './search_box';
|
||||
|
||||
describe('manager.ui.components.search_box', () => {
|
||||
describe('render', () => {
|
||||
test('should render FuzzySearch inside ReactModal', () => {
|
||||
const wrap = shallow(<SearchBox showSearchBox />);
|
||||
|
||||
const modal = wrap.find(ReactModal);
|
||||
expect(modal).toBePresent();
|
||||
expect(modal).toHaveProp('isOpen', true);
|
||||
expect(modal).toHaveProp('contentLabel', 'Search');
|
||||
|
||||
const search = modal.find(FuzzySearch);
|
||||
expect(search).toBePresent();
|
||||
expect(search).toHaveProp('keys', ['value', 'type']);
|
||||
expect(search).toHaveProp('autoFocus', true);
|
||||
});
|
||||
|
||||
test('should format stories', () => {
|
||||
const stories = [
|
||||
{
|
||||
kind: 'a',
|
||||
stories: ['b', 'c'],
|
||||
},
|
||||
];
|
||||
const wrap = shallow(<SearchBox stories={stories} />);
|
||||
const search = wrap.find(FuzzySearch);
|
||||
|
||||
const expectedList = [
|
||||
{
|
||||
type: 'kind',
|
||||
value: 'a',
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
type: 'story',
|
||||
value: 'b',
|
||||
id: 2,
|
||||
kind: 'a',
|
||||
},
|
||||
{
|
||||
type: 'story',
|
||||
value: 'c',
|
||||
id: 3,
|
||||
kind: 'a',
|
||||
},
|
||||
];
|
||||
expect(search).toHaveProp('list', expectedList);
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
test('should call the onClose prop when modal requests it', () => {
|
||||
const onClose = jest.fn();
|
||||
const wrap = shallow(<SearchBox onClose={onClose} />);
|
||||
|
||||
const modal = wrap.find(ReactModal);
|
||||
modal.simulate('requestClose');
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle selecting a kind', () => {
|
||||
const onSelectStory = jest.fn();
|
||||
const onClose = jest.fn();
|
||||
const wrap = shallow(<SearchBox onSelectStory={onSelectStory} onClose={onClose} />);
|
||||
|
||||
const modal = wrap.find(FuzzySearch);
|
||||
modal.simulate('select', {
|
||||
type: 'kind',
|
||||
value: 'a',
|
||||
});
|
||||
|
||||
expect(onSelectStory).toHaveBeenCalledWith('a', null);
|
||||
expect(onClose).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('should handle selecting a story', () => {
|
||||
const onSelectStory = jest.fn();
|
||||
const onClose = jest.fn();
|
||||
const wrap = shallow(<SearchBox onSelectStory={onSelectStory} onClose={onClose} />);
|
||||
|
||||
const modal = wrap.find(FuzzySearch);
|
||||
modal.simulate('select', {
|
||||
type: 'story',
|
||||
value: 'a',
|
||||
kind: 'b',
|
||||
});
|
||||
|
||||
expect(onSelectStory).toHaveBeenCalledWith('b', 'a');
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@ -38,7 +38,7 @@ export function getShortcuts(platform) {
|
||||
// if it is mac platform
|
||||
if (platform && platform.indexOf('mac') !== -1) {
|
||||
return [
|
||||
{ name: 'Toggle Search Box', keys: ['⌘ ⇧ P', '⌃ ⇧ P'] },
|
||||
{ name: 'Show Search Box', keys: ['⌘ ⇧ P', '⌃ ⇧ P'] },
|
||||
{ name: 'Toggle Action Logger position', keys: ['⌘ ⇧ J', '⌃ ⇧ J'] },
|
||||
{ name: 'Toggle Fullscreen Mode', keys: ['⌘ ⇧ F', '⌃ ⇧ F'] },
|
||||
{ name: 'Toggle Left Panel', keys: ['⌘ ⇧ L', '⌃ ⇧ L'] },
|
||||
@ -49,7 +49,7 @@ export function getShortcuts(platform) {
|
||||
}
|
||||
|
||||
return [
|
||||
{ name: 'Toggle Search Box', keys: ['Ctrl + Shift + P'] },
|
||||
{ name: 'Show Search Box', keys: ['Ctrl + Shift + P'] },
|
||||
{ name: 'Toggle Action Logger position', keys: ['Ctrl + Shift + J'] },
|
||||
{ name: 'Toggle Fullscreen Mode', keys: ['Ctrl + Shift + F'] },
|
||||
{ name: 'Toggle Left Panel', keys: ['Ctrl + Shift + L'] },
|
||||
|
@ -5,13 +5,7 @@ export const config = {
|
||||
insidePopState: false,
|
||||
};
|
||||
|
||||
export function changeUrl(clientStore) {
|
||||
// Do not change the URL if we are inside a popState event.
|
||||
if (config.insidePopState) return;
|
||||
|
||||
const data = clientStore.getAll();
|
||||
if (!data.selectedKind) return;
|
||||
|
||||
export function getUrlState(data) {
|
||||
const { selectedKind, selectedStory, customQueryParams } = data;
|
||||
|
||||
const {
|
||||
@ -36,7 +30,7 @@ export function changeUrl(clientStore) {
|
||||
|
||||
const url = `?${qs.stringify(urlObj)}`;
|
||||
|
||||
const state = {
|
||||
return {
|
||||
...urlObj,
|
||||
full,
|
||||
down,
|
||||
@ -44,8 +38,17 @@ export function changeUrl(clientStore) {
|
||||
panelRight,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
window.history.pushState(state, '', url);
|
||||
export function changeUrl(clientStore) {
|
||||
// Do not change the URL if we are inside a popState event.
|
||||
if (config.insidePopState) return;
|
||||
|
||||
const data = clientStore.getAll();
|
||||
if (!data.selectedKind) return;
|
||||
|
||||
const state = getUrlState(data);
|
||||
window.history.pushState(state, '', state.url);
|
||||
}
|
||||
|
||||
export function updateStore(queryParams, actions) {
|
||||
|
@ -6,8 +6,9 @@ import { createHierarchy, resolveStoryHierarchy } from '../libs/hierarchy';
|
||||
|
||||
export const mapper = (state, props, { actions }) => {
|
||||
const actionMap = actions();
|
||||
|
||||
const { stories, selectedKind, selectedStory, uiOptions, storyFilter } = state;
|
||||
const { name, url, sortStoriesByKind, hierarchySeparator } = uiOptions;
|
||||
const { name, url, sortStoriesByKind, hierarchySeparator, sidebarAnimations } = uiOptions;
|
||||
const filteredStories = filters.storyFilter(
|
||||
stories,
|
||||
storyFilter,
|
||||
@ -29,6 +30,7 @@ export const mapper = (state, props, { actions }) => {
|
||||
onStoryFilter: actionMap.ui.setStoryFilter,
|
||||
|
||||
openShortcutsHelp: actionMap.ui.toggleShortcutsHelp,
|
||||
sidebarAnimations,
|
||||
name,
|
||||
url,
|
||||
};
|
||||
|
14
lib/ui/src/modules/ui/containers/routed_link.js
Normal file
14
lib/ui/src/modules/ui/containers/routed_link.js
Normal file
@ -0,0 +1,14 @@
|
||||
import RoutedLink from '../components/routed_link';
|
||||
import genPoddaLoader from '../libs/gen_podda_loader';
|
||||
import { getUrlState } from '../configs/handle_routing';
|
||||
import compose from '../../../compose';
|
||||
|
||||
export function mapper(state, props) {
|
||||
const { url } = getUrlState({ ...state, ...props.overrideParams });
|
||||
|
||||
return {
|
||||
href: url,
|
||||
};
|
||||
}
|
||||
|
||||
export default compose(genPoddaLoader(mapper))(RoutedLink);
|
21
lib/ui/src/modules/ui/containers/routed_link.test.js
Normal file
21
lib/ui/src/modules/ui/containers/routed_link.test.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { mapper } from './routed_link';
|
||||
|
||||
describe('manager.ui.containers.routed_link', () => {
|
||||
describe('mapper', () => {
|
||||
test('should give correct data', () => {
|
||||
const state = {
|
||||
shortcutOptions: {},
|
||||
};
|
||||
const props = {
|
||||
overrideParams: {
|
||||
selectedKind: 'kind',
|
||||
selectedStory: 'story',
|
||||
},
|
||||
};
|
||||
const { href } = mapper(state, props);
|
||||
|
||||
expect(href).toContain('selectedKind=kind');
|
||||
expect(href).toContain('selectedStory=story');
|
||||
});
|
||||
});
|
||||
});
|
@ -8,7 +8,11 @@ export const mapper = (state, props, { actions }) => {
|
||||
showSearchBox: state.shortcutOptions.showSearchBox,
|
||||
stories: state.stories,
|
||||
onSelectStory: actionMap.api.selectStory,
|
||||
handleEvent: actionMap.shortcuts.handleEvent,
|
||||
onClose() {
|
||||
actionMap.shortcuts.setOptions({
|
||||
showSearchBox: false,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
43
lib/ui/src/modules/ui/containers/search_box.test.js
Normal file
43
lib/ui/src/modules/ui/containers/search_box.test.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { mapper } from './search_box';
|
||||
|
||||
describe('manager.ui.containers.search_box', () => {
|
||||
describe('mapper', () => {
|
||||
test('should give correct data', () => {
|
||||
const stories = [{ kind: 'sk', stories: ['dd'] }];
|
||||
const state = {
|
||||
shortcutOptions: {
|
||||
showSearchBox: true,
|
||||
},
|
||||
stories,
|
||||
};
|
||||
|
||||
const selectStory = () => 'selectStory';
|
||||
const setOptions = jest.fn();
|
||||
const props = {};
|
||||
const env = {
|
||||
actions: () => ({
|
||||
api: {
|
||||
selectStory,
|
||||
},
|
||||
shortcuts: {
|
||||
setOptions,
|
||||
},
|
||||
}),
|
||||
};
|
||||
const data = mapper(state, props, env);
|
||||
|
||||
const expectedData = {
|
||||
showSearchBox: true,
|
||||
stories,
|
||||
onSelectStory: selectStory,
|
||||
};
|
||||
|
||||
expect(data).toMatchObject(expectedData);
|
||||
|
||||
data.onClose();
|
||||
expect(setOptions).toHaveBeenCalledWith({
|
||||
showSearchBox: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user