X-Git-Url: https://git.arvados.org/arvados-workbench2.git/blobdiff_plain/857ac804982b4c54c995d25f413a8811da56b63c..3ddd45b007768a39591cd116b4a213cd39019e0c:/src/components/autocomplete/autocomplete.tsx diff --git a/src/components/autocomplete/autocomplete.tsx b/src/components/autocomplete/autocomplete.tsx index 46df8c69..52918c34 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, ListItemText, ListItem, List, FormHelperText } 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'; @@ -15,6 +15,7 @@ export interface AutocompleteProps { suggestions?: Suggestion[]; error?: boolean; helperText?: string; + autofocus?: boolean; onChange: (event: React.ChangeEvent) => void; onBlur?: (event: React.FocusEvent) => void; onFocus?: (event: React.FocusEvent) => void; @@ -27,11 +28,14 @@ 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(); @@ -57,6 +61,7 @@ export class Autocomplete extends React.Component extends React.Component; } - renderHelperText(){ + renderHelperText() { return {this.props.helperText}; } @@ -75,13 +81,18 @@ export class Autocomplete extends React.Component 0} - anchorEl={this.containerRef.current}> + open={this.isSuggestionBoxOpen()} + anchorEl={this.inputRef.current} + key={suggestions.length}> {suggestions.map( (suggestion, index) => - + {this.renderSuggestion(suggestion)} )} @@ -91,6 +102,11 @@ export class Autocomplete extends React.Component 0; + } + handleFocus = (event: React.FocusEvent) => { const { onFocus = noop } = this.props; this.setState({ suggestionsOpen: true }); @@ -105,15 +121,46 @@ export class Autocomplete extends React.Component) => { - const { onCreate = noop } = this.props; - if (key === 'Enter' && this.props.value.length > 0) { - onCreate(); + 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(); + } } } + 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. + */ + if (items.length === 0) { + return; + } + return items.map( (item, index) => = () => ({ }); const Input = withStyles(inputStyles)(MuiInput); + +const Paper = withStyles({ + root: { + maxHeight: '80vh', + overflowY: 'auto', + } +})(MuiPaper);