aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/quickSwitch/Component.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/quickSwitch/Component.js')
-rw-r--r--src/features/quickSwitch/Component.js291
1 files changed, 291 insertions, 0 deletions
diff --git a/src/features/quickSwitch/Component.js b/src/features/quickSwitch/Component.js
new file mode 100644
index 000000000..3217a3d93
--- /dev/null
+++ b/src/features/quickSwitch/Component.js
@@ -0,0 +1,291 @@
1import React, { Component, createRef } from 'react';
2import PropTypes from 'prop-types';
3import { observer, inject } from 'mobx-react';
4import { reaction } from 'mobx';
5import injectSheet from 'react-jss';
6import { defineMessages, intlShape } from 'react-intl';
7import { Input } from '@meetfranz/forms';
8
9import Modal from '../../components/ui/Modal';
10import { state as ModalState } from '.';
11import ServicesStore from '../../stores/ServicesStore';
12
13const messages = defineMessages({
14 search: {
15 id: 'feature.quickSwitch.search',
16 defaultMessage: '!!!Search...',
17 },
18 info: {
19 id: 'feature.quickSwitch.info',
20 defaultMessage: '!!!Select a service with TAB, ↑ and ↓. Open a service with ENTER.',
21 },
22});
23
24const styles = theme => ({
25 modal: {
26 width: '80%',
27 maxWidth: 600,
28 background: theme.styleTypes.primary.contrast,
29 color: theme.styleTypes.primary.accent,
30 paddingTop: 30,
31 },
32 services: {
33 width: '100%',
34 marginTop: 30,
35 },
36 service: {
37 background: theme.styleTypes.primary.contrast,
38 color: theme.colorText,
39 borderColor: theme.styleTypes.primary.accent,
40 borderStyle: 'solid',
41 borderWidth: 1,
42 borderRadius: 6,
43 padding: '3px 25px',
44 marginBottom: 10,
45 display: 'flex',
46 alignItems: 'center',
47 '&:hover': {
48 background: theme.styleTypes.primary.accent,
49 color: theme.styleTypes.primary.contrast,
50 cursor: 'pointer',
51 },
52 },
53 activeService: {
54 background: theme.styleTypes.primary.accent,
55 color: theme.styleTypes.primary.contrast,
56 cursor: 'pointer',
57 },
58 serviceIcon: {
59 width: 50,
60 height: 50,
61 paddingRight: 20,
62 objectFit: 'contain',
63 },
64});
65
66export default @injectSheet(styles) @inject('stores', 'actions') @observer class QuickSwitchModal extends Component {
67 static propTypes = {
68 classes: PropTypes.object.isRequired,
69 };
70
71 static contextTypes = {
72 intl: intlShape,
73 };
74
75 state = {
76 selected: 0,
77 search: '',
78 wasPrevVisible: false,
79 }
80
81 ARROW_DOWN = 40;
82
83 ARROW_UP = 38;
84
85 ENTER = 13;
86
87 TAB = 9;
88
89 inputRef = createRef();
90
91 constructor(props) {
92 super(props);
93
94 this._handleKeyDown = this._handleKeyDown.bind(this);
95 this._handleSearchUpdate = this._handleSearchUpdate.bind(this);
96 this._handleVisibilityChange = this._handleVisibilityChange.bind(this);
97 this.openService = this.openService.bind(this);
98
99 reaction(
100 () => ModalState.isModalVisible,
101 this._handleVisibilityChange,
102 );
103 }
104
105 // Add global keydown listener when component mounts
106 componentDidMount() {
107 document.addEventListener('keydown', this._handleKeyDown);
108 }
109
110 // Remove global keydown listener when component unmounts
111 componentWillUnmount() {
112 document.removeEventListener('keydown', this._handleKeyDown);
113 }
114
115 // Get currently shown services
116 services() {
117 let services = this.props.stores.services.allDisplayed;
118 if (this.state.search) {
119 // Apply simple search algorythm
120 services = services.filter(service => service.name.toLowerCase().includes(this.state.search.toLowerCase()));
121 }
122
123 return services;
124 }
125
126 openService(index) {
127 // Open service
128 const service = this.services()[index];
129 this.props.actions.service.setActive({ serviceId: service.id });
130
131 // Reset and close modal
132 this.setState({
133 search: '',
134 });
135 this.close();
136 }
137
138 // Change the selected service
139 // factor should be -1 or 1
140 changeSelected(factor) {
141 this.setState((state) => {
142 let newSelected = state.selected + factor;
143 const services = this.services().length;
144
145 // Roll around when on edge of list
146 if (state.selected < 1 && factor === -1) {
147 newSelected = services - 1;
148 } else if ((state.selected >= (services - 1)) && factor === 1) {
149 newSelected = 0;
150 }
151
152 return {
153 selected: newSelected,
154 };
155 });
156 }
157
158 // Handle global key presses to change the selection
159 _handleKeyDown(event) {
160 if (ModalState.isModalVisible) {
161 switch (event.keyCode) {
162 case this.ARROW_DOWN:
163 this.changeSelected(1);
164 break;
165 case this.TAB:
166 this.changeSelected(1);
167 break;
168 case this.ARROW_UP:
169 this.changeSelected(-1);
170 break;
171 case this.ENTER:
172 this.openService(this.state.selected);
173 break;
174 default:
175 break;
176 }
177 }
178 }
179
180 // Handle update of the search query
181 _handleSearchUpdate(evt) {
182 this.setState({
183 search: evt.target.value,
184 });
185 }
186
187 _handleVisibilityChange() {
188 const { isModalVisible } = ModalState;
189
190 if (isModalVisible && !this.state.wasPrevVisible) {
191 // Set focus back on current window if its in a service
192 // TODO: Find a way to gain back focus
193
194 // The input "focus" attribute will only work on first modal open
195 // Manually add focus to the input element
196 // Wrapped inside timeout to let the modal render first
197 setTimeout(() => {
198 if (this.inputRef.current) {
199 this.inputRef.current.getElementsByTagName('input')[0].focus();
200 }
201 }, 10);
202
203 this.setState({
204 wasPrevVisible: true,
205 });
206 } else if (!isModalVisible && this.state.wasPrevVisible) {
207 // Manually blur focus from the input element to prevent
208 // search query change when modal not visible
209 setTimeout(() => {
210 if (this.inputRef.current) {
211 this.inputRef.current.getElementsByTagName('input')[0].blur();
212 }
213 }, 100);
214
215 this.setState({
216 wasPrevVisible: false,
217 });
218 }
219 }
220
221 // Close this modal
222 close() {
223 ModalState.isModalVisible = false;
224 }
225
226 render() {
227 const { isModalVisible } = ModalState;
228
229 const {
230 openService,
231 } = this;
232
233 const {
234 classes,
235 } = this.props;
236
237 const services = this.services();
238
239 const { intl } = this.context;
240
241 return (
242 <Modal
243 isOpen={isModalVisible}
244 className={classes.modal}
245 shouldCloseOnOverlayClick
246 close={this.close.bind(this)}
247 >
248 <div ref={this.inputRef}>
249 <Input
250 placeholder={intl.formatMessage(messages.search)}
251 focus
252 value={this.state.search}
253 onChange={this._handleSearchUpdate}
254 />
255 </div>
256
257 <div className={classes.services}>
258 { services.map((service, index) => (
259 <div
260 className={`${classes.service} ${this.state.selected === index ? classes.activeService : ''}`}
261 onClick={() => openService(index)}
262 key={service.id}
263 >
264 <img
265 src={service.icon}
266 className={classes.serviceIcon}
267 alt={service.recipe.name}
268 />
269 <div>
270 { service.name }
271 </div>
272 </div>
273 ))}
274 </div>
275
276 <p>{intl.formatMessage(messages.info)}</p>
277 </Modal>
278 );
279 }
280}
281
282QuickSwitchModal.wrappedComponent.propTypes = {
283 stores: PropTypes.shape({
284 services: PropTypes.instanceOf(ServicesStore).isRequired,
285 }).isRequired,
286 actions: PropTypes.shape({
287 service: PropTypes.shape({
288 setActive: PropTypes.func.isRequired,
289 }).isRequired,
290 }).isRequired,
291};