1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import * as React from 'react';
6 import { Input as MuiInput, Chip as MuiChip, Popper as MuiPopper, Paper, FormControl, InputLabel, StyleRulesCallback, withStyles, RootRef, ListItemText, ListItem, List, FormHelperText } from '@material-ui/core';
7 import { PopperProps } from '@material-ui/core/Popper';
8 import { WithStyles } from '@material-ui/core/styles';
9 import { noop } from 'lodash';
11 export interface AutocompleteProps<Item, Suggestion> {
15 suggestions?: Suggestion[];
18 onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
19 onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
20 onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
21 onCreate?: () => void;
22 onDelete?: (item: Item, index: number) => void;
23 onSelect?: (suggestion: Suggestion) => void;
24 renderChipValue?: (item: Item) => string;
25 renderSuggestion?: (suggestion: Suggestion) => React.ReactNode;
28 export interface AutocompleteState {
29 suggestionsOpen: boolean;
31 export class Autocomplete<Value, Suggestion> extends React.Component<AutocompleteProps<Value, Suggestion>, AutocompleteState> {
34 suggestionsOpen: false,
37 containerRef = React.createRef<HTMLDivElement>();
38 inputRef = React.createRef<HTMLInputElement>();
42 <RootRef rootRef={this.containerRef}>
43 <FormControl fullWidth error={this.props.error}>
46 {this.renderHelperText()}
47 {this.renderSuggestions()}
54 const { label } = this.props;
55 return label && <InputLabel>{label}</InputLabel>;
60 inputRef={this.inputRef}
61 value={this.props.value}
62 startAdornment={this.renderChips()}
63 onFocus={this.handleFocus}
64 onBlur={this.handleBlur}
65 onChange={this.props.onChange}
66 onKeyPress={this.handleKeyPress}
71 return <FormHelperText>{this.props.helperText}</FormHelperText>;
75 const { suggestions = [] } = this.props;
78 open={this.state.suggestionsOpen && suggestions.length > 0}
79 anchorEl={this.containerRef.current}>
80 <Paper onMouseDown={this.preventBlur}>
81 <List dense style={{ width: this.getSuggestionsWidth() }}>
83 (suggestion, index) =>
84 <ListItem button key={index} onClick={this.handleSelect(suggestion)}>
85 {this.renderSuggestion(suggestion)}
94 handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
95 const { onFocus = noop } = this.props;
96 this.setState({ suggestionsOpen: true });
100 handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
102 const { onBlur = noop } = this.props;
103 this.setState({ suggestionsOpen: false });
108 handleKeyPress = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
109 const { onCreate = noop } = this.props;
110 if (key === 'Enter' && this.props.value.length > 0) {
116 const { items, onDelete } = this.props;
120 label={this.renderChipValue(item)}
122 onDelete={() => onDelete ? onDelete(item, index) : undefined} />
126 renderChipValue(value: Value) {
127 const { renderChipValue } = this.props;
128 return renderChipValue ? renderChipValue(value) : JSON.stringify(value);
131 preventBlur = (event: React.MouseEvent<HTMLElement>) => {
132 event.preventDefault();
135 handleClickAway = (event: React.MouseEvent<HTMLElement>) => {
136 if (event.target !== this.inputRef.current) {
137 this.setState({ suggestionsOpen: false });
141 handleSelect(suggestion: Suggestion) {
143 const { onSelect = noop } = this.props;
144 const { current } = this.inputRef;
148 onSelect(suggestion);
152 renderSuggestion(suggestion: Suggestion) {
153 const { renderSuggestion } = this.props;
154 return renderSuggestion
155 ? renderSuggestion(suggestion)
156 : <ListItemText>{JSON.stringify(suggestion)}</ListItemText>;
159 getSuggestionsWidth() {
160 return this.containerRef.current ? this.containerRef.current.offsetWidth : 'auto';
164 type ChipClasses = 'root';
166 const chipStyles: StyleRulesCallback<ChipClasses> = theme => ({
168 marginRight: theme.spacing.unit / 4,
169 height: theme.spacing.unit * 3,
173 const Chip = withStyles(chipStyles)(MuiChip);
175 type PopperClasses = 'root';
177 const popperStyles: StyleRulesCallback<ChipClasses> = theme => ({
179 zIndex: theme.zIndex.modal,
183 const Popper = withStyles(popperStyles)(
184 ({ classes, ...props }: PopperProps & WithStyles<PopperClasses>) =>
185 <MuiPopper {...props} className={classes.root} />
188 type InputClasses = 'root';
190 const inputStyles: StyleRulesCallback<InputClasses> = () => ({
201 const Input = withStyles(inputStyles)(MuiInput);