diff options
Diffstat (limited to 'packages/forms/src/select/index.tsx')
-rw-r--r-- | packages/forms/src/select/index.tsx | 460 |
1 files changed, 0 insertions, 460 deletions
diff --git a/packages/forms/src/select/index.tsx b/packages/forms/src/select/index.tsx deleted file mode 100644 index d965d3c93..000000000 --- a/packages/forms/src/select/index.tsx +++ /dev/null | |||
@@ -1,460 +0,0 @@ | |||
1 | import { | ||
2 | mdiArrowRightDropCircleOutline, | ||
3 | mdiCloseCircle, | ||
4 | mdiMagnify, | ||
5 | } from '@mdi/js'; | ||
6 | import Icon from '@mdi/react'; | ||
7 | import classnames from 'classnames'; | ||
8 | import { ChangeEvent, Component, createRef } from 'react'; | ||
9 | import injectStyle from 'react-jss'; | ||
10 | |||
11 | import { IFormField, IWithStyle } from '../typings/generic'; | ||
12 | import { Theme } from '../../../theme'; | ||
13 | |||
14 | import { Error } from '../error'; | ||
15 | import { Label } from '../label'; | ||
16 | import { Wrapper } from '../wrapper'; | ||
17 | |||
18 | interface IOptions { | ||
19 | [index: string]: string; | ||
20 | } | ||
21 | |||
22 | interface IData { | ||
23 | [index: string]: string; | ||
24 | } | ||
25 | |||
26 | interface IProps extends IFormField, IWithStyle { | ||
27 | actionText: string; | ||
28 | className?: string; | ||
29 | inputClassName?: string; | ||
30 | defaultValue?: string; | ||
31 | disabled?: boolean; | ||
32 | id?: string; | ||
33 | name: string; | ||
34 | options: IOptions; | ||
35 | value: string; | ||
36 | onChange: (event: ChangeEvent<HTMLInputElement>) => void; | ||
37 | showSearch: boolean; | ||
38 | data: IData; | ||
39 | } | ||
40 | |||
41 | interface IState { | ||
42 | open: boolean; | ||
43 | value: string; | ||
44 | needle: string; | ||
45 | selected: number; | ||
46 | options: IOptions; | ||
47 | } | ||
48 | |||
49 | let popupTransition: string = 'none'; | ||
50 | let toggleTransition: string = 'none'; | ||
51 | |||
52 | if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) { | ||
53 | popupTransition = 'all 0.3s'; | ||
54 | toggleTransition = 'transform 0.3s'; | ||
55 | } | ||
56 | |||
57 | const styles = (theme: Theme) => ({ | ||
58 | select: { | ||
59 | background: theme.selectBackground, | ||
60 | border: theme.selectBorder, | ||
61 | borderRadius: theme.borderRadiusSmall, | ||
62 | height: theme.selectHeight, | ||
63 | fontSize: theme.uiFontSize, | ||
64 | width: '100%', | ||
65 | display: 'flex', | ||
66 | alignItems: 'center', | ||
67 | textAlign: 'left', | ||
68 | color: theme.selectColor, | ||
69 | }, | ||
70 | label: { | ||
71 | '& > div': { | ||
72 | marginTop: 5, | ||
73 | }, | ||
74 | }, | ||
75 | popup: { | ||
76 | opacity: 0, | ||
77 | height: 0, | ||
78 | overflowX: 'scroll', | ||
79 | border: theme.selectBorder, | ||
80 | borderTop: 0, | ||
81 | transition: popupTransition, | ||
82 | }, | ||
83 | open: { | ||
84 | opacity: 1, | ||
85 | height: 350, | ||
86 | background: theme.selectPopupBackground, | ||
87 | }, | ||
88 | option: { | ||
89 | padding: 10, | ||
90 | borderBottom: theme.selectOptionBorder, | ||
91 | color: theme.selectOptionColor, | ||
92 | |||
93 | '&:hover': { | ||
94 | background: theme.selectOptionItemHover, | ||
95 | color: theme.selectOptionItemHoverColor, | ||
96 | }, | ||
97 | '&:active': { | ||
98 | background: theme.selectOptionItemActive, | ||
99 | color: theme.selectOptionItemActiveColor, | ||
100 | }, | ||
101 | }, | ||
102 | selected: { | ||
103 | background: theme.selectOptionItemActive, | ||
104 | color: theme.selectOptionItemActiveColor, | ||
105 | }, | ||
106 | toggle: { | ||
107 | marginLeft: 'auto', | ||
108 | fill: theme.selectToggleColor, | ||
109 | transition: toggleTransition, | ||
110 | }, | ||
111 | toggleOpened: { | ||
112 | transform: 'rotateZ(90deg)', | ||
113 | }, | ||
114 | searchContainer: { | ||
115 | display: 'flex', | ||
116 | background: theme.selectSearchBackground, | ||
117 | alignItems: 'center', | ||
118 | paddingLeft: 10, | ||
119 | color: theme.selectColor, | ||
120 | |||
121 | '& svg': { | ||
122 | fill: theme.selectSearchColor, | ||
123 | }, | ||
124 | }, | ||
125 | search: { | ||
126 | border: 0, | ||
127 | width: '100%', | ||
128 | fontSize: theme.uiFontSize, | ||
129 | background: 'none', | ||
130 | marginLeft: 10, | ||
131 | padding: [10, 0], | ||
132 | color: theme.selectSearchColor, | ||
133 | }, | ||
134 | clearNeedle: { | ||
135 | background: 'none', | ||
136 | border: 0, | ||
137 | }, | ||
138 | focused: { | ||
139 | fontWeight: 'bold', | ||
140 | background: theme.selectOptionItemHover, | ||
141 | color: theme.selectOptionItemHoverColor, | ||
142 | }, | ||
143 | hasError: { | ||
144 | borderColor: theme.brandDanger, | ||
145 | }, | ||
146 | disabled: { | ||
147 | opacity: theme.selectDisabledOpacity, | ||
148 | }, | ||
149 | }); | ||
150 | |||
151 | class SelectComponent extends Component<IProps> { | ||
152 | public static defaultProps = { | ||
153 | onChange: () => {}, | ||
154 | showLabel: true, | ||
155 | disabled: false, | ||
156 | error: '', | ||
157 | }; | ||
158 | |||
159 | state = { | ||
160 | open: false, | ||
161 | value: '', | ||
162 | needle: '', | ||
163 | selected: 0, | ||
164 | options: null, | ||
165 | }; | ||
166 | |||
167 | private componentRef = createRef<HTMLDivElement>(); | ||
168 | |||
169 | private inputRef = createRef<HTMLInputElement>(); | ||
170 | |||
171 | private searchInputRef = createRef<HTMLInputElement>(); | ||
172 | |||
173 | private scrollContainerRef = createRef<HTMLDivElement>(); | ||
174 | |||
175 | private activeOptionRef = createRef<HTMLDivElement>(); | ||
176 | |||
177 | private keyListener: any; | ||
178 | |||
179 | componentWillReceiveProps(nextProps: IProps) { | ||
180 | if (nextProps.value && nextProps.value !== this.props.value) { | ||
181 | this.setState({ | ||
182 | value: nextProps.value, | ||
183 | }); | ||
184 | } | ||
185 | } | ||
186 | |||
187 | componentDidUpdate() { | ||
188 | const { open } = this.state; | ||
189 | |||
190 | if (this.searchInputRef && this.searchInputRef.current && open) { | ||
191 | this.searchInputRef.current.focus(); | ||
192 | } | ||
193 | } | ||
194 | |||
195 | componentDidMount() { | ||
196 | if (this.inputRef && this.inputRef.current) { | ||
197 | const { data } = this.props; | ||
198 | |||
199 | if (data) { | ||
200 | Object.keys(data).map( | ||
201 | key => (this.inputRef.current!.dataset[key] = data[key]), | ||
202 | ); | ||
203 | } | ||
204 | } | ||
205 | |||
206 | window.addEventListener('keydown', this.arrowKeysHandler.bind(this), false); | ||
207 | } | ||
208 | |||
209 | componentWillMount() { | ||
210 | const { value } = this.props; | ||
211 | |||
212 | if (this.componentRef && this.componentRef.current) { | ||
213 | this.componentRef.current.removeEventListener( | ||
214 | 'keydown', | ||
215 | this.keyListener, | ||
216 | ); | ||
217 | } | ||
218 | |||
219 | if (value) { | ||
220 | this.setState({ | ||
221 | value, | ||
222 | }); | ||
223 | } | ||
224 | |||
225 | this.setFilter(); | ||
226 | } | ||
227 | |||
228 | componentWillUnmount() { | ||
229 | // eslint-disable-next-line unicorn/no-invalid-remove-event-listener | ||
230 | window.removeEventListener('keydown', this.arrowKeysHandler.bind(this)); | ||
231 | } | ||
232 | |||
233 | setFilter(needle = '') { | ||
234 | const { options } = this.props; | ||
235 | |||
236 | let filteredOptions = {}; | ||
237 | if (needle) { | ||
238 | Object.keys(options).map(key => { | ||
239 | if ( | ||
240 | key.toLocaleLowerCase().startsWith(needle.toLocaleLowerCase()) || | ||
241 | options[key] | ||
242 | .toLocaleLowerCase() | ||
243 | .startsWith(needle.toLocaleLowerCase()) | ||
244 | ) { | ||
245 | Object.assign(filteredOptions, { | ||
246 | [`${key}`]: options[key], | ||
247 | }); | ||
248 | } | ||
249 | }); | ||
250 | } else { | ||
251 | filteredOptions = options; | ||
252 | } | ||
253 | |||
254 | this.setState({ | ||
255 | needle, | ||
256 | options: filteredOptions, | ||
257 | selected: 0, | ||
258 | }); | ||
259 | } | ||
260 | |||
261 | select(key: string) { | ||
262 | this.setState(() => ({ | ||
263 | value: key, | ||
264 | open: false, | ||
265 | })); | ||
266 | |||
267 | this.setFilter(); | ||
268 | |||
269 | if (this.props.onChange) { | ||
270 | this.props.onChange(key as any); | ||
271 | } | ||
272 | } | ||
273 | |||
274 | arrowKeysHandler(e: KeyboardEvent) { | ||
275 | const { selected, open, options } = this.state; | ||
276 | |||
277 | if (!open) return; | ||
278 | |||
279 | if (e.keyCode === 38 || e.keyCode === 40) { | ||
280 | e.preventDefault(); | ||
281 | } | ||
282 | |||
283 | if (this.componentRef && this.componentRef.current) { | ||
284 | if (e.keyCode === 38 && selected > 0) { | ||
285 | this.setState((state: IState) => ({ | ||
286 | selected: state.selected - 1, | ||
287 | })); | ||
288 | } else if ( | ||
289 | e.keyCode === 40 && | ||
290 | selected < Object.keys(options!).length - 1 | ||
291 | ) { | ||
292 | this.setState((state: IState) => ({ | ||
293 | selected: state.selected + 1, | ||
294 | })); | ||
295 | } else if (e.keyCode === 13) { | ||
296 | this.select(Object.keys(options!)[selected]); | ||
297 | } | ||
298 | |||
299 | if ( | ||
300 | this.activeOptionRef && | ||
301 | this.activeOptionRef.current && | ||
302 | this.scrollContainerRef && | ||
303 | this.scrollContainerRef.current | ||
304 | ) { | ||
305 | const containerTopOffset = this.scrollContainerRef.current.offsetTop; | ||
306 | const optionTopOffset = this.activeOptionRef.current.offsetTop; | ||
307 | |||
308 | const topOffset = optionTopOffset - containerTopOffset; | ||
309 | |||
310 | this.scrollContainerRef.current.scrollTop = topOffset - 35; | ||
311 | } | ||
312 | } | ||
313 | |||
314 | switch (e.keyCode) { | ||
315 | case 37: | ||
316 | case 39: | ||
317 | case 38: | ||
318 | case 40: // Arrow keys | ||
319 | case 32: | ||
320 | break; // Space | ||
321 | default: | ||
322 | break; // do not block other keys | ||
323 | } | ||
324 | } | ||
325 | |||
326 | render() { | ||
327 | const { | ||
328 | actionText, | ||
329 | classes, | ||
330 | className, | ||
331 | defaultValue, | ||
332 | disabled, | ||
333 | error, | ||
334 | id, | ||
335 | inputClassName, | ||
336 | name, | ||
337 | label, | ||
338 | showLabel, | ||
339 | showSearch, | ||
340 | onChange, | ||
341 | required, | ||
342 | } = this.props; | ||
343 | |||
344 | const { open, needle, value, selected, options } = this.state; | ||
345 | |||
346 | let selection = ''; | ||
347 | if (!value && defaultValue && options![defaultValue]) { | ||
348 | selection = options![defaultValue]; | ||
349 | } else if (value && options![value]) { | ||
350 | selection = options![value]; | ||
351 | } else { | ||
352 | selection = actionText; | ||
353 | } | ||
354 | |||
355 | return ( | ||
356 | <Wrapper className={className} identifier="franz-select"> | ||
357 | <Label | ||
358 | title={label} | ||
359 | showLabel={showLabel} | ||
360 | htmlFor={id} | ||
361 | className={classes.label} | ||
362 | isRequired={required} | ||
363 | > | ||
364 | <div | ||
365 | className={classnames({ | ||
366 | [`${classes.hasError}`]: error, | ||
367 | [`${classes.disabled}`]: disabled, | ||
368 | })} | ||
369 | ref={this.componentRef} | ||
370 | > | ||
371 | <button | ||
372 | type="button" | ||
373 | className={classnames({ | ||
374 | [`${inputClassName}`]: inputClassName, | ||
375 | [`${classes.select}`]: true, | ||
376 | [`${classes.hasError}`]: error, | ||
377 | })} | ||
378 | onClick={ | ||
379 | !disabled | ||
380 | ? () => | ||
381 | this.setState((state: IState) => ({ | ||
382 | open: !state.open, | ||
383 | })) | ||
384 | : () => {} | ||
385 | } | ||
386 | > | ||
387 | {selection} | ||
388 | <Icon | ||
389 | path={mdiArrowRightDropCircleOutline} | ||
390 | size={0.8} | ||
391 | className={classnames({ | ||
392 | [`${classes.toggle}`]: true, | ||
393 | [`${classes.toggleOpened}`]: open, | ||
394 | })} | ||
395 | /> | ||
396 | </button> | ||
397 | {showSearch && open && ( | ||
398 | <div className={classes.searchContainer}> | ||
399 | <Icon path={mdiMagnify} size={0.8} /> | ||
400 | <input | ||
401 | type="text" | ||
402 | value={needle} | ||
403 | onChange={e => this.setFilter(e.currentTarget.value)} | ||
404 | placeholder="Search" | ||
405 | className={classes.search} | ||
406 | ref={this.searchInputRef} | ||
407 | /> | ||
408 | {needle && ( | ||
409 | <button | ||
410 | type="button" | ||
411 | className={classes.clearNeedle} | ||
412 | onClick={() => this.setFilter()} | ||
413 | > | ||
414 | <Icon path={mdiCloseCircle} size={0.7} /> | ||
415 | </button> | ||
416 | )} | ||
417 | </div> | ||
418 | )} | ||
419 | <div | ||
420 | className={classnames({ | ||
421 | [`${classes.popup}`]: true, | ||
422 | [`${classes.open}`]: open, | ||
423 | })} | ||
424 | ref={this.scrollContainerRef} | ||
425 | > | ||
426 | {Object.keys(options!).map((key, i) => ( | ||
427 | <div | ||
428 | key={key} | ||
429 | onClick={() => this.select(key)} | ||
430 | className={classnames({ | ||
431 | [`${classes.option}`]: true, | ||
432 | [`${classes.selected}`]: options![key] === selection, | ||
433 | [`${classes.focused}`]: selected === i, | ||
434 | })} | ||
435 | onMouseOver={() => this.setState({ selected: i })} | ||
436 | ref={selected === i ? this.activeOptionRef : null} | ||
437 | > | ||
438 | {options![key]} | ||
439 | </div> | ||
440 | ))} | ||
441 | </div> | ||
442 | </div> | ||
443 | <input | ||
444 | className={classes.input} | ||
445 | id={id} | ||
446 | name={name} | ||
447 | type="hidden" | ||
448 | defaultValue={value} | ||
449 | onChange={onChange} | ||
450 | disabled={disabled} | ||
451 | ref={this.inputRef} | ||
452 | /> | ||
453 | </Label> | ||
454 | {error && <Error message={error} />} | ||
455 | </Wrapper> | ||
456 | ); | ||
457 | } | ||
458 | } | ||
459 | |||
460 | export const Select = injectStyle(styles)(SelectComponent); | ||