aboutsummaryrefslogtreecommitdiffstats
path: root/packages/forms/src/select/index.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/forms/src/select/index.tsx')
-rw-r--r--packages/forms/src/select/index.tsx460
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 @@
1import {
2 mdiArrowRightDropCircleOutline,
3 mdiCloseCircle,
4 mdiMagnify,
5} from '@mdi/js';
6import Icon from '@mdi/react';
7import classnames from 'classnames';
8import { ChangeEvent, Component, createRef } from 'react';
9import injectStyle from 'react-jss';
10
11import { IFormField, IWithStyle } from '../typings/generic';
12import { Theme } from '../../../theme';
13
14import { Error } from '../error';
15import { Label } from '../label';
16import { Wrapper } from '../wrapper';
17
18interface IOptions {
19 [index: string]: string;
20}
21
22interface IData {
23 [index: string]: string;
24}
25
26interface 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
41interface IState {
42 open: boolean;
43 value: string;
44 needle: string;
45 selected: number;
46 options: IOptions;
47}
48
49let popupTransition: string = 'none';
50let toggleTransition: string = 'none';
51
52if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) {
53 popupTransition = 'all 0.3s';
54 toggleTransition = 'transform 0.3s';
55}
56
57const 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
151class 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
460export const Select = injectStyle(styles)(SelectComponent);