import React, { Component, Fragment } from 'react' import cc from 'classcat' import { canUseEventListeners } from 'exenv' import isEmpty from 'lodash.isempty' import isEqual from 'lodash.isequal' import { ItemStatus } from '../_internals/item' import { prefix } from '../_utils' import { ItemChoice } from '../itemChoice' import { ItemsList } from '../itemsList' export type AutocompleteItem = { readonly id: string readonly label: string readonly labelInfo?: string readonly leftAddon?: JSX.Element } export type AutoCompleteListProps = Readonly<{ name: string onSelect?: (item: AutocompleteItem) => void className?: string items?: Array maxItems?: number itemClassName?: string onDoneAnimationEnd?: () => void itemKey?: (item: AutocompleteItem) => string visible?: boolean selectedItemStatus?: ItemStatus withSeparators?: boolean }> type AutoCompleteListState = { highlightedIndex: number selectedIndex: number } export class AutoCompleteList extends Component { static defaultProps: Partial = { maxItems: Infinity, itemKey: ({ leftAddon, ...item }) => JSON.stringify(item), visible: false, withSeparators: true, } state: AutoCompleteListState = { highlightedIndex: null, selectedIndex: null, } componentDidMount() { if (canUseEventListeners) { document.addEventListener('keydown', this.handleKeydown) } } componentDidUpdate(prevProps: AutoCompleteListProps) { if (!isEqual(prevProps.items, this.props.items)) { // eslint-disable-next-line react/no-did-update-set-state this.setState({ highlightedIndex: null, }) } } componentWillUnmount() { if (canUseEventListeners) { document.removeEventListener('keydown', this.handleKeydown) } } onKeyboardEventArrowDown = (e: Event) => { e.preventDefault() if (isEmpty(this.props.items)) { return } const { highlightedIndex } = this.state const index = highlightedIndex === null || highlightedIndex === this.props.items.length - 1 ? 0 : highlightedIndex + 1 this.setState({ highlightedIndex: index, }) } onKeyboardEventArrowUp = (e: Event) => { e.preventDefault() if (!isEmpty(this.props.items)) { const { highlightedIndex } = this.state const index = highlightedIndex === null || highlightedIndex === 0 ? this.props.items.length - 1 : highlightedIndex - 1 this.setState({ highlightedIndex: index, }) } } onKeyboardEventEnter = (e: Event) => { e.preventDefault() const { highlightedIndex } = this.state if (highlightedIndex == null) { return } const item = this.props.items[highlightedIndex] this.setState({ selectedIndex: highlightedIndex }) this.props.onSelect(item) } handleKeydown = (e: KeyboardEvent) => { if (e.keyCode === 13) { this.onKeyboardEventEnter(e) } else if (e.keyCode === 38) { this.onKeyboardEventArrowUp(e) } else if (e.keyCode === 40) { this.onKeyboardEventArrowDown(e) } } onSelect = (itemIndex: number, item: AutocompleteItem) => { this.setState({ selectedIndex: itemIndex }, () => { this.props.onSelect(item) }) } render() { if (!this.props.visible || isEmpty(this.props.items)) { return null } return ( this.props.itemKey(this.props.items[index])} role="listbox" withSeparators={this.props.withSeparators} > {this.props.items.slice(0, this.props.maxItems).map((item, index) => { const status = this.state.selectedIndex === index ? this.props.selectedItemStatus : ItemStatus.DEFAULT const isHighlighted = index === this.state.highlightedIndex const { id, ...itemChoiceProps } = item return ( { this.onSelect(index, item) }} onDoneAnimationEnd={this.props.onDoneAnimationEnd} key={this.props.itemKey(item)} tabIndex={-1} /> ) })} ) } }