Fix infinite 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         setText = (event: React.ChangeEvent<HTMLInputElement>) => {
46             this.setState({ text: event.target.value });
47         }
48
49         handleKeyPress = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
50             if (key === 'Enter') {
51                 this.createNewValue();
52             } else if (key === 'Backspace') {
53                 this.deleteLastValue();
54             }
55         }
56
57         createNewValue = () => {
58             if (this.state.text) {
59                 const newValue = this.props.createNewValue(this.state.text);
60                 this.setState({ text: '' });
61                 this.props.onChange([...this.props.values, newValue]);
62             }
63         }
64
65         deleteLastValue = () => {
66             if (this.state.text.length === 0 && this.props.values.length > 0) {
67                 this.props.onChange(this.props.values.slice(0, -1));
68             }
69         }
70
71         updateCursorPosition = () => {
72             if (this.timeout) {
73                 clearTimeout(this.timeout);
74             }
75             this.timeout = setTimeout(() => this.setState({ ...this.state }));
76         }
77
78         getInputStyles = (): React.CSSProperties => ({
79             width: this.filler.current
80                 ? this.filler.current.offsetWidth + 8
81                 : '100%',
82             right: this.filler.current
83                 ? `calc(${this.filler.current.offsetWidth}px - 100%)`
84                 : 0,
85
86         })
87
88         componentDidMount() {
89             this.updateCursorPosition();
90         }
91
92         render() {
93             return <>
94                 <div className={this.props.classes.chips}>
95                     <Chips
96                         {...this.props}
97                         filler={<div ref={this.filler} />}
98                     />
99                 </div>
100                 <Input
101                     value={this.state.text}
102                     onChange={this.setText}
103                     onKeyDown={this.handleKeyPress}
104                     inputProps={{
105                         className: this.props.classes.input,
106                         style: this.getInputStyles(),
107                     }}
108                     fullWidth
109                     className={this.props.classes.inputContainer} />
110             </>;
111         }
112
113         componentDidUpdate(prevProps: ChipsInputProps<Value>) {
114             if (prevProps.values !== this.props.values) {
115                 this.updateCursorPosition();
116             }
117         }
118         componentWillUnmount() {
119             clearTimeout(this.timeout);
120         }
121     });