export interface ShowSearchType { filter?: (inputValue: string, options: OptionType[], fieldNames: FieldNames) => boolean; render?: (arg?: { inputValue: string; path: OptionType[]; prefixCls: string; fieldNames: FieldNames; }) => any; sort?: (a: OptionType[], b: OptionType[], inputValue: string, fieldNames: FieldNames) => number; matchInputWidth?: boolean; limit?: number | false; } export interface FieldNames { label?: string; value?: string; children?: string; } export interface InternalFieldNames extends Required { key: string; } export type SingleValueType = (string | number)[]; export type ValueType = SingleValueType | SingleValueType[]; export interface BaseOptionType { disabled?: boolean; [name: string]: any; } export interface DefaultOptionType extends BaseOptionType { label: React.ReactNode; value?: string | number | null; children?: DefaultOptionType[]; } interface BaseCascaderProps extends Omit< BaseSelectPropsWithoutPrivate, 'tokenSeparators' | 'labelInValue' | 'mode' | 'showSearch' > { // MISC id?: string; prefixCls?: string; fieldNames?: FieldNames; children?: React.ReactElement; // Value value?: ValueType; defaultValue?: ValueType; changeOnSelect?: boolean; onChange?: (value: ValueType, selectedOptions?: OptionType[] | OptionType[][]) => void; displayRender?: (label: string[], selectedOptions?: OptionType[]) => React.ReactNode; checkable?: boolean | React.ReactNode; // Search showSearch?: boolean | ShowSearchType; searchValue?: string; onSearch?: (value: string) => void; // Trigger expandTrigger?: 'hover' | 'click'; // Options options?: OptionType[]; /** @private Internal usage. Do not use in your production. */ dropdownPrefixCls?: string; loadData?: (selectOptions: OptionType[]) => void; // Open /** @deprecated Use `open` instead */ popupVisible?: boolean; /** @deprecated Use `dropdownClassName` instead */ popupClassName?: string; dropdownClassName?: string; dropdownMenuColumnStyle?: React.CSSProperties; /** @deprecated Use `placement` instead */ popupPlacement?: Placement; placement?: Placement; /** @deprecated Use `onDropdownVisibleChange` instead */ onPopupVisibleChange?: (open: boolean) => void; onDropdownVisibleChange?: (open: boolean) => void; // Icon expandIcon?: React.ReactNode; loadingIcon?: React.ReactNode; } type OnSingleChange = (value: SingleValueType, selectOptions: OptionType[]) => void; type OnMultipleChange = ( value: SingleValueType[], selectOptions: OptionType[][], ) => void; export interface SingleCascaderProps extends BaseCascaderProps { checkable?: false; onChange?: OnSingleChange; } export interface MultipleCascaderProps extends BaseCascaderProps { checkable: true | React.ReactNode; onChange?: OnMultipleChange; } export type CascaderProps = | SingleCascaderProps | MultipleCascaderProps; type InternalCascaderProps = Omit< SingleCascaderProps | MultipleCascaderProps, 'onChange' > & { onChange?: ( value: SingleValueType | SingleValueType[], selectOptions: OptionType[] | OptionType[][], ) => void; }; export type CascaderRef = Omit; function isMultipleValue(value: ValueType): value is SingleValueType[] { return Array.isArray(value) && Array.isArray(value[0]); } function toRawValues(value: ValueType): SingleValueType[] { if (!value) { return []; } if (isMultipleValue(value)) { return value; } return value.length === 0 ? [] : [value]; } const Cascader = React.forwardRef((props, ref) => { const { // MISC id, prefixCls = 'rc-cascader', fieldNames, // Value defaultValue, value, changeOnSelect, onChange, displayRender, checkable, // Search searchValue, onSearch, showSearch, // Trigger expandTrigger, // Options options, dropdownPrefixCls, loadData, // Open popupVisible, open, popupClassName, dropdownClassName, dropdownMenuColumnStyle, popupPlacement, placement, onDropdownVisibleChange, onPopupVisibleChange, // Icon expandIcon = '>', loadingIcon, // Children children, ...restProps } = props; const mergedId = useId(id); const multiple = !!checkable; // =========================== Values =========================== const [rawValues, setRawValues] = useMergedState(defaultValue, { value, postState: toRawValues, }); // ========================= FieldNames ========================= const mergedFieldNames = React.useMemo( () => fillFieldNames(fieldNames), /* eslint-disable react-hooks/exhaustive-deps */ [JSON.stringify(fieldNames)], /* eslint-enable react-hooks/exhaustive-deps */ ); // =========================== Option =========================== const mergedOptions = React.useMemo(() => options || [], [options]); // Only used in multiple mode, this fn will not call in single mode const getPathKeyEntities = useEntities(mergedOptions, mergedFieldNames); /** Convert path key back to value format */ const getValueByKeyPath = React.useCallback( (pathKeys: React.Key[]): SingleValueType[] => { const ketPathEntities = getPathKeyEntities(); return pathKeys.map(pathKey => { const { nodes } = ketPathEntities[pathKey]; return nodes.map(node => node[mergedFieldNames.value]); }); }, [getPathKeyEntities, mergedFieldNames], ); // =========================== Search =========================== const [mergedSearchValue, setSearchValue] = useMergedState('', { value: searchValue, postState: search => search || '', }); const onInternalSearch: BaseSelectProps['onSearch'] = (searchText, info) => { setSearchValue(searchText); if (info.source !== 'blur' && onSearch) { onSearch(searchText); } }; const [mergedShowSearch, searchConfig] = useSearchConfig(showSearch); const searchOptions = useSearchOptions( mergedSearchValue, mergedOptions, mergedFieldNames, dropdownPrefixCls || prefixCls, searchConfig, changeOnSelect, ); // =========================== Values =========================== const getMissingValues = useMissingValues(mergedOptions, mergedFieldNames); // Fill `rawValues` with checked conduction values const [checkedValues, halfCheckedValues, missingCheckedValues] = React.useMemo(() => { const [existValues, missingValues] = getMissingValues(rawValues); if (!multiple || !rawValues.length) { return [existValues, [], missingValues]; } const keyPathValues = toPathKeys(existValues); const ketPathEntities = getPathKeyEntities(); const { checkedKeys, halfCheckedKeys } = conductCheck(keyPathValues, true, ketPathEntities); // Convert key back to value cells return [getValueByKeyPath(checkedKeys), getValueByKeyPath(halfCheckedKeys), missingValues]; }, [multiple, rawValues, getPathKeyEntities, getValueByKeyPath, getMissingValues]); const deDuplicatedValues = React.useMemo(() => { const checkedKeys = toPathKeys(checkedValues); const deduplicateKeys = formatStrategyValues(checkedKeys, getPathKeyEntities); return [...missingCheckedValues, ...getValueByKeyPath(deduplicateKeys)]; }, [checkedValues, getPathKeyEntities, getValueByKeyPath, missingCheckedValues]); const displayValues = useDisplayValues( deDuplicatedValues, mergedOptions, mergedFieldNames, multiple, displayRender, ); // =========================== Change =========================== const triggerChange = useRefFunc((nextValues: ValueType) => { setRawValues(nextValues); // Save perf if no need trigger event if (onChange) { const nextRawValues = toRawValues(nextValues); const valueOptions = nextRawValues.map(valueCells => toPathOptions(valueCells, mergedOptions, mergedFieldNames).map(valueOpt => valueOpt.option), ); const triggerValues = multiple ? nextRawValues : nextRawValues[0]; const triggerOptions = multiple ? valueOptions : valueOptions[0]; onChange(triggerValues, triggerOptions); } }); // =========================== Select =========================== const onInternalSelect = useRefFunc((valuePath: SingleValueType) => { if (!multiple) { triggerChange(valuePath); } else { // Prepare conduct required info const pathKey = toPathKey(valuePath); const checkedPathKeys = toPathKeys(checkedValues); const halfCheckedPathKeys = toPathKeys(halfCheckedValues); const existInChecked = checkedPathKeys.includes(pathKey); const existInMissing = missingCheckedValues.some( valueCells => toPathKey(valueCells) === pathKey, ); // Do update let nextCheckedValues = checkedValues; let nextMissingValues = missingCheckedValues; if (existInMissing && !existInChecked) { // Missing value only do filter nextMissingValues = missingCheckedValues.filter( valueCells => toPathKey(valueCells) !== pathKey, ); } else { // Update checked key first const nextRawCheckedKeys = existInChecked ? checkedPathKeys.filter(key => key !== pathKey) : [...checkedPathKeys, pathKey]; const pathKeyEntities = getPathKeyEntities(); // Conduction by selected or not let checkedKeys: React.Key[]; if (existInChecked) { ({ checkedKeys } = conductCheck( nextRawCheckedKeys, { checked: false, halfCheckedKeys: halfCheckedPathKeys }, pathKeyEntities, )); } else { ({ checkedKeys } = conductCheck(nextRawCheckedKeys, true, pathKeyEntities)); } // Roll up to parent level keys const deDuplicatedKeys = formatStrategyValues(checkedKeys, getPathKeyEntities); nextCheckedValues = getValueByKeyPath(deDuplicatedKeys); } triggerChange([...nextMissingValues, ...nextCheckedValues]); } }); // Display Value change logic const onDisplayValuesChange: BaseSelectProps['onDisplayValuesChange'] = (_, info) => { if (info.type === 'clear') { triggerChange([]); return; } // Cascader do not support `add` type. Only support `remove` const { valueCells } = info.values[0] as DisplayValueType & { valueCells: SingleValueType }; onInternalSelect(valueCells); }; // ============================ Open ============================ if (process.env.NODE_ENV !== 'production') { warning( !onPopupVisibleChange, '`onPopupVisibleChange` is deprecated. Please use `onDropdownVisibleChange` instead.', ); warning(popupVisible === undefined, '`popupVisible` is deprecated. Please use `open` instead.'); warning( popupClassName === undefined, '`popupClassName` is deprecated. Please use `dropdownClassName` instead.', ); warning( popupPlacement === undefined, '`popupPlacement` is deprecated. Please use `placement` instead.', ); } const mergedOpen = open !== undefined ? open : popupVisible; const mergedDropdownClassName = dropdownClassName || popupClassName; const mergedPlacement = placement || popupPlacement; const onInternalDropdownVisibleChange = (nextVisible: boolean) => { onDropdownVisibleChange?.(nextVisible); onPopupVisibleChange?.(nextVisible); }; // ========================== Context =========================== const cascaderContext = React.useMemo( () => ({ options: mergedOptions, fieldNames: mergedFieldNames, values: checkedValues, halfValues: halfCheckedValues, changeOnSelect, onSelect: onInternalSelect, checkable, searchOptions, dropdownPrefixCls, loadData, expandTrigger, expandIcon, loadingIcon, dropdownMenuColumnStyle, }), [ mergedOptions, mergedFieldNames, checkedValues, halfCheckedValues, changeOnSelect, onInternalSelect, checkable, searchOptions, dropdownPrefixCls, loadData, expandTrigger, expandIcon, loadingIcon, dropdownMenuColumnStyle, ], ); // ============================================================== // == Render == // ============================================================== const emptyOptions = !(mergedSearchValue ? searchOptions : mergedOptions).length; const dropdownStyle: React.CSSProperties = // Search to match width (mergedSearchValue && searchConfig.matchInputWidth) || // Empty keep the width emptyOptions ? {} : { minWidth: 'auto', }; return ( children} /> ); }) as (( props: React.PropsWithChildren> & { ref?: React.Ref; }, ) => React.ReactElement) & { displayName?: string; }; if (process.env.NODE_ENV !== 'production') { Cascader.displayName = 'Cascader'; } export default Cascader;