refs #14926-modal-with-inputs-is-empty
[arvados.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 as MuiInput, withStyles, WithStyles } from '@material-ui/core';
8 import { StyleRulesCallback } from '@material-ui/core/styles';
9 import { InputProps } from '@material-ui/core/Input';
10
11 interface ChipsInputProps<Value> {
12     value: Value[];
13     getLabel?: (value: Value) => string;
14     onChange: (value: Value[]) => void;
15     createNewValue: (value: string) => Value;
16     inputComponent?: React.ComponentType<InputProps>;
17     inputProps?: InputProps;
18     deletable?: boolean;
19     orderable?: boolean;
20     disabled?: boolean;
21 }
22
23 type CssRules = 'chips' | 'input' | 'inputContainer';
24
25 const styles: StyleRulesCallback = ({ spacing }) => ({
26     chips: {
27         minHeight: spacing.unit * 5,
28         zIndex: 1,
29         position: 'relative',
30     },
31     input: {
32         zIndex: 1,
33         marginBottom: 8,
34         position: 'relative',
35     },
36     inputContainer: {
37         marginTop: -34
38     },
39 });
40
41 export const ChipsInput = withStyles(styles)(
42     class ChipsInput<Value> extends React.Component<ChipsInputProps<Value> & WithStyles<CssRules>> {
43
44         state = {
45             text: '',
46         };
47
48         filler = React.createRef<HTMLDivElement>();
49         timeout = -1;
50
51         setText = (event: React.ChangeEvent<HTMLInputElement>) => {
52             this.setState({ text: event.target.value });
53         }
54
55         handleKeyPress = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
56             if (key === 'Enter') {
57                 this.createNewValue();
58             } else if (key === 'Backspace') {
59                 this.deleteLastValue();
60             }
61         }
62
63         createNewValue = () => {
64             if (this.state.text) {
65                 const newValue = this.props.createNewValue(this.state.text);
66                 this.setState({ text: '' });
67                 this.props.onChange([...this.props.value, newValue]);
68             }
69         }
70
71         deleteLastValue = () => {
72             if (this.state.text.length === 0 && this.props.value.length > 0) {
73                 this.props.onChange(this.props.value.slice(0, -1));
74             }
75         }
76
77         updateCursorPosition = () => {
78             if (this.timeout) {
79                 clearTimeout(this.timeout);
80             }
81             this.timeout = setTimeout(() => this.setState({ ...this.state }));
82         }
83
84         getInputStyles = (): React.CSSProperties => ({
85             width: this.filler.current
86                 ? this.filler.current.offsetWidth
87                 : '100%',
88             right: this.filler.current
89                 ? `calc(${this.filler.current.offsetWidth}px - 100%)`
90                 : 0,
91
92         })
93
94         componentDidMount() {
95             this.updateCursorPosition();
96         }
97
98         render() {
99             return <>
100                 {this.renderChips()}
101                 {this.renderInput()}
102             </>;
103         }
104
105         renderChips() {
106             const { classes, value, ...props } = this.props;
107             return <div className={classes.chips}>
108                 <Chips
109                     {...props}
110                     clickable={!props.disabled}
111                     values={value}
112                     filler={<div ref={this.filler} />}
113                 />
114             </div>;
115         }
116
117         renderInput() {
118             const { inputProps: InputProps, inputComponent: Input = MuiInput, classes } = this.props;
119             return <Input
120                 {...InputProps}
121                 value={this.state.text}
122                 onChange={this.setText}
123                 disabled={this.props.disabled}
124                 onKeyDown={this.handleKeyPress}
125                 inputProps={{
126                     ...(InputProps && InputProps.inputProps),
127                     className: classes.input,
128                     style: this.getInputStyles(),
129                 }}
130                 fullWidth
131                 className={classes.inputContainer} />;
132         }
133
134         componentDidUpdate(prevProps: ChipsInputProps<Value>) {
135             if (prevProps.value !== this.props.value) {
136                 this.updateCursorPosition();
137             }
138         }
139         componentWillUnmount() {
140             clearTimeout(this.timeout);
141         }
142     });