diff --git a/src/components/Search/Search-story.js b/src/components/Search/Search-story.js index bf604b6caa79..4b3e16041b53 100644 --- a/src/components/Search/Search-story.js +++ b/src/components/Search/Search-story.js @@ -4,6 +4,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import Search from '../Search'; +import SearchFilterButton from '../SearchFilterButton'; const searchProps = { className: 'some-class', @@ -87,4 +88,24 @@ storiesOf('Search', module) return ; } + ) + .addWithInfo( + 'Custom set of buttons', + ` + You can control what set of buttons you want. + `, + () => ( + { + console.log('onChange'); + action('onChange'); + }}> + + + ) ); diff --git a/src/components/Search/Search-test.js b/src/components/Search/Search-test.js index d12168704ad4..09e18af0722d 100644 --- a/src/components/Search/Search-test.js +++ b/src/components/Search/Search-test.js @@ -120,12 +120,6 @@ describe('Search', () => { const icon = wrapper.find(Icon).at(3); expect(icon.props().name).toEqual('list'); }); - - it('should use "grid" icon when format state is not "list"', () => { - wrapper.setState({ format: 'not-list' }); - const icon = wrapper.find(Icon).at(3); - expect(icon.props().name).toEqual('grid'); - }); }); }); diff --git a/src/components/Search/Search.js b/src/components/Search/Search.js index 2f9a1b66df77..674d69c08ab2 100644 --- a/src/components/Search/Search.js +++ b/src/components/Search/Search.js @@ -2,6 +2,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import classNames from 'classnames'; import Icon from '../Icon'; +import SearchFilterButton from '../SearchFilterButton'; +import SearchLayoutButton from '../SearchLayoutButton'; export default class Search extends Component { static propTypes = { @@ -24,7 +26,6 @@ export default class Search extends Component { }; state = { - format: 'list', hasContent: this.props.value || this.props.defaultValue || false, }; @@ -44,18 +45,6 @@ export default class Search extends Component { this.setState({ hasContent: false }, () => this.input.focus()); }; - toggleLayout = () => { - if (this.state.format === 'list') { - this.setState({ - format: 'grid', - }); - } else { - this.setState({ - format: 'list', - }); - } - }; - handleChange = evt => { this.setState({ hasContent: evt.target.value !== '', @@ -64,55 +53,6 @@ export default class Search extends Component { this.props.onChange(evt); }; - // eslint-disable-next-line consistent-return - searchFilterBtn = () => { - if (!this.props.small) { - return ( - - ); - } - }; - - // eslint-disable-next-line consistent-return - searchLayoutBtn = () => { - if (!this.props.small) { - return ( - - ); - } - }; - render() { const { className, @@ -124,7 +64,10 @@ export default class Search extends Component { .substr(2)}`), placeHolderText, labelText, + searchButtonLabelText, + layoutButtonLabelText, small, + children, ...other } = this.props; @@ -142,6 +85,8 @@ export default class Search extends Component { 'bx--search-close--hidden': !hasContent, }); + const renderButtons = !children && !small; + return (
- {this.searchFilterBtn()} - {this.searchLayoutBtn()} + {children} + {renderButtons && ( + + )} + {renderButtons && ( + + )}
); } diff --git a/src/components/SearchFilterButton/SearchFilterButton-test.js b/src/components/SearchFilterButton/SearchFilterButton-test.js new file mode 100644 index 000000000000..dd672914ee6f --- /dev/null +++ b/src/components/SearchFilterButton/SearchFilterButton-test.js @@ -0,0 +1,28 @@ +import React from 'react'; +import Icon from '../Icon'; +import SearchFilterButton from '../SearchFilterButton'; +import { mount } from 'enzyme'; + +describe('SearchFilterButton', () => { + const wrapper = mount(); + + describe('buttons', () => { + const btn = wrapper.find('button'); + + it('should have type="button"', () => { + const type = btn.instance().getAttribute('type'); + expect(type).toEqual('button'); + }); + + it('has expected class', () => { + expect(btn.hasClass('bx--search-button')).toEqual(true); + }); + }); + + describe('icons', () => { + it('should use "filter--glyph" icon', () => { + const icon = wrapper.find(Icon); + expect(icon.props().name).toEqual('filter--glyph'); + }); + }); +}); diff --git a/src/components/SearchFilterButton/SearchFilterButton.js b/src/components/SearchFilterButton/SearchFilterButton.js new file mode 100644 index 000000000000..25c19412d860 --- /dev/null +++ b/src/components/SearchFilterButton/SearchFilterButton.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Icon from '../Icon'; + +/** + * The filter button for ``. + */ +const SearchFilterButton = ({ labelText }) => ( + +); + +SearchFilterButton.propTypes = { + /** + * The a11y label text. + */ + labelText: PropTypes.string, +}; + +SearchFilterButton.defaultProps = { + labelText: 'Search', +}; + +export default SearchFilterButton; diff --git a/src/components/SearchFilterButton/index.js b/src/components/SearchFilterButton/index.js new file mode 100644 index 000000000000..e09592fdebcd --- /dev/null +++ b/src/components/SearchFilterButton/index.js @@ -0,0 +1 @@ +export default from './SearchFilterButton'; diff --git a/src/components/SearchLayoutButton/SearchLayoutButton-test.js b/src/components/SearchLayoutButton/SearchLayoutButton-test.js new file mode 100644 index 000000000000..00fb11926759 --- /dev/null +++ b/src/components/SearchLayoutButton/SearchLayoutButton-test.js @@ -0,0 +1,56 @@ +import React from 'react'; +import Icon from '../Icon'; +import SearchLayoutButton from '../SearchLayoutButton'; +import { mount } from 'enzyme'; + +describe('SearchLayoutButton', () => { + const wrapper = mount(); + + describe('buttons', () => { + const btn = wrapper.find('button'); + + it('should have type="button"', () => { + const type = btn.instance().getAttribute('type'); + expect(type).toEqual('button'); + }); + + it('has expected class for sort button', () => { + expect(btn.hasClass('bx--search-button')).toEqual(true); + }); + }); + + describe('icons', () => { + it('should use "list" icon for toggle button', () => { + const icon = wrapper.find(Icon); + expect(icon.props().name).toEqual('list'); + }); + + it('should use "grid" icon when format state is not "list"', () => { + wrapper.setState({ format: 'not-list' }); + const icon = wrapper.find(Icon); + expect(icon.props().name).toEqual('grid'); + }); + + it('should support specifying the layout via props', () => { + const wrapperWithFormatProps = mount( + + ); + expect(wrapperWithFormatProps.find(Icon).props().name).toEqual('grid'); + wrapperWithFormatProps.setProps({ format: 'list' }); + expect(wrapperWithFormatProps.find(Icon).props().name).toEqual('list'); + }); + + it('should support being notified of change in layout', () => { + const onChangeFormat = jest.fn(); + const wrapperWithFormatProps = mount( + + ); + wrapperWithFormatProps.find('button').simulate('click'); + wrapperWithFormatProps.find('button').simulate('click'); + expect(onChangeFormat.mock.calls).toEqual([ + [{ format: 'list' }], + [{ format: 'grid' }], + ]); + }); + }); +}); diff --git a/src/components/SearchLayoutButton/SearchLayoutButton.js b/src/components/SearchLayoutButton/SearchLayoutButton.js new file mode 100644 index 000000000000..73e3945b8cb3 --- /dev/null +++ b/src/components/SearchLayoutButton/SearchLayoutButton.js @@ -0,0 +1,84 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Icon from '../Icon'; + +/** + * The layout button for ``. + */ +class SearchLayoutButton extends Component { + static propTypes = { + /** + * The layout. + */ + format: PropTypes.oneOf(['list', 'grid']), + + /** + * The a11y label text. + */ + labelText: PropTypes.string, + + /** + * The callback called when layout switches. + */ + onChangeFormat: PropTypes.func, + }; + + static defaultProps = { + labelText: 'Filter', + }; + + state = { + /** + * The current layout. + * @type {string} + */ + format: this.props.format || 'list', + }; + + componentWillReceiveProps({ format }) { + const { format: prevFormat } = this.props; + if (prevFormat !== format) { + this.setState({ format: format || 'list' }); + } + } + + /** + * Toggles the button state upon user-initiated event. + */ + toggleLayout = () => { + const format = this.state.format === 'list' ? 'grid' : 'list'; + this.setState({ format }, () => { + const { onChangeFormat } = this.props; + if (typeof onChangeFormat === 'function') { + onChangeFormat({ format }); + } + }); + }; + + render() { + const { labelText } = this.props; + return ( + + ); + } +} + +export default SearchLayoutButton; diff --git a/src/components/SearchLayoutButton/index.js b/src/components/SearchLayoutButton/index.js new file mode 100644 index 000000000000..3f2269357422 --- /dev/null +++ b/src/components/SearchLayoutButton/index.js @@ -0,0 +1 @@ +export default from './SearchLayoutButton'; diff --git a/src/index.js b/src/index.js index db16ab726933..2ca06eac948e 100644 --- a/src/index.js +++ b/src/index.js @@ -76,6 +76,8 @@ export { export RadioButton from './components/RadioButton'; export RadioButtonGroup from './components/RadioButtonGroup'; export Search from './components/Search'; +export SearchFilterButton from './components/SearchFilterButton'; +export SearchLayoutButton from './components/SearchLayoutButton'; export SecondaryButton from './components/SecondaryButton'; export Select from './components/Select'; export SelectItem from './components/SelectItem';