diff --git a/package.json b/package.json index 8ac2c216..c00b4a9e 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "gh-pages": "^3.1.0", "glob": "^7.1.6", "np": "^7.6.0", + "prettier": "^2.7.1", "rc-form": "^2.4.0", "rc-trigger": "^5.0.4", "react": "^16.0.0", diff --git a/src/Cascader.tsx b/src/Cascader.tsx index 2b5d4b11..778e8370 100644 --- a/src/Cascader.tsx +++ b/src/Cascader.tsx @@ -1,21 +1,21 @@ -import * as React from 'react'; +import type { BaseSelectProps, BaseSelectPropsWithoutPrivate, BaseSelectRef } from 'rc-select'; +import { BaseSelect } from 'rc-select'; +import type { DisplayValueType, Placement } from 'rc-select/lib/BaseSelect'; import useId from 'rc-select/lib/hooks/useId'; import { conductCheck } from 'rc-tree/lib/utils/conductUtil'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; -import type { DisplayValueType, Placement } from 'rc-select/lib/BaseSelect'; -import type { BaseSelectRef, BaseSelectPropsWithoutPrivate, BaseSelectProps } from 'rc-select'; -import { BaseSelect } from 'rc-select'; -import OptionList from './OptionList'; +import * as React from 'react'; import CascaderContext from './context'; -import { fillFieldNames, toPathKey, toPathKeys, SHOW_PARENT, SHOW_CHILD } from './utils/commonUtil'; import useDisplayValues from './hooks/useDisplayValues'; -import useRefFunc from './hooks/useRefFunc'; import useEntities from './hooks/useEntities'; -import { formatStrategyValues, toPathOptions } from './utils/treeUtil'; +import useMissingValues from './hooks/useMissingValues'; +import useRefFunc from './hooks/useRefFunc'; import useSearchConfig from './hooks/useSearchConfig'; import useSearchOptions from './hooks/useSearchOptions'; -import warning from 'rc-util/lib/warning'; -import useMissingValues from './hooks/useMissingValues'; +import OptionList from './OptionList'; +import { fillFieldNames, SHOW_CHILD, SHOW_PARENT, toPathKey, toPathKeys } from './utils/commonUtil'; +import { formatStrategyValues, toPathOptions } from './utils/treeUtil'; +import warningProps, { warningNullOptions } from './utils/warningPropsUtil'; export interface ShowSearchType { filter?: (inputValue: string, options: OptionType[], fieldNames: FieldNames) => boolean; @@ -134,7 +134,7 @@ export type CascaderProps | SingleCascaderProps | MultipleCascaderProps; -type InternalCascaderProps = Omit< +export type InternalCascaderProps = Omit< SingleCascaderProps | MultipleCascaderProps, 'onChange' > & { @@ -412,22 +412,6 @@ const Cascader = React.forwardRef((props, re }; // ============================ 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; @@ -439,6 +423,12 @@ const Cascader = React.forwardRef((props, re onPopupVisibleChange?.(nextVisible); }; + // ========================== Warning =========================== + if (process.env.NODE_ENV !== 'production') { + warningProps(props); + warningNullOptions(mergedOptions, mergedFieldNames); + } + // ========================== Context =========================== const cascaderContext = React.useMemo( () => ({ diff --git a/src/utils/warningPropsUtil.ts b/src/utils/warningPropsUtil.ts new file mode 100644 index 00000000..2b406996 --- /dev/null +++ b/src/utils/warningPropsUtil.ts @@ -0,0 +1,47 @@ +import warning from 'rc-util/lib/warning'; +import type { DefaultOptionType, FieldNames, InternalCascaderProps } from '../Cascader'; + +function warningProps(props: InternalCascaderProps) { + const { onPopupVisibleChange, popupVisible, popupClassName, popupPlacement } = props; + + 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.', + ); +} + +// value in Cascader options should not be null +export function warningNullOptions(options: DefaultOptionType[], fieldNames: FieldNames) { + if (options) { + const recursiveOptions = (optionsList: DefaultOptionType[]) => { + for (let i = 0; i < optionsList.length; i++) { + const option = optionsList[i]; + + if (option[fieldNames?.value] === null) { + warning(false, '`value` in Cascader options should not be `null`.'); + return true; + } + + if ( + Array.isArray(option[fieldNames?.children]) && + recursiveOptions(option[fieldNames?.children]) + ) { + return true; + } + } + }; + + recursiveOptions(options); + } +} + +export default warningProps; diff --git a/tests/fieldNames.spec.tsx b/tests/fieldNames.spec.tsx index 864c963d..33560b79 100644 --- a/tests/fieldNames.spec.tsx +++ b/tests/fieldNames.spec.tsx @@ -135,4 +135,36 @@ describe('Cascader.FieldNames', () => { expect(wrapper.find('.rc-cascader-menu-item').last().text()).toEqual('Not Found'); }); + + it('`null` is a value in fieldNames options should throw a warning', () => { + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => null); + mount( + , + ); + + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: `value` in Cascader options should not be `null`.', + ); + errorSpy.mockReset(); + }); }); diff --git a/tests/index.spec.tsx b/tests/index.spec.tsx index 904bf9fa..960e970b 100644 --- a/tests/index.spec.tsx +++ b/tests/index.spec.tsx @@ -946,4 +946,35 @@ describe('Cascader.Basic', () => { it('not crash when value type is not array', () => { mount(); }); + + it('`null` is a value in Cascader options should throw a warning', () => { + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => null); + mount( + , + ); + + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: `value` in Cascader options should not be `null`.', + ); + errorSpy.mockReset(); + }); });