import * as React from 'react'; import { ValidationRule, ValidationState } from '../../models/Validation'; import { TooltipProps } from '../tooltip/Tooltip'; import { Colors } from '../../models/colors'; import { Query } from '../../models/query'; export interface AutoCompleteProps { label?: string; type?: string; name?: string; automationName?: string; placeholder?: string; validationRules?: Array; options?: Array<{ label: string; id: string }>; value?: string; query?: Query; valid?: ValidationState; required?: boolean; errorMessage?: string; palette?: Colors; tooltip?: React.ReactElement; noResults?: string; notifyValidity?: (name: string, state: ValidationState, errorMessage?: string) => void; onChange?: (name: string, value: string) => void; onOptionClick?: (option: any) => void; translator?: Function; } type AutocompleteContainer = AutoCompleteProps & { render: ( onChange: (name: string, value: string) => void, onOptionClick: (option: { label: string; id: string }) => void, onFocus: (name: string, value: string, e?: React.FocusEvent) => void, onclick: (e: React.MouseEvent) => void, onKeyDown: ( value: string, e: React.KeyboardEvent, option?: { label: string; id: string } ) => void, value: string, opened: boolean, ref: React.RefObject, query: Query, close: () => void ) => JSX.Element; }; interface AutoCompleteState { value: string; opened: boolean; focusedLi: number; } class Autocomplete extends React.PureComponent { private ref: React.RefObject; constructor(props: AutocompleteContainer) { super(props); this.state = { value: this.props.value, opened: false, focusedLi: -1 }; this.ref = React.createRef(); } componentDidUpdate(prevProps: AutoCompleteProps) { if (prevProps.options !== this.props.options) { this.open(false); } } onChange = (name: string, value: string) => { document.activeElement.id = this.props.name; this.props.onChange(name, value); this.setState({ value, focusedLi: -1 }); }; open = (forceOpen: boolean) => { if (this.state.opened) return; if ( (this.props.options && this.props.options.length > 0 && document.activeElement.id === this.props.name) || forceOpen ) { this.setState({ opened: true }); if (this.props.query !== Query.small) { document.addEventListener('click', this.close); } } }; close = () => { this.setState({ opened: false, focusedLi: -1 }); if (this.props.query !== Query.small) { document.removeEventListener('click', this.close); } }; onOptionClick = (option: { label: string; id: string }) => { if (this.props.onOptionClick) { this.props.onOptionClick(option); } this.setState({ value: option.label }); this.close(); }; onFocus = (name: string, value: string, e: React.FocusEvent) => { e.stopPropagation(); if (this.props.options || this.props.query === Query.small) { this.open(true); } }; onClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); }; onKeyDown = (value: string, e: React.KeyboardEvent, option?: { label: string; id: string }) => { if (e.keyCode === 40) { //arrow down e.preventDefault(); let focusedIndex = this.state.focusedLi === this.props.options.length - 1 ? 0 : this.state.focusedLi + 1; this.ref.current.querySelectorAll('li')[focusedIndex].focus(); this.setState({ focusedLi: focusedIndex, value: this.props.options[focusedIndex].label }); } if (e.keyCode === 38) { //arrow up e.preventDefault(); let focusedIndex = this.state.focusedLi === 0 ? this.props.options.length - 1 : this.state.focusedLi - 1; this.ref.current.querySelectorAll('li')[focusedIndex].focus(); this.setState({ focusedLi: focusedIndex, value: this.props.options[focusedIndex].label }); } if (e.keyCode === 13) { //enter this.onOptionClick(option); } if (e.keyCode === 9) { //tab this.close(); } }; render() { return this.props.render( this.onChange, this.onOptionClick, this.onFocus, this.onClick, this.onKeyDown, this.state.value, this.state.opened, this.ref, this.props.query, this.close ); } } export default Autocomplete;