From c8225e1fd44115254cb37d509c8a174e9584b513 Mon Sep 17 00:00:00 2001 From: "Taushanova-Atanasova, Inna" Date: Fri, 21 Dec 2018 15:15:32 -0500 Subject: [PATCH] Issue#121: refactor search input --- src/SearchInput/SearchInput.Component.js | 233 ++++--------- src/SearchInput/SearchInput.js | 328 ++++++++++-------- src/SearchInput/SearchInput.test.js | 208 ----------- .../__snapshots__/SearchInput.test.js.snap | 192 ---------- 4 files changed, 249 insertions(+), 712 deletions(-) delete mode 100644 src/SearchInput/SearchInput.test.js delete mode 100644 src/SearchInput/__snapshots__/SearchInput.test.js.snap diff --git a/src/SearchInput/SearchInput.Component.js b/src/SearchInput/SearchInput.Component.js index 6f166c631..9cbfd726a 100644 --- a/src/SearchInput/SearchInput.Component.js +++ b/src/SearchInput/SearchInput.Component.js @@ -3,144 +3,70 @@ import { DocsTile, DocsText, Separator, Header, Description, Import, Properties import { SearchInput } from './SearchInput'; export class SearchInputComponent extends Component { - data = [ - 'Apple', - 'Apricot', - 'Avocado', - 'Abiu', - 'Acai', - 'Acerola', - 'Ackee', - 'Arhat', - 'American Mayapple', - 'African Cherry Orange', - 'Amazon grape', - 'Araza', - 'Alligator apple', - 'Ambarella', - 'African Cucumber', - 'African Medlar', - 'African Moringa', - 'Agave Plant', - 'Aizen Fruit', - 'American Black Elderberry', - 'American Chestnut', - 'American Hazelnut Shrub', - 'American Red Raspberry', - 'Aprium', - 'Atemoya', - 'Atherton Raspberry Banana', - 'Berry', - 'Bayberry', - 'Blueberry', - 'Blackberry', - 'Boysenberry', - 'Bearberry', - 'Bilberry', - 'Barberry', - 'Buffaloberry', - 'Black cherry', - 'Beach plum', - 'Black raspberry', - 'Black apple', - 'Blue tongue', - 'Bolwarra', - 'Burdekin plum', - 'Bramble', - 'Broadleaf Bramble', - 'Black mulberry', - 'Blood orange', - 'Babaco', - 'Bael', - 'Barbadine', - 'Barbados cherry', - 'Betel nut', - 'Bilimbi', - 'Bitter gourd', - 'Black sapote', - 'Bottle gourd', - 'Brazil nut', - 'Breadfruit', - 'Burmese grape', - 'Blackcurrant', - 'Bignay', - 'Beechnut', - 'Bacuri Fruit', - 'Balsam Apple', - 'Batuan Fruit', - 'Blood Lime', - 'Brazilian Guava', - 'Brush cherry Cantaloupe', - 'Chokeberry', - 'Cranberry', - 'Cloudberry', - 'Crowberry', - 'Conkerberry', - 'Calabash', - 'Calamansi', - 'Calamondins', - 'Canistel', - 'Cape Gooseberry', - 'Capuli Cherry', - 'Carob Fruit', - 'Cashew Apple', - 'Cedar Bay Cherry', - 'Cempedak', - 'Ceylon Gooseberry', - 'Charichuelo Fruit', - 'Chayote Fruit', - 'Cherimoya Fruit', - 'cherry Fruit', - 'Chokecherry', - 'Citrofortunella', - 'Clementines', - 'Cluster Fig', - 'Coco Plum', - 'Common Apple Berry', - 'Cornelian Cherry', - 'Cucumber', - 'Cupuacu' + searchData = [ + { text: 'apple', callback: () => alert('apple') }, + { text: 'apricot', callback: () => alert('apricot') }, + { text: 'banana', callback: () => alert('banana') }, + { text: 'blueberry', callback: () => alert('blueberry') }, + { text: 'blackberry', callback: () => alert('blackberry') }, + { text: 'calabash', callback: () => alert('calabash') }, + { text: 'clementines', callback: () => alert('clementines') }, + { text: 'kiwi', callback: () => alert('kiwi') }, + { text: 'orange', callback: () => alert('orange') } ]; constructor(props) { super(props); - this.state = { - data: [] + inputValue: '' }; } - searchInputCode = ``; - - autoCompleteSearchInputCode = ``; - - exampleAutoCompleteMethod = `performAutoComplete = searchTerm => { - const searchResults = this.data.filter(item => { - return item.toLowerCase().startsWith(searchTerm.toLowerCase()); - }); - - this.setState({ data: searchResults }); -};`; - - performAutoComplete = searchTerm => { - const searchResults = this.data.filter(item => { - return item.toLowerCase().startsWith(searchTerm.toLowerCase()); + getInputValue(value) { + this.setState({ + inputValue: value }); - this.setState({ data: searchResults }); - }; + setTimeout( + function() { + alert('You just set the inputValue of the state to ' + this.state.inputValue); + }.bind(this), + 100 + ); + } + + searchInputCode = ` this.getInputValue(term)} +/> + +************************************ Data ************************************ + +getInputValue(value) { + this.setState({ + inputValue: value + }); + + setTimeout( + function() { + alert('You just set the inputValue of the state to ' + this.state.inputValue); + }.bind(this), + 1000 + ); +} - performSearch = searchTerm => { - console.log(`search fired for ${searchTerm}`); - }; +searchData = [ + { text: 'apple', callback: () => alert('apple') }, + { text: 'apricot', callback: () => alert('apricot') }, + { text: 'banana', callback: () => alert('banana') }, + { text: 'blueberry', callback: () => alert('blueberry') }, + { text: 'blackberry', callback: () => alert('blackberry') }, + { text: 'calabash', callback: () => alert('calabash') }, + { text: 'clementines', callback: () => alert('clementines') }, + { text: 'kiwi', callback: () => alert('kiwi') }, + { text: 'orange', callback: () => alert('orange') } +];`; render() { return ( @@ -155,67 +81,30 @@ export class SearchInputComponent extends Component { type='Inputs' properties={[ { - name: 'onSearch', - description: - 'func (Required) - Method to execute on click of Search icon, selection of auto-complete item or by pressing the Enter key.' - }, - { - name: 'placeHolder', + name: 'placeholder', description: 'string - The text to use as placeholder when no text is entered.' }, { - name: 'data', + name: 'searchList', description: 'array - Collection of items to display in auto-complete list.' }, { - name: 'onAutoComplete', - description: - 'func - Method that receives search input box text, to perform auto complete query.' + name: 'onEnter', + description: 'func - Method to execute by pressing the Enter key.' } ]} /> -

Default Search

- - A text input with Search button. Clicking on the Search button or pressing the Enter key will - execute the onSearch method. - - -
- -
-
- {this.searchInputCode} - - - -

Auto Complete Search

- - A text input with Search button that includes auto-complete functionality. Clicking on the Search - button, selecting an auto-complete item or pressing the Enter key will execute the onSearch method.{' '} -
-
- For the auto-complete functionality to work the parent component needs to pass a method to the{' '} - onAutoComplete property. The Search Input component will pass to the onAutoComplete method - the value entered in to the search box. It is up to the parent component to create the array of - values to display and set it to the data property of the Search Input component. -
+ placeholder='Enter a fruit' + searchList={this.searchData} + onEnter={term => this.getInputValue(term)} />
- {this.autoCompleteSearchInputCode} - - - Sample auto-complete method which filters an array of fruit based on the search input value. - - {this.exampleAutoCompleteMethod} + {this.searchInputCode} diff --git a/src/SearchInput/SearchInput.js b/src/SearchInput/SearchInput.js index e03c9955e..fc78ad51f 100644 --- a/src/SearchInput/SearchInput.js +++ b/src/SearchInput/SearchInput.js @@ -2,158 +2,206 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; export class SearchInput extends Component { - constructor(props) { - super(props); - - this.state = { - searchTerm: '', - bShowList: false - }; - } - - // fired on change of text input - handleSearch = event => { - const searchTerm = event.target.value; - - // check if control auto complete - if (this.props.onAutoComplete) { - if (searchTerm) { - this.setState({ searchTerm: searchTerm, bShowList: true }); - } else { - // no search term, remove text from search box and hide list - this.setState({ searchTerm: searchTerm, bShowList: false }); - } - this.props.onAutoComplete(searchTerm); - } else { - this.setState({ searchTerm: searchTerm }); + constructor(props) { + super(props); + this.state = { + isExpanded: false, + searchExpanded: false, + value: '', + searchList: this.props.searchList, + filteredResult: this.props.searchList + }; + + this.onChangeHandler = this.onChangeHandler.bind(this); + this.onClickHandler = this.onClickHandler.bind(this); + this.onEscHandler = this.onEscHandler.bind(this); + this.onOutsideClickHandler = this.onOutsideClickHandler.bind(this); + this.onKeyPressHandler = this.onKeyPressHandler.bind(this); + this.listItemClickHandler = this.listItemClickHandler.bind(this); + this.onSearchBtnHandler = this.onSearchBtnHandler.bind(this); + } + + onKeyPressHandler(event) { + if (event.key === 'Enter') { + this.props.onEnter(this.state.value); + } } - }; - // fired on list item selection - selectTerm = event => { - const searchTerm = event.target.innerText; - this.setState({ searchTerm: searchTerm, bShowList: false }); + listItemClickHandler(item) { + item.callback(); + } - this.props.onSearch(searchTerm); - }; + onChangeHandler(event) { + if (this.state.searchList) { + let filteredResult = this.state.searchList.filter(item => + item.text.toLowerCase().startsWith(event.target.value.toLowerCase()) + ); - // check for enter key - checkKey = event => { - const key = event.key; + this.setState({ + filteredResult: filteredResult + }); + } - if (key === 'Enter') { - if (this.props.data && this.props.data.length > 0) { - this.setState( - { searchTerm: this.props.data[0], bShowList: false }, - () => this.props.onSearch(this.state.searchTerm) - ); - } else { - this.props.onSearch(this.state.searchTerm); - } + if (!this.state.isExpanded) { + this.setState({ + isExpanded: true + }); + } + + this.setState({ + value: event.target.value + }); + } + + onClickHandler() { + if (!this.state.isExpanded) { + document.addEventListener('click', this.onOutsideClickHandler, false); + } else { + document.removeEventListener('click', this.onOutsideClickHandler, false); + } + + this.setState(prevState => ({ + isExpanded: !prevState.isExpanded + })); + } + + onSearchBtnHandler() { + this.setState(prevState => ({ + searchExpanded: !prevState.searchExpanded + })); + + if (this.state.searchExpanded && this.state.isExpanded) { + this.setState({ + isExpanded: false + }); + } } - }; - - // create search box - createSearchInput = onAutoComplete => { - let searchInput = ( - - ); - - // include auto complete functionality if onAutoComplete method is passed to component - if (onAutoComplete) { - searchInput = ( - { - this.setState({ bShowList: false }); - }} /> - ); + + onEscHandler(event) { + if ((event.keyCode === 27 && this.state.isExpanded === true) || (event.keyCode === 27 && this.state.searchExpanded === true)) { + this.setState({ + isExpanded: false, + searchExpanded: false, + value: '', + searchList: this.props.searchList, + filteredResult: this.props.searchList + }); + } } - return searchInput; - }; + onOutsideClickHandler(e) { + e.stopPropagation(); + if (this.node && !this.node.contains(e.target)) { + if (this.state.isExpanded) { + this.setState({ + isExpanded: false + }); - // create auto complete search items - createAutoCompleteItems = data => { - return data && data.length > 0 ? ( - data.map((item, index) => { - let classNames = 'fd-menu__item'; - if (index === 0) { - classNames += ' is-selected'; + if (this.props.inShellbar && this.state.searchExpanded && !this.state.value) { + this.setState({ + searchExpanded: false + }); + } + } else { + return; + } } + } + + componentDidMount() { + document.addEventListener('keydown', this.onEscHandler, false); + document.addEventListener('click', this.onOutsideClickHandler, false); + } + componentWillUnmount() { + document.removeEventListener('keydown', this.onEscHandler, false); + document.removeEventListener('click', this.onOutsideClickHandler, false); + } + + render() { + const { placeholder, inShellbar, onSearch } = this.props; + return ( -
  • - - {item} - -
  • - ); - }) - ) : ( -
  • - - No results found - -
  • - ); - }; - - render() { - const { data, onSearch, onAutoComplete } = this.props; - - return ( -
    -
    -
    -
    -
    - {this.createSearchInput(onAutoComplete)} - -
    + ) : ( +
    +
    +
    + this.onClickHandler()} + ref={node => (this.node = node)} /> + + +
    +
    -
    + )} + {this.state.filteredResult && ( +
    +
    + +
    +
    + )}
    - {data && onAutoComplete ? ( -
    - -
    - ) : ( - '' - )}
    -
    - ); - } + ); + } } - -SearchInput.propTypes = { - onSearch: PropTypes.func.isRequired, - data: PropTypes.array, - placeHolder: PropTypes.string, - onAutoComplete: PropTypes.func -}; diff --git a/src/SearchInput/SearchInput.test.js b/src/SearchInput/SearchInput.test.js deleted file mode 100644 index 24696a010..000000000 --- a/src/SearchInput/SearchInput.test.js +++ /dev/null @@ -1,208 +0,0 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import { SearchInput } from './SearchInput'; -import Enzyme, { shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; - -Enzyme.configure({ adapter: new Adapter() }); - -describe('', () => { - const mockOnSearch = jest.fn(); - const mockOnAutoComplete = jest.fn(); - const data = ['apple', 'banana', 'orange']; - const defaultSearchInput = ( - - ); - const autoCompleteSearchInput = ( - - ); - const autoCompleteNoDataSearchInput = ( - - ); - const searchInput = 'input[type="text"].fd-input'; - - const getListStatus = (wrapper, bIsShown) => { - const combobox = wrapper.find( - `div.fd-combobox-control[aria-expanded=${bIsShown}]` - ); - - const popover = wrapper.find( - `div.fd-popover__body.fd-popover__body--no-arrow[aria-hidden=${!bIsShown}]` - ); - - return { combobox: combobox, popover: popover }; - }; - - // create default search input component - test('create basic Search input', () => { - const component = renderer.create(defaultSearchInput); - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); - }); - - test('check for enter key press on basic search input', () => { - const wrapper = shallow(defaultSearchInput); - - // enter text into search input - wrapper - .find(searchInput) - .simulate('change', { target: { value: data[0] } }); - - wrapper - .find('input[type="text"].fd-input') - .simulate('keypress', { key: 'Enter' }); - - expect(wrapper.state(['searchTerm'])).toBe(data[0]); - }); - - test('create autocomplete Search input with no data', () => { - const component = renderer.create(autoCompleteNoDataSearchInput); - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); - }); - - // create auto complete search input component - test('create auto-complete Search input', () => { - const component = renderer.create(autoCompleteSearchInput); - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); - - const wrapper = shallow(autoCompleteSearchInput); - - // check to see if 3 list items are created for auto complete list - expect(wrapper.find('li')).toHaveLength(3); - - // check to see if first item is selected in auto complete list - expect(wrapper.find('li:first-child a').hasClass('is-selected')).toEqual( - true - ); - }); - - test('check that auto complete list is hidden', () => { - const wrapper = shallow(autoCompleteSearchInput); - - // check if bShowList state is changed - expect(wrapper.state(['bShowList'])).toBe(false); - - // check to see if list is not shown - let results = getListStatus(wrapper, false); - expect(results.combobox).toHaveLength(1); - expect(results.popover).toHaveLength(1); - }); - - test('check that auto complete list is hidden on blur', () => { - const wrapper = shallow(autoCompleteSearchInput); - - wrapper - .find(searchInput) - .simulate('change', { target: { value: data[0] } }); - - // check if bShowList state is changed - expect(wrapper.state(['bShowList'])).toBe(true); - - // check to see if list is shown - let results = getListStatus(wrapper, true); - expect(results.combobox).toHaveLength(1); - expect(results.popover).toHaveLength(1); - - wrapper.find(searchInput).simulate('blur'); - - // check to see if list is not shown - results = getListStatus(wrapper, false); - expect(results.combobox).toHaveLength(1); - expect(results.popover).toHaveLength(1); - }); - - test('check that auto complete list is shown when text entered', () => { - const wrapper = shallow(autoCompleteSearchInput); - wrapper - .find(searchInput) - .simulate('change', { target: { value: data[0] } }); - - // check if searchTerm state is updated - expect(wrapper.state(['searchTerm'])).toBe(data[0]); - - // check if bShowList state is changed - expect(wrapper.state(['bShowList'])).toBe(true); - - // check to see if list is shown - let results = getListStatus(wrapper, true); - expect(results.combobox).toHaveLength(1); - expect(results.popover).toHaveLength(1); - }); - - test('check that auto complete list is not shown when text removed', () => { - const wrapper = shallow(autoCompleteSearchInput); - wrapper - .find(searchInput) - .simulate('change', { target: { value: data[0] } }); - - // check if searchTerm state is updated - expect(wrapper.state(['searchTerm'])).toBe(data[0]); - - // check if bShowList state is changed - expect(wrapper.state(['bShowList'])).toBe(true); - - // check to see if list is shown - let results = getListStatus(wrapper, true); - expect(results.combobox).toHaveLength(1); - expect(results.popover).toHaveLength(1); - - wrapper.find(searchInput).simulate('change', { target: { value: '' } }); - - // check if searchTerm state is updated - expect(wrapper.state(['searchTerm'])).toBe(''); - - // check if bShowList state is changed - expect(wrapper.state(['bShowList'])).toBe(false); - - // check to see if list is shown - results = getListStatus(wrapper, false); - expect(results.combobox).toHaveLength(1); - expect(results.popover).toHaveLength(1); - }); - - test('check for enter key press on auto complete search input', () => { - const wrapper = shallow(autoCompleteSearchInput); - wrapper.find(searchInput).simulate('keypress', { key: 'Esc' }); - wrapper.find(searchInput).simulate('keypress', { key: 'Enter' }); - - expect(wrapper.state(['searchTerm'])).toBe(data[0]); - }); - - test('check search executed on search button click', () => { - const wrapper = shallow(autoCompleteSearchInput); - wrapper - .find(searchInput) - .simulate('change', { target: { value: data[0] } }); - - // check if searchTerm state is updated - expect(wrapper.state(['searchTerm'])).toBe(data[0]); - - wrapper.find('.fd-button--light.sap-icon--search').simulate('click'); - - expect(wrapper.state(['searchTerm'])).toBe(data[0]); - }); - - test('set search term on list item selection', () => { - const wrapper = shallow(autoCompleteSearchInput); - wrapper - .find('li a') - .at(2) - .simulate('click', { target: { innerText: data[2] } }); - - // check if searchTerm state is updated - expect(wrapper.state(['searchTerm'])).toBe(data[2]); - - // check if bShowList state is changed - expect(wrapper.state(['bShowList'])).toBe(false); - }); -}); diff --git a/src/SearchInput/__snapshots__/SearchInput.test.js.snap b/src/SearchInput/__snapshots__/SearchInput.test.js.snap deleted file mode 100644 index 41b1819d6..000000000 --- a/src/SearchInput/__snapshots__/SearchInput.test.js.snap +++ /dev/null @@ -1,192 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` create auto-complete Search input 1`] = ` -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - -
    -
    -
    -`; - -exports[` create autocomplete Search input with no data 1`] = ` -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - -
    -
    -
    -`; - -exports[` create basic Search input 1`] = ` -
    -
    -
    -
    -
    - - -
    -
    -
    - -
    -
    -`;