X-Git-Url: https://git.arvados.org/arvados-workbench2.git/blobdiff_plain/3e620538c5c0431bc587bb4c92da8c483f22a053..c2ba0170975fd01e2b3d9229b491ef2dc8c2f010:/src/components/autocomplete/autocomplete.tsx diff --git a/src/components/autocomplete/autocomplete.tsx b/src/components/autocomplete/autocomplete.tsx index 85704c35..17d85e85 100644 --- a/src/components/autocomplete/autocomplete.tsx +++ b/src/components/autocomplete/autocomplete.tsx @@ -2,8 +2,14 @@ // // 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 } from '@material-ui/core'; +import 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, Tooltip +} from '@material-ui/core'; import { PopperProps } from '@material-ui/core/Popper'; import { WithStyles } from '@material-ui/core/styles'; import { noop } from 'lodash'; @@ -12,7 +18,11 @@ export interface AutocompleteProps { label?: string; value: string; items: Item[]; + disabled?: boolean; suggestions?: Suggestion[]; + error?: boolean; + helperText?: string; + autofocus?: boolean; onChange: (event: React.ChangeEvent) => void; onBlur?: (event: React.FocusEvent) => void; onFocus?: (event: React.FocusEvent) => void; @@ -20,16 +30,20 @@ export interface AutocompleteProps { onDelete?: (item: Item, index: number) => void; onSelect?: (suggestion: Suggestion) => void; renderChipValue?: (item: Item) => string; + renderChipTooltip?: (item: Item) => string; renderSuggestion?: (suggestion: Suggestion) => React.ReactNode; } export interface AutocompleteState { suggestionsOpen: boolean; + selectedSuggestionIndex: number; } + export class Autocomplete extends React.Component, AutocompleteState> { state = { suggestionsOpen: false, + selectedSuggestionIndex: 0, }; containerRef = React.createRef(); @@ -38,9 +52,10 @@ export class Autocomplete extends React.Component - + {this.renderLabel()} {this.renderInput()} + {this.renderHelperText()} {this.renderSuggestions()} @@ -54,6 +69,8 @@ export class Autocomplete extends React.Component extends React.Component; } + renderHelperText() { + return {this.props.helperText}; + } + renderSuggestions() { const { suggestions = [] } = this.props; return ( 0} - anchorEl={this.containerRef.current}> + open={this.isSuggestionBoxOpen()} + anchorEl={this.inputRef.current} + key={suggestions.length}> {suggestions.map( (suggestion, index) => - + {this.renderSuggestion(suggestion)} )} @@ -84,6 +111,11 @@ export class Autocomplete extends React.Component 0; + } + handleFocus = (event: React.FocusEvent) => { const { onFocus = noop } = this.props; this.setState({ suggestionsOpen: true }); @@ -98,21 +130,64 @@ 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. + * 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) => - onDelete ? onDelete(item, index) : undefined} /> + (item, index) => { + const tooltip = this.props.renderChipTooltip ? this.props.renderChipTooltip(item) : ''; + if (tooltip && tooltip.length) { + return + + onDelete(item, index)) : undefined} /> + + } else { + return onDelete(item, index)) : undefined} /> + } + } ); } @@ -192,3 +267,10 @@ const inputStyles: StyleRulesCallback = () => ({ }); const Input = withStyles(inputStyles)(MuiInput); + +const Paper = withStyles({ + root: { + maxHeight: '80vh', + overflowY: 'auto', + } +})(MuiPaper);