// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; import { Input as MuiInput, Chip as MuiChip, Popper as MuiPopper, Paper as MuiPaper, FormControl, InputLabel, StyleRulesCallback, withStyles, RootRef, ListItemText, ListItem, List, FormHelperText } from '@material-ui/core'; import { PopperProps } from '@material-ui/core/Popper'; import { WithStyles } from '@material-ui/core/styles'; import { noop } from 'lodash'; export interface AutocompleteProps { label?: string; value: string; items: Item[]; suggestions?: Suggestion[]; error?: boolean; helperText?: string; onChange: (event: React.ChangeEvent) => void; onBlur?: (event: React.FocusEvent) => void; onFocus?: (event: React.FocusEvent) => void; onCreate?: () => void; onDelete?: (item: Item, index: number) => void; onSelect?: (suggestion: Suggestion) => void; renderChipValue?: (item: Item) => string; renderSuggestion?: (suggestion: Suggestion) => React.ReactNode; } export interface AutocompleteState { suggestionsOpen: boolean; } export class Autocomplete extends React.Component, AutocompleteState> { state = { suggestionsOpen: false, }; containerRef = React.createRef(); inputRef = React.createRef(); render() { return ( {this.renderLabel()} {this.renderInput()} {this.renderHelperText()} {this.renderSuggestions()} ); } renderLabel() { const { label } = this.props; return label && {label}; } renderInput() { return ; } renderHelperText(){ return {this.props.helperText}; } renderSuggestions() { const { suggestions = [] } = this.props; return ( 0} anchorEl={this.inputRef.current}> {suggestions.map( (suggestion, index) => {this.renderSuggestion(suggestion)} )} ); } handleFocus = (event: React.FocusEvent) => { const { onFocus = noop } = this.props; this.setState({ suggestionsOpen: true }); onFocus(event); } handleBlur = (event: React.FocusEvent) => { setTimeout(() => { const { onBlur = noop } = this.props; this.setState({ suggestionsOpen: false }); onBlur(event); }); } handleKeyPress = ({ key }: React.KeyboardEvent) => { const { onCreate = noop } = this.props; if (key === 'Enter' && this.props.value.length > 0) { onCreate(); } } renderChips() { const { items, onDelete } = this.props; return items.map( (item, index) => onDelete ? onDelete(item, index) : undefined} /> ); } renderChipValue(value: Value) { const { renderChipValue } = this.props; return renderChipValue ? renderChipValue(value) : JSON.stringify(value); } preventBlur = (event: React.MouseEvent) => { event.preventDefault(); } handleClickAway = (event: React.MouseEvent) => { if (event.target !== this.inputRef.current) { this.setState({ suggestionsOpen: false }); } } handleSelect(suggestion: Suggestion) { return () => { const { onSelect = noop } = this.props; const { current } = this.inputRef; if (current) { current.focus(); } onSelect(suggestion); }; } renderSuggestion(suggestion: Suggestion) { const { renderSuggestion } = this.props; return renderSuggestion ? renderSuggestion(suggestion) : {JSON.stringify(suggestion)}; } getSuggestionsWidth() { return this.containerRef.current ? this.containerRef.current.offsetWidth : 'auto'; } } type ChipClasses = 'root'; const chipStyles: StyleRulesCallback = theme => ({ root: { marginRight: theme.spacing.unit / 4, height: theme.spacing.unit * 3, } }); const Chip = withStyles(chipStyles)(MuiChip); type PopperClasses = 'root'; const popperStyles: StyleRulesCallback = theme => ({ root: { zIndex: theme.zIndex.modal, } }); const Popper = withStyles(popperStyles)( ({ classes, ...props }: PopperProps & WithStyles) => ); type InputClasses = 'root'; const inputStyles: StyleRulesCallback = () => ({ root: { display: 'flex', flexWrap: 'wrap', }, input: { minWidth: '20%', flex: 1, }, }); const Input = withStyles(inputStyles)(MuiInput); const Paper = withStyles({ root: { maxHeight: '80vh', overflowY: 'auto', } })(MuiPaper);