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, Menu, MenuItem, ListItemText, ListItem, List } 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[];
16 onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
17 onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
18 onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
19 onCreate?: () => void;
20 onDelete?: (item: Item, index: number) => void;
21 onSelect?: (suggestion: Suggestion) => void;
22 renderChipValue?: (item: Item) => string;
23 renderSuggestion?: (suggestion: Suggestion) => React.ReactNode;
26 export interface AutocompleteState {
27 suggestionsOpen: boolean;
29 export class Autocomplete<Value, Suggestion> extends React.Component<AutocompleteProps<Value, Suggestion>, AutocompleteState> {
32 suggestionsOpen: false,
35 containerRef = React.createRef<HTMLDivElement>();
36 inputRef = React.createRef<HTMLInputElement>();
39 <RootRef rootRef={this.containerRef}>
40 <FormControl fullWidth>
43 {this.renderSuggestions()}
50 const { label } = this.props;
51 return label && <InputLabel>{label}</InputLabel>;
56 inputRef={this.inputRef}
57 value={this.props.value}
58 startAdornment={this.renderChips()}
59 onFocus={this.handleFocus}
60 onBlur={this.handleBlur}
61 onChange={this.props.onChange}
62 onKeyPress={this.handleKeyPress}
66 handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
67 const { onFocus = noop } = this.props;
68 this.setState({ suggestionsOpen: true });
72 handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
73 const { onBlur = noop } = this.props;
74 this.setState({ suggestionsOpen: true });
78 handleKeyPress = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
79 const { onCreate = noop } = this.props;
80 if (key === 'Enter') {
86 const { items, onDelete } = this.props;
90 label={this.renderChipValue(item)}
92 onDelete={() => onDelete ? onDelete(item, index) : undefined} />
96 renderChipValue(value: Value) {
97 const { renderChipValue } = this.props;
98 return renderChipValue ? renderChipValue(value) : JSON.stringify(value);
101 renderSuggestions() {
102 const { suggestions } = this.props;
103 return suggestions && suggestions.length > 0
105 open={this.state.suggestionsOpen}
106 anchorEl={this.containerRef.current}>
108 <List dense style={{ width: this.getSuggestionsWidth() }}>
110 (suggestion, index) =>
111 <ListItem button key={index} onClick={this.handleSelect(suggestion)}>
112 {this.renderSuggestion(suggestion)}
121 handleSelect(suggestion: Suggestion) {
123 const { onSelect = noop } = this.props;
124 const { current } = this.inputRef;
128 onSelect(suggestion);
132 renderSuggestion(suggestion: Suggestion) {
133 const { renderSuggestion } = this.props;
134 return renderSuggestion
135 ? renderSuggestion(suggestion)
136 : <ListItemText>{JSON.stringify(suggestion)}</ListItemText>;
139 getSuggestionsWidth() {
140 return this.containerRef.current ? this.containerRef.current.offsetWidth : 'auto';
144 type ChipClasses = 'root';
146 const chipStyles: StyleRulesCallback<ChipClasses> = theme => ({
148 marginRight: theme.spacing.unit / 4,
149 height: theme.spacing.unit * 3,
153 const Chip = withStyles(chipStyles)(MuiChip);
155 type PopperClasses = 'root';
157 const popperStyles: StyleRulesCallback<ChipClasses> = theme => ({
159 zIndex: theme.zIndex.modal,
163 const Popper = withStyles(popperStyles)(
164 ({ classes, ...props }: PopperProps & WithStyles<PopperClasses>) =>
165 <MuiPopper {...props} className={classes.root} />
168 type InputClasses = 'root';
170 const inputStyles: StyleRulesCallback<InputClasses> = () => ({
181 const Input = withStyles(inputStyles)(MuiInput);