mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-20 05:02:37 +08:00
Merge pull request #1548 from storybooks/modal-search-box
Use ReactModal for search box
This commit is contained in:
commit
f36dac32f8
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -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'] },
|
||||
|
@ -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