X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/6aaf65540506d590fa826b08166c050b924a99ff..02fe86a56f080ed1d5770ad6c6856a15f50ab508:/src/components/autocomplete/autocomplete.tsx diff --git a/src/components/autocomplete/autocomplete.tsx b/src/components/autocomplete/autocomplete.tsx index e2f354747c..4b19b77115 100644 --- a/src/components/autocomplete/autocomplete.tsx +++ b/src/components/autocomplete/autocomplete.tsx @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -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'; +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'; @@ -13,6 +13,9 @@ export interface AutocompleteProps { value: string; items: Item[]; suggestions?: Suggestion[]; + error?: boolean; + helperText?: string; + autofocus?: boolean; onChange: (event: React.ChangeEvent) => void; onBlur?: (event: React.FocusEvent) => void; onFocus?: (event: React.FocusEvent) => void; @@ -25,21 +28,26 @@ export interface AutocompleteProps { export interface AutocompleteState { suggestionsOpen: boolean; + selectedSuggestionIndex: number; } + export class Autocomplete extends React.Component, AutocompleteState> { state = { suggestionsOpen: false, + selectedSuggestionIndex: 0, }; containerRef = React.createRef(); inputRef = React.createRef(); + render() { return ( - + {this.renderLabel()} {this.renderInput()} + {this.renderHelperText()} {this.renderSuggestions()} @@ -53,6 +61,7 @@ export class Autocomplete extends React.Component extends React.Component; } + renderHelperText() { + return {this.props.helperText}; + } + + renderSuggestions() { + const { suggestions = [] } = this.props; + return ( + + + + {suggestions.map( + (suggestion, index) => + + {this.renderSuggestion(suggestion)} + + )} + + + + ); + } + + isSuggestionBoxOpen() { + const { suggestions = [] } = this.props; + return this.state.suggestionsOpen && suggestions.length > 0; + } + handleFocus = (event: React.FocusEvent) => { const { onFocus = noop } = this.props; this.setState({ suggestionsOpen: true }); @@ -70,20 +114,54 @@ export class Autocomplete extends React.Component) => { - const { onBlur = noop } = this.props; - this.setState({ suggestionsOpen: true }); - onBlur(event); + setTimeout(() => { + const { onBlur = noop } = this.props; + this.setState({ suggestionsOpen: false }); + onBlur(event); + }); + } + + handleKeyPress = (event: React.KeyboardEvent) => { + const { onCreate = noop, onSelect = noop, suggestions = [] } = this.props; + const { selectedSuggestionIndex } = this.state; + if (event.key === 'Enter') { + if (this.isSuggestionBoxOpen() && selectedSuggestionIndex < suggestions.length) { + // prevent form submissions when selecting a suggestion + event.preventDefault(); + onSelect(suggestions[selectedSuggestionIndex]); + } else if (this.props.value.length > 0) { + onCreate(); + } + } } - handleKeyPress = ({ key }: React.KeyboardEvent) => { - const { onCreate = noop } = this.props; - if (key === 'Enter') { - onCreate(); + handleNavigationKeyPress = ({ key }: React.KeyboardEvent) => { + if (key === 'ArrowUp') { + this.updateSelectedSuggestionIndex(-1); + } else if (key === 'ArrowDown') { + this.updateSelectedSuggestionIndex(1); } } + updateSelectedSuggestionIndex(value: -1 | 1) { + const { suggestions = [] } = this.props; + this.setState(({ selectedSuggestionIndex }) => ({ + selectedSuggestionIndex: (selectedSuggestionIndex + value) % suggestions.length + })); + } + renderChips() { const { items, onDelete } = this.props; + + /** + * If input startAdornment prop is not undefined, input's label will stay above the input. + * If there is not items, we want the label to go back to placeholder position. + * That why we return without a value instead of returning a result of a _map_ which is an empty array. + */ + if (items.length === 0) { + return; + } + return items.map( (item, index) => extends React.Component 0 - ? - - - {suggestions.map( - (suggestion, index) => - - {this.renderSuggestion(suggestion)} - - )} - - - - : null; + preventBlur = (event: React.MouseEvent) => { + event.preventDefault(); + } + + handleClickAway = (event: React.MouseEvent) => { + if (event.target !== this.inputRef.current) { + this.setState({ suggestionsOpen: false }); + } } handleSelect(suggestion: Suggestion) { @@ -179,3 +247,10 @@ const inputStyles: StyleRulesCallback = () => ({ }); const Input = withStyles(inputStyles)(MuiInput); + +const Paper = withStyles({ + root: { + maxHeight: '80vh', + overflowY: 'auto', + } +})(MuiPaper);