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)