Skip to content

Commit

Permalink
feat(searchBox): add event handling
Browse files Browse the repository at this point in the history
the SearchBox widget has three new props:
- onSubmit (magnifying glass icon)
- onReset (clear icon)
- on*, event forwarded to the actual input, to listen for keystrokes

fixes #2017
  • Loading branch information
Maxime Janton authored and vvo committed Mar 13, 2017
1 parent 96efa65 commit e267ab6
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 2 deletions.
22 changes: 22 additions & 0 deletions packages/react-instantsearch/src/components/SearchBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class SearchBox extends Component {

searchAsYouType: PropTypes.bool,
onSubmit: PropTypes.func,
onReset: PropTypes.func,
onChange: PropTypes.func,

// For testing purposes
__inputRef: PropTypes.func,
Expand Down Expand Up @@ -134,11 +136,19 @@ class SearchBox extends Component {

onChange = e => {
this.setQuery(e.target.value);

if (this.props.onChange) {
this.props.onChange(e);
}
};

onReset = () => {
this.setQuery('');
this.input.focus();

if (this.props.onReset) {
this.props.onReset();
}
};

render() {
Expand All @@ -160,6 +170,17 @@ class SearchBox extends Component {
<use xlinkHref="#sbx-icon-clear-3" />
</svg>;

const searchInputEvents = Object.keys(this.props).reduce((props, prop) => {
if (
['onsubmit', 'onreset', 'onchange'].indexOf(prop.toLowerCase()) === -1 &&
prop.indexOf('on') === 0
) {
return {...props, [prop]: this.props[prop]};
}

return props;
}, {});

/* eslint-disable max-len */
return (
<form
Expand Down Expand Up @@ -194,6 +215,7 @@ class SearchBox extends Component {
required
value={query}
onChange={this.onChange}
{...searchInputEvents}
{...cx('input')}
/>
<button type="submit" title={translate('submitTitle')} {...cx('submit')}>
Expand Down
30 changes: 30 additions & 0 deletions packages/react-instantsearch/src/components/SearchBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,34 @@ describe('SearchBox', () => {
document.dispatchEvent(event3);
expect(input.focus.mock.calls.length).toBe(2);
});

it('should accept `onXXX` events', () => {
const onSubmit = jest.fn();
const onReset = jest.fn();

const inputEventsList = ['onChange', 'onFocus', 'onBlur', 'onSelect', 'onKeyDown', 'onKeyPress'];
const inputProps = inputEventsList.reduce((props, prop) => ({...props, [prop]: jest.fn()}), {});

const wrapper = mount(
<SearchBox
refine={ () => null }
onSubmit={ onSubmit }
onReset={ onReset }
{ ...inputProps }
/>
);

// simulate form events `onReset` && `onSubmit`
wrapper.find('form').simulate('submit');
expect(onSubmit).toBeCalled();

wrapper.find('form').simulate('reset');
expect(onReset).toBeCalled();

// simulate input search events
inputEventsList.forEach(eventName => {
wrapper.find('input').simulate(eventName.replace(/^on/, '').toLowerCase());
expect(inputProps[eventName]).toBeCalled();
});
});
});
6 changes: 6 additions & 0 deletions packages/react-instantsearch/src/widgets/SearchBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import SearchBoxComponent from '../components/SearchBox.js';
* @propType {string[]} [focusShortcuts=['s','/']] - List of keyboard shortcuts that focus the search box. Accepts key names and key codes.
* @propType {boolean} [autoFocus=false] - Should the search box be focused on render?
* @propType {boolean} [searchAsYouType=true] - Should we search on every change to the query? If you disable this option, new searches will only be triggered by clicking the search button or by pressing the enter key while the search box is focused.
* @propType {function} [onSubmit] - Intercept submit event sent from the SearchBox form container.
* @propType {function} [onReset] - Listen to `reset` event sent from the SearchBox form container.
* @propType {function} [on*] - Listen to any events sent form the search input itself.
* @propType {React.Element} [submitComponent] - Change the apparence of the default submit button (magnifying glass).
* @propType {React.Element} [resetComponent] - Change the apparence of the default reset button (cross).
* @propType {string} [defaultRefinement] - Provide default refinement value when component is mounted.
* @themeKey ais-SearchBox__root - the root of the component
* @themeKey ais-SearchBox__wrapper - the search box wrapper
* @themeKey ais-SearchBox__input - the search box input
Expand Down
80 changes: 78 additions & 2 deletions stories/SearchBox.stories.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import {storiesOf} from '@kadira/storybook';
import React, {Component} from 'react';
import {storiesOf, action} from '@kadira/storybook';
import {SearchBox} from '../packages/react-instantsearch/dom';
import {withKnobs, object} from '@kadira/storybook-addon-knobs';
import {WrapWithHits} from './util';
Expand Down Expand Up @@ -43,3 +43,79 @@ stories.add('default', () =>
/>
</WrapWithHits>
);

// with event listeners
// --------------------
class SearchBoxContainer extends Component {

state = {selectedEvents: {onChange: true}}

get supportedEvents() {
return [
'onChange', 'onFocus', 'onBlur',
'onSelect', 'onKeyDown', 'onKeyPress',
'onSubmit', 'onReset',
];
}

handleSelectedEvent = eventName => ({target: {checked}}) => {
const {selectedEvents} = this.state;
this.setState({selectedEvents: {...selectedEvents, [eventName]: checked}});
}

handleSubmit = event => {
// we dont want the page to reload after the submit event
event.preventDefault();
event.stopPropagation();

this.logAction('onSubmit')(event);
}

logAction = eventName => event => {
// we dont want to log unselected event
if (this.state.selectedEvents[eventName]) {
action(eventName)(event);
}
}

render() {
return (
<WrapWithHits searchBox={ false } hasPlayground={ true } linkedStoryGroup="searchBox">
<div style={ {color: '#999', borderBottom: '1px solid #E4E4E4', marginBottom: 10} }>
{/* events checkboxes */}
{ this.supportedEvents.map(eventName =>
<label key={ eventName } style={ {marginRight: 10} }>
<input
name={ `selectEvent-${eventName}` }
type="checkbox"
checked={ this.state.selectedEvents[eventName] }
onChange={ this.handleSelectedEvent(eventName) }
/>
{ eventName }
</label>
) }

<div style={ {marginBottom: 5, marginTop: 5, fontSize: 12} }>
<em>
(Click on the "action logger" tab of the right sidebar to see events logs)
</em>
</div>
</div>

<SearchBox
onSubmit={ this.handleSubmit }
onReset={ this.logAction('onReset') }
onChange={ this.logAction('onChange') }
onFocus={ this.logAction('onFocus') }
onBlur={ this.logAction('onBlur') }
onSelect={ this.logAction('onSelect') }
onKeyDown={ this.logAction('onKeyDown') }
onKeyPress={ this.logAction('onKeyPress') }
/>
</WrapWithHits>
);
}

}

stories.add('with event listeners', () => <SearchBoxContainer />);

0 comments on commit e267ab6

Please sign in to comment.