Replace ClickAwayListener with Popover
[arvados-workbench2.git] / src / views-components / search-bar / search-bar-view.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 {
7     IconButton,
8     Paper,
9     StyleRulesCallback,
10     withStyles,
11     WithStyles,
12     Tooltip,
13     InputAdornment, Input,
14     Popover
15 } from '@material-ui/core';
16 import SearchIcon from '@material-ui/icons/Search';
17 import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
18 import { ArvadosTheme } from '~/common/custom-theme';
19 import { SearchView } from '~/store/search-bar/search-bar-reducer';
20 import {
21     SearchBarBasicView,
22     SearchBarBasicViewDataProps,
23     SearchBarBasicViewActionProps
24 } from '~/views-components/search-bar/search-bar-basic-view';
25 import {
26     SearchBarAutocompleteView,
27     SearchBarAutocompleteViewDataProps,
28     SearchBarAutocompleteViewActionProps
29 } from '~/views-components/search-bar/search-bar-autocomplete-view';
30 import {
31     SearchBarAdvancedView,
32     SearchBarAdvancedViewDataProps,
33     SearchBarAdvancedViewActionProps
34 } from '~/views-components/search-bar/search-bar-advanced-view';
35 import { KEY_CODE_DOWN, KEY_CODE_ESC, KEY_CODE_UP, KEY_ENTER } from "~/common/codes";
36
37 type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'view';
38
39 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
40     return {
41         container: {
42             position: 'relative',
43             width: '100%',
44             borderRadius: theme.spacing.unit / 2
45         },
46         containerSearchViewOpened: {
47             position: 'relative',
48             width: '100%',
49             borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`
50         },
51         input: {
52             border: 'none',
53             padding: `0`
54         },
55         view: {
56             position: 'absolute',
57             width: '100%',
58             zIndex: 1
59         }
60     };
61 };
62
63 export type SearchBarDataProps = SearchBarViewDataProps
64     & SearchBarAutocompleteViewDataProps
65     & SearchBarAdvancedViewDataProps
66     & SearchBarBasicViewDataProps;
67
68 interface SearchBarViewDataProps {
69     searchValue: string;
70     currentView: string;
71     isPopoverOpen: boolean;
72     debounce?: number;
73 }
74
75 export type SearchBarActionProps = SearchBarViewActionProps
76     & SearchBarAutocompleteViewActionProps
77     & SearchBarAdvancedViewActionProps
78     & SearchBarBasicViewActionProps;
79
80 interface SearchBarViewActionProps {
81     onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
82     onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
83     onSetView: (currentView: string) => void;
84     closeView: () => void;
85     openSearchView: () => void;
86     loadRecentQueries: () => string[];
87     moveUp: () => void;
88     moveDown: () => void;
89     setAdvancedDataFromSearchValue: (search: string) => void;
90 }
91
92 type SearchBarViewProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
93
94 const handleKeyDown = (e: React.KeyboardEvent, props: SearchBarViewProps) => {
95     if (e.keyCode === KEY_CODE_DOWN) {
96         e.preventDefault();
97         if (!props.isPopoverOpen) {
98             props.onSetView(SearchView.AUTOCOMPLETE);
99             props.openSearchView();
100         } else {
101             props.moveDown();
102         }
103     } else if (e.keyCode === KEY_CODE_UP) {
104         e.preventDefault();
105         props.moveUp();
106     } else if (e.keyCode === KEY_CODE_ESC) {
107         e.preventDefault();
108         props.closeView();
109     } else if (e.keyCode === KEY_ENTER) {
110         if (props.currentView === SearchView.BASIC) {
111             e.preventDefault();
112             props.onSearch(props.selectedItem.query);
113         } else if (props.currentView === SearchView.AUTOCOMPLETE) {
114             if (props.selectedItem.id !== props.searchValue) {
115                 e.preventDefault();
116                 props.navigateTo(props.selectedItem.id);
117             }
118         }
119     }
120 };
121
122 const handleInputClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
123     if (props.searchValue) {
124         props.onSetView(SearchView.AUTOCOMPLETE);
125         props.openSearchView();
126     } else {
127         props.closeView();
128     }
129 };
130
131 const handleDropdownClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
132     e.stopPropagation();
133     if (props.isPopoverOpen) {
134         if (props.currentView === SearchView.ADVANCED) {
135             props.closeView();
136         } else {
137             props.setAdvancedDataFromSearchValue(props.searchValue);
138             props.onSetView(SearchView.ADVANCED);
139         }
140     } else {
141         props.setAdvancedDataFromSearchValue(props.searchValue);
142         props.onSetView(SearchView.ADVANCED);
143     }
144 };
145
146 export const SearchBarView = withStyles(styles)(
147     class SearchBarView extends React.Component<SearchBarViewProps> {
148
149         viewAnchorRef = React.createRef<HTMLDivElement>();
150
151         render() {
152             const { children, ...props } = this.props;
153             const { classes, isPopoverOpen } = props;
154             return (
155                 <Paper
156                     className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container}>
157                     <div ref={this.viewAnchorRef}>
158                         <form onSubmit={props.onSubmit}>
159                             <Input
160                                 className={classes.input}
161                                 onChange={props.onChange}
162                                 placeholder="Search"
163                                 value={props.searchValue}
164                                 fullWidth={true}
165                                 disableUnderline={true}
166                                 onClick={e => handleInputClick(e, props)}
167                                 onKeyDown={e => handleKeyDown(e, props)}
168                                 startAdornment={
169                                     <InputAdornment position="start">
170                                         <Tooltip title='Search'>
171                                             <IconButton type="submit">
172                                                 <SearchIcon />
173                                             </IconButton>
174                                         </Tooltip>
175                                     </InputAdornment>
176                                 }
177                                 endAdornment={
178                                     <InputAdornment position="end">
179                                         <Tooltip title='Advanced search'>
180                                             <IconButton onClick={e => handleDropdownClick(e, props)}>
181                                                 <ArrowDropDownIcon />
182                                             </IconButton>
183                                         </Tooltip>
184                                     </InputAdornment>
185                                 } />
186                         </form>
187                     </div>
188                     <Popover
189                         PaperProps={{
190                             style: {
191                                 width: this.getViewWidth(),
192                                 borderRadius: '0 0 4px 4px',
193                             }
194                         }}
195                         anchorEl={this.viewAnchorRef.current}
196                         anchorOrigin={{
197                             vertical: 'bottom',
198                             horizontal: 'left',
199                         }}
200                         disableAutoFocus
201                         open={isPopoverOpen}
202                         onClose={props.closeView}>
203                         {getView({ ...props })}
204                     </Popover>
205                 </Paper >
206             );
207         }
208
209         getViewWidth() {
210             const { current } = this.viewAnchorRef;
211             return current ? current.offsetWidth : 0;
212         }
213     }
214
215 );
216
217 const getView = (props: SearchBarViewProps) => {
218     switch (props.currentView) {
219         case SearchView.AUTOCOMPLETE:
220             return <SearchBarAutocompleteView
221                 navigateTo={props.navigateTo}
222                 searchResults={props.searchResults}
223                 searchValue={props.searchValue}
224                 selectedItem={props.selectedItem} />;
225         case SearchView.ADVANCED:
226             return <SearchBarAdvancedView
227                 closeAdvanceView={props.closeAdvanceView}
228                 tags={props.tags}
229                 saveQuery={props.saveQuery} />;
230         default:
231             return <SearchBarBasicView
232                 onSetView={props.onSetView}
233                 onSearch={props.onSearch}
234                 loadRecentQueries={props.loadRecentQueries}
235                 savedQueries={props.savedQueries}
236                 deleteSavedQuery={props.deleteSavedQuery}
237                 editSavedQuery={props.editSavedQuery}
238                 selectedItem={props.selectedItem} />;
239     }
240 };