Add arrow navigation in autocomplete view
[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     ClickAwayListener
15 } from '@material-ui/core';
16 import SearchIcon from '@material-ui/icons/Search';
17 import { ArvadosTheme } from '~/common/custom-theme';
18 import { SearchView } from '~/store/search-bar/search-bar-reducer';
19 import {
20     SearchBarBasicView,
21     SearchBarBasicViewDataProps,
22     SearchBarBasicViewActionProps
23 } from '~/views-components/search-bar/search-bar-basic-view';
24 import {
25     SearchBarAutocompleteView,
26     SearchBarAutocompleteViewDataProps,
27     SearchBarAutocompleteViewActionProps
28 } from '~/views-components/search-bar/search-bar-autocomplete-view';
29 import {
30     SearchBarAdvancedView,
31     SearchBarAdvancedViewDataProps,
32     SearchBarAdvancedViewActionProps
33 } from '~/views-components/search-bar/search-bar-advanced-view';
34 import { KEY_CODE_DOWN, KEY_CODE_ESC, KEY_CODE_UP, KEY_ENTER } from "~/common/codes";
35
36 type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'view';
37
38 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
39     return {
40         container: {
41             position: 'relative',
42             width: '100%',
43             borderRadius: theme.spacing.unit / 2
44         },
45         containerSearchViewOpened: {
46             position: 'relative',
47             width: '100%',
48             borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`
49         },
50         input: {
51             border: 'none',
52             padding: `0px ${theme.spacing.unit}px`
53         },
54         view: {
55             position: 'absolute',
56             width: '100%',
57             zIndex: 1
58         }
59     };
60 };
61
62 export type SearchBarDataProps = SearchBarViewDataProps
63     & SearchBarAutocompleteViewDataProps
64     & SearchBarAdvancedViewDataProps
65     & SearchBarBasicViewDataProps;
66
67 interface SearchBarViewDataProps {
68     searchValue: string;
69     currentView: string;
70     isPopoverOpen: boolean;
71     debounce?: number;
72 }
73
74 export type SearchBarActionProps = SearchBarViewActionProps
75     & SearchBarAutocompleteViewActionProps
76     & SearchBarAdvancedViewActionProps
77     & SearchBarBasicViewActionProps;
78
79 interface SearchBarViewActionProps {
80     onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
81     onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
82     onSetView: (currentView: string) => void;
83     closeView: () => void;
84     openSearchView: () => void;
85     loadRecentQueries: () => string[];
86     moveUp: () => void;
87     moveDown: () => void;
88 }
89
90 type SearchBarViewProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
91
92 export const SearchBarView = withStyles(styles)(
93     (props : SearchBarViewProps) => {
94         const { classes, isPopoverOpen } = props;
95         return (
96             <ClickAwayListener onClickAway={props.closeView}>
97                 <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
98                     <form onSubmit={props.onSubmit}>
99                         <Input
100                             className={classes.input}
101                             onChange={props.onChange}
102                             placeholder="Search"
103                             value={props.searchValue}
104                             fullWidth={true}
105                             disableUnderline={true}
106                             onClick={props.openSearchView}
107                             onKeyDown={e => {
108                                 if (e.keyCode === KEY_CODE_DOWN) {
109                                     e.preventDefault();
110                                     if (!isPopoverOpen) {
111                                         props.openSearchView();
112                                     } else {
113                                         props.moveDown();
114                                     }
115                                 } else if (e.keyCode === KEY_CODE_UP) {
116                                     e.preventDefault();
117                                     props.moveUp();
118                                 } else if (e.keyCode === KEY_CODE_ESC) {
119                                     props.closeView();
120                                 } else if (e.keyCode === KEY_ENTER) {
121                                     if (props.selectedItem !== props.searchValue) {
122                                         e.preventDefault();
123                                         props.navigateTo(props.selectedItem);
124                                     }
125                                 }
126                             }}
127                             endAdornment={
128                                 <InputAdornment position="end">
129                                     <Tooltip title='Search'>
130                                         <IconButton type="submit">
131                                             <SearchIcon />
132                                         </IconButton>
133                                     </Tooltip>
134                                 </InputAdornment>
135                             } />
136                     </form>
137                     <div className={classes.view}>
138                         {isPopoverOpen && getView({...props})}
139                     </div>
140                 </Paper >
141             </ClickAwayListener>
142         );
143     }
144 );
145
146 const getView = (props: SearchBarViewProps) => {
147     switch (props.currentView) {
148         case SearchView.AUTOCOMPLETE:
149             return <SearchBarAutocompleteView
150                 navigateTo={props.navigateTo}
151                 searchResults={props.searchResults}
152                 searchValue={props.searchValue}
153                 selectedItem={props.selectedItem} />;
154         case SearchView.ADVANCED:
155             return <SearchBarAdvancedView
156                 closeAdvanceView={props.closeAdvanceView}
157                 tags={props.tags} />;
158         default:
159             return <SearchBarBasicView
160                 onSetView={props.onSetView}
161                 onSearch={props.onSearch}
162                 loadRecentQueries={props.loadRecentQueries}
163                 savedQueries={props.savedQueries}
164                 deleteSavedQuery={props.deleteSavedQuery}
165                 editSavedQuery={props.editSavedQuery} />;
166     }
167 };