import * as React from 'react'; import classNames from 'classnames'; import RcCascader from 'rc-cascader'; import type { CascaderProps as RcCascaderProps } from 'rc-cascader'; import type { ShowSearchType, FieldNames, DataNode } from 'rc-cascader/lib/interface'; import omit from 'rc-util/lib/omit'; import RightOutlined from '@ant-design/icons/RightOutlined'; import RedoOutlined from '@ant-design/icons/RedoOutlined'; import LeftOutlined from '@ant-design/icons/LeftOutlined'; import devWarning from '../_util/devWarning'; import { ConfigContext } from '../config-provider'; import type { SizeType } from '../config-provider/SizeContext'; import SizeContext from '../config-provider/SizeContext'; import getIcons from '../select/utils/iconUtil'; import { getTransitionName } from '../_util/motion'; // Align the design since we use `rc-select` in root. This help: // - List search content will show all content // - Hover opacity style // - Search filter match case export type BasicDataNode = Omit; export type FieldNamesType = FieldNames; export type FilledFieldNamesType = Required; function highlightKeyword(str: string, lowerKeyword: string, prefixCls: string | undefined) { const cells = str .toLowerCase() .split(lowerKeyword) .reduce((list, cur, index) => (index === 0 ? [cur] : [...list, lowerKeyword, cur]), []); const fillCells: React.ReactNode[] = []; let start = 0; cells.forEach((cell, index) => { const end = start + cell.length; let originWorld: React.ReactNode = str.slice(start, end); start = end; if (index % 2 === 1) { originWorld = ( {originWorld} ); } fillCells.push(originWorld); }); return fillCells; } const defaultSearchRender: ShowSearchType['render'] = (inputValue, path, prefixCls, fieldNames) => { const optionList: React.ReactNode[] = []; // We do lower here to save perf const lower = inputValue.toLowerCase(); path.forEach((node, index) => { if (index !== 0) { optionList.push(' / '); } let label = (node as any)[fieldNames.label!]; const type = typeof label; if (type === 'string' || type === 'number') { label = highlightKeyword(String(label), lower, prefixCls); } optionList.push(label); }); return optionList; }; export interface CascaderProps extends Omit { multiple?: boolean; size?: SizeType; bordered?: boolean; suffixIcon?: React.ReactNode; options?: DataNodeType[]; } export interface CascaderRef { focus: () => void; blur: () => void; } const Cascader = React.forwardRef((props: CascaderProps, ref: React.Ref) => { const { prefixCls: customizePrefixCls, size: customizeSize, className, multiple, bordered = true, transitionName, choiceTransitionName = '', popupClassName, dropdownClassName, expandIcon, showSearch, allowClear = true, notFoundContent, direction, getPopupContainer, ...rest } = props; const restProps = omit(rest, ['suffixIcon' as any]); const { getPopupContainer: getContextPopupContainer, getPrefixCls, renderEmpty, direction: rootDirection, // virtual, // dropdownMatchSelectWidth, } = React.useContext(ConfigContext); const mergedDirection = direction || rootDirection; const isRtl = mergedDirection === 'rtl'; // =================== Warning ===================== if (process.env.NODE_ENV !== 'production') { devWarning( popupClassName === undefined, 'Cascader', '`popupClassName` is deprecated. Please use `dropdownClassName` instead.', ); devWarning( !multiple || !props.displayRender, 'Cascader', '`displayRender` not work on `multiple`. Please use `tagRender` instead.', ); } // =================== No Found ==================== const mergedNotFoundContent = notFoundContent || renderEmpty('Cascader'); // ==================== Prefix ===================== const rootPrefixCls = getPrefixCls(); const prefixCls = getPrefixCls('select', customizePrefixCls); const cascaderPrefixCls = getPrefixCls('cascader', customizePrefixCls); // =================== Dropdown ==================== const mergedDropdownClassName = classNames( dropdownClassName || popupClassName, `${cascaderPrefixCls}-dropdown`, { [`${cascaderPrefixCls}-dropdown-rtl`]: mergedDirection === 'rtl', }, ); // ==================== Search ===================== const mergedShowSearch = React.useMemo(() => { if (!showSearch) { return showSearch; } let searchConfig: ShowSearchType = { render: defaultSearchRender, }; if (typeof showSearch === 'object') { searchConfig = { ...searchConfig, ...showSearch, }; } return searchConfig; }, [showSearch]); // ===================== Size ====================== const size = React.useContext(SizeContext); const mergedSize = customizeSize || size; // ===================== Icon ====================== let mergedExpandIcon = expandIcon; if (!expandIcon) { mergedExpandIcon = isRtl ? : ; } const loadingIcon = ( ); // =================== Multiple ==================== const checkable = React.useMemo( () => (multiple ? : false), [multiple], ); // ===================== Icons ===================== const { suffixIcon, removeIcon, clearIcon } = getIcons({ ...props, multiple, prefixCls, }); // ==================== Render ===================== return ( ); }) as (( props: React.PropsWithChildren> & { ref?: React.Ref }, ) => React.ReactElement) & { displayName: string; }; Cascader.displayName = 'Cascader'; export default Cascader;