Merge branch 'origin/master' into 13666-data-explorer-mapper
[arvados-workbench2.git] / src / components / search-input / search-input.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import * as React from 'react';
6 import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles, FormControl, InputLabel, Input, InputAdornment, FormHelperText } from '@material-ui/core';
7 import SearchIcon from '@material-ui/icons/Search';
8
9 interface SearchInputDataProps {
10     value: string;
11 }
12
13 interface SearchInputActionProps {
14     onSearch: (value: string) => any;
15     debounce?: number;
16 }
17
18 type SearchInputProps = SearchInputDataProps & SearchInputActionProps & WithStyles<CssRules>;
19
20 interface SearchInputState {
21     value: string;
22 }
23
24 export const DEFAULT_SEARCH_DEBOUNCE = 1000;
25
26 class SearchInput extends React.Component<SearchInputProps> {
27
28     state: SearchInputState = {
29         value: ""
30     };
31
32     timeout: number;
33
34     render() {
35         const { classes } = this.props;
36         return <form onSubmit={this.handleSubmit}>
37             <FormControl>
38                 <InputLabel>Search</InputLabel>
39                 <Input
40                     type="text"
41                     value={this.state.value}
42                     onChange={this.handleChange}
43                     endAdornment={
44                         <InputAdornment position="end">
45                             <IconButton
46                                 onClick={this.handleSubmit}>
47                                 <SearchIcon />
48                             </IconButton>
49                         </InputAdornment>
50                     } />
51             </FormControl>
52         </form>;
53     }
54
55     componentDidMount() {
56         this.setState({ value: this.props.value });
57     }
58
59     componentWillReceiveProps(nextProps: SearchInputProps) {
60         if (nextProps.value !== this.props.value) {
61             this.setState({ value: nextProps.value });
62         }
63     }
64
65     componentWillUnmount() {
66         clearTimeout(this.timeout);
67     }
68
69     handleSubmit = (event: React.FormEvent<HTMLElement>) => {
70         event.preventDefault();
71         clearTimeout(this.timeout);
72         this.props.onSearch(this.state.value);
73     }
74
75     handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
76         clearTimeout(this.timeout);
77         this.setState({ value: event.target.value });
78         this.timeout = window.setTimeout(
79             () => this.props.onSearch(this.state.value),
80             this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
81         );
82
83     }
84
85 }
86
87 type CssRules = 'container' | 'input' | 'button';
88
89 const styles: StyleRulesCallback<CssRules> = theme => {
90     return {
91         container: {
92             position: 'relative',
93             width: '100%'
94         },
95         input: {
96             border: 'none',
97             borderRadius: theme.spacing.unit / 4,
98             boxSizing: 'border-box',
99             padding: theme.spacing.unit,
100             paddingRight: theme.spacing.unit * 4,
101             width: '100%',
102         },
103         button: {
104             position: 'absolute',
105             top: theme.spacing.unit / 2,
106             right: theme.spacing.unit / 2,
107             width: theme.spacing.unit * 3,
108             height: theme.spacing.unit * 3
109         }
110     };
111 };
112
113 export default withStyles(styles)(SearchInput);