import React, { ReactNode, RefObject, useCallback, useMemo, useRef, useState } from 'react' import RcCascader, { CascaderProps as RcCascaderProps, BaseOptionType as RcCascaderOption, ShowSearchType } from 'rc-cascader' import { CascaderRef, DefaultOptionType } from 'rc-cascader/lib/Cascader' import cns from 'classnames' import { useLangData, usePrefix } from '../ConfigProvider' import { useGetPopupContainer } from '../PopupContainerProvider' import { CommonProps, useInputSize, useInputStyle } from '../Input/input' import { concat } from '../utils' import { RightThinIcon, CloseCircleFilledIcon, DownIcon, SearchThinIcon } from '../icons' import { useUpdate, useValue } from '../utils/hooks' import { highlightKeyword } from './utils' import { Empty } from '../Empty' export type CascaderValue = Array export type CascaderOption = { /** 级联项的值 */ value: string | number /** 级联项的文本 */ label: NonNullable /** 是否禁用级联项 */ disabled?: boolean /** 子级项 */ children?: CascaderOption[] } export interface CascaderBaseProps extends Omit, 'onChange'> { /** 可选项数据源 */ options: CascaderOption[] /** 选择后的回调 */ onChange?: (value: T, selectedOptions: CascaderOption[]) => void /** * 是否选择后即时触发改变 * @default false */ changeOnSelect?: boolean /** * 次级菜单的展开方式 * @default hover */ expandTrigger?: 'hover' | 'click' /** 浮层样式名 */ overlayClassName?: string /** 选择后展示的渲染函数 */ displayRender?: (selectedOptions: CascaderOption[]) => ReactNode /** 是否支持搜索 */ searchable?: boolean /** 是否有清空功能 */ clearable?: boolean } export interface CascaderProps extends CascaderBaseProps { onChange?: (value: T | null, selectedOptions: CascaderOption[]) => void } export interface MultiCascaderProps extends CascaderBaseProps { onChange?: (value: T[], selectedOptions: CascaderOption[]) => void } function defaultDisplayRender(selectedOptions: CascaderOption[]) { return selectedOptions.map(it => it.label).join(' / ') } function useSharedProps( props: CascaderBaseProps, { multiple = false } ): Omit & { ref: RefObject } { const { value: valueFromProps, defaultValue, options, style, size: sizeFromProps, validateStatus, placeholder, disabled = false, changeOnSelect = false, expandTrigger = 'hover', onChange, className, overlayClassName, displayRender = defaultDisplayRender, searchable = false, clearable = false, ...restProps } = props const [visible, setVisible] = useState(false) const size = useInputSize(sizeFromProps) const prefix = usePrefix('cascader') const { emptyDataText } = useLangData('cascader') const ref = useRef(null) const getPopupContainer = useGetPopupContainer() const dropdownRender = useCallback((menu: React.ReactElement>) => { if (options.length === 0) { return } return menu }, [emptyDataText, options.length, prefix]) const checkable = useMemo( () => (multiple ? : false), [multiple, prefix] ) const mergedShowSearch = useMemo(() => { if (!searchable) { return searchable } return { render: defaultSearchRender } }, [searchable]) useUpdate(() => { if (visible === false) { ref.current?.blur() } }, [visible]) return { ref, notFoundContent: , className: cns(useInputStyle({ size, validateStatus, disabled, focused: visible }), concat(prefix, 'size', size)), displayRender: ( _: string[], selectedOptions: DefaultOptionType[] | undefined ) => selectedOptions && displayRender(toCascaderOption(selectedOptions)), options, disabled, prefixCls: prefix, open: visible, showSearch: mergedShowSearch, onDropdownVisibleChange: setVisible, expandTrigger, expandIcon: , inputIcon: searchable ? : , dropdownRender, allowClear: clearable, clearIcon: , removeIcon: , dropdownClassName: cns(overlayClassName, concat(prefix, 'dropdown', 'size', size)), getPopupContainer, changeOnSelect, placeholder, maxTagCount: 'responsive', checkable, style, dropdownAlign: { offset: [0, 8] }, ...restProps } } function toCascaderOption(options: DefaultOptionType[]): CascaderOption[] { return options.map(opt => ({ label: opt.label!, value: opt.value! })) } export default function Cascader(props: CascaderProps) { const { value: valueFromProps, defaultValue = null, onChange } = props const { value, setValue } = useValue({ valueFromProps, defaultValue, onChange: useCallback((v: T | null, o: CascaderOption[]) => { // 兼容之前空为 null 的写法,感觉 [] 更适合 onChange?.(v ?? null, o) }, [onChange]) }) const { checkable, ...rcCascaderProps } = useSharedProps(props, {}) const handleChange = useCallback((innerValue: CascaderValue, innerOptions: RcCascaderOption[]) => { // innerValue 和 innerOptions 类型说不会为 undefined,实际上清空会返回 undefined setValue(innerValue as T, innerOptions as CascaderOption[] ?? []) }, [setValue]) return ( ) } export function MultiCascader(props: MultiCascaderProps) { const { value: valueFromProps, defaultValue = null, onChange } = props const { value, setValue } = useValue({ valueFromProps, defaultValue, onChange: useCallback((v: T[] | null, o: CascaderOption[]) => { onChange?.(v ?? [], o) }, [onChange]) }) const rcCascaderProps = useSharedProps(props, { multiple: true }) const handleChange = useCallback((innerValue: CascaderValue[], innerOptions: RcCascaderOption[]) => { setValue(innerValue as T[], innerOptions as CascaderOption[]) }, [setValue]) return ( ) } 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 }