import React, { useRef, useState, useMemo, useEffect, InputHTMLAttributes, CSSProperties, ReactNode, forwardRef, useImperativeHandle, } from 'react' import { Input } from '../input' import { Popover } from '../popover' import { List } from '../list' import _ from 'lodash' import classNames from 'classnames' import { ListDataItem } from '../../types' export interface AutoCompleteOption { value: string } type InputProps = InputHTMLAttributes export interface AutoCompleteProps extends Omit { /** input value */ value?: string /** 填充文本列表 */ options?: AutoCompleteOption[] /** 自定义 options 外层容器类名 */ optionsWrapClassName?: string /** 自定义 options 外层容器行内样式 */ optionsWrapStyle?: CSSProperties /** 自定义 option 渲染 */ renderOption?: (value: AutoCompleteOption, index: number) => ReactNode /** 在 options 前追加内容 */ addonOptionsBefore?: () => ReactNode /** 在 options 后追加内容 */ addonOptionsAfter?: () => ReactNode /** input onChange 触发、点击 option 时触发、键盘选择 option 并回车时触发 */ onChange?: (value: string) => void } export interface AutoCompleteRef { /** input DOM 节点 */ input?: HTMLInputElement | null /** 控制弹出层显示 */ triggerPopover: (value: boolean) => void } const preventDefault = (e: React.MouseEvent) => { e.preventDefault() } const LIST_ITEM_PROPS: React.HTMLAttributes = { onPointerDown: preventDefault, onMouseDown: preventDefault, onClick: preventDefault, } const listItemProps = () => LIST_ITEM_PROPS const AutoComplete = forwardRef((props, ref) => { const { value, options: externalOptions, optionsWrapClassName, optionsWrapStyle, onChange, onKeyDown, onBlur, renderOption, addonOptionsAfter, addonOptionsBefore, ...rest } = props const [willActiveIndex, setWillActiveIndex] = useState(0) const popoverNode = useRef(null) const popoverVisible = useRef(false) const inputNode = useRef(null) const triggerPopover = (value: boolean) => { if (value === true && options.length <= 0) return popoverNode.current && popoverNode.current.apiDoSetActive(value) } const options = useMemo(() => { return _.map(externalOptions, (item) => ({ text: item.value, value: item.value })) }, [externalOptions]) useEffect(() => { if (_.isNil(value) || options.length <= 0) return setWillActiveIndex(() => { return options.findIndex((item) => item.value === value) }) }, [value, options]) const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { if (willActiveIndex >= 0) { const val = options[willActiveIndex]?.value if (!_.isNil(val) && value !== val) { onChange && onChange(val) triggerPopover(false) return } if (popoverVisible.current) { triggerPopover(false) return } } onKeyDown && onKeyDown(event) return } if (!popoverVisible.current) { onKeyDown && onKeyDown(event) return } if (event.key === 'Escape') { triggerPopover(false) } if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') { onKeyDown && onKeyDown(event) return } let index = willActiveIndex if (event.key === 'ArrowUp') { index-- } else if (event.key === 'ArrowDown') { index++ } if (index < 0) { index = options.length - 1 } else if (index > options.length - 1) { index = 0 } setWillActiveIndex(index) } const renderItem = useMemo(() => { if (!renderOption) return return (value: ListDataItem, index: number) => { return renderOption({ value: value.value }, index) } }, [renderOption]) useImperativeHandle(ref, () => ({ input: inputNode.current, triggerPopover, })) return ( { popoverVisible.current = visible }} popup={ <> {addonOptionsBefore &&
{addonOptionsBefore()}
} { onChange && onChange(val as string) triggerPopover(false) inputNode.current && inputNode.current.focus() }} willActiveIndex={willActiveIndex} className={classNames('gm-border-0', optionsWrapClassName)} renderItem={renderItem} style={{ maxHeight: 250, ...optionsWrapStyle }} getItemProps={listItemProps} /> {addonOptionsAfter &&
{addonOptionsAfter()}
} } > { const val = e.target.value triggerPopover(val.length === 0) onChange && onChange(val) }} onBlur={(e) => { triggerPopover(false) onBlur && onBlur(e) }} onKeyDown={handleKeyDown} />
) }) export default AutoComplete