| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999 | import React, { Fragment, ReactNode, CSSProperties, MouseEvent, KeyboardEvent } from 'react';import ReactDOM from 'react-dom';import cls from 'classnames';import PropTypes from 'prop-types';import CascaderFoundation, {    /* Corresponding to the state of react */    BasicCascaderInnerData,    /* Corresponding to the props of react */    BasicCascaderProps,    BasicTriggerRenderProps,    BasicScrollPanelProps,    CascaderAdapter,    CascaderType} from '@douyinfe/semi-foundation/cascader/foundation';import { cssClasses, strings } from '@douyinfe/semi-foundation/cascader/constants';import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';import { isSet, isEqual, isString, isEmpty, isFunction, isNumber, noop, flatten } from 'lodash';import '@douyinfe/semi-foundation/cascader/cascader.scss';import { IconClear, IconChevronDown } from '@douyinfe/semi-icons';import { findKeysForValues, convertDataToEntities, calcMergeType } from '@douyinfe/semi-foundation/cascader/util';import { calcCheckedKeys, normalizeKeyList, calcDisabledKeys } from '@douyinfe/semi-foundation/tree/treeUtil';import ConfigContext, { ContextValue } from '../configProvider/context';import BaseComponent, { ValidateStatus } from '../_base/baseComponent';import Input from '../input/index';import Popover, { PopoverProps } from '../popover/index';import Item, { CascaderData, Entities, Entity, Data } from './item';import Trigger from '../trigger';import Tag from '../tag';import TagInput from '../tagInput';import { Motion } from '../_base/base';import { isSemiIcon } from '../_utils';import { Position } from '../tooltip/index';export { CascaderType, ShowNextType } from '@douyinfe/semi-foundation/cascader/foundation';export { CascaderData, Entity, Data, CascaderItemProps } from './item';export interface ScrollPanelProps extends BasicScrollPanelProps {    activeNode: CascaderData;}export interface TriggerRenderProps extends BasicTriggerRenderProps {    componentProps: CascaderProps;    onClear: (e: React.MouseEvent) => void;}/* The basic type of the value of Cascader */export type SimpleValueType = string | number | CascaderData;/* The value of Cascader */export type Value = SimpleValueType | Array<SimpleValueType> | Array<Array<SimpleValueType>>;export interface CascaderProps extends BasicCascaderProps {    'aria-describedby'?: React.AriaAttributes['aria-describedby'];    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];    'aria-invalid'?: React.AriaAttributes['aria-invalid'];    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];    'aria-required'?: React.AriaAttributes['aria-required'];    'aria-label'?: React.AriaAttributes['aria-label'];    arrowIcon?: ReactNode;    defaultValue?: Value;    dropdownStyle?: CSSProperties;    emptyContent?: ReactNode;    motion?: Motion;    treeData?: Array<CascaderData>;    restTagsPopoverProps?: PopoverProps;    children?: React.ReactNode;    value?: Value;    prefix?: ReactNode;    suffix?: ReactNode;    id?: string;    insetLabel?: ReactNode;    insetLabelId?: string;    style?: CSSProperties;    bottomSlot?: ReactNode;    topSlot?: ReactNode;    triggerRender?: (props: TriggerRenderProps) => ReactNode;    onListScroll?: (e: React.UIEvent<HTMLUListElement, UIEvent>, panel: ScrollPanelProps) => void;    loadData?: (selectOptions: CascaderData[]) => Promise<void>;    onLoad?: (newLoadedKeys: Set<string>, data: CascaderData) => void;    onChange?: (value: Value) => void;    onExceed?: (checkedItem: Entity[]) => void;    displayRender?: (selected: Array<string> | Entity, idx?: number) => ReactNode;    onBlur?: (e: MouseEvent) => void;    onFocus?: (e: MouseEvent) => void;    validateStatus?: ValidateStatus;    position?: Position;}export interface CascaderState extends BasicCascaderInnerData {    keyEntities: Entities;    prevProps: CascaderProps;    treeData?: Array<CascaderData>;}const prefixcls = cssClasses.PREFIX;const resetkey = 0;class Cascader extends BaseComponent<CascaderProps, CascaderState> {    static contextType = ConfigContext;    static propTypes = {        'aria-labelledby': PropTypes.string,        'aria-invalid': PropTypes.bool,        'aria-errormessage': PropTypes.string,        'aria-describedby': PropTypes.string,        'aria-required': PropTypes.bool,        'aria-label': PropTypes.string,        arrowIcon: PropTypes.node,        changeOnSelect: PropTypes.bool,        defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),        disabled: PropTypes.bool,        dropdownClassName: PropTypes.string,        dropdownStyle: PropTypes.object,        emptyContent: PropTypes.node,        motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),        /* show search input, if passed in a function, used as custom filter */        filterTreeNode: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),        filterLeafOnly: PropTypes.bool,        placeholder: PropTypes.string,        searchPlaceholder: PropTypes.string,        size: PropTypes.oneOf<CascaderType>(strings.SIZE_SET),        style: PropTypes.object,        className: PropTypes.string,        treeData: PropTypes.arrayOf(            PropTypes.shape({                value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),                label: PropTypes.any,            })        ),        treeNodeFilterProp: PropTypes.string,        suffix: PropTypes.node,        prefix: PropTypes.node,        insetLabel: PropTypes.node,        insetLabelId: PropTypes.string,        id: PropTypes.string,        displayProp: PropTypes.string,        displayRender: PropTypes.func,        onChange: PropTypes.func,        onSearch: PropTypes.func,        onSelect: PropTypes.func,        onBlur: PropTypes.func,        onFocus: PropTypes.func,        children: PropTypes.node,        getPopupContainer: PropTypes.func,        zIndex: PropTypes.number,        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),        validateStatus: PropTypes.oneOf<CascaderProps['validateStatus']>(strings.VALIDATE_STATUS),        showNext: PropTypes.oneOf([strings.SHOW_NEXT_BY_CLICK, strings.SHOW_NEXT_BY_HOVER]),        stopPropagation: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),        showClear: PropTypes.bool,        defaultOpen: PropTypes.bool,        autoAdjustOverflow: PropTypes.bool,        onDropdownVisibleChange: PropTypes.func,        triggerRender: PropTypes.func,        onListScroll: PropTypes.func,        onChangeWithObject: PropTypes.bool,        bottomSlot: PropTypes.node,        topSlot: PropTypes.node,        multiple: PropTypes.bool,        autoMergeValue: PropTypes.bool,        maxTagCount: PropTypes.number,        showRestTagsPopover: PropTypes.bool,        restTagsPopoverProps: PropTypes.object,        max: PropTypes.number,        separator: PropTypes.string,        onExceed: PropTypes.func,        onClear: PropTypes.func,        loadData: PropTypes.func,        onLoad: PropTypes.func,        loadedKeys: PropTypes.array,        disableStrictly: PropTypes.bool,        leafOnly: PropTypes.bool,        enableLeafClick: PropTypes.bool,        preventScroll: PropTypes.bool,        position:PropTypes.string    };    static defaultProps = {        leafOnly: false,        arrowIcon: <IconChevronDown />,        stopPropagation: true,        motion: true,        defaultOpen: false,        zIndex: popoverNumbers.DEFAULT_Z_INDEX,        showClear: false,        autoClearSearchValue: true,        changeOnSelect: false,        disableStrictly: false,        autoMergeValue: true,        multiple: false,        filterTreeNode: false,        filterLeafOnly: true,        showRestTagsPopover: false,        restTagsPopoverProps: {},        separator: ' / ',        size: 'default' as const,        treeNodeFilterProp: 'label' as const,        displayProp: 'label' as const,        treeData: [] as Array<CascaderData>,        showNext: strings.SHOW_NEXT_BY_CLICK,        onExceed: noop,        onClear: noop,        onDropdownVisibleChange: noop,        onListScroll: noop,        enableLeafClick: false,        'aria-label': 'Cascader',    };    options: any;    isEmpty: boolean;    inputRef: React.RefObject<typeof Input>;    triggerRef: React.RefObject<HTMLDivElement>;    optionsRef: React.RefObject<any>;    clickOutsideHandler: any;    mergeType: string;    context: ContextValue;    constructor(props: CascaderProps) {        super(props);        this.state = {            disabledKeys: new Set(),            isOpen: props.defaultOpen,            /* By changing rePosKey, the dropdown position can be refreshed */            rePosKey: resetkey,            /* A data structure for storing cascader data items */            keyEntities: {},            /* Selected and show tick icon */            selectedKeys: new Set([]),            /* The key of the activated node */            activeKeys: new Set([]),            /* The key of the filtered node */            filteredKeys: new Set([]),            /* Value of input box */            inputValue: '',            /* Is searching */            isSearching: false,            /* The placeholder of input box */            inputPlaceHolder: props.searchPlaceholder || props.placeholder,            /* Cache props */            prevProps: {},            /* Is hovering */            isHovering: false,            /* Key of checked node, when multiple */            checkedKeys: new Set([]),            /* Key of half checked node, when multiple */            halfCheckedKeys: new Set([]),            /* Auto merged checkedKeys or leaf checkedKeys, when multiple */            resolvedCheckedKeys: new Set([]),            /* Keys of loaded item */            loadedKeys: new Set(),            /* Keys of loading item */            loadingKeys: new Set(),            /* Mark whether this rendering has triggered asynchronous loading of data */            loading: false,            showInput: false,        };        this.options = {};        this.isEmpty = false;        this.mergeType = calcMergeType(props.autoMergeValue, props.leafOnly);        this.inputRef = React.createRef();        this.triggerRef = React.createRef();        this.optionsRef = React.createRef();        this.clickOutsideHandler = null;        this.foundation = new CascaderFoundation(this.adapter);    }    get adapter(): CascaderAdapter {        const filterAdapter: Pick<CascaderAdapter, 'updateInputValue' | 'updateInputPlaceHolder' | 'focusInput'> = {            updateInputValue: value => {                this.setState({ inputValue: value });            },            updateInputPlaceHolder: value => {                this.setState({ inputPlaceHolder: value });            },            focusInput: () => {                const { preventScroll } = this.props;                if (this.inputRef && this.inputRef.current) {                    // TODO: check the reason                    (this.inputRef.current as any).focus({ preventScroll });                }            },        };        const cascaderAdapter: Pick<        CascaderAdapter,        'registerClickOutsideHandler' | 'unregisterClickOutsideHandler' | 'rePositionDropdown'        > = {            registerClickOutsideHandler: cb => {                const clickOutsideHandler = (e: Event) => {                    const optionInstance = this.optionsRef && this.optionsRef.current;                    const triggerDom = this.triggerRef && this.triggerRef.current;                    const optionsDom = ReactDOM.findDOMNode(optionInstance);                    const target = e.target as Element;                    if (                        optionsDom &&                        (!optionsDom.contains(target) || !optionsDom.contains(target.parentNode)) &&                        triggerDom &&                        !triggerDom.contains(target)                    ) {                        cb(e);                    }                };                this.clickOutsideHandler = clickOutsideHandler;                document.addEventListener('mousedown', clickOutsideHandler, false);            },            unregisterClickOutsideHandler: () => {                document.removeEventListener('mousedown', this.clickOutsideHandler, false);            },            rePositionDropdown: () => {                let { rePosKey } = this.state;                rePosKey = rePosKey + 1;                this.setState({ rePosKey });            },        };        return {            ...super.adapter,            ...filterAdapter,            ...cascaderAdapter,            updateStates: states => {                this.setState({ ...states } as CascaderState);            },            openMenu: () => {                this.setState({ isOpen: true });            },            closeMenu: cb => {                this.setState({ isOpen: false }, () => {                    cb && cb();                });            },            updateSelection: selectedKeys => this.setState({ selectedKeys }),            notifyChange: value => {                this.props.onChange && this.props.onChange(value);            },            notifySelect: selected => {                this.props.onSelect && this.props.onSelect(selected);            },            notifyOnSearch: input => {                this.props.onSearch && this.props.onSearch(input);            },            notifyFocus: (...v) => {                this.props.onFocus && this.props.onFocus(...v);            },            notifyBlur: (...v) => {                this.props.onBlur && this.props.onBlur(...v);            },            notifyDropdownVisibleChange: visible => {                this.props.onDropdownVisibleChange(visible);            },            toggleHovering: bool => {                this.setState({ isHovering: bool });            },            notifyLoadData: (selectedOpt, callback) => {                const { loadData } = this.props;                if (loadData) {                    new Promise<void>(resolve => {                        loadData(selectedOpt).then(() => {                            callback();                            this.setState({ loading: false });                            resolve();                        });                    });                }            },            notifyOnLoad: (newLoadedKeys, data) => {                const { onLoad } = this.props;                onLoad && onLoad(newLoadedKeys, data);            },            notifyListScroll: (e, { panelIndex, activeNode }) => {                this.props.onListScroll(e, { panelIndex, activeNode });            },            notifyOnExceed: data => this.props.onExceed(data),            notifyClear: () => this.props.onClear(),            toggleInputShow: (showInput: boolean, cb: (...args: any) => void) => {                this.setState({ showInput }, () => {                    cb();                });            },            updateFocusState: (isFocus: boolean) => {                this.setState({ isFocus });            },        };    }    static getDerivedStateFromProps(props: CascaderProps, prevState: CascaderState) {        const { multiple, value, defaultValue, onChangeWithObject, leafOnly, autoMergeValue } = props;        const { prevProps } = prevState;        let keyEntities = prevState.keyEntities || {};        const newState: Partial<CascaderState> = {};        const needUpdate = (name: string) => {            const firstInProps = isEmpty(prevProps) && name in props;            const nameHasChange = prevProps && !isEqual(prevProps[name], props[name]);            return firstInProps || nameHasChange;        };        const needUpdateData = () => {            const firstInProps = !prevProps && 'treeData' in props;            const treeDataHasChange = prevProps && prevProps.treeData !== props.treeData;            return firstInProps || treeDataHasChange;        };        const getRealKeys = (realValue: Value, keyEntities: Entities) => {            // normallizedValue is used to save the value in two-dimensional array format            let normallizedValue: SimpleValueType[][] = [];            if (Array.isArray(realValue)) {                normallizedValue = Array.isArray(realValue[0])                    ? (realValue as SimpleValueType[][])                    : ([realValue] as SimpleValueType[][]);            } else {                if (realValue !==  undefined) {                    normallizedValue = [[realValue]];                }            }            // formatValuePath is used to save value of valuePath            const formatValuePath: (string | number)[][] = [];            normallizedValue.forEach((valueItem: SimpleValueType[]) => {                const formatItem: (string | number)[] = onChangeWithObject ?                    (valueItem as CascaderData[]).map(i => i?.value) :                    valueItem as (string | number)[];                formatValuePath.push(formatItem);            });            // formatKeys is used to save key of value            const formatKeys: any[] = [];            formatValuePath.forEach(v => {                const formatKeyItem = findKeysForValues(v, keyEntities);                !isEmpty(formatKeyItem) && formatKeys.push(formatKeyItem);            });            return formatKeys;        };        const needUpdateTreeData = needUpdate('treeData') || needUpdateData();        const needUpdateValue = needUpdate('value') || (isEmpty(prevProps) && defaultValue);        if (multiple) {            // when value and treedata need updated            if (needUpdateTreeData || needUpdateValue) {                // update state.keyEntities                if (needUpdateTreeData) {                    newState.treeData = props.treeData;                    keyEntities = convertDataToEntities(props.treeData);                    newState.keyEntities = keyEntities;                }                let realKeys: Array<string> | Set<string> = prevState.checkedKeys;                // when data was updated                if (needUpdateValue) {                    const realValue = needUpdate('value') ? value : defaultValue;                    realKeys = getRealKeys(realValue, keyEntities);                } else {                    // needUpdateValue is false                    // if treeData is updated & Cascader is controlled, realKeys should be recalculated                    if (needUpdateTreeData && 'value' in props) {                        const realValue = value;                        realKeys = getRealKeys(realValue, keyEntities);                    }                }                if (isSet(realKeys)) {                    realKeys = [...realKeys];                }                const calRes = calcCheckedKeys(flatten(realKeys), keyEntities);                const checkedKeys = new Set(calRes.checkedKeys);                const halfCheckedKeys = new Set(calRes.halfCheckedKeys);                // disableStrictly                if (props.disableStrictly) {                    newState.disabledKeys = calcDisabledKeys(keyEntities);                }                const isLeafOnlyMerge = calcMergeType(autoMergeValue, leafOnly) === strings.LEAF_ONLY_MERGE_TYPE;                newState.prevProps = props;                newState.checkedKeys = checkedKeys;                newState.halfCheckedKeys = halfCheckedKeys;                newState.resolvedCheckedKeys = new Set(normalizeKeyList(checkedKeys, keyEntities, isLeafOnlyMerge));            }        }        return newState;    }    componentDidMount() {        this.foundation.init();    }    componentWillUnmount() {        this.foundation.destroy();    }    componentDidUpdate(prevProps: CascaderProps) {        let isOptionsChanged = false;        if (!isEqual(prevProps.treeData, this.props.treeData)) {            isOptionsChanged = true;            this.foundation.collectOptions();        }        if (prevProps.value !== this.props.value && !isOptionsChanged) {            this.foundation.handleValueChange(this.props.value);        }    }    handleInputChange = (value: string) => {        this.foundation.handleInputChange(value);    };    handleTagRemove = (e: any, tagValuePath: Array<string | number>) => {        this.foundation.handleTagRemove(e, tagValuePath);    };    renderTagItem = (value: string | Array<string>, idx: number, type: string) => {        const { keyEntities, disabledKeys } = this.state;        const { size, disabled, displayProp, displayRender, disableStrictly } = this.props;        const nodeKey = type === strings.IS_VALUE ? findKeysForValues(value, keyEntities)[0] : value;        const isDsiabled =            disabled || keyEntities[nodeKey].data.disabled || (disableStrictly && disabledKeys.has(nodeKey));        if (!isEmpty(keyEntities) && !isEmpty(keyEntities[nodeKey])) {            const tagCls = cls(`${prefixcls}-selection-tag`, {                [`${prefixcls}-selection-tag-disabled`]: isDsiabled,            });            // custom render tags            if (isFunction(displayRender)) {                return displayRender(keyEntities[nodeKey], idx);                // default render tags            } else {                return (                    <Tag                        size={size === 'default' ? 'large' : size}                        key={`tag-${nodeKey}-${idx}`}                        color="white"                        className={tagCls}                        closable                        onClose={(tagChildren, e) => {                            // When value has not changed, prevent clicking tag closeBtn to close tag                            e.preventDefault();                            this.handleTagRemove(e, keyEntities[nodeKey].valuePath);                        }}                    >                        {keyEntities[nodeKey].data[displayProp]}                    </Tag>                );            }        }        return null;    };    renderTagInput() {        const { size, disabled, placeholder, maxTagCount, showRestTagsPopover, restTagsPopoverProps } = this.props;        const { inputValue, checkedKeys, keyEntities, resolvedCheckedKeys } = this.state;        const tagInputcls = cls(`${prefixcls}-tagInput-wrapper`);        const tagValue: Array<Array<string>> = [];        const realKeys = this.mergeType === strings.NONE_MERGE_TYPE ? checkedKeys : resolvedCheckedKeys;        [...realKeys].forEach(checkedKey => {            if (!isEmpty(keyEntities[checkedKey])) {                tagValue.push(keyEntities[checkedKey].valuePath);            }        });        return (            <TagInput                className={tagInputcls}                ref={this.inputRef as any}                disabled={disabled}                size={size}                // TODO Modify logic, not modify type                value={(tagValue as unknown) as string[]}                showRestTagsPopover={showRestTagsPopover}                restTagsPopoverProps={restTagsPopoverProps}                maxTagCount={maxTagCount}                renderTagItem={(value, index) => this.renderTagItem(value, index, strings.IS_VALUE)}                inputValue={inputValue}                onInputChange={this.handleInputChange}                // TODO Modify logic, not modify type                onRemove={v => this.handleTagRemove(null, (v as unknown) as (string | number)[])}                placeholder={placeholder}            />        );    }    renderInput() {        const { size, disabled } = this.props;        const inputcls = cls(`${prefixcls}-input`);        const { inputValue, inputPlaceHolder, showInput } = this.state;        const inputProps = {            disabled,            value: inputValue,            className: inputcls,            onChange: this.handleInputChange,        };        const wrappercls = cls({            [`${prefixcls}-search-wrapper`]: true,        });        const displayText = this.renderDisplayText();        const spanCls = cls({            [`${prefixcls}-selection-placeholder`]: !displayText,            [`${prefixcls}-selection-text-hide`]: showInput && inputValue,            [`${prefixcls}-selection-text-inactive`]: showInput && !inputValue,        });        return (            <div className={wrappercls}>                <span className={spanCls}>{displayText ? displayText : inputPlaceHolder}</span>                {showInput && <Input ref={this.inputRef as any} size={size} {...inputProps} />}            </div>        );    }    handleItemClick = (e: MouseEvent | KeyboardEvent, item: Entity | Data) => {        this.foundation.handleItemClick(e, item);    };    handleItemHover = (e: MouseEvent, item: Entity) => {        this.foundation.handleItemHover(e, item);    };    onItemCheckboxClick = (item: Entity | Data) => {        this.foundation.onItemCheckboxClick(item);    };    handleListScroll = (e: React.UIEvent<HTMLUListElement, UIEvent>, ind: number) => {        this.foundation.handleListScroll(e, ind);    };    renderContent = () => {        const {            inputValue,            isSearching,            activeKeys,            selectedKeys,            checkedKeys,            halfCheckedKeys,            loadedKeys,            loadingKeys,        } = this.state;        const {            filterTreeNode,            dropdownClassName,            dropdownStyle,            loadData,            emptyContent,            separator,            topSlot,            bottomSlot,            showNext,            multiple,        } = this.props;        const searchable = Boolean(filterTreeNode) && isSearching;        const popoverCls = cls(dropdownClassName, `${prefixcls}-popover`);        const renderData = this.foundation.getRenderData();        const content = (            <div className={popoverCls} role="listbox" style={dropdownStyle}>                {topSlot}                <Item                    activeKeys={activeKeys}                    selectedKeys={selectedKeys}                    separator={separator}                    loadedKeys={loadedKeys}                    loadingKeys={loadingKeys}                    onItemClick={this.handleItemClick}                    onItemHover={this.handleItemHover}                    showNext={showNext}                    onItemCheckboxClick={this.onItemCheckboxClick}                    onListScroll={this.handleListScroll}                    searchable={searchable}                    keyword={inputValue}                    emptyContent={emptyContent}                    loadData={loadData}                    data={renderData}                    multiple={multiple}                    checkedKeys={checkedKeys}                    halfCheckedKeys={halfCheckedKeys}                />                {bottomSlot}            </div>        );        return content;    };    renderPlusN = (hiddenTag: Array<ReactNode>) => {        const { disabled, showRestTagsPopover, restTagsPopoverProps } = this.props;        const plusNCls = cls(`${prefixcls}-selection-n`, {            [`${prefixcls}-selection-n-disabled`]: disabled,        });        const renderPlusNChildren = <span className={plusNCls}>+{hiddenTag.length}</span>;        return showRestTagsPopover && !disabled ? (            <Popover                content={hiddenTag}                showArrow                trigger="hover"                position="top"                autoAdjustOverflow                {...restTagsPopoverProps}            >                {renderPlusNChildren}            </Popover>        ) : (            renderPlusNChildren        );    };    renderMultipleTags = () => {        const { autoMergeValue, maxTagCount } = this.props;        const { checkedKeys, resolvedCheckedKeys } = this.state;        const realKeys = this.mergeType === strings.NONE_MERGE_TYPE ? checkedKeys : resolvedCheckedKeys;        const displayTag: Array<ReactNode> = [];        const hiddenTag: Array<ReactNode> = [];        [...realKeys].forEach((checkedKey, idx) => {            const notExceedMaxTagCount = !isNumber(maxTagCount) || maxTagCount >= idx + 1;            const item = this.renderTagItem(checkedKey, idx, strings.IS_KEY);            if (notExceedMaxTagCount) {                displayTag.push(item);            } else {                hiddenTag.push(item);            }        });        return (            <>                {displayTag}                {!isEmpty(hiddenTag) && this.renderPlusN(hiddenTag)}            </>        );    };    renderDisplayText = (): ReactNode => {        const { displayProp, separator, displayRender } = this.props;        const { selectedKeys } = this.state;        let displayText: ReactNode = '';        if (selectedKeys.size) {            const displayPath = this.foundation.getItemPropPath([...selectedKeys][0], displayProp);            if (displayRender && typeof displayRender === 'function') {                displayText = displayRender(displayPath);            } else {                displayText = displayPath.map((path: ReactNode, index: number) => (                    <Fragment key={`${path}-${index}`}>                        {index < displayPath.length - 1 ? (                            <>                                {path}                                {separator}                            </>                        ) : (                            path                        )}                    </Fragment>                ));            }        }        return displayText;    };    renderSelectContent = () => {        const { placeholder, filterTreeNode, multiple } = this.props;        const { checkedKeys } = this.state;        const searchable = Boolean(filterTreeNode);        if (!searchable) {            if (multiple) {                if (isEmpty(checkedKeys)) {                    return <span className={`${prefixcls}-selection-placeholder`}>{placeholder}</span>;                }                return this.renderMultipleTags();            } else {                const displayText = this.renderDisplayText();                const spanCls = cls({                    [`${prefixcls}-selection-placeholder`]: !displayText,                });                return <span className={spanCls}>{displayText ? displayText : placeholder}</span>;            }        }        const input = multiple ? this.renderTagInput() : this.renderInput();        return input;    };    renderSuffix = () => {        const { suffix }: any = this.props;        const suffixWrapperCls = cls({            [`${prefixcls}-suffix`]: true,            [`${prefixcls}-suffix-text`]: suffix && isString(suffix),            [`${prefixcls}-suffix-icon`]: isSemiIcon(suffix),        });        return (            <div className={suffixWrapperCls} x-semi-prop="suffix">                {suffix}            </div>        );    };    renderPrefix = () => {        const { prefix, insetLabel, insetLabelId } = this.props;        const labelNode: any = prefix || insetLabel;        const prefixWrapperCls = cls({            [`${prefixcls}-prefix`]: true,            // to be doublechecked            [`${prefixcls}-inset-label`]: insetLabel,            [`${prefixcls}-prefix-text`]: labelNode && isString(labelNode),            [`${prefixcls}-prefix-icon`]: isSemiIcon(labelNode),        });        return (            <div className={prefixWrapperCls} id={insetLabelId} x-semi-prop="prefix,insetLabel">                {labelNode}            </div>        );    };    renderCustomTrigger = () => {        const { disabled, triggerRender, multiple } = this.props;        const { selectedKeys, inputValue, inputPlaceHolder, resolvedCheckedKeys, checkedKeys } = this.state;        let realValue;        if (multiple) {            if (this.mergeType === strings.NONE_MERGE_TYPE) {                realValue = checkedKeys;            } else {                realValue = resolvedCheckedKeys;            }        } else {            realValue = [...selectedKeys][0];        }        return (            <Trigger                value={realValue}                inputValue={inputValue}                onChange={this.handleInputChange}                onClear={this.handleClear}                placeholder={inputPlaceHolder}                disabled={disabled}                triggerRender={triggerRender}                componentName={'Cascader'}                componentProps={{ ...this.props }}            />        );    };    handleMouseOver = () => {        this.foundation.toggleHoverState(true);    };    handleMouseLeave = () => {        this.foundation.toggleHoverState(false);    };    handleClear = (e: MouseEvent) => {        e && e.stopPropagation();        this.foundation.handleClear();    };    /**     * A11y: simulate clear button click     */    /* istanbul ignore next */    handleClearEnterPress = (e: KeyboardEvent) => {        e && e.stopPropagation();        this.foundation.handleClearEnterPress(e);    };    showClearBtn = () => {        const { showClear, disabled, multiple } = this.props;        const { selectedKeys, isOpen, isHovering, checkedKeys } = this.state;        const hasValue = selectedKeys.size;        const multipleWithHaveValue = multiple && checkedKeys.size;        return showClear && (hasValue || multipleWithHaveValue) && !disabled && (isOpen || isHovering);    };    renderClearBtn = () => {        const clearCls = cls(`${prefixcls}-clearbtn`);        const allowClear = this.showClearBtn();        if (allowClear) {            return (                <div                    className={clearCls}                    onClick={this.handleClear}                    onKeyPress={this.handleClearEnterPress}                    role="button"                    tabIndex={0}                >                    <IconClear />                </div>            );        }        return null;    };    renderArrow = () => {        const { arrowIcon } = this.props;        const showClearBtn = this.showClearBtn();        if (showClearBtn) {            return null;        }        return arrowIcon ? (            <div className={cls(`${prefixcls}-arrow`)} x-semi-prop="arrowIcon">                {arrowIcon}            </div>        ) : null;    };    renderSelection = () => {        const {            disabled,            multiple,            filterTreeNode,            style,            size,            className,            validateStatus,            prefix,            suffix,            insetLabel,            triggerRender,            showClear,            id,        } = this.props;        const { isOpen, isFocus, isInput, checkedKeys } = this.state;        const filterable = Boolean(filterTreeNode);        const useCustomTrigger = typeof triggerRender === 'function';        const classNames = useCustomTrigger ?            cls(className) :            cls(prefixcls, className, {                [`${prefixcls}-focus`]: isFocus || (isOpen && !isInput),                [`${prefixcls}-disabled`]: disabled,                [`${prefixcls}-single`]: true,                [`${prefixcls}-filterable`]: filterable,                [`${prefixcls}-error`]: validateStatus === 'error',                [`${prefixcls}-warning`]: validateStatus === 'warning',                [`${prefixcls}-small`]: size === 'small',                [`${prefixcls}-large`]: size === 'large',                [`${prefixcls}-with-prefix`]: prefix || insetLabel,                [`${prefixcls}-with-suffix`]: suffix,            });        const mouseEvent = showClear ?            {                onMouseEnter: () => this.handleMouseOver(),                onMouseLeave: () => this.handleMouseLeave(),            } :            {};        const sectionCls = cls(`${prefixcls}-selection`, {            [`${prefixcls}-selection-multiple`]: multiple && !isEmpty(checkedKeys),        });        const inner = useCustomTrigger            ? this.renderCustomTrigger()            : [                <Fragment key={'prefix'}>{prefix || insetLabel ? this.renderPrefix() : null}</Fragment>,                <Fragment key={'selection'}>                    <div className={sectionCls}>{this.renderSelectContent()}</div>                </Fragment>,                <Fragment key={'clearbtn'}>{this.renderClearBtn()}</Fragment>,                <Fragment key={'suffix'}>{suffix ? this.renderSuffix() : null}</Fragment>,                <Fragment key={'arrow'}>{this.renderArrow()}</Fragment>,            ];        /**         * Reasons for disabling the a11y eslint rule:         * The following attributes(aria-controls,aria-expanded) will be automatically added by Tooltip, no need to declare here         */        return (            <div                className={classNames}                style={style}                ref={this.triggerRef}                onClick={e => this.foundation.handleClick(e)}                onKeyPress={e => this.foundation.handleSelectionEnterPress(e)}                aria-invalid={this.props['aria-invalid']}                aria-errormessage={this.props['aria-errormessage']}                aria-label={this.props['aria-label']}                aria-labelledby={this.props['aria-labelledby']}                aria-describedby={this.props['aria-describedby']}                aria-required={this.props['aria-required']}                id={id}                {...mouseEvent}                // eslint-disable-next-line jsx-a11y/role-has-required-aria-props                role="combobox"                tabIndex={0}            >                {inner}            </div>        );    };    render() {        const {            zIndex,            getPopupContainer,            autoAdjustOverflow,            stopPropagation,            mouseLeaveDelay,            mouseEnterDelay,            position        } = this.props;        const { isOpen, rePosKey } = this.state;        const { direction } = this.context;        const content = this.renderContent();        const selection = this.renderSelection();        const pos = position ?? (direction === 'rtl' ? 'bottomRight' : 'bottomLeft');        const mergedMotion: Motion = this.foundation.getMergedMotion();        return (            <Popover                getPopupContainer={getPopupContainer}                zIndex={zIndex}                motion={mergedMotion}                ref={this.optionsRef}                content={content}                visible={isOpen}                trigger="custom"                rePosKey={rePosKey}                position={pos}                autoAdjustOverflow={autoAdjustOverflow}                stopPropagation={stopPropagation}                mouseLeaveDelay={mouseLeaveDelay}                mouseEnterDelay={mouseEnterDelay}            >                {selection}            </Popover>        );    }}export default Cascader;
 |