| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 | 
							- /* eslint-disable @typescript-eslint/ban-types, max-len */
 
- import React, { ReactNode } from 'react';
 
- import PropTypes from 'prop-types';
 
- import cls from 'classnames';
 
- import { isEqual, noop } from 'lodash';
 
- import { strings, cssClasses } from '@douyinfe/semi-foundation/autoComplete/constants';
 
- import AutoCompleteFoundation, { AutoCompleteAdapter, StateOptionItem, DataItem } from '@douyinfe/semi-foundation/autoComplete/foundation';
 
- import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
 
- import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
 
- import { Position } from '../tooltip';
 
- import Spin from '../spin';
 
- import Popover from '../popover';
 
- import Input from '../input';
 
- import Trigger from '../trigger';
 
- import Option from './option';
 
- import warning from '@douyinfe/semi-foundation/utils/warning';
 
- import '@douyinfe/semi-foundation/autoComplete/autoComplete.scss';
 
- import { Motion } from '../_base/base';
 
- const prefixCls = cssClasses.PREFIX;
 
- const sizeSet = strings.SIZE;
 
- const positionSet = strings.POSITION;
 
- const statusSet = strings.STATUS;
 
- /**
 
-  * AutoComplete is an enhanced Input (candidates suggest that users can choose or not),
 
-  * and the Select positioning that supports Search is still a selector.
 
-  * 1. When you click to expand, Select will clear all input values, but AutoComplete will not
 
-  * 2. AutoComplete's renderSelectedItem only supports simple string returns, while Select's renderSelectedItem can return ReactNode
 
-  * 3. Select props.value supports incoming object, but autoComplete only supports string (because the value needs to be displayed in Input)
 
-  */
 
- export interface BaseDataItem extends DataItem {
 
-     label?: React.ReactNode;
 
- }
 
- export type AutoCompleteItems = BaseDataItem | string | number;
 
- export interface AutoCompleteProps<T extends AutoCompleteItems> {
 
-     'aria-describedby'?: React.AriaAttributes['aria-describedby'];
 
-     'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
 
-     'aria-invalid'?: React.AriaAttributes['aria-invalid'];
 
-     'aria-label'?: React.AriaAttributes['aria-label'];
 
-     'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
 
-     'aria-required'?: React.AriaAttributes['aria-required'];
 
-     autoAdjustOverflow?: boolean;
 
-     autoFocus?: boolean;
 
-     className?: string;
 
-     children?: ReactNode | undefined;
 
-     data?: T[];
 
-     disabled?: boolean;
 
-     defaultOpen?: boolean;
 
-     defaultValue?: T;
 
-     defaultActiveFirstOption?: boolean;
 
-     dropdownMatchSelectWidth?: boolean;
 
-     dropdownClassName?: string;
 
-     dropdownStyle?: React.CSSProperties;
 
-     emptyContent?: React.ReactNode;
 
-     getPopupContainer?: () => HTMLElement;
 
-     insetLabel?: React.ReactNode;
 
-     insetLabelId?: string;
 
-     id?: string;
 
-     loading?: boolean;
 
-     motion?: Motion;
 
-     maxHeight?: string | number;
 
-     mouseEnterDelay?: number;
 
-     mouseLeaveDelay?: number;
 
-     onFocus?: (e: React.FocusEvent) => void;
 
-     onBlur?: (e: React.FocusEvent) => void;
 
-     onChange?: (value: string | number) => void;
 
-     onSearch?: (inputValue: string) => void;
 
-     onSelect?: (value: T) => void;
 
-     onClear?: () => void;
 
-     onChangeWithObject?: boolean;
 
-     onSelectWithObject?: boolean;
 
-     onDropdownVisibleChange?: (visible: boolean) => void;
 
-     onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
 
-     prefix?: React.ReactNode;
 
-     placeholder?: string;
 
-     position?: Position;
 
-     renderItem?: (option: T) => React.ReactNode;
 
-     renderSelectedItem?: (option: T) => string;
 
-     size?: 'small' | 'default' | 'large';
 
-     style?: React.CSSProperties;
 
-     suffix?: React.ReactNode;
 
-     showClear?: boolean;
 
-     triggerRender?: (props?: any) => React.ReactNode;
 
-     stopPropagation?: boolean | string;
 
-     value?: string | number;
 
-     validateStatus?: ValidateStatus;
 
-     zIndex?: number;
 
- }
 
- interface KeyboardEventType {
 
-     onKeyDown?: React.KeyboardEventHandler;
 
- }
 
- interface AutoCompleteState {
 
-     dropdownMinWidth: null | number;
 
-     inputValue: string | undefined | number;
 
-     options: StateOptionItem[];
 
-     visible: boolean;
 
-     focusIndex: number;
 
-     selection: Map<any, any>;
 
-     rePosKey: number;
 
-     keyboardEventSet?: KeyboardEventType;
 
- }
 
- class AutoComplete<T extends AutoCompleteItems> extends BaseComponent<AutoCompleteProps<T>, AutoCompleteState> {
 
-     static propTypes = {
 
-         'aria-label': PropTypes.string,
 
-         'aria-labelledby': PropTypes.string,
 
-         'aria-invalid': PropTypes.bool,
 
-         'aria-errormessage': PropTypes.string,
 
-         'aria-describedby': PropTypes.string,
 
-         'aria-required': PropTypes.bool,
 
-         autoFocus: PropTypes.bool,
 
-         autoAdjustOverflow: PropTypes.bool,
 
-         className: PropTypes.string,
 
-         children: PropTypes.node,
 
-         data: PropTypes.array,
 
-         defaultOpen: PropTypes.bool,
 
-         defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
 
-         defaultActiveFirstOption: PropTypes.bool,
 
-         disabled: PropTypes.bool,
 
-         dropdownMatchSelectWidth: PropTypes.bool,
 
-         dropdownClassName: PropTypes.string,
 
-         dropdownStyle: PropTypes.object,
 
-         emptyContent: PropTypes.node,
 
-         id: PropTypes.string,
 
-         insetLabel: PropTypes.node,
 
-         insetLabelId: PropTypes.string,
 
-         onSearch: PropTypes.func,
 
-         onSelect: PropTypes.func,
 
-         onClear: PropTypes.func,
 
-         onBlur: PropTypes.func,
 
-         onFocus: PropTypes.func,
 
-         onChange: PropTypes.func,
 
-         onKeyDown: PropTypes.func,
 
-         position: PropTypes.oneOf(positionSet),
 
-         placeholder: PropTypes.string,
 
-         prefix: PropTypes.node,
 
-         onChangeWithObject: PropTypes.bool,
 
-         onSelectWithObject: PropTypes.bool,
 
-         renderItem: PropTypes.func,
 
-         renderSelectedItem: PropTypes.func,
 
-         suffix: PropTypes.node,
 
-         showClear: PropTypes.bool,
 
-         size: PropTypes.oneOf(sizeSet),
 
-         style: PropTypes.object,
 
-         stopPropagation: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
 
-         maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
 
-         mouseEnterDelay: PropTypes.number,
 
-         mouseLeaveDelay: PropTypes.number,
 
-         motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),
 
-         getPopupContainer: PropTypes.func,
 
-         triggerRender: PropTypes.func,
 
-         value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
 
-         validateStatus: PropTypes.oneOf(statusSet),
 
-         zIndex: PropTypes.number,
 
-     };
 
-     static Option = Option;
 
-     static defaultProps = {
 
-         stopPropagation: true,
 
-         motion: true,
 
-         zIndex: popoverNumbers.DEFAULT_Z_INDEX,
 
-         position: 'bottomLeft' as const,
 
-         data: [] as [],
 
-         showClear: false,
 
-         size: 'default' as const,
 
-         onFocus: noop,
 
-         onSearch: noop,
 
-         onClear: noop,
 
-         onBlur: noop,
 
-         onSelect: noop,
 
-         onChange: noop,
 
-         onSelectWithObject: false,
 
-         onDropdownVisibleChange: noop,
 
-         defaultActiveFirstOption: false,
 
-         dropdownMatchSelectWidth: true,
 
-         loading: false,
 
-         maxHeight: 300,
 
-         validateStatus: 'default' as const,
 
-         autoFocus: false,
 
-         emptyContent: null as null,
 
-         onKeyDown: noop,
 
-         // onPressEnter: () => undefined,
 
-         // defaultOpen: false,
 
-     };
 
-     triggerRef: React.RefObject<HTMLDivElement> | null;
 
-     optionsRef: React.RefObject<HTMLDivElement> | null;
 
-     private clickOutsideHandler: () => void | null;
 
-     constructor(props: AutoCompleteProps<T>) {
 
-         super(props);
 
-         this.foundation = new AutoCompleteFoundation(this.adapter);
 
-         const initRePosKey = 1;
 
-         this.state = {
 
-             dropdownMinWidth: null,
 
-             inputValue: '',
 
-             // option list
 
-             options: [],
 
-             // popover visible
 
-             visible: false,
 
-             // current focus option index
 
-             focusIndex: props.defaultActiveFirstOption ? 0 : -1,
 
-             // current selected options
 
-             selection: new Map(),
 
-             rePosKey: initRePosKey,
 
-         };
 
-         this.triggerRef = React.createRef();
 
-         this.optionsRef = React.createRef();
 
-         this.clickOutsideHandler = null;
 
-         warning(
 
-             'triggerRender' in this.props && typeof this.props.triggerRender === 'function',
 
-             `[Semi AutoComplete]
 
-             - If you are using the following props: 'suffix', 'prefix', 'showClear', 'validateStatus', and 'size',
 
-             please notice that they will be removed in the next major version.
 
-             Please use 'componentProps' to retrieve these props instead.
 
-             - If you are using 'onBlur', 'onFocus', please try to avoid using them and look for changes in the future.`
 
-         );
 
-     }
 
-     get adapter(): AutoCompleteAdapter<AutoCompleteProps<T>, AutoCompleteState> {
 
-         const keyboardAdapter = {
 
-             registerKeyDown: (cb: any): void => {
 
-                 const keyboardEventSet = {
 
-                     onKeyDown: cb,
 
-                 };
 
-                 this.setState({ keyboardEventSet });
 
-             },
 
-             unregisterKeyDown: (cb: any): void => {
 
-                 this.setState({ keyboardEventSet: {} });
 
-             },
 
-             updateFocusIndex: (focusIndex: number): void => {
 
-                 this.setState({ focusIndex });
 
-             },
 
-         };
 
-         return {
 
-             ...super.adapter,
 
-             ...keyboardAdapter,
 
-             getTriggerWidth: () => {
 
-                 const el = this.triggerRef.current;
 
-                 return el && el.getBoundingClientRect().width;
 
-             },
 
-             setOptionWrapperWidth: width => {
 
-                 this.setState({ dropdownMinWidth: width });
 
-             },
 
-             updateInputValue: inputValue => {
 
-                 this.setState({ inputValue });
 
-             },
 
-             toggleListVisible: isShow => {
 
-                 this.setState({ visible: isShow });
 
-             },
 
-             updateOptionList: optionList => {
 
-                 this.setState({ options: optionList });
 
-             },
 
-             updateSelection: selection => {
 
-                 this.setState({ selection });
 
-             },
 
-             notifySearch: inputValue => {
 
-                 this.props.onSearch(inputValue);
 
-             },
 
-             notifyChange: value => {
 
-                 this.props.onChange(value);
 
-             },
 
-             notifySelect: (option: StateOptionItem | string | number): void => {
 
-                 this.props.onSelect(option as T);
 
-             },
 
-             notifyDropdownVisibleChange: (isVisible: boolean): void => {
 
-                 this.props.onDropdownVisibleChange(isVisible);
 
-             },
 
-             notifyClear: () => {
 
-                 this.props.onClear();
 
-             },
 
-             notifyFocus: (event: React.FocusEvent) => {
 
-                 this.props.onFocus(event);
 
-             },
 
-             notifyBlur: (event: React.FocusEvent) => {
 
-                 this.props.onBlur(event);
 
-             },
 
-             notifyKeyDown: e => {
 
-                 this.props.onKeyDown(e);
 
-             },
 
-             rePositionDropdown: () => {
 
-                 let { rePosKey } = this.state;
 
-                 rePosKey = rePosKey + 1;
 
-                 this.setState({ rePosKey });
 
-             }
 
-         };
 
-     }
 
-     componentDidMount() {
 
-         this.foundation.init();
 
-     }
 
-     componentWillUnmount() {
 
-         this.foundation.destroy();
 
-     }
 
-     componentDidUpdate(prevProps: AutoCompleteProps<T>, prevState: AutoCompleteState) {
 
-         if (!isEqual(this.props.data, prevProps.data)) {
 
-             this.foundation.handleDataChange(this.props.data);
 
-         }
 
-         if (this.props.value !== prevProps.value) {
 
-             this.foundation.handleValueChange(this.props.value);
 
-         }
 
-     }
 
-     onSelect = (option: StateOptionItem, optionIndex: number, e: React.MouseEvent | React.KeyboardEvent): void => {
 
-         this.foundation.handleSelect(option, optionIndex);
 
-     };
 
-     onSearch = (value: string): void => {
 
-         this.foundation.handleSearch(value);
 
-     };
 
-     onBlur = (e: React.FocusEvent): void => this.foundation.handleBlur(e);
 
-     onFocus = (e: React.FocusEvent): void => this.foundation.handleFocus(e);
 
-     onInputClear = (): void => this.foundation.handleClear();
 
-     handleInputClick = (e: React.MouseEvent): void => this.foundation.handleInputClick(e);
 
-     renderInput(): React.ReactNode {
 
-         const {
 
-             size,
 
-             prefix,
 
-             insetLabel,
 
-             insetLabelId,
 
-             suffix,
 
-             placeholder,
 
-             style,
 
-             className,
 
-             showClear,
 
-             disabled,
 
-             triggerRender,
 
-             validateStatus,
 
-             autoFocus,
 
-             value,
 
-             id,
 
-         } = this.props;
 
-         const { inputValue, keyboardEventSet, selection } = this.state;
 
-         const useCustomTrigger = typeof triggerRender === 'function';
 
-         const outerProps = {
 
-             style,
 
-             className: useCustomTrigger
 
-                 ? cls(className)
 
-                 : cls(
 
-                     {
 
-                         [prefixCls]: true,
 
-                         [`${prefixCls}-disabled`]: disabled,
 
-                     },
 
-                     className
 
-                 ),
 
-             onClick: this.handleInputClick,
 
-             ref: this.triggerRef,
 
-             id,
 
-             ...keyboardEventSet,
 
-             // tooltip give tabindex 0 to children by default, autoComplete just need the input get focus, so outer div's tabindex set to -1
 
-             tabIndex: -1
 
-         };
 
-         const innerProps = {
 
-             disabled,
 
-             placeholder,
 
-             autofocus: autoFocus,
 
-             onChange: this.onSearch,
 
-             onClear: this.onInputClear,
 
-             'aria-label': this.props['aria-label'],
 
-             'aria-labelledby': this.props['aria-labelledby'],
 
-             'aria-invalid': this.props['aria-invalid'],
 
-             'aria-errormessage': this.props['aria-errormessage'],
 
-             'aria-describedby': this.props['aria-describedby'],
 
-             'aria-required': this.props['aria-required'],
 
-             // TODO: remove in next major version
 
-             suffix,
 
-             prefix: prefix || insetLabel,
 
-             insetLabelId,
 
-             showClear,
 
-             validateStatus,
 
-             size,
 
-             onBlur: this.onBlur,
 
-             onFocus: this.onFocus,
 
-         };
 
-         return (
 
-             <div {...outerProps}>
 
-                 {typeof triggerRender === 'function' ? (
 
-                     <Trigger
 
-                         {...innerProps}
 
-                         inputValue={(typeof value !== 'undefined' ? value : inputValue) as string}
 
-                         value={Array.from(selection.values())}
 
-                         triggerRender={triggerRender}
 
-                         componentName="AutoComplete"
 
-                         componentProps={{ ...this.props }}
 
-                     />
 
-                 ) : (
 
-                     <Input {...innerProps} value={typeof value !== 'undefined' ? value : inputValue} />
 
-                 )}
 
-             </div>
 
-         );
 
-     }
 
-     renderLoading() {
 
-         const loadingWrapperCls = `${prefixCls}-loading-wrapper`;
 
-         return (
 
-             <div className={loadingWrapperCls}>
 
-                 <Spin />
 
-             </div>
 
-         );
 
-     }
 
-     renderOption(option: StateOptionItem, optionIndex: number): React.ReactNode {
 
-         const { focusIndex } = this.state;
 
-         const isFocused = optionIndex === focusIndex;
 
-         return (
 
-             <Option
 
-                 showTick={false}
 
-                 onSelect={(v: StateOptionItem, e: React.MouseEvent | React.KeyboardEvent) => this.onSelect(v, optionIndex, e)}
 
-                 // selected={selection.has(option.label)}
 
-                 focused={isFocused}
 
-                 onMouseEnter={() => this.foundation.handleOptionMouseEnter(optionIndex)}
 
-                 key={option.key || option.label + option.value + optionIndex}
 
-                 {...option}
 
-             >
 
-                 {option.label}
 
-             </Option>
 
-         );
 
-     }
 
-     renderOptionList(): React.ReactNode {
 
-         const { maxHeight, dropdownStyle, dropdownClassName, loading, emptyContent } = this.props;
 
-         const { options, dropdownMinWidth } = this.state;
 
-         const listCls = cls(
 
-             {
 
-                 [`${prefixCls}-option-list`]: true,
 
-             },
 
-             dropdownClassName
 
-         );
 
-         let optionsNode;
 
-         if (options.length === 0) {
 
-             optionsNode = emptyContent;
 
-         } else {
 
-             optionsNode = options.filter(option => option.show).map((option, i) => this.renderOption(option, i));
 
-         }
 
-         const style = {
 
-             maxHeight: maxHeight,
 
-             minWidth: dropdownMinWidth,
 
-             ...dropdownStyle,
 
-         };
 
-         return (
 
-             <div className={listCls} role="listbox" style={style}>
 
-                 {!loading ? optionsNode : this.renderLoading()}
 
-             </div>
 
-         );
 
-     }
 
-     render(): React.ReactNode {
 
-         const {
 
-             position,
 
-             motion,
 
-             zIndex,
 
-             mouseEnterDelay,
 
-             mouseLeaveDelay,
 
-             autoAdjustOverflow,
 
-             stopPropagation,
 
-             getPopupContainer,
 
-         } = this.props;
 
-         const { visible, rePosKey } = this.state;
 
-         const input = this.renderInput();
 
-         const optionList = this.renderOptionList();
 
-         return (
 
-             <Popover
 
-                 mouseEnterDelay={mouseEnterDelay}
 
-                 mouseLeaveDelay={mouseLeaveDelay}
 
-                 autoAdjustOverflow={autoAdjustOverflow}
 
-                 trigger="custom"
 
-                 motion={motion}
 
-                 visible={visible}
 
-                 content={optionList}
 
-                 position={position}
 
-                 ref={this.optionsRef as any}
 
-                 // TransformFromCenter TODO: need to confirm
 
-                 zIndex={zIndex}
 
-                 stopPropagation={stopPropagation}
 
-                 getPopupContainer={getPopupContainer}
 
-                 rePosKey={rePosKey}
 
-             >
 
-                 {input}
 
-             </Popover>
 
-         );
 
-     }
 
- }
 
- export default AutoComplete;
 
 
  |