From 52598014a1d288895e42273460099008a080b6d4 Mon Sep 17 00:00:00 2001 From: Haofu Zhu Date: Tue, 21 May 2019 10:58:50 +1000 Subject: [PATCH] feat: new search component 1. Added new Search component 2. Removed the old Search and SearchBar component 3. Changed props of TreePicker component 4. Updated related docs --- docs/components/Layout/index.jsx | 6 +- docs/components/MigrationNote/index.jsx | 39 +++ docs/components/SearchBar/index.jsx | 34 +- docs/examples/SearchBarExample.jsx | 94 ------ docs/examples/SearchExample.jsx | 179 +++++------ docs/examples/TreePickerExample.jsx | 12 +- src/components/adslot-ui/Search/index.jsx | 147 +++++---- .../adslot-ui/Search/index.spec.jsx | 293 ++++++++++-------- src/components/adslot-ui/Search/styles.scss | 105 +++++-- src/components/adslot-ui/SearchBar/index.jsx | 62 ---- .../adslot-ui/SearchBar/index.spec.jsx | 74 ----- .../adslot-ui/SearchBar/styles.scss | 27 -- .../adslot-ui/TreePicker/Nav/index.jsx | 57 ++-- .../adslot-ui/TreePicker/Nav/index.spec.jsx | 16 + src/components/adslot-ui/TreePicker/index.jsx | 12 +- .../adslot-ui/TreePicker/index.spec.jsx | 5 +- src/components/adslot-ui/index.js | 2 - src/index.js | 2 - src/styles/icons/search/cancel-gray.svg | 1 + src/styles/icons/search/cancel.svg | 1 + src/styles/icons/search/search-gray.svg | 3 + src/styles/icons/search/search-primary.svg | 3 + 22 files changed, 529 insertions(+), 645 deletions(-) delete mode 100644 docs/examples/SearchBarExample.jsx delete mode 100644 src/components/adslot-ui/SearchBar/index.jsx delete mode 100644 src/components/adslot-ui/SearchBar/index.spec.jsx delete mode 100644 src/components/adslot-ui/SearchBar/styles.scss create mode 100644 src/styles/icons/search/cancel-gray.svg create mode 100644 src/styles/icons/search/cancel.svg create mode 100644 src/styles/icons/search/search-gray.svg create mode 100644 src/styles/icons/search/search-primary.svg diff --git a/docs/components/Layout/index.jsx b/docs/components/Layout/index.jsx index 9577d60dc..525eafdf2 100644 --- a/docs/components/Layout/index.jsx +++ b/docs/components/Layout/index.jsx @@ -52,8 +52,6 @@ import HelpIconPopoverExample from '../../examples/HelpIconPopoverExample'; import ListPickerExample from '../../examples/ListPickerExample'; import PagedGridExample from '../../examples/PagedGridExample'; import PanelExample from '../../examples/PanelExample'; -import SearchExample from '../../examples/SearchExample'; -import SearchBarExample from '../../examples/SearchBarExample'; import TreePickerExample from '../../examples/TreePickerExample'; import UserListPickerExample from '../../examples/UserListPickerExample'; import InformationBoxExample from '../../examples/InformationBoxExample'; @@ -61,6 +59,7 @@ import SplitPaneExample from '../../examples/SplitPaneExample'; import HoverDropdownMenuExample from '../../examples/HoverDropdownMenuExample'; import NavigationExample from '../../examples/NavigationExample'; import OverlayLoaderExample from '../../examples/OverlayLoaderExample'; +import SearchExample from '../../examples/SearchExample'; import './styles.scss'; import '../../examples/styles.scss'; @@ -97,7 +96,7 @@ const componentsBySection = { 'feedback-and-states': ['alert', 'empty', 'spinner', 'overlay-loader', 'pretty-diff', 'status-pill'], dialogue: ['popover', 'help-icon-popover', 'avatar'], modals: ['confirm-modal'], - search: ['search', 'search-bar', 'tag'], + search: ['search', 'tag'], grouping: [ 'page-title', 'card', @@ -228,7 +227,6 @@ class PageLayout extends React.Component { - diff --git a/docs/components/MigrationNote/index.jsx b/docs/components/MigrationNote/index.jsx index 2632c7f50..be3d3d376 100644 --- a/docs/components/MigrationNote/index.jsx +++ b/docs/components/MigrationNote/index.jsx @@ -30,6 +30,45 @@ class MigrationNote extends React.Component {

Migration Guide


+

Search Component

+

+ The new {``} component will replace the old {``} amd {``} component. +

+

+ 1. onSearch function is the only required prop. It can work as an uncontrolled component without{' '} + value and onChange. +

+

+ 2. New Search bar will by default trigger onSearch when input changes. You can set{' '} + searchOnEnter to true and onSearch will be triggered only when user press Enter or click the + search button. +

+

+ 3. icons needs to be provided as an object of nodes: +

+ + {` + { + search: React.Node, + close: React.Node, + loader: React.Node + } + `} + +

New Search component will use its default icons if none or some of the icons are not provided.

+

+ For more information check the example: Search Example +

+
+

TreePicker Component

+

+ searchOnChange prop is removed and searchOnEnterKey prop is renamed searchOnEnter. + Tree picker is search on input by default. +

+

+ For more information check the example: TreePicker Example +

+

Popover Component

The {``} component can be themed by providing one of the following values to the theme{' '} diff --git a/docs/components/SearchBar/index.jsx b/docs/components/SearchBar/index.jsx index 08837f3c9..ada92eac8 100644 --- a/docs/components/SearchBar/index.jsx +++ b/docs/components/SearchBar/index.jsx @@ -1,32 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { SearchBar } from '../../../src'; +import { Search } from '../../../src'; import './styles.scss'; -class SearchBarComponent extends React.Component { - constructor(props) { - super(props); - this.state = { - searchBarString: '', - }; - this.handleStringChange = searchBarString => { - this.setState({ searchBarString: searchBarString.trim() }); - }; - } - - render() { - return ( - this.props.onSearch(this.state.searchBarString)} - /> - ); - } -} +const SearchBarComponent = ({ onSearch }) => ( + onSearch(value.trim())} + searchOnEnter + /> +); SearchBarComponent.propTypes = { onSearch: PropTypes.func.isRequired, diff --git a/docs/examples/SearchBarExample.jsx b/docs/examples/SearchBarExample.jsx deleted file mode 100644 index e5433dabc..000000000 --- a/docs/examples/SearchBarExample.jsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import Example from '../components/Example'; -import { SearchBar } from '../../src'; - -class SearchBarExample extends React.Component { - constructor() { - super(); - this.state = { - searchBarString: '', - }; - this.setSearchBarString = this.setSearchBarString.bind(this); - this.performSearchBarSearch = this.performSearchBarSearch.bind(this); - } - - setSearchBarString(searchBarString) { - this.setState({ searchBarString }); - } - - performSearchBarSearch() { - return; - } - - render() { - return ( - - ); - } -} - -const exampleProps = { - componentName: 'SearchBar', - designNotes:

Search Bar is commonly used above the pages designed to manage filter pills and search.

, - exampleCodeSnippet: ` - `, - propTypeSectionArray: [ - { - propTypes: [ - { - propType: 'additionalClassNames', - type: 'array', - defaultValue: [], - note: 'array of strings', - }, - { - propType: 'searchString', - type: 'string', - note: 'required', - }, - { - propType: 'searchPlaceholder', - type: 'string', - }, - { - propType: 'searchIconHref', - type: 'string', - note: 'required', - }, - { - propType: 'onSearchStringChange', - type: 'func', - note: 'required', - }, - { - propType: 'onSearch', - type: 'func', - note: 'required', - }, - { - propType: 'dts', - type: 'string', - note: 'render `data-test-selector` onto the component. It can be useful for testing.', - }, - ], - }, - ], -}; - -export default () => ( - - - -); diff --git a/docs/examples/SearchExample.jsx b/docs/examples/SearchExample.jsx index 13618c271..377d09347 100644 --- a/docs/examples/SearchExample.jsx +++ b/docs/examples/SearchExample.jsx @@ -2,66 +2,105 @@ import React from 'react'; import Example from '../components/Example'; import { Search } from '../../src'; -class SearchExample extends React.Component { - constructor() { - super(); - this.state = { - value: '', - }; - this.onChange = this.onChange.bind(this); - this.onSearch = this.onSearch.bind(this); - } +class SearchExample extends React.PureComponent { + state = { + value: '', + }; - onChange(value) { + onChange = value => { this.setState({ value, }); - } + }; - onSearch() { + searchOnInputChange = () => { this.setState({ - isLoading: true, + searchOnInputChangeLoading: true, }); - setTimeout(() => this.setState({ isLoading: false }), 950); - } + setTimeout(() => this.setState({ searchOnInputChangeLoading: false }), 950); + }; + + searchOnEnterKey = value => { + this.setState({ + searchOnEnterKeyLoading: true, + }); + setTimeout( + () => this.setState({ searchOnEnterKeyLoading: false }, () => alert(`You are searching for '${value}'`)), + 950 + ); + }; render() { return ( - + <> + Search on input Changed + +
+ Search on Enter key pressed + + ); } } const exampleProps = { componentName: 'Search', - designNotes: ( -

- Search field is more commonly used within the pickers and modals with spinner indicating search action when you - begin typing. -

- ), + notes: + 'Search with search button is commonly used above the pages designed to manage filter pills and search while Search without search button is more commonly used within the pickers and modals.', exampleCodeSnippet: ` `, propTypeSectionArray: [ { + label: '', propTypes: [ + { + propType: 'className', + type: 'string', + defaultValue: '-', + }, + { + propType: 'debounceInterval', + type: 'number', + defaultValue: '0', + note: 'Milliseconds', + }, { propType: 'disabled', type: 'bool', defaultValue: 'false', - note: 'determine if the Search bar is disabled', + note: 'Determine whether the text area is disabled', + }, + { + propType: 'dts', + type: 'string', + defaultValue: '-', + note: 'Render `data-test-selector` onto the component. It can be useful for testing', + }, + { + propType: 'icons', + type: 'object', + defaultValue: '{}', + note: `{ + search: React.Node, + loader: React.Node, + close: React.Node + }`, }, { propType: 'isLoading', @@ -71,85 +110,35 @@ const exampleProps = { { propType: 'onChange', type: 'func', - note: onChange(value), + defaultValue: '-', }, { propType: 'onClear', type: 'func', - note: onClear(value), + defaultValue: '-', }, { propType: 'onSearch', type: 'func', - note: onSearch(value), + defaultValue: '-', + note: 'Required', }, { propType: 'placeholder', type: 'string', - defaultValue: '', - }, - { - propType: 'svgSymbolCancel', - type: ( - - shapeOf SVG Symbol prop types. - - ), - defaultValue: ( -
-              {JSON.stringify(
-                {
-                  classSuffixes: ['gray-darker'],
-                  href: './assets/svg-symbols.svg#cancel',
-                },
-                null,
-                2
-              )}
-            
- ), - }, - { - propType: 'svgSymbolSearch', - type: ( - - shapeOf SVG Symbol prop types. - - ), - defaultValue: ( -
-              {JSON.stringify(
-                {
-                  classSuffixes: ['gray-light'],
-                  href: '/assets/svg-symbols.svg',
-                },
-                null,
-                2
-              )}
-            
- ), - }, - { - propType: 'value', - type: 'string', - defaultValue: '', - note: 'As value within search component is uncontrolled we need to pass in the search value externally.', + defaultValue: '', }, { - propType: 'searchOnChange', - type: 'bool', - defaultValue: 'true', - note: 'determine if onSearch() will be fired upon text changes', - }, - { - propType: 'searchOnEnterKey', + propType: 'searchOnEnter', type: 'bool', defaultValue: 'false', - note: 'determine if onSearch() will be fired upon pressing Enter key', + note: + 'Determines whether onSearch() will be fired on ENTER key press (Default behaviour is to fire onSearch() when the input changes)', }, { - propType: 'debounceInterval', - type: 'number', - defaultValue: '0', + propType: 'value', + type: 'string', + defaultValue: '', }, ], }, diff --git a/docs/examples/TreePickerExample.jsx b/docs/examples/TreePickerExample.jsx index 5c3094900..d6efadd84 100644 --- a/docs/examples/TreePickerExample.jsx +++ b/docs/examples/TreePickerExample.jsx @@ -235,20 +235,16 @@ const exampleProps = { note: 'not specified', }, { - propType: 'searchOnChange', - type: 'bool', - defaultValue: true, - note: 'When true, search is triggered as soon as the user types in the search field', - }, - { - propType: 'searchOnEnterKey', + propType: 'searchOnEnter', type: 'bool', defaultValue: false, - note: 'When true, triggers search when the user presses the Enter key', + note: + 'Default behavior is to trigger onSearch as soon as the user types in the search field, if set to true, onSearch() will only be triggered when the user presses the Enter key', }, { propType: 'searchPlaceholder', type: 'string', + defaultValue: 'Search', }, { propType: 'searchValue', diff --git a/src/components/adslot-ui/Search/index.jsx b/src/components/adslot-ui/Search/index.jsx index 1f619ea6b..556118f5f 100644 --- a/src/components/adslot-ui/Search/index.jsx +++ b/src/components/adslot-ui/Search/index.jsx @@ -1,116 +1,147 @@ import _ from 'lodash'; -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { SvgSymbol, Spinner } from 'alexandria'; +import { Spinner } from 'alexandria'; +import classnames from 'classnames'; + import './styles.scss'; -class Search extends Component { +class Search extends React.Component { constructor(props) { super(props); - this.debounceOnSearch = _.debounce(props.onSearch, props.debounceInterval); + this.state = { + value: '', + }; this.onChange = this.onChange.bind(this); this.onClear = this.onClear.bind(this); this.onKeyPress = this.onKeyPress.bind(this); + this.onSearch = this.onSearch.bind(this); + this.onSearchButtonClick = this.onSearchButtonClick.bind(this); + this.debounceOnSearch = _.debounce(props.onSearch, props.debounceInterval); } onChange(event) { - const { disabled, searchOnChange, onChange } = this.props; - if (disabled) return; - - const value = _.get(event, 'target.value'); - onChange(value); - if (searchOnChange) this.debounceOnSearch(value); + const eventValue = _.get(event, 'target.value'); + const { onChange, searchOnEnter } = this.props; + if (onChange) { + onChange(eventValue); + } else { + this.setState({ value: eventValue }); + } + if (!searchOnEnter) { + this.onSearch(eventValue); + } } - onKeyPress(event) { - const { disabled, searchOnEnterKey, onSearch } = this.props; - if (disabled) return; + onClear() { + const { onChange, searchOnEnter, onClear } = this.props; + const emptyValue = ''; - if (searchOnEnterKey && event.which === 13) { - const value = _.get(event, 'target.value'); - onSearch(value); + if (onChange) { + onChange(emptyValue); + } else { + this.setState({ value: '' }); } + if (!searchOnEnter) this.onSearch(emptyValue); + if (onClear) onClear(emptyValue); } - onClear() { - const { disabled, onChange, onClear, onSearch, searchOnChange } = this.props; - if (disabled) return; + onKeyPress(event) { + const { searchOnEnter } = this.props; + if (searchOnEnter && event.key === 'Enter') { + event.preventDefault(); + this.onSearch(_.get(event, 'target.value')); + } + } - const value = ''; + onSearch(searchValue) { + const { onSearch, debounceInterval } = this.props; + const search = debounceInterval ? this.debounceOnSearch : onSearch; + search(searchValue); + } - onChange(value); - if (searchOnChange) { - onSearch(value); - } - onClear(value); + onSearchButtonClick(event) { + event.preventDefault(); + const searchValue = this.props.value || this.state.value; + this.onSearch(searchValue); } render() { - const { disabled, isLoading, placeholder, svgSymbolCancel, svgSymbolSearch, value } = this.props; - const searchClassSuffixes = disabled ? ['color-disabled'] : svgSymbolSearch.classSuffixes; - const cancelClassSuffixes = disabled ? ['color-disabled'] : svgSymbolCancel.classSuffixes; + const { className, disabled, dts, icons, isLoading, onChange, placeholder, searchOnEnter, value } = this.props; + + const inputValue = value || this.state.value; + + if (value && !onChange) + console.warn( + 'Failed prop type: You have provided a `value` prop to Search Component without an `onChange` handler. This will render a read-only field.' + ); + + const searchIcon = icons.search ? icons.search :
; + const closeIcon = icons.close ? icons.close :
; + const loaderIcon = icons.loader ? icons.loader : ; + const isValueEmpty = _.isEmpty(value) && _.isEmpty(this.state.value); return ( -
+
- {isLoading ? : null} - {_.isEmpty(value) ? ( - + {isLoading && !searchOnEnter && {loaderIcon}} + {searchOnEnter && !isValueEmpty && ( + + {closeIcon} + + )} + {searchOnEnter ? ( + ) : ( - + + {isValueEmpty ? searchIcon : closeIcon} + )}
); } } -Search.displayName = 'AdslotUiSearchComponent'; - Search.propTypes = { + className: PropTypes.string, + debounceInterval: PropTypes.number, disabled: PropTypes.bool, + dts: PropTypes.string, + icons: PropTypes.shape({ + search: PropTypes.node, + loader: PropTypes.node, + close: PropTypes.node, + }), isLoading: PropTypes.bool, onChange: PropTypes.func, onClear: PropTypes.func, - onSearch: PropTypes.func, + onSearch: PropTypes.func.isRequired, placeholder: PropTypes.string, - svgSymbolCancel: PropTypes.shape(SvgSymbol.propTypes), - svgSymbolSearch: PropTypes.shape(SvgSymbol.propTypes), + searchOnEnter: PropTypes.bool, value: PropTypes.string, - searchOnChange: PropTypes.bool, - searchOnEnterKey: PropTypes.bool, - debounceInterval: PropTypes.number, }; Search.defaultProps = { + debounceInterval: 0, disabled: false, isLoading: false, - onChange: _.noop, - onClear: _.noop, - onSearch: _.noop, + searchOnEnter: false, placeholder: '', - svgSymbolCancel: { - classSuffixes: ['gray-darker'], - href: '/assets/svg-symbols.svg#cancel', - }, - svgSymbolSearch: { - classSuffixes: ['gray-light'], - href: '/assets/svg-symbols.svg#search', - }, value: '', - searchOnChange: true, - searchOnEnterKey: false, - debounceInterval: 0, + icons: {}, }; export default Search; diff --git a/src/components/adslot-ui/Search/index.spec.jsx b/src/components/adslot-ui/Search/index.spec.jsx index 38388d815..eb4df1bad 100644 --- a/src/components/adslot-ui/Search/index.spec.jsx +++ b/src/components/adslot-ui/Search/index.spec.jsx @@ -1,166 +1,205 @@ -/* eslint-disable lodash/prefer-lodash-method */ -import _ from 'lodash'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; import React from 'react'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +import Search from 'adslot-ui/Search'; import SvgSymbol from 'alexandria/SvgSymbol'; import Spinner from 'alexandria/Spinner'; -import Search from './'; describe('Search', () => { - let sandbox = null; - let props = {}; - - before(() => { - sandbox = sinon.sandbox.create(); + const onSearch = sinon.spy(); + const props = { + className: 'additional-class', + dts: 'test-dts', + placeholder: 'search', + value: 'abc', + onSearch, + }; + + it('should render with defaults', () => { + const wrapper = shallow(); + + expect(wrapper.prop('className')).to.equal('aui--search-component'); + expect(wrapper.find(Spinner).length).to.equal(0); + expect(wrapper.find('button').length).to.equal(0); + + const inputEle = wrapper.find('input'); + expect(inputEle.prop('className')).to.equal('aui--search-component-input'); + expect(inputEle.prop('placeholder')).to.equal(''); + expect(inputEle.prop('value')).to.equal(''); + expect(inputEle.prop('disabled')).to.equal(false); + + const svgSymbolEle = wrapper.find('.search-icon'); + expect(svgSymbolEle.length).to.equal(1); }); - beforeEach(() => { - props = { onClear: _.noop, onChange: _.noop, onSearch: _.noop }; - sandbox.spy(props, 'onClear'); - sandbox.spy(props, 'onChange'); - sandbox.spy(props, 'onSearch'); + it('should render search button if searchOnEnter is true', () => { + const wrapper = shallow(); + expect(wrapper.find('button').length).to.equal(1); }); - afterEach(() => sandbox.restore()); - - it('should render using defaultProps', () => { - const component = shallow(); - expect(component.prop('className')).to.equal('search-component'); - expect(component.find(Spinner).length).to.equal(0); - - const inputEl = component.find('input'); - expect(inputEl.prop('className')).to.equal('search-component-input'); - expect(inputEl.prop('disabled')).to.equal(false); - expect(inputEl.prop('name')).to.equal('search'); - expect(inputEl.prop('onChange')).to.be.a('function'); - expect(inputEl.prop('placeholder')).to.equal('Search '); - expect(inputEl.prop('type')).to.equal('search'); - expect(inputEl.prop('value')).to.equal(''); - - const svgSymbolEl = component.find(SvgSymbol); - expect(svgSymbolEl.prop('href')).to.equal('/assets/svg-symbols.svg#search'); - expect(svgSymbolEl.prop('classSuffixes')).to.deep.equal(['gray-light']); - expect(svgSymbolEl.prop('onClick')).to.be.an('undefined'); - }); + it('should render with given props', () => { + const wrapper = shallow(); - it('should render using a placeholder', () => { - const component = shallow(); + expect(wrapper.prop('className')).to.equal('aui--search-component additional-class'); + expect(wrapper.prop('data-test-selector')).to.equal('test-dts'); - const inputEl = component.find('input'); - expect(inputEl.prop('placeholder')).to.equal('Search your feelings'); + const inputEle = wrapper.find('input'); + expect(inputEle.prop('placeholder')).to.equal('search'); + expect(inputEle.prop('value')).to.equal('abc'); }); - describe('onChange()', () => { - it('should fire onChange when the user changes the value', () => { - const component = shallow(); + it('should disable input when disabled is true', () => { + const wrapper = shallow(); - const inputEl = component.find('input'); - inputEl.simulate('change', { target: { value: 'needle' } }); - expect(props.onChange.calledOnce).to.equal(true); - expect(props.onChange.calledWith('needle')).to.equal(true); - }); - - it('should do nothing if `disabled` flag is on', () => { - const component = shallow(); + const inputEle = wrapper.find('input'); + expect(inputEle.prop('disabled')).to.equal(true); + }); - const inputEl = component.find('input'); - inputEl.simulate('change', { target: { value: 'needle' } }); - expect(props.onChange.calledOnce).to.equal(false); + describe('Icons', () => { + it('should render given search icons', () => { + const icons = { + search: , + }; + const wrapper = shallow(); + expect(wrapper.find(SvgSymbol).prop('href')).to.equal('svg_path'); }); - it('should not fire onSearch if `searchOnChange` is false', done => { - const component = shallow(); - const inputEl = component.find('input'); - inputEl.simulate('change', { target: { value: 'needle' } }); - setImmediate(() => { - expect(props.onSearch.calledOnce).to.equal(false); - done(); - }); + it('should render given cancel icons', () => { + const icons = { + close: , + }; + const wrapper = shallow(); + expect(wrapper.find(SvgSymbol).prop('href')).to.equal('svg_path'); }); - }); - it('should not fire onClear, onChange, onSearch when user clicks the icon while disabled', () => { - _.assign(props, { - disabled: true, - value: 'some value', + it('should render given loading spinner if isLoading is true', () => { + const icons = { + loader: , + }; + const wrapper = shallow(); + expect(wrapper.find(Spinner).length).to.equal(1); }); - const component = shallow(); - - const svgSymbolEl = component.find(SvgSymbol); - svgSymbolEl.simulate('click'); - expect(props.onChange.called).to.equal(false); - expect(props.onClear.called).to.equal(false); - expect(props.onSearch.called).to.equal(false); }); - it('should fire onClear, onChange, onSearch once with empty string value when user clicks the icon', () => { - props.value = 'some value'; - const component = shallow(); - - const svgSymbolEl = component.find(SvgSymbol); - svgSymbolEl.simulate('click'); - expect(props.onClear.calledOnce).to.equal(true); - expect(props.onClear.calledWith('')).to.equal(true); - expect(props.onChange.calledOnce).to.equal(true); - expect(props.onChange.calledWith('')).to.equal(true); - expect(props.onSearch.calledOnce).to.equal(true); - expect(props.onSearch.calledWith('')).to.equal(true); + it('should render given icons', () => { + const icons = { + search: , + }; + const wrapper = shallow(); + expect(wrapper.find(SvgSymbol).prop('href')).to.equal('svg_path'); }); - it('should render with a value', () => { - props.value = 'some value'; - const component = shallow(); - const inputEl = component.find('input'); - - expect(inputEl.prop('value')).to.equal('some value'); - }); - - describe('with searchOnEnterKey prop', () => { - let disabledStub = null; + describe('Clear Button', () => { + it('should render clear button when value is not empty and search button is not shown', () => { + const wrapper = shallow(); + const svgSymbolEle = wrapper.find('.cancel-icon'); + expect(svgSymbolEle.length).to.equal(1); + }); - beforeEach(() => { - props.searchOnEnterKey = true; - props.searchOnChange = false; - props.disabled = false; - disabledStub = sandbox.stub(props, 'disabled'); + it('should fire onClear when clear button is clicked', () => { + const callbacks = { + onClear: sinon.spy(), + }; + const wrapper = shallow(); + const clearBtn = wrapper.find('span.aui--search-component-icon'); + clearBtn.simulate('click'); + expect(callbacks.onClear.calledOnce).to.equal(true); + expect(callbacks.onClear.calledWith('')).to.equal(true); }); - it('should not call props.onSearch when disabled with enter key pressed', () => { - disabledStub.value(true); - const component = shallow(); - const inputEl = component.find('input'); - inputEl.simulate('keyPress', { key: 'Enter', which: 13 }); - expect(props.onSearch.called).to.equal(false); + it('should fire onChange when clear button is clicked', () => { + const callbacks = { + onChange: sinon.spy(), + }; + const wrapper = shallow(); + const clearBtn = wrapper.find('span.aui--search-component-icon'); + clearBtn.simulate('click'); + expect(callbacks.onChange.calledOnce).to.equal(true); + expect(callbacks.onChange.calledWith('')).to.equal(true); }); - it('should call props.onSearch when enter key pressed', () => { - disabledStub.value(false); - const component = shallow(); - const inputEl = component.find('input'); - inputEl.simulate('keyPress', { key: 'Enter', which: 13 }); - expect(props.onSearch.calledOnce).to.equal(true); + it('should fire onSearch when clear button is clicked', () => { + const callbacks = { + onSearch: sinon.spy(), + }; + const wrapper = shallow(); + const clearBtn = wrapper.find('span.aui--search-component-icon'); + clearBtn.simulate('click'); + expect(callbacks.onSearch.calledOnce).to.equal(true); + expect(callbacks.onSearch.calledWith('')).to.equal(true); }); - it('should not call props.onEnterKey when not enter key pressed', () => { - disabledStub.value(false); - const component = shallow(); - const inputEl = component.find('input'); - inputEl.simulate('keyPress', { key: 'a', which: 97 }); - expect(props.onSearch.called).to.equal(false); + it('should not fire onSearch if searchOnEnter is true', () => { + const callbacks = { + onSearch: sinon.spy(), + }; + const wrapper = shallow(); + wrapper.instance().onClear(); + expect(callbacks.onSearch.calledOnce).to.equal(false); }); + }); - it('should not fire onSearch when user clicks the icon', () => { - const component = shallow(); + describe('Value Changed', () => { + it('should fire onChange and onSearch with the latest value when value changed', () => { + const callbacks = { + onChange: sinon.spy(), + onSearch: sinon.spy(), + }; + const wrapper = shallow(); + const inputEle = wrapper.find('input'); + inputEle.simulate('change', { target: { value: 'new-value' } }); + expect(callbacks.onChange.calledOnce).to.equal(true); + expect(callbacks.onChange.calledWith('new-value')).to.equal(true); + expect(callbacks.onSearch.calledOnce).to.equal(true); + expect(callbacks.onSearch.calledWith('new-value')).to.equal(true); + }); - const svgSymbolEl = component.find(SvgSymbol); - svgSymbolEl.simulate('click'); - expect(props.onSearch.called).to.equal(false); + it('should fire onSearch after debounceInterval', done => { + const callbacks = { + onSearch: sinon.spy(), + }; + const wrapper = shallow(); + const inputEle = wrapper.find('input'); + inputEle.simulate('change', { target: { value: 'new-value' } }); + setTimeout(() => { + expect(callbacks.onSearch.calledOnce).to.equal(true); + expect(callbacks.onSearch.calledWith('new-value')).to.equal(true); + done(); + }, 500); }); - it('should render loading spinner when isLoading set to true', () => { - const component = shallow(); - expect(component.find(Spinner).length).to.equal(1); + it('should not fire onSearch when value changed if searchOnEnter is true', () => { + const callbacks = { + onSearch: sinon.spy(), + }; + const wrapper = shallow(); + const inputEle = wrapper.find('input'); + inputEle.simulate('change', { target: { value: 'new-value' } }); + expect(callbacks.onSearch.calledOnce).to.equal(false); }); }); + + it('should fire onSearch when searchOnEnter is true and search button is clicked', () => { + const callbacks = { + onSearch: sinon.spy(), + }; + const wrapper = shallow(); + const buttonEle = wrapper.find('button'); + wrapper.setState({ value: 'some-value' }); + buttonEle.simulate('click', { preventDefault: sinon.spy() }); + expect(callbacks.onSearch.calledOnce).to.equal(true); + expect(callbacks.onSearch.calledWith('some-value')).to.equal(true); + }); + + it('should fire onSearch when searchOnEnter is true and ENTER key is pressed', () => { + const callbacks = { + onSearch: sinon.spy(), + }; + const wrapper = shallow(); + const inputEle = wrapper.find('input'); + inputEle.simulate('keypress', { key: 'Enter', preventDefault: sinon.spy() }); + expect(callbacks.onSearch.calledOnce).to.equal(true); + inputEle.simulate('keypress', { key: 'a' }); + expect(callbacks.onSearch.calledOnce).to.equal(true); + }); }); diff --git a/src/components/adslot-ui/Search/styles.scss b/src/components/adslot-ui/Search/styles.scss index 7587a91c3..da15eed2b 100644 --- a/src/components/adslot-ui/Search/styles.scss +++ b/src/components/adslot-ui/Search/styles.scss @@ -1,31 +1,18 @@ -@import '~styles/border'; -@import '~styles/color'; -@import '~styles/font-size'; -@import '~styles/font-weight'; +@import '~styles/variable'; -.search-component { +.aui--search-component { $search-height: 26px; height: $search-height; - line-height: $search-height; - position: relative; width: 100%; + position: relative; + display: flex; - ::-webkit-search-cancel-button, - ::-webkit-search-decoration { - appearance: none; - } - - &-input { - -webkit-appearance: textfield; // For Safari. + .aui--search-component-input { + flex: 1; + padding: 0 10px; border: $border-lighter; border-radius: $border-radius; - box-sizing: border-box; - color: $color-text; - font-size: $font-size-body; - height: inherit; - margin: 0; - padding: 0 0 0 10px; width: inherit; &::placeholder { @@ -36,27 +23,81 @@ &:active, &:focus { border-color: $color-border; - outline: 0; // Override UA - } + outline: 0; - &:disabled { - border: $border-lighter; - color: $color-text-disabled; + &~.aui--search-component-button { + border-color: $color-border; + } } } - .svg-symbol-component { - box-sizing: content-box; + .aui--search-component-icon { height: 100%; - padding: 0 4px; + padding: 5px; position: absolute; right: 0; - top: 1px; + top: 0; + + &.with-button { + right: 40px; + } + + .search-icon, + .cancel-icon { + width: 16px; + height: 16px; + background-repeat: no-repeat; + background-size: contain; + } + + .search-icon { + background-image: url('~styles/icons/search/search-gray.svg'); + } + + .cancel-icon { + background-image: url('~styles/icons/search/cancel-gray.svg'); + + &:hover { + background-image: url('~styles/icons/search/cancel.svg'); + } + } + } + + .aui--search-component-button { + margin-left: -2px; + border: $border-lighter; + display: flex; + justify-content: center; + align-items: center; + padding: 2px 7px; + border-radius: 0 $border-radius $border-radius 0; + + &:hover { + background-color: $color-gray-white; + } + + &:focus { + outline: none; + } + + span { + width: 16px; + height: 16px; + } + + .search-icon { + width: 16px; + height: 16px; + background-repeat: no-repeat; + background-size: contain; + background-image: url('~styles/icons/search/search-primary.svg'); + flex-shrink: 0; + } } - .spinner-component { + .aui--search-component-spinner { position: absolute; - top: 4px; - right: 24px; + top: 5px; + right: 25px; } } diff --git a/src/components/adslot-ui/SearchBar/index.jsx b/src/components/adslot-ui/SearchBar/index.jsx deleted file mode 100644 index 6c50fb139..000000000 --- a/src/components/adslot-ui/SearchBar/index.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Button from 'react-bootstrap/lib/Button'; -import SvgSymbol from 'alexandria/SvgSymbol'; - -require('./styles.scss'); - -const SearchBarComponent = ({ - additionalClassNames, - searchString, - searchPlaceholder, - searchIconHref, - onSearchStringChange, - onSearch, - dts, -}) => { - const className = ['search-bar-component'].concat(additionalClassNames).join(' '); - const placeholder = searchPlaceholder || 'Search'; - const onSearchStringChangeBound = event => onSearchStringChange(event.target.value); - const onTextInputKeyPress = event => { - const ENTER_KEY = 13; - - // event.keyCode always returns 0 on Chrome (a known bug), so we must do a check - // for event.which as well. For more info on the bug, see this SO entry: - // http://stackoverflow.com/questions/1897333/firing-a-keyboard-event-on-chrome - if (event.which === ENTER_KEY || event.keyCode === ENTER_KEY) { - onSearch(); - } - }; - - return ( -
- - -
- ); -}; - -SearchBarComponent.propTypes = { - additionalClassNames: PropTypes.arrayOf(PropTypes.string), - searchString: PropTypes.string.isRequired, - searchPlaceholder: PropTypes.string, - searchIconHref: PropTypes.string.isRequired, - onSearchStringChange: PropTypes.func.isRequired, - onSearch: PropTypes.func.isRequired, - dts: PropTypes.string, -}; - -SearchBarComponent.defaultProps = { - additionalClassNames: [], -}; - -export default SearchBarComponent; diff --git a/src/components/adslot-ui/SearchBar/index.spec.jsx b/src/components/adslot-ui/SearchBar/index.spec.jsx deleted file mode 100644 index 6ca2e758c..000000000 --- a/src/components/adslot-ui/SearchBar/index.spec.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import SearchBar from 'adslot-ui/SearchBar'; -import SvgSymbol from 'alexandria/SvgSymbol'; -import Button from 'react-bootstrap/lib/Button'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; - -const defaultProps = { - searchString: '', - searchIconHref: '', - onSearchStringChange: _.noop, - onSearch: _.noop, -}; - -const props = { - additionalClassNames: ['class-a', 'class-b'], - searchString: '', - searchIconHref: '', - onSearchStringChange: _.noop, - onSearch: _.noop, - dts: 'test-dts', -}; - -describe('SearchBarComponent', () => { - it('should render with defaults', () => { - const component = shallow(); - expect(component.prop('className')).to.equal('search-bar-component'); - - const inputElement = component.find('input'); - expect(inputElement).to.have.length(1); - - const buttonElement = component.find(Button); - expect(buttonElement).to.have.length(1); - - const svgSymbolElement = buttonElement.find(SvgSymbol); - expect(svgSymbolElement).to.have.length(1); - }); - - it('should render with props', () => { - const component = shallow(); - expect(component.prop('className')).to.equal('search-bar-component class-a class-b'); - expect(component.prop('data-test-selector')).to.equal('test-dts'); - }); - - it('should bind onSearchStringChange to search input change event', () => { - const callback = sinon.spy(); - const component = shallow(); - const inputElement = component.find('input'); - inputElement.simulate('change', { target: { value: 'Granny Smith' } }); - expect(callback.calledOnce).to.equal(true); - expect(callback.calledWith('Granny Smith')); - }); - - it('should bind onSearch to search input key press event', () => { - const callback = sinon.spy(); - const component = shallow(); - const inputElement = component.find('input'); - const ENTER_KEY = 13; - inputElement.simulate('keypress', { keyCode: ENTER_KEY }); - expect(callback.calledOnce).to.equal(true); - const A_KEY = 65; - inputElement.simulate('keypress', { keyCode: A_KEY }); - expect(callback.calledOnce).to.equal(true); - }); - - it('should bind onSearch to search button click event', () => { - const callback = sinon.spy(); - const component = shallow(); - const buttonElement = component.find(Button); - buttonElement.simulate('click'); - expect(callback.calledOnce).to.equal(true); - }); -}); diff --git a/src/components/adslot-ui/SearchBar/styles.scss b/src/components/adslot-ui/SearchBar/styles.scss deleted file mode 100644 index 88cdd5c98..000000000 --- a/src/components/adslot-ui/SearchBar/styles.scss +++ /dev/null @@ -1,27 +0,0 @@ -@import '~styles/variable'; - -.search-bar-component { - $component-height: 30px; - $component-padding: 10px; - display: flex; - - &-text-input { - &.form-control { - flex: 1; - height: $component-height + 2; - margin-right: -3px; // have the search button overlap on borders - padding: $padding-base-vertical $component-padding; - } - } - - &-button { - &.btn { - line-height: 1; - width: 36px; - - > .svg-symbol-component-search-icon { - fill: $color-primary; - } - } - } -} diff --git a/src/components/adslot-ui/TreePicker/Nav/index.jsx b/src/components/adslot-ui/TreePicker/Nav/index.jsx index 8587b989e..f7fd28093 100644 --- a/src/components/adslot-ui/TreePicker/Nav/index.jsx +++ b/src/components/adslot-ui/TreePicker/Nav/index.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import _ from 'lodash'; import PropTypes from 'prop-types'; import Search from 'adslot-ui/Search'; import Breadcrumb from 'alexandria/Breadcrumb'; @@ -16,29 +17,36 @@ const TreePickerNavComponent = ({ onClear, onChange, onSearch, - searchOnChange, - searchOnEnterKey, + searchOnEnter, + searchPlaceholder, searchValue, svgSymbolCancel, svgSymbolSearch, -}) => ( -
- - -
-); +}) => { + const icons = {}; + if (svgSymbolSearch) + icons.search = ; + if (svgSymbolCancel) + icons.close = ; + + return ( +
+ + +
+ ); +}; TreePickerNavComponent.displayName = 'AdslotUiTreePickerNavComponent'; TreePickerNavComponent.propTypes = { @@ -50,8 +58,8 @@ TreePickerNavComponent.propTypes = { onClear: PropTypes.func, onSearch: PropTypes.func, debounceInterval: PropTypes.number, - searchOnChange: PropTypes.bool, - searchOnEnterKey: PropTypes.bool, + searchOnEnter: PropTypes.bool, + searchPlaceholder: PropTypes.string, searchValue: PropTypes.string, svgSymbolCancel: PropTypes.shape(SvgSymbol.propTypes), svgSymbolSearch: PropTypes.shape(SvgSymbol.propTypes), @@ -61,8 +69,9 @@ TreePickerNavComponent.defaultProps = { debounceInterval: 0, disabled: false, isLoading: false, - searchOnChange: true, - searchOnEnterKey: false, + searchOnEnter: false, + searchPlaceholder: '', + onSearch: _.noop, }; export default TreePickerNavComponent; diff --git a/src/components/adslot-ui/TreePicker/Nav/index.spec.jsx b/src/components/adslot-ui/TreePicker/Nav/index.spec.jsx index 94a312665..9410a088e 100644 --- a/src/components/adslot-ui/TreePicker/Nav/index.spec.jsx +++ b/src/components/adslot-ui/TreePicker/Nav/index.spec.jsx @@ -34,6 +34,8 @@ describe('TreePickerNavComponent', () => { const searchElement = component.find(Search); expect(searchElement).to.have.length(1); + expect(searchElement.prop('onSearch')).to.be.a('function'); + expect(_.isEmpty(searchElement.prop('icons'))).to.equal(true); const breadcrumbElement = component.find(Breadcrumb); expect(breadcrumbElement).to.have.length(1); @@ -55,6 +57,20 @@ describe('TreePickerNavComponent', () => { expect(breadcrumbElement.prop('onClick')).to.be.a('function'); }); + it('should render icons with given svgSymbol and pass them to Search', () => { + const svgSymbolCancel = { + href: '/assets/svg-symbols.svg#cancel', + }; + const svgSymbolSearch = { + href: '/assets/svg-symbols.svg#search', + }; + const component = shallow( + + ); + const searchElement = component.find(Search); + expect(searchElement.prop('icons')).to.have.keys(['search', 'close']); + }); + it('should call breadcrumbOnClick when clicked on breadcrumbs node', () => { sandbox.spy(props, 'breadcrumbOnClick'); const component = mount(); diff --git a/src/components/adslot-ui/TreePicker/index.jsx b/src/components/adslot-ui/TreePicker/index.jsx index ccdb34e75..2dab3dc74 100644 --- a/src/components/adslot-ui/TreePicker/index.jsx +++ b/src/components/adslot-ui/TreePicker/index.jsx @@ -40,8 +40,7 @@ const TreePickerSimplePureComponent = ({ onChange, onClear, onSearch, - searchOnChange, - searchOnEnterKey, + searchOnEnter, searchPlaceholder, searchValue, selectedNodes, @@ -73,8 +72,7 @@ const TreePickerSimplePureComponent = ({ onClear, onChange, onSearch, - searchOnChange, - searchOnEnterKey, + searchOnEnter, searchPlaceholder, searchValue, svgSymbolCancel, @@ -149,8 +147,7 @@ TreePickerSimplePureComponent.propTypes = { onChange: PropTypes.func, onClear: PropTypes.func, onSearch: PropTypes.func, - searchOnChange: PropTypes.bool, - searchOnEnterKey: PropTypes.bool, + searchOnEnter: PropTypes.bool, searchPlaceholder: PropTypes.string, searchValue: PropTypes.string, selectedNodes: PropTypes.arrayOf(TreePickerPropTypes.node.isRequired).isRequired, @@ -167,8 +164,7 @@ TreePickerSimplePureComponent.defaultProps = { disabled: false, displayGroupHeader: true, isLoading: false, - searchOnChange: true, - searchOnEnterKey: false, + searchOnEnter: false, hideSearchOnRoot: false, }; diff --git a/src/components/adslot-ui/TreePicker/index.spec.jsx b/src/components/adslot-ui/TreePicker/index.spec.jsx index e1f9212eb..1f138dcbd 100644 --- a/src/components/adslot-ui/TreePicker/index.spec.jsx +++ b/src/components/adslot-ui/TreePicker/index.spec.jsx @@ -28,8 +28,7 @@ describe('TreePickerSimplePureComponent', () => { onClear: _.noop, onChange: _.noop, onSearch: _.noop, - searchOnChange: true, - searchOnEnterKey: false, + searchOnEnter: false, searchPlaceholder: 'Search Geometry', searchValue: '', selectedNodes: initialSelection, @@ -62,7 +61,7 @@ describe('TreePickerSimplePureComponent', () => { 'onClear', 'onChange', 'onSearch', - 'searchOnChange', + 'searchOnEnter', 'searchPlaceholder', 'searchValue', 'svgSymbolCancel', diff --git a/src/components/adslot-ui/index.js b/src/components/adslot-ui/index.js index 27604c532..5f3651030 100644 --- a/src/components/adslot-ui/index.js +++ b/src/components/adslot-ui/index.js @@ -16,7 +16,6 @@ import Panel from 'adslot-ui/Panel'; import Radio from 'adslot-ui/Radio'; import RadioGroup from 'adslot-ui/RadioGroup'; import Search from 'adslot-ui/Search'; -import SearchBar from 'adslot-ui/SearchBar'; import SplitPane from 'adslot-ui/SplitPane'; import StatusPill from 'adslot-ui/StatusPill'; import Tab from 'adslot-ui/Tab'; @@ -55,7 +54,6 @@ export { Radio, RadioGroup, Search, - SearchBar, SplitPane, StatusPill, Tab, diff --git a/src/index.js b/src/index.js index 8032f76cf..cb40e9657 100644 --- a/src/index.js +++ b/src/index.js @@ -63,7 +63,6 @@ import { Radio, RadioGroup, Search, - SearchBar, SplitPane, StatusPill, Tab, @@ -123,7 +122,6 @@ export { Radio, RadioGroup, Search, - SearchBar, Select, Slicey, Spinner, diff --git a/src/styles/icons/search/cancel-gray.svg b/src/styles/icons/search/cancel-gray.svg new file mode 100644 index 000000000..8650205c4 --- /dev/null +++ b/src/styles/icons/search/cancel-gray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/styles/icons/search/cancel.svg b/src/styles/icons/search/cancel.svg new file mode 100644 index 000000000..ae6f5e187 --- /dev/null +++ b/src/styles/icons/search/cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/styles/icons/search/search-gray.svg b/src/styles/icons/search/search-gray.svg new file mode 100644 index 000000000..ea272935d --- /dev/null +++ b/src/styles/icons/search/search-gray.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/styles/icons/search/search-primary.svg b/src/styles/icons/search/search-primary.svg new file mode 100644 index 000000000..3bccaaafc --- /dev/null +++ b/src/styles/icons/search/search-primary.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file