Fix inifite render loop
[arvados-workbench2.git] / src / components / chips-input / chips-input.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 { Chips } from '~/components/chips/chips';
7 import { Input, withStyles, WithStyles } from '@material-ui/core';
8 import { StyleRulesCallback } from '@material-ui/core/styles';
9
10 interface ChipsInputProps<Value> {
11     values: Value[];
12     getLabel?: (value: Value) => string;
13     onChange: (value: Value[]) => void;
14     createNewValue: (value: string) => Value;
15 }
16
17 type CssRules = 'chips' | 'input' | 'inputContainer';
18
19 const styles: StyleRulesCallback = () => ({
20     chips: {
21         minHeight: '40px',
22         zIndex: 1,
23         position: 'relative',
24     },
25     input: {
26         position: 'relative',
27         top: '-5px',
28         zIndex: 1,
29     },
30     inputContainer: {
31         top: '-24px',
32     },
33 });
34
35 export const ChipsInput = withStyles(styles)(
36     class ChipsInput<Value> extends React.Component<ChipsInputProps<Value> & WithStyles<CssRules>> {
37
38         state = {
39             text: '',
40         };
41
42         filler = React.createRef<HTMLDivElement>();
43         timeout = -1;
44
45         componentDidMount() {
46             this.updateCursorPosition();
47         }
48
49         setText = (event: React.ChangeEvent<HTMLInputElement>) => {
50             this.setState({ text: event.target.value });
51         }
52
53         handleKeyPress = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
54             if (key === 'Enter') {
55                 this.createNewValue();
56             } else if (key === 'Backspace') {
57                 this.deleteLastValue();
58             }
59         }
60
61         createNewValue = () => {
62             if (this.state.text) {
63                 const newValue = this.props.createNewValue(this.state.text);
64                 this.setState({ text: '' });
65                 this.props.onChange([...this.props.values, newValue]);
66             }
67         }
68
69         deleteLastValue = () => {
70             if (this.state.text.length === 0 && this.props.values.length > 0) {
71                 this.props.onChange(this.props.values.slice(0, -1));
72             }
73         }
74
75         updateCursorPosition = () => {
76             if (this.timeout) {
77                 clearTimeout(this.timeout);
78             }
79             this.timeout = setTimeout(() => this.forceUpdate());
80         }
81
82         componentDidUpdate(prevProps: ChipsInputProps<Value>){
83             if(prevProps.values !== this.props.values){
84                 this.updateCursorPosition();
85             }
86         }
87
88         render() {
89             return <>
90                 <div className={this.props.classes.chips}>
91                     <Chips
92                         {...this.props}
93                         filler={<div ref={this.filler} />}
94                     />
95                 </div>
96                 <Input
97                     value={this.state.text}
98                     onChange={this.setText}
99                     onKeyDown={this.handleKeyPress}
100                     inputProps={{
101                         className: this.props.classes.input,
102                         style: this.getInputStyles(),
103                     }}
104                     fullWidth
105                     className={this.props.classes.inputContainer} />
106             </>;
107         }
108
109         getInputStyles = (): React.CSSProperties => ({
110             width: this.filler.current
111                 ? this.filler.current.offsetWidth + 8
112                 : '100%',
113             right: this.filler.current
114                 ? `calc(${this.filler.current.offsetWidth}px - 100%)`
115                 : 0,
116
117         })
118     });