UNPKG

metadata-based-explorer1

Version:
237 lines (213 loc) 7.95 kB
// @flow import * as React from 'react'; import classNames from 'classnames'; import Tooltip from '../tooltip'; import { KEYS } from '../../constants'; import Pill from './Pill'; import SuggestedPillsRow from './SuggestedPillsRow'; import type { Option, OptionValue, SelectedOptions, SuggestedPillsFilter } from './flowTypes'; function stopDefaultEvent(event) { event.preventDefault(); event.stopPropagation(); } type Props = { allowInvalidPills: boolean, className?: string, disabled?: boolean, error?: React.Node, inputProps: Object, onInput: Function, onRemove: Function, onSuggestedPillAdd?: Function, placeholder: string, selectedOptions: SelectedOptions, suggestedPillsData?: Array<Object>, suggestedPillsFilter?: SuggestedPillsFilter, suggestedPillsTitle?: string, validator: (option: Option | OptionValue) => boolean, }; type State = { isFocused: boolean, selectedIndex: number, }; class PillSelector extends React.Component<Props, State> { static defaultProps = { allowInvalidPills: false, disabled: false, error: '', inputProps: {}, placeholder: '', selectedOptions: [], validator: () => true, }; state = { isFocused: false, selectedIndex: -1, }; getNumSelected = (): number => { const { selectedOptions } = this.props; return typeof selectedOptions.size === 'number' ? selectedOptions.size : selectedOptions.length; }; getPillsByKey = (key: string): Array<any> => { const { selectedOptions } = this.props; return selectedOptions.map(option => option[key]); }; inputEl: HTMLInputElement; handleClick = () => { this.inputEl.focus(); }; handleFocus = () => { this.setState({ isFocused: true }); }; handleBlur = () => { this.setState({ isFocused: false }); }; hiddenEl: HTMLSpanElement; handleKeyDown = (event: SyntheticKeyboardEvent<>) => { const inputValue = this.inputEl.value; const numPills = this.getNumSelected(); const { selectedIndex } = this.state; switch (event.key) { case KEYS.backspace: { let index = -1; if (selectedIndex >= 0) { // remove selected pill index = selectedIndex; this.resetSelectedIndex(); this.inputEl.focus(); } else if (inputValue === '') { // remove last pill index = numPills - 1; } if (index >= 0) { const { onRemove, selectedOptions } = this.props; const selectedOption = // $FlowFixMe typeof selectedOptions.get === 'function' ? selectedOptions.get(index) : selectedOptions[index]; onRemove(selectedOption, index); stopDefaultEvent(event); } break; } case KEYS.arrowLeft: if (selectedIndex >= 0) { // select previous pill this.setState({ selectedIndex: Math.max(selectedIndex - 1, 0), }); stopDefaultEvent(event); } else if (inputValue === '' && numPills > 0) { // select last pill this.hiddenEl.focus(); this.setState({ selectedIndex: numPills - 1 }); stopDefaultEvent(event); } break; case KEYS.arrowRight: { if (selectedIndex >= 0) { const index = selectedIndex + 1; if (index >= numPills) { // deselect last pill this.resetSelectedIndex(); this.inputEl.focus(); } else { // select next pill this.setState({ selectedIndex: index }); } stopDefaultEvent(event); } break; } // no default } }; hiddenRef = (hiddenEl: ?HTMLSpanElement) => { if (hiddenEl) { this.hiddenEl = hiddenEl; } }; resetSelectedIndex = () => { if (this.state.selectedIndex !== -1) { this.setState({ selectedIndex: -1 }); } }; render() { const { isFocused, selectedIndex } = this.state; const { allowInvalidPills, className, disabled, error, inputProps, onInput, onRemove, onSuggestedPillAdd, placeholder, selectedOptions, suggestedPillsData, suggestedPillsFilter, suggestedPillsTitle, validator, ...rest } = this.props; const suggestedPillsEnabled = suggestedPillsData && suggestedPillsData.length > 0; const classes = classNames('pill-selector-input-wrapper', { 'is-disabled': disabled, 'is-focused': isFocused, 'show-error': !!error, 'pill-selector-suggestions-enabled': suggestedPillsEnabled, }); return ( <Tooltip isShown={!!error} text={error || ''} position="middle-right" theme="error"> {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} <span className={classes} onBlur={this.handleBlur} onClick={this.handleClick} onFocus={this.handleFocus} onKeyDown={this.handleKeyDown} > {selectedOptions.map((option: Option, index: number) => ( <Pill isValid={allowInvalidPills ? validator(option) : true} isDisabled={disabled} isSelected={index === selectedIndex} key={option.value} onRemove={onRemove.bind(this, option, index)} // $FlowFixMe option.text is for backwards compatibility text={option.displayText || option.text} /> ))} {/* hidden element for focus/key events during pill selection */} <span aria-hidden="true" className="accessibility-hidden" onBlur={this.resetSelectedIndex} ref={this.hiddenRef} tabIndex={-1} /> <textarea {...rest} {...inputProps} autoComplete="off" className={classNames('pill-selector-input', className)} disabled={disabled} onInput={onInput} placeholder={this.getNumSelected() === 0 ? placeholder : ''} ref={input => { this.inputEl = input; }} /> <SuggestedPillsRow onSuggestedPillAdd={onSuggestedPillAdd} selectedPillsValues={this.getPillsByKey('value')} suggestedPillsFilter={suggestedPillsFilter} suggestedPillsData={suggestedPillsData} title={suggestedPillsTitle} /> </span> </Tooltip> ); } } export default PillSelector;