Merge pull request #1548 from storybooks/modal-search-box

Use ReactModal for search box
This commit is contained in:
Norbert de Langen 2017-08-05 10:04:55 +02:00 committed by GitHub
commit f36dac32f8
7 changed files with 195 additions and 28 deletions

View File

@ -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;

View File

@ -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:

View File

@ -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,
};

View 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();
});
});
});

View File

@ -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'] },

View File

@ -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,
});
},
};
};

View 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,
});
});
});
});