├── .babelrc ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── demo.gif ├── package-lock.json ├── package.json ├── src ├── components │ └── SearchBar │ │ ├── SearchBar.js │ │ ├── SearchBar.md │ │ └── index.js ├── index.d.ts ├── index.js └── styleguide │ └── Wrapper.js └── styleguide.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react", "@babel/preset-env"], 3 | "plugins": [["@babel/plugin-proposal-class-properties", { "loose": true }]] 4 | } 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [leMaik,saschb2b] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | build 4 | lib 5 | styleguide/* 6 | storybook/* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | src/* 4 | styleguide/* 5 | .storybook/* 6 | storybook/* 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2020 Wertarbyte and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Material Search Bar 2 | 3 | ![Example](demo.gif) 4 | 5 | See this component in [action](https://teamwertarbyte.github.io/material-ui-search-bar/) 6 | 7 | ## Installation 8 | 9 | ```shell 10 | npm i --save material-ui-search-bar 11 | ``` 12 | 13 | Note: If you're still using Material-UI v3, please install v0.x of the search bar using `npm i --save material-ui-search-bar@beta` 14 | 15 | ## Usage 16 | 17 | The `SearchBar` is a _controlled input_, meaning that _you_ need to keep the input state. This allows for much flexibility, e.g. you can change and clear the search input just by changing its props. 18 | 19 | ```js 20 | import SearchBar from "material-ui-search-bar"; 21 | // *snip* 22 | 23 | return ( 24 | this.setState({ value: newValue })} 27 | onRequestSearch={() => doSomethingWith(this.state.value)} 28 | /> 29 | ); 30 | ``` 31 | 32 | ### SearchBar Properties 33 | 34 | | Name | Type | Default | Description | 35 | | --------------- | -------- | --------------------------------------------- | ------------------------------------------------------- | 36 | | cancelOnEscape | `bool` | | Whether to clear search on escape | 37 | | classes\* | `object` | | Override or extend the styles applied to the component. | 38 | | className | `string` | `''` | Custom top-level class | 39 | | closeIcon | `node` | `` | Override the close icon. | 40 | | disabled | `bool` | `false` | Disables text field. | 41 | | onCancelSearch | `func` | | Fired when the search is cancelled. | 42 | | onChange | `func` | | Fired when the text value changes. | 43 | | onRequestSearch | `func` | | Fired when the search icon is clicked. | 44 | | placeholder | `string` | `'Search'` | Sets placeholder text for the embedded text field. | 45 | | searchIcon | `node` | `` | Override the search icon. | 46 | | style | `object` | `null` | Override the inline-styles of the root element. | 47 | | value | `string` | `''` | The value of the text field. | 48 | 49 | \* required property 50 | 51 | Any other properties supplied will be spread to the underlying [`Input`](https://material-ui.com/api/input/#input) component. 52 | 53 | ## License 54 | 55 | The files included in this repository are licensed under the MIT license. 56 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamWertarbyte/material-ui-search-bar/cd03c4114f296c177d3448b312166ec2736ba471/demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-ui-search-bar", 3 | "version": "1.0.1", 4 | "description": "Material style search bar", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "babel src -d lib --copy-files", 9 | "prepare": "babel src -d lib --copy-files", 10 | "styleguide": "styleguidist server", 11 | "styleguide:build": "styleguidist build", 12 | "publish-docs": "npm run styleguide:build && npm run gh-pages", 13 | "gh-pages": "./node_modules/.bin/gh-pages -d styleguide", 14 | "prettier": "prettier --write \"**/*.{json,md,js}\"" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/TeamWertarbyte/material-ui-search-bar.git" 19 | }, 20 | "keywords": [ 21 | "react", 22 | "material", 23 | "material-design", 24 | "material-ui", 25 | "search", 26 | "react-component" 27 | ], 28 | "author": "Wertarbyte (https://wertarbyte.com)", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/TeamWertarbyte/material-ui-search-bar/issues" 32 | }, 33 | "homepage": "https://github.com/TeamWertarbyte/material-ui-search-bar#readme", 34 | "devDependencies": { 35 | "@babel/cli": "^7.10.5", 36 | "@babel/core": "^7.11.0", 37 | "@babel/plugin-proposal-class-properties": "^7.10.4", 38 | "@babel/preset-env": "^7.11.0", 39 | "@babel/preset-react": "^7.10.4", 40 | "@material-ui/core": "^4.11.0", 41 | "@material-ui/icons": "^4.9.1", 42 | "gh-pages": "^1.2.0", 43 | "prettier": "^2.0.5", 44 | "react": "^16.10.2", 45 | "react-dom": "^16.10.2", 46 | "react-styleguidist": "^11.0.8", 47 | "webpack": "^4.44.1", 48 | "webpack-blocks": "^2.1.0" 49 | }, 50 | "peerDependencies": { 51 | "@material-ui/core": "^4.0.0", 52 | "@material-ui/icons": "^4.0.0", 53 | "react": ">=16.8.0" 54 | }, 55 | "dependencies": { 56 | "classnames": "^2.2.5", 57 | "prop-types": "^15.5.8" 58 | }, 59 | "standard": { 60 | "parser": "babel-eslint" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/SearchBar/SearchBar.js: -------------------------------------------------------------------------------- 1 | import React 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 withStyles from "@material-ui/core/styles/withStyles"; 9 | import classNames from "classnames"; 10 | 11 | const styles = (theme) => ({ 12 | root: { 13 | height: theme.spacing(6), 14 | display: "flex", 15 | justifyContent: "space-between", 16 | }, 17 | iconButton: { 18 | color: theme.palette.action.active, 19 | transform: "scale(1, 1)", 20 | transition: theme.transitions.create(["transform", "color"], { 21 | duration: theme.transitions.duration.shorter, 22 | easing: theme.transitions.easing.easeInOut, 23 | }), 24 | }, 25 | iconButtonHidden: { 26 | transform: "scale(0, 0)", 27 | "& > $icon": { 28 | opacity: 0, 29 | }, 30 | }, 31 | searchIconButton: { 32 | marginRight: theme.spacing(-6), 33 | }, 34 | icon: { 35 | transition: theme.transitions.create(["opacity"], { 36 | duration: theme.transitions.duration.shorter, 37 | easing: theme.transitions.easing.easeInOut, 38 | }), 39 | }, 40 | input: { 41 | width: "100%", 42 | }, 43 | searchContainer: { 44 | margin: "auto 16px", 45 | width: `calc(100% - ${theme.spacing(6 + 4)}px)`, // 6 button + 4 margin 46 | }, 47 | }); 48 | 49 | /** 50 | * Material design search bar 51 | * @see [Search patterns](https://material.io/archive/guidelines/patterns/search.html) 52 | */ 53 | const SearchBar = React.forwardRef( 54 | ( 55 | { 56 | cancelOnEscape, 57 | className, 58 | classes, 59 | closeIcon, 60 | disabled, 61 | onCancelSearch, 62 | onRequestSearch, 63 | searchIcon, 64 | style, 65 | ...inputProps 66 | }, 67 | ref 68 | ) => { 69 | const inputRef = React.useRef(); 70 | const [value, setValue] = React.useState(inputProps.value); 71 | 72 | React.useEffect(() => { 73 | setValue(inputProps.value); 74 | }, [inputProps.value]); 75 | 76 | const handleFocus = React.useCallback( 77 | (e) => { 78 | if (inputProps.onFocus) { 79 | inputProps.onFocus(e); 80 | } 81 | }, 82 | [inputProps.onFocus] 83 | ); 84 | 85 | const handleBlur = React.useCallback( 86 | (e) => { 87 | setValue((v) => v.trim()); 88 | if (inputProps.onBlur) { 89 | inputProps.onBlur(e); 90 | } 91 | }, 92 | [inputProps.onBlur] 93 | ); 94 | 95 | const handleInput = React.useCallback( 96 | (e) => { 97 | setValue(e.target.value); 98 | if (inputProps.onChange) { 99 | inputProps.onChange(e.target.value); 100 | } 101 | }, 102 | [inputProps.onChange] 103 | ); 104 | 105 | const handleCancel = React.useCallback(() => { 106 | setValue(""); 107 | if (onCancelSearch) { 108 | onCancelSearch(); 109 | } 110 | }, [onCancelSearch]); 111 | 112 | const handleRequestSearch = React.useCallback(() => { 113 | if (onRequestSearch) { 114 | onRequestSearch(value); 115 | } 116 | }, [onRequestSearch, value]); 117 | 118 | const handleKeyUp = React.useCallback( 119 | (e) => { 120 | if (e.charCode === 13 || e.key === "Enter") { 121 | handleRequestSearch(); 122 | } else if ( 123 | cancelOnEscape && 124 | (e.charCode === 27 || e.key === "Escape") 125 | ) { 126 | handleCancel(); 127 | } 128 | if (inputProps.onKeyUp) { 129 | inputProps.onKeyUp(e); 130 | } 131 | }, 132 | [handleRequestSearch, cancelOnEscape, handleCancel, inputProps.onKeyUp] 133 | ); 134 | 135 | React.useImperativeHandle(ref, () => ({ 136 | focus: () => { 137 | inputRef.current.focus(); 138 | }, 139 | blur: () => { 140 | inputRef.current.blur(); 141 | }, 142 | })); 143 | 144 | return ( 145 | 146 |
147 | 160 |
161 | 168 | {React.cloneElement(searchIcon, { 169 | classes: { root: classes.icon }, 170 | })} 171 | 172 | 179 | {React.cloneElement(closeIcon, { 180 | classes: { root: classes.icon }, 181 | })} 182 | 183 |
184 | ); 185 | } 186 | ); 187 | 188 | SearchBar.defaultProps = { 189 | className: "", 190 | closeIcon: , 191 | disabled: false, 192 | placeholder: "Search", 193 | searchIcon: , 194 | style: null, 195 | value: "", 196 | }; 197 | 198 | SearchBar.propTypes = { 199 | /** Whether to clear search on escape */ 200 | cancelOnEscape: PropTypes.bool, 201 | /** Override or extend the styles applied to the component. */ 202 | classes: PropTypes.object.isRequired, 203 | /** Custom top-level class */ 204 | className: PropTypes.string, 205 | /** Override the close icon. */ 206 | closeIcon: PropTypes.node, 207 | /** Disables text field. */ 208 | disabled: PropTypes.bool, 209 | /** Fired when the search is cancelled. */ 210 | onCancelSearch: PropTypes.func, 211 | /** Fired when the text value changes. */ 212 | onChange: PropTypes.func, 213 | /** Fired when the search icon is clicked. */ 214 | onRequestSearch: PropTypes.func, 215 | /** Sets placeholder text for the embedded text field. */ 216 | placeholder: PropTypes.string, 217 | /** Override the search icon. */ 218 | searchIcon: PropTypes.node, 219 | /** Override the inline-styles of the root element. */ 220 | style: PropTypes.object, 221 | /** The value of the text field. */ 222 | value: PropTypes.string, 223 | }; 224 | 225 | export default withStyles(styles)(SearchBar); 226 | -------------------------------------------------------------------------------- /src/components/SearchBar/SearchBar.md: -------------------------------------------------------------------------------- 1 | SearchBar example: 2 | 3 | ``` 4 | console.log('onChange')} 6 | onRequestSearch={() => console.log('onRequestSearch')} 7 | style={{ 8 | margin: '0 auto', 9 | maxWidth: 800 10 | }} 11 | cancelOnEscape 12 | /> 13 | ``` 14 | 15 | SearchBar Disabled example: 16 | 17 | ``` 18 | console.log('onChange')} 20 | onRequestSearch={() => console.log('onRequestSearch')} 21 | style={{ 22 | margin: '0 auto', 23 | maxWidth: 800 24 | }} 25 | disabled 26 | /> 27 | ``` 28 | 29 | Blur on search: 30 | 31 | ``` 32 | let ref = React.createRef(); 33 | 34 | console.log('onChange')} 37 | onRequestSearch={() => { 38 | ref.current.blur() 39 | console.log('onRequestSearch') 40 | }} 41 | style={{ 42 | margin: '0 auto', 43 | maxWidth: 800 44 | }} 45 | /> 46 | ``` 47 | -------------------------------------------------------------------------------- /src/components/SearchBar/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SearchBar"; 2 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Material Search Bar 2 | // Project: https://github.com/TeamWertarbyte/material-ui-search-bar 3 | // Original definitions by: [Tyler Kellogg] 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | 6 | /// 7 | 8 | declare module 'material-ui-search-bar' { 9 | export interface SearchBarProps { 10 | /** 11 | * Whether to clear search on escape. 12 | */ 13 | cancelOnEscape?: boolean; 14 | /** 15 | * Override or extend the styles applied to the component. 16 | */ 17 | classes?: { 18 | root?: string, 19 | iconButton?: string, 20 | iconButtonHidden?: string, 21 | iconButtonDisabled?: string, 22 | searchIconButton?: string, 23 | icon?: string, 24 | input?: string, 25 | searchContainer?: string 26 | }; 27 | /** 28 | * Custom top-level class. 29 | */ 30 | className?: string; 31 | /** 32 | * Override the close icon. 33 | */ 34 | closeIcon?: JSX.Element; 35 | /** 36 | * Disables text field. 37 | */ 38 | disabled?: boolean; 39 | /** 40 | * Fired when the search is cancelled. 41 | */ 42 | onCancelSearch?(): void; 43 | /** 44 | * Fired when the text value changes. 45 | */ 46 | onChange?(query: string): void; 47 | /** 48 | * Fired when the search icon is clicked. 49 | */ 50 | onRequestSearch?(): void; 51 | /** 52 | * Sets placeholder for the embedded text field. 53 | */ 54 | placeholder?: string; 55 | /** 56 | * Override the search icon. 57 | */ 58 | searchIcon?: JSX.Element; 59 | /** 60 | * Override the inline-styles of the root element. 61 | */ 62 | style?: object; 63 | /** 64 | * The value of the text field. 65 | */ 66 | value?: string; 67 | } 68 | 69 | const SearchBar: React.ComponentType; 70 | export default SearchBar; 71 | } 72 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./components/SearchBar"; 2 | -------------------------------------------------------------------------------- /src/styleguide/Wrapper.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | export default class Wrapper extends Component { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /styleguide.config.js: -------------------------------------------------------------------------------- 1 | const { createConfig, babel } = require("webpack-blocks"); 2 | 3 | module.exports = { 4 | skipComponentsWithoutExample: true, 5 | components: "src/components/**/[A-Z]*.js", 6 | webpackConfig: createConfig([babel()]), 7 | }; 8 | --------------------------------------------------------------------------------