/ src / components / SearchBar.js
SearchBar.js
  1  import React, { Component } from 'react'
  2  import PropTypes from 'prop-types'
  3  import IconButton from '@material-ui/core/IconButton'
  4  import Input from '@material-ui/core/Input'
  5  import Paper from '@material-ui/core/Paper'
  6  import ClearIcon from '@material-ui/icons/Clear'
  7  import SearchIcon from '@material-ui/icons/Search'
  8  import { grey } from '@material-ui/core/colors'
  9  import withStyles from '@material-ui/core/styles/withStyles'
 10  import classNames from 'classnames'
 11  
 12  const styles = {
 13    root: {
 14      height: 48,
 15      display: 'flex',
 16      justifyContent: 'space-between'
 17    },
 18    iconButton: {
 19      opacity: 0.54,
 20      transform: 'scale(1, 1)',
 21      transition: 'transform 200ms cubic-bezier(0.4, 0.0, 0.2, 1)'
 22    },
 23    iconButtonHidden: {
 24      transform: 'scale(0, 0)',
 25      '& > $icon': {
 26        opacity: 0
 27      }
 28    },
 29    iconButtonDisabled: {
 30      opacity: 0.38
 31    },
 32    searchIconButton: {
 33      marginRight: -48
 34    },
 35    icon: {
 36      opacity: 0.54,
 37      transition: 'opacity 200ms cubic-bezier(0.4, 0.0, 0.2, 1)'
 38    },
 39    input: {
 40      width: '100%'
 41    },
 42    searchContainer: {
 43      margin: 'auto 16px',
 44      width: 'calc(100% - 48px - 32px)' // 48px button + 32px margin
 45    }
 46  }
 47  
 48  /**
 49   * Material design search bar
 50   * @see [Search patterns](https://material.io/guidelines/patterns/search.html)
 51   */
 52  class SearchBar extends Component {
 53    constructor (props) {
 54      super(props)
 55      this.state = {
 56        focus: false,
 57        value: this.props.value,
 58        active: false
 59      }
 60    }
 61  
 62    componentWillReceiveProps (nextProps) {
 63      if (this.props.value !== nextProps.value) {
 64        this.setState({...this.state, value: nextProps.value})
 65      }
 66    }
 67  
 68    handleFocus = (e) => {
 69      this.setState({focus: true})
 70      if (this.props.onFocus) {
 71        this.props.onFocus(e)
 72      }
 73    }
 74  
 75    handleBlur = (e) => {
 76      this.setState({focus: false})
 77      if (this.state.value.trim().length === 0) {
 78        this.setState({value: ''})
 79      }
 80      if (this.props.onBlur) {
 81        this.props.onBlur(e)
 82      }
 83    }
 84  
 85    handleInput = (e) => {
 86      this.setState({value: e.target.value})
 87      if (this.props.onChange) {
 88        this.props.onChange(e.target.value)
 89      }
 90    }
 91  
 92    handleCancel = () => {
 93      this.setState({active: false, value: ''})
 94      if (this.props.onCancelSearch) {
 95        this.props.onCancelSearch()
 96      }
 97    }
 98  
 99    handleKeyUp = (e) => {
100      if (e.charCode === 13 || e.key === 'Enter') {
101        this.handleRequestSearch()
102      } else if (this.props.cancelOnEscape && (e.charCode === 27 || e.key === 'Escape')) {
103        this.handleCancel()
104      }
105      if (this.props.onKeyUp) {
106        this.props.onKeyUp(e)
107      }
108    }
109  
110    handleRequestSearch = () => {
111      if (this.props.onRequestSearch) {
112        this.props.onRequestSearch(this.state.value)
113      }
114    }
115  
116    render () {
117      const { value } = this.state
118      const {
119        cancelOnEscape,
120        className,
121        classes,
122        closeIcon,
123        disabled,
124        onCancelSearch,
125        onRequestSearch,
126        searchIcon,
127        style,
128        ...inputProps
129      } = this.props
130  
131      return (
132        <Paper
133          className={classNames(classes.root, className)}
134          style={style}
135        >
136          <div className={classes.searchContainer}>
137            <Input
138              {...inputProps}
139              onBlur={this.handleBlur}
140              value={value}
141              onChange={this.handleInput}
142              onKeyUp={this.handleKeyUp}
143              onFocus={this.handleFocus}
144              fullWidth
145              className={classes.input}
146              disableUnderline
147              disabled={disabled}
148            />
149          </div>
150          <IconButton
151            onClick={this.handleRequestSearch}
152            classes={{
153              root: classNames(classes.iconButton, classes.searchIconButton, {
154                [classes.iconButtonHidden]: value !== ''
155              }),
156              disabled: classes.iconButtonDisabled
157            }}
158            disabled={disabled}
159          >
160            {React.cloneElement(searchIcon, {
161              classes: { root: classes.icon }
162            })}
163          </IconButton>
164          <IconButton
165            onClick={this.handleCancel}
166            classes={{
167              root: classNames(classes.iconButton, {
168                [classes.iconButtonHidden]: value === ''
169              }),
170              disabled: classes.iconButtonDisabled
171            }}
172            disabled={disabled}
173          >
174            {React.cloneElement(closeIcon, {
175              classes: { root: classes.icon }
176            })}
177          </IconButton>
178        </Paper>
179      )
180    }
181  }
182  
183  SearchBar.defaultProps = {
184    className: '',
185    closeIcon: <ClearIcon style={{ color: grey[500] }} />,
186    disabled: false,
187    placeholder: 'Search',
188    searchIcon: <SearchIcon style={{ color: grey[500] }} />,
189    style: null,
190    value: ''
191  }
192  
193  SearchBar.propTypes = {
194    /** Whether to clear search on escape */
195    cancelOnEscape: PropTypes.bool,
196    /** Override or extend the styles applied to the component. */
197    classes: PropTypes.object.isRequired,
198    /** Custom top-level class */
199    className: PropTypes.string,
200    /** Override the close icon. */
201    closeIcon: PropTypes.node,
202    /** Disables text field. */
203    disabled: PropTypes.bool,
204    /** Fired when the search is cancelled. */
205    onCancelSearch: PropTypes.func,
206    /** Fired when the text value changes. */
207    onChange: PropTypes.func,
208    /** Fired when the search icon is clicked. */
209    onRequestSearch: PropTypes.func,
210    /** Sets placeholder text for the embedded text field. */
211    placeholder: PropTypes.string,
212    /** Override the search icon. */
213    searchIcon: PropTypes.node,
214    /** Override the inline-styles of the root element. */
215    style: PropTypes.object,
216    /** The value of the text field. */
217    value: PropTypes.string
218  }
219  
220  export default withStyles(styles)(SearchBar)