diff --git a/components/_util/warning.ts b/components/_util/warning.ts index 994ed46d86..a201d7f61c 100644 --- a/components/_util/warning.ts +++ b/components/_util/warning.ts @@ -1,3 +1,4 @@ +import * as React from 'react'; import rcWarning, { resetWarned } from 'rc-util/lib/warning'; export { resetWarned }; @@ -18,4 +19,42 @@ if (process.env.NODE_ENV !== 'production') { }; } +type TypeWarning = ( + valid: boolean, + component: string, + /** + * - deprecated: Some API will be removed in future but still support now. + * - usage: Some API usage is not correct. + * - breaking: Breaking change like API is removed. + */ + type: 'deprecated' | 'usage' | 'breaking', + message?: string, +) => void; + +export interface WarningContextProps { + deprecated?: boolean; +} + +export const WarningContext = React.createContext({}); + +/** + * This is a hook but we not named as `useWarning` + * since this is only used in development. + * We should always wrap this in `if (process.env.NODE_ENV !== 'production')` condition + */ +export const devUseWarning: () => TypeWarning = + process.env.NODE_ENV !== 'production' + ? () => { + const { deprecated } = React.useContext(WarningContext); + + const typeWarning: TypeWarning = (valid, component, type, message) => { + if (deprecated !== false || type !== 'deprecated') { + warning(valid, component, message); + } + }; + + return typeWarning; + } + : () => noop; + export default warning; diff --git a/components/alert/Alert.tsx b/components/alert/Alert.tsx index 20a416c8e8..70e7e678b4 100644 --- a/components/alert/Alert.tsx +++ b/components/alert/Alert.tsx @@ -1,3 +1,5 @@ +import type { ReactElement } from 'react'; +import * as React from 'react'; import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled'; import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled'; import CloseOutlined from '@ant-design/icons/CloseOutlined'; @@ -6,12 +8,10 @@ import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled'; import classNames from 'classnames'; import CSSMotion from 'rc-motion'; import pickAttrs from 'rc-util/lib/pickAttrs'; -import type { ReactElement } from 'react'; -import * as React from 'react'; -import { replaceElement } from '../_util/reactNode'; -import warning from '../_util/warning'; -import { ConfigContext } from '../config-provider'; +import { replaceElement } from '../_util/reactNode'; +import { devUseWarning } from '../_util/warning'; +import { ConfigContext } from '../config-provider'; // CSSINJS import useStyle from './style'; @@ -118,8 +118,16 @@ const Alert: React.FC = (props) => { } = props; const [closed, setClosed] = React.useState(false); + if (process.env.NODE_ENV !== 'production') { - warning(!closeText, 'Alert', '`closeText` is deprecated. Please use `closeIcon` instead.'); + const warning = devUseWarning(); + + warning( + !closeText, + 'Alert', + 'deprecated', + '`closeText` is deprecated. Please use `closeIcon` instead.', + ); } const ref = React.useRef(null); const { getPrefixCls, direction, alert } = React.useContext(ConfigContext); diff --git a/components/anchor/Anchor.tsx b/components/anchor/Anchor.tsx index af5436e79a..8719d69588 100644 --- a/components/anchor/Anchor.tsx +++ b/components/anchor/Anchor.tsx @@ -1,18 +1,17 @@ +import * as React from 'react'; import classNames from 'classnames'; import { useEvent } from 'rc-util'; -import * as React from 'react'; import scrollIntoView from 'scroll-into-view-if-needed'; import getScroll from '../_util/getScroll'; import scrollTo from '../_util/scrollTo'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import Affix from '../affix'; import type { ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider'; import type { AnchorLinkBaseProps } from './AnchorLink'; import AnchorLink from './AnchorLink'; import AnchorContext from './context'; - import useStyle from './style'; export interface AnchorLinkItemProps extends AnchorLinkBaseProps { @@ -133,13 +132,19 @@ const AnchorContent: React.FC = (props) => { // =================== Warning ===================== if (process.env.NODE_ENV !== 'production') { - warning(!children, 'Anchor', '`Anchor children` is deprecated. Please use `items` instead.'); - } + const warning = devUseWarning(); + + warning( + !children, + 'Anchor', + 'deprecated', + '`Anchor children` is deprecated. Please use `items` instead.', + ); - if (process.env.NODE_ENV !== 'production') { warning( !(anchorDirection === 'horizontal' && items?.some((n) => 'children' in n)), 'Anchor', + 'usage', '`Anchor items#children` is not supported when `Anchor` direction is horizontal.', ); } diff --git a/components/anchor/AnchorLink.tsx b/components/anchor/AnchorLink.tsx index 21b3e31b71..612468949e 100644 --- a/components/anchor/AnchorLink.tsx +++ b/components/anchor/AnchorLink.tsx @@ -1,6 +1,7 @@ -import classNames from 'classnames'; import * as React from 'react'; -import warning from '../_util/warning'; +import classNames from 'classnames'; + +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import type { AntAnchor } from './Anchor'; import AnchorContext from './context'; @@ -51,9 +52,12 @@ const AnchorLink: React.FC = (props) => { // =================== Warning ===================== if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( !children || direction !== 'horizontal', 'Anchor.Link', + 'usage', '`Anchor.Link children` is not supported when `Anchor` direction is horizontal', ); } diff --git a/components/auto-complete/index.tsx b/components/auto-complete/index.tsx index e3bc7bb0b7..66463412b9 100755 --- a/components/auto-complete/index.tsx +++ b/components/auto-complete/index.tsx @@ -1,12 +1,13 @@ +import * as React from 'react'; import classNames from 'classnames'; import type { BaseSelectRef } from 'rc-select'; import toArray from 'rc-util/lib/Children/toArray'; import omit from 'rc-util/lib/omit'; -import * as React from 'react'; + import genPurePanel from '../_util/PurePanel'; import { isValidElement } from '../_util/reactNode'; import type { InputStatus } from '../_util/statusUtils'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import type { ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider'; import type { @@ -33,6 +34,7 @@ export interface AutoCompleteProps< InternalSelectProps, 'loading' | 'mode' | 'optionLabelProp' | 'labelInValue' > { + /** @deprecated Please use `options` instead */ dataSource?: DataSourceItemType[]; status?: InputStatus; popupClassName?: string; @@ -102,11 +104,6 @@ const AutoComplete: React.ForwardRefRenderFunction 2), - 'Avatar', - `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`, - ); + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + + warning( + !(typeof icon === 'string' && icon.length > 2), + 'Avatar', + 'deprecated', + `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`, + ); + } const prefixCls = getPrefixCls('avatar', customizePrefixCls); const [wrapSSR, hashId] = useStyle(prefixCls); diff --git a/components/back-top/index.tsx b/components/back-top/index.tsx index 8cfa5e2f17..2fbcdee2f6 100644 --- a/components/back-top/index.tsx +++ b/components/back-top/index.tsx @@ -1,13 +1,14 @@ +import * as React from 'react'; import VerticalAlignTopOutlined from '@ant-design/icons/VerticalAlignTopOutlined'; import classNames from 'classnames'; import CSSMotion from 'rc-motion'; import omit from 'rc-util/lib/omit'; -import * as React from 'react'; + import getScroll from '../_util/getScroll'; import { cloneElement } from '../_util/reactNode'; import scrollTo from '../_util/scrollTo'; import throttleByAnimationFrame from '../_util/throttleByAnimationFrame'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import type { ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider'; import useStyle from './style'; @@ -49,7 +50,14 @@ const BackTop: React.FC = (props) => { ); if (process.env.NODE_ENV !== 'production') { - warning(false, 'BackTop', '`BackTop` is deprecated, please use `FloatButton.BackTop` instead.'); + const warning = devUseWarning(); + + warning( + false, + 'BackTop', + 'deprecated', + '`BackTop` is deprecated, please use `FloatButton.BackTop` instead.', + ); } React.useEffect(() => { diff --git a/components/breadcrumb/Breadcrumb.tsx b/components/breadcrumb/Breadcrumb.tsx index 997438e814..e2de8c06e4 100755 --- a/components/breadcrumb/Breadcrumb.tsx +++ b/components/breadcrumb/Breadcrumb.tsx @@ -1,19 +1,19 @@ +import * as React from 'react'; import classNames from 'classnames'; import toArray from 'rc-util/lib/Children/toArray'; import pickAttrs from 'rc-util/lib/pickAttrs'; -import * as React from 'react'; + import { cloneElement } from '../_util/reactNode'; -import warning from '../_util/warning'; +import type { AnyObject } from '../_util/type'; +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; +import type { DropdownProps } from '../dropdown'; import type { BreadcrumbItemProps } from './BreadcrumbItem'; import BreadcrumbItem, { InternalBreadcrumbItem } from './BreadcrumbItem'; import BreadcrumbSeparator from './BreadcrumbSeparator'; - -import type { DropdownProps } from '../dropdown'; import useStyle from './style'; import useItemRender from './useItemRender'; import useItems from './useItems'; -import type { AnyObject } from '../_util/type'; export interface BreadcrumbItemType { key?: React.Key; @@ -99,7 +99,39 @@ const Breadcrumb = (props: BreadcrumbProps) const mergedItems = useItems(items, legacyRoutes); if (process.env.NODE_ENV !== 'production') { - warning(!legacyRoutes, 'Breadcrumb', '`routes` is deprecated. Please use `items` instead.'); + const warning = devUseWarning(); + + warning( + !legacyRoutes, + 'Breadcrumb', + 'deprecated', + '`routes` is deprecated. Please use `items` instead.', + ); + + // Deprecated warning for breadcrumb children + if (!mergedItems || mergedItems.length === 0) { + const childList = toArray(children); + + warning( + childList.length === 0, + 'Breadcrumb', + 'deprecated', + '`Breadcrumb.Item and Breadcrumb.Separator` is deprecated. Please use `items` instead.', + ); + + childList.forEach((element: any) => { + if (element) { + warning( + element.type && + (element.type.__ANT_BREADCRUMB_ITEM === true || + element.type.__ANT_BREADCRUMB_SEPARATOR === true), + 'Breadcrumb', + 'usage', + "Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children", + ); + } + }); + } } const mergedItemRender = useItemRender(prefixCls, itemRender); @@ -170,21 +202,7 @@ const Breadcrumb = (props: BreadcrumbProps) if (!element) { return element; } - // =================== Warning ===================== - if (process.env.NODE_ENV !== 'production') { - warning( - !element, - 'Breadcrumb', - '`Breadcrumb.Item and Breadcrumb.Separator` is deprecated. Please use `items` instead.', - ); - } - warning( - element.type && - (element.type.__ANT_BREADCRUMB_ITEM === true || - element.type.__ANT_BREADCRUMB_SEPARATOR === true), - 'Breadcrumb', - "Only accepts Breadcrumb.Item and Breadcrumb.Separator as it's children", - ); + const isLastItem = index === childrenLength - 1; return cloneElement(element, { separator: isLastItem ? '' : separator, diff --git a/components/breadcrumb/BreadcrumbItem.tsx b/components/breadcrumb/BreadcrumbItem.tsx index 820d409ba0..0a34e205b4 100644 --- a/components/breadcrumb/BreadcrumbItem.tsx +++ b/components/breadcrumb/BreadcrumbItem.tsx @@ -1,6 +1,6 @@ import DownOutlined from '@ant-design/icons/DownOutlined'; import * as React from 'react'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import type { DropdownProps } from '../dropdown/dropdown'; import Dropdown from '../dropdown/dropdown'; @@ -42,9 +42,12 @@ export const InternalBreadcrumbItem: React.FC = (props) => // Warning for deprecated usage if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( !('overlay' in props), 'Breadcrumb.Item', + 'deprecated', '`overlay` is deprecated. Please use `menu` instead.', ); } diff --git a/components/button/button-group.tsx b/components/button/button-group.tsx index f9644f40f2..55583f953a 100644 --- a/components/button/button-group.tsx +++ b/components/button/button-group.tsx @@ -1,6 +1,7 @@ -import classNames from 'classnames'; import * as React from 'react'; -import warning from '../_util/warning'; +import classNames from 'classnames'; + +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import type { SizeType } from '../config-provider/SizeContext'; import { useToken } from '../theme/internal'; @@ -33,10 +34,19 @@ const ButtonGroup: React.FC = (props) => { sizeCls = 'sm'; break; case 'middle': - case undefined: - break; default: - warning(!size, 'Button.Group', 'Invalid prop `size`.'); + // Do nothing + } + + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + + warning( + !size || ['large', 'small', 'middle'].includes(size), + 'Button.Group', + 'usage', + 'Invalid prop `size`.', + ); } const classes = classNames( diff --git a/components/button/button.tsx b/components/button/button.tsx index 02b741a74d..c1249b778a 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -12,7 +12,7 @@ import classNames from 'classnames'; import omit from 'rc-util/lib/omit'; import { composeRef } from 'rc-util/lib/ref'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import Wave from '../_util/wave'; import { ConfigContext } from '../config-provider'; import DisabledContext from '../config-provider/DisabledContext'; @@ -192,17 +192,23 @@ const InternalButton: React.ForwardRefRenderFunction< (onClick as React.MouseEventHandler)?.(e); }; - warning( - !(typeof icon === 'string' && icon.length > 2), - 'Button', - `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`, - ); + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); - warning( - !(ghost && isUnBorderedButtonType(type)), - 'Button', - "`link` or `text` button can't be a `ghost` button.", - ); + warning( + !(typeof icon === 'string' && icon.length > 2), + 'Button', + 'breaking', + `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`, + ); + + warning( + !(ghost && isUnBorderedButtonType(type)), + 'Button', + 'usage', + "`link` or `text` button can't be a `ghost` button.", + ); + } const autoInsertSpace = autoInsertSpaceInButton !== false; const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction); diff --git a/components/calendar/generateCalendar.tsx b/components/calendar/generateCalendar.tsx index 6098527ea0..2a5700fea9 100644 --- a/components/calendar/generateCalendar.tsx +++ b/components/calendar/generateCalendar.tsx @@ -1,20 +1,20 @@ +import * as React from 'react'; import classNames from 'classnames'; import { PickerPanel as RCPickerPanel } from 'rc-picker'; +import type { GenerateConfig } from 'rc-picker/lib/generate'; +import type { CellRenderInfo } from 'rc-picker/lib/interface'; import type { PickerPanelBaseProps as RCPickerPanelBaseProps, PickerPanelDateProps as RCPickerPanelDateProps, PickerPanelTimeProps as RCPickerPanelTimeProps, } from 'rc-picker/lib/PickerPanel'; -import type { GenerateConfig } from 'rc-picker/lib/generate'; -import type { CellRenderInfo } from 'rc-picker/lib/interface'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; -import * as React from 'react'; + +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import { useLocale } from '../locale'; import CalendarHeader from './Header'; import enUS from './locale/en_US'; - -import warning from '../_util/warning'; import useStyle from './style'; type InjectDefaultProps = Omit< @@ -125,24 +125,30 @@ function generateCalendar(generateConfig: GenerateConfig) { // ====================== Warning ======================= if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( !dateFullCellRender, 'Calendar', + 'deprecated', '`dateFullCellRender` is deprecated. Please use `fullCellRender` instead.', ); warning( !dateCellRender, 'Calendar', + 'deprecated', '`dateCellRender` is deprecated. Please use `cellRender` instead.', ); warning( !monthFullCellRender, 'Calendar', + 'deprecated', '`monthFullCellRender` is deprecated. Please use `fullCellRender` instead.', ); warning( !monthCellRender, 'Calendar', + 'deprecated', '`monthCellRender` is deprecated. Please use `cellRender` instead.', ); } diff --git a/components/cascader/index.tsx b/components/cascader/index.tsx index ebc399c44e..223fd03111 100644 --- a/components/cascader/index.tsx +++ b/components/cascader/index.tsx @@ -20,7 +20,7 @@ import { getTransitionName } from '../_util/motion'; import genPurePanel from '../_util/PurePanel'; import type { InputStatus } from '../_util/statusUtils'; import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import DefaultRenderEmpty from '../config-provider/defaultRenderEmpty'; import DisabledContext from '../config-provider/DisabledContext'; @@ -30,7 +30,7 @@ import { FormItemInputContext } from '../form/context'; import useSelectStyle from '../select/style'; import useBuiltinPlacements from '../select/useBuiltinPlacements'; import useShowArrow from '../select/useShowArrow'; -import getIcons from '../select/utils/iconUtil'; +import useIcons from '../select/useIcons'; import { useCompactItemContext } from '../space/Compact'; import useStyle from './style'; @@ -194,15 +194,19 @@ const Cascader = React.forwardRef>((props, ref) // =================== Warning ===================== if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( !dropdownClassName, 'Cascader', + 'deprecated', '`dropdownClassName` is deprecated. Please use `popupClassName` instead.', ); warning( !('showArrow' in props), 'Cascader', + 'deprecated', '`showArrow` is deprecated which will be removed in next major version. It will be a default behavior, you can hide it by setting `suffixIcon` to null.', ); } @@ -279,7 +283,7 @@ const Cascader = React.forwardRef>((props, ref) // ===================== Icons ===================== const showSuffixIcon = useShowArrow(props.suffixIcon, showArrow); - const { suffixIcon, removeIcon, clearIcon } = getIcons({ + const { suffixIcon, removeIcon, clearIcon } = useIcons({ ...props, hasFeedback, feedbackIcon, diff --git a/components/checkbox/Checkbox.tsx b/components/checkbox/Checkbox.tsx index 7a2d64386e..c0341efa24 100644 --- a/components/checkbox/Checkbox.tsx +++ b/components/checkbox/Checkbox.tsx @@ -1,16 +1,16 @@ +import * as React from 'react'; import classNames from 'classnames'; import type { CheckboxRef } from 'rc-checkbox'; import RcCheckbox from 'rc-checkbox'; -import * as React from 'react'; -import warning from '../_util/warning'; + +import { devUseWarning } from '../_util/warning'; +import Wave from '../_util/wave'; +import { TARGET_CLS } from '../_util/wave/interface'; import { ConfigContext } from '../config-provider'; import DisabledContext from '../config-provider/DisabledContext'; import { FormItemInputContext } from '../form/context'; import GroupContext from './GroupContext'; - import useStyle from './style'; -import Wave from '../_util/wave'; -import { TARGET_CLS } from '../_util/wave/interface'; export interface AbstractCheckboxProps { prefixCls?: string; @@ -77,13 +77,19 @@ const InternalCheckbox: React.ForwardRefRenderFunction { - checkboxGroup?.registerValue(restProps.value); + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( 'checked' in restProps || !!checkboxGroup || !('value' in restProps), 'Checkbox', + 'usage', '`value` is not a valid prop, do you mean `checked`?', ); + } + + React.useEffect(() => { + checkboxGroup?.registerValue(restProps.value); }, []); React.useEffect(() => { diff --git a/components/collapse/Collapse.tsx b/components/collapse/Collapse.tsx index 643e8ede97..5b086f2fa7 100644 --- a/components/collapse/Collapse.tsx +++ b/components/collapse/Collapse.tsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import RightOutlined from '@ant-design/icons/RightOutlined'; import classNames from 'classnames'; import type { CollapseProps as RcCollapseProps } from 'rc-collapse'; @@ -5,13 +6,13 @@ import RcCollapse from 'rc-collapse'; import type { CSSMotionProps } from 'rc-motion'; import toArray from 'rc-util/lib/Children/toArray'; import omit from 'rc-util/lib/omit'; -import * as React from 'react'; + import initCollapseMotion from '../_util/motion'; import { cloneElement } from '../_util/reactNode'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; -import type { SizeType } from '../config-provider/SizeContext'; import useSize from '../config-provider/hooks/useSize'; +import type { SizeType } from '../config-provider/SizeContext'; import type { CollapsibleType } from './CollapsePanel'; import CollapsePanel from './CollapsePanel'; import useStyle from './style'; @@ -77,12 +78,17 @@ const Collapse = React.forwardRef((props, ref) => const rootPrefixCls = getPrefixCls(); const [wrapSSR, hashId] = useStyle(prefixCls); - // Warning if use legacy type `expandIconPosition` - warning( - expandIconPosition !== 'left' && expandIconPosition !== 'right', - 'Collapse', - '`expandIconPosition` with `left` or `right` is deprecated. Please use `start` or `end` instead.', - ); + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + + // Warning if use legacy type `expandIconPosition` + warning( + expandIconPosition !== 'left' && expandIconPosition !== 'right', + 'Collapse', + 'deprecated', + '`expandIconPosition` with `left` or `right` is deprecated. Please use `start` or `end` instead.', + ); + } // Align with logic position const mergedExpandIconPosition = React.useMemo(() => { diff --git a/components/collapse/CollapsePanel.tsx b/components/collapse/CollapsePanel.tsx index 9fbb4bbacb..f1b36ad570 100644 --- a/components/collapse/CollapsePanel.tsx +++ b/components/collapse/CollapsePanel.tsx @@ -1,7 +1,8 @@ +import * as React from 'react'; import classNames from 'classnames'; import RcCollapse from 'rc-collapse'; -import * as React from 'react'; -import warning from '../_util/warning'; + +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; export type CollapsibleType = 'header' | 'icon' | 'disabled'; @@ -23,11 +24,16 @@ export interface CollapsePanelProps { } const CollapsePanel = React.forwardRef((props, ref) => { - warning( - !('disabled' in props), - 'Collapse.Panel', - '`disabled` is deprecated. Please use `collapsible="disabled"` instead.', - ); + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + + warning( + !('disabled' in props), + 'Collapse.Panel', + 'deprecated', + '`disabled` is deprecated. Please use `collapsible="disabled"` instead.', + ); + } const { getPrefixCls } = React.useContext(ConfigContext); const { prefixCls: customizePrefixCls, className, showArrow = true } = props; diff --git a/components/color-picker/ColorPicker.tsx b/components/color-picker/ColorPicker.tsx index 7f435b5d9c..7345061fd9 100644 --- a/components/color-picker/ColorPicker.tsx +++ b/components/color-picker/ColorPicker.tsx @@ -9,7 +9,7 @@ import useMergedState from 'rc-util/lib/hooks/useMergedState'; import genPurePanel from '../_util/PurePanel'; import { getStatusClassNames } from '../_util/statusUtils'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import type { ConfigConsumerProps } from '../config-provider/context'; import { ConfigContext } from '../config-provider/context'; import useSize from '../config-provider/hooks/useSize'; @@ -151,9 +151,12 @@ const ColorPicker: CompoundedComponent = (props) => { // ===================== Warning ====================== if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( !(disabledAlpha && isAlphaColor), 'ColorPicker', + 'usage', '`disabledAlpha` will make the alpha to be 100% when use alpha color.', ); } diff --git a/components/config-provider/PropWarning.tsx b/components/config-provider/PropWarning.tsx new file mode 100644 index 0000000000..dfc4ae2d84 --- /dev/null +++ b/components/config-provider/PropWarning.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; + +import { devUseWarning } from '../_util/warning'; + +export interface PropWarningProps { + dropdownMatchSelectWidth?: boolean; +} + +/** + * Warning for ConfigProviderProps. + * This will be empty function in production. + */ +const PropWarning = React.memo(({ dropdownMatchSelectWidth }: PropWarningProps) => { + const warning = devUseWarning(); + + warning( + dropdownMatchSelectWidth === undefined, + 'ConfigProvider', + 'deprecated', + '`dropdownMatchSelectWidth` is deprecated. Please use `popupMatchSelectWidth` instead.', + ); + + return null; +}); + +if (process.env.NODE_ENV !== 'production') { + PropWarning.displayName = 'PropWarning'; +} + +export default process.env.NODE_ENV !== 'production' ? PropWarning : () => null; diff --git a/components/config-provider/__tests__/index.test.tsx b/components/config-provider/__tests__/index.test.tsx index 905a2d80a1..aa0f9a48b4 100644 --- a/components/config-provider/__tests__/index.test.tsx +++ b/components/config-provider/__tests__/index.test.tsx @@ -1,5 +1,6 @@ -import { SmileOutlined } from '@ant-design/icons'; import React, { useState } from 'react'; +import { SmileOutlined } from '@ant-design/icons'; + import type { ConfigConsumerProps } from '..'; import ConfigProvider, { ConfigContext } from '..'; import mountTest from '../../../tests/shared/mountTest'; @@ -123,4 +124,13 @@ describe('ConfigProvider', () => { expect(rendered).toBeTruthy(); expect(cacheRenderEmpty).toBeFalsy(); }); + + // it('warning support filter level', () => { + // resetWarned(); + // const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // render(); + + // expect(errSpy).not.toHaveBeenCalled(); + // }); }); diff --git a/components/config-provider/context.ts b/components/config-provider/context.ts index d01049f9d2..953d8ca2ea 100644 --- a/components/config-provider/context.ts +++ b/components/config-provider/context.ts @@ -1,17 +1,19 @@ -import type { ValidateMessages } from 'rc-field-form/lib/interface'; import * as React from 'react'; +import type { ValidateMessages } from 'rc-field-form/lib/interface'; import type { Options } from 'scroll-into-view-if-needed'; + +import type { WarningContextProps } from '../_util/warning'; +import type { ShowWaveEffect } from '../_util/wave/interface'; import type { BadgeProps } from '../badge'; import type { ButtonProps } from '../button'; import type { RequiredMark } from '../form/Form'; import type { InputProps } from '../input'; import type { Locale } from '../locale'; import type { SpaceProps } from '../space'; -import type { AliasToken, MappingAlgorithm, OverrideToken } from '../theme/interface'; -import type { SizeType } from './SizeContext'; -import type { RenderEmptyHandler } from './defaultRenderEmpty'; -import type { ShowWaveEffect } from '../_util/wave/interface'; import type { TabsProps } from '../tabs'; +import type { AliasToken, MappingAlgorithm, OverrideToken } from '../theme/interface'; +import type { RenderEmptyHandler } from './defaultRenderEmpty'; +import type { SizeType } from './SizeContext'; export const defaultIconPrefixCls = 'anticon'; @@ -154,6 +156,8 @@ export interface ConfigConsumerProps { datePicker?: ComponentStyleConfig; wave?: WaveConfig; + + warning?: WarningContextProps; } const defaultGetPrefixCls = (suffixCls?: string, customizePrefixCls?: string) => { diff --git a/components/config-provider/index.tsx b/components/config-provider/index.tsx index 4a3a36d52d..b1a42944ae 100644 --- a/components/config-provider/index.tsx +++ b/components/config-provider/index.tsx @@ -1,11 +1,11 @@ +import * as React from 'react'; import { createTheme } from '@ant-design/cssinjs'; import IconContext from '@ant-design/icons/lib/components/Context'; import type { ValidateMessages } from 'rc-field-form/lib/interface'; import useMemo from 'rc-util/lib/hooks/useMemo'; import { merge } from 'rc-util/lib/utils/set'; -import type { ReactElement } from 'react'; -import * as React from 'react'; import type { Options } from 'scroll-into-view-if-needed'; + import warning from '../_util/warning'; import type { RequiredMark } from '../form/Form'; import ValidateMessagesContext from '../form/validateMessagesContext'; @@ -16,6 +16,8 @@ import type { LocaleContextProps } from '../locale/context'; import LocaleContext from '../locale/context'; import defaultLocale from '../locale/en_US'; import type { SpaceProps } from '../space'; +import type { TabsProps } from '../tabs'; +import { defaultTheme } from '../theme/context'; import { DesignTokenContext } from '../theme/internal'; import defaultSeedToken from '../theme/themes/seed'; import type { @@ -37,11 +39,10 @@ import { DisabledContextProvider } from './DisabledContext'; import useConfig from './hooks/useConfig'; import useTheme from './hooks/useTheme'; import MotionWrapper from './MotionWrapper'; +import PropWarning from './PropWarning'; import type { SizeType } from './SizeContext'; import SizeContext, { SizeContextProvider } from './SizeContext'; import useStyle from './style'; -import { defaultTheme } from '../theme/context'; -import type { TabsProps } from '../tabs'; /** * Since too many feedback using static method like `Modal.confirm` not getting theme, we record the @@ -85,7 +86,10 @@ export const configConsumerProps = [ ]; // These props is used by `useContext` directly in sub component -const PASSED_PROPS: Exclude[] = [ +const PASSED_PROPS: Exclude< + keyof ConfigConsumerProps, + 'rootPrefixCls' | 'getPrefixCls' | 'warning' +>[] = [ 'getTargetContainer', 'getPopupContainer', 'renderEmpty', @@ -141,6 +145,10 @@ export interface ConfigProviderProps { popupMatchSelectWidth?: boolean; popupOverflow?: PopupOverflow; theme?: ThemeConfig; + + // TODO: wait for https://github.com/ant-design/ant-design/discussions/44551 + // warning?: WarningContextProps; + alert?: ComponentStyleConfig; anchor?: ComponentStyleConfig; button?: ButtonConfig; @@ -329,17 +337,9 @@ const ProviderChildren: React.FC = (props) => { colorPicker, datePicker, wave, + // warning: warningConfig, } = props; - // =================================== Warning =================================== - if (process.env.NODE_ENV !== 'production') { - warning( - dropdownMatchSelectWidth === undefined, - 'ConfigProvider', - '`dropdownMatchSelectWidth` is deprecated. Please use `popupMatchSelectWidth` instead.', - ); - } - // =================================== Context =================================== const getPrefixCls = React.useCallback( (suffixCls: string, customizePrefixCls?: string) => { @@ -357,10 +357,9 @@ const ProviderChildren: React.FC = (props) => { ); const iconPrefixCls = customIconPrefixCls || parentContext.iconPrefixCls || defaultIconPrefixCls; - const shouldWrapSSR = iconPrefixCls !== parentContext.iconPrefixCls; const csp = customCsp || parentContext.csp; - const wrapSSR = useStyle(iconPrefixCls, csp); + useStyle(iconPrefixCls, csp); const mergedTheme = useTheme(theme, parentContext.theme); @@ -428,6 +427,7 @@ const ProviderChildren: React.FC = (props) => { colorPicker, datePicker, wave, + // warning: warningConfig, }; const config = { @@ -468,7 +468,12 @@ const ProviderChildren: React.FC = (props) => { [iconPrefixCls, csp], ); - let childNode = shouldWrapSSR ? wrapSSR(children as ReactElement) : children; + let childNode = ( + <> + + {children} + + ); const validateMessages = React.useMemo( () => @@ -484,7 +489,7 @@ const ProviderChildren: React.FC = (props) => { if (Object.keys(validateMessages).length > 0) { childNode = ( - {children} + {childNode} ); } @@ -556,6 +561,13 @@ const ProviderChildren: React.FC = (props) => { ); } + // ================================== Warning =================================== + // if (memoedConfig.warning) { + // childNode = ( + // {childNode} + // ); + // } + // =================================== Render =================================== if (componentDisabled !== undefined) { childNode = ( diff --git a/components/date-picker/generatePicker/generateRangePicker.tsx b/components/date-picker/generatePicker/generateRangePicker.tsx index 45939059ed..2b18aa9a7b 100644 --- a/components/date-picker/generatePicker/generateRangePicker.tsx +++ b/components/date-picker/generatePicker/generateRangePicker.tsx @@ -1,3 +1,5 @@ +import * as React from 'react'; +import { forwardRef, useContext, useImperativeHandle } from 'react'; import CalendarOutlined from '@ant-design/icons/CalendarOutlined'; import ClockCircleOutlined from '@ant-design/icons/ClockCircleOutlined'; import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled'; @@ -5,11 +7,10 @@ import SwapRightOutlined from '@ant-design/icons/SwapRightOutlined'; import classNames from 'classnames'; import { RangePicker as RCRangePicker } from 'rc-picker'; import type { GenerateConfig } from 'rc-picker/lib/generate/index'; -import * as React from 'react'; -import { forwardRef, useContext, useImperativeHandle } from 'react'; + import type { RangePickerProps } from '.'; import { getMergedStatus, getStatusClassNames } from '../../_util/statusUtils'; -import warning from '../../_util/warning'; +import { devUseWarning } from '../../_util/warning'; import { ConfigContext } from '../../config-provider'; import DisabledContext from '../../config-provider/DisabledContext'; import useSize from '../../config-provider/hooks/useSize'; @@ -77,9 +78,12 @@ export default function generateRangePicker(generateConfig: GenerateCo // =================== Warning ===================== if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( !dropdownClassName, 'DatePicker.RangePicker', + 'deprecated', '`dropdownClassName` is deprecated. Please use `popupClassName` instead.', ); } diff --git a/components/date-picker/generatePicker/generateSinglePicker.tsx b/components/date-picker/generatePicker/generateSinglePicker.tsx index 44b988d4fb..72185f4edc 100644 --- a/components/date-picker/generatePicker/generateSinglePicker.tsx +++ b/components/date-picker/generatePicker/generateSinglePicker.tsx @@ -1,3 +1,5 @@ +import * as React from 'react'; +import { forwardRef, useContext, useImperativeHandle } from 'react'; import CalendarOutlined from '@ant-design/icons/CalendarOutlined'; import ClockCircleOutlined from '@ant-design/icons/ClockCircleOutlined'; import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled'; @@ -5,12 +7,11 @@ import classNames from 'classnames'; import RCPicker from 'rc-picker'; import type { GenerateConfig } from 'rc-picker/lib/generate/index'; import type { PickerMode } from 'rc-picker/lib/interface'; -import * as React from 'react'; -import { forwardRef, useContext, useImperativeHandle } from 'react'; + import type { PickerProps, PickerTimeProps } from '.'; import type { InputStatus } from '../../_util/statusUtils'; import { getMergedStatus, getStatusClassNames } from '../../_util/statusUtils'; -import warning from '../../_util/warning'; +import { devUseWarning } from '../../_util/warning'; import { ConfigContext } from '../../config-provider'; import DisabledContext from '../../config-provider/DisabledContext'; import useSize from '../../config-provider/hooks/useSize'; @@ -105,15 +106,19 @@ export default function generatePicker(generateConfig: GenerateConfig< // =================== Warning ===================== if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( picker !== 'quarter', displayName!, + 'deprecated', `DatePicker.${displayName} is legacy usage. Please use DatePicker[picker='${picker}'] directly.`, ); warning( !dropdownClassName, displayName || 'DatePicker', + 'deprecated', '`dropdownClassName` is deprecated. Please use `popupClassName` instead.', ); } diff --git a/components/descriptions/hooks/useRow.ts b/components/descriptions/hooks/useRow.ts index de2c3fcaad..823bca270c 100644 --- a/components/descriptions/hooks/useRow.ts +++ b/components/descriptions/hooks/useRow.ts @@ -1,34 +1,36 @@ import { useMemo } from 'react'; import type { InternalDescriptionsItemType } from '..'; -import warning from '../../_util/warning'; +import { devUseWarning } from '../../_util/warning'; function getFilledItem( rowItem: InternalDescriptionsItemType, rowRestCol: number, span?: number, -): InternalDescriptionsItemType { +): [item: InternalDescriptionsItemType, exceed: boolean] { let clone = rowItem; + let exceed = false; if (span === undefined || span > rowRestCol) { clone = { ...rowItem, span: rowRestCol, }; - warning( - span === undefined, - 'Descriptions', - 'Sum of column `span` in a line not match `column` of Descriptions.', - ); + + exceed = span !== undefined; } - return clone; + return [clone, exceed]; } // Calculate the sum of span in a row -function getCalcRows(rowItems: InternalDescriptionsItemType[], mergedColumn: number) { +function getCalcRows( + rowItems: InternalDescriptionsItemType[], + mergedColumn: number, +): [rows: InternalDescriptionsItemType[][], exceed: boolean] { const rows: InternalDescriptionsItemType[][] = []; let tmpRow: InternalDescriptionsItemType[] = []; let rowRestCol = mergedColumn; + let exceed = false; rowItems .filter((n) => n) @@ -38,7 +40,10 @@ function getCalcRows(rowItems: InternalDescriptionsItemType[], mergedColumn: num // Additional handle last one if (index === rowItems.length - 1) { - tmpRow.push(getFilledItem(rowItem, rowRestCol, span)); + const [item, itemExceed] = getFilledItem(rowItem, rowRestCol, span); + exceed = exceed || itemExceed; + + tmpRow.push(item); rows.push(tmpRow); return; } @@ -47,18 +52,32 @@ function getCalcRows(rowItems: InternalDescriptionsItemType[], mergedColumn: num rowRestCol -= mergedSpan; tmpRow.push(rowItem); } else { - tmpRow.push(getFilledItem(rowItem, rowRestCol, mergedSpan)); + const [item, itemExceed] = getFilledItem(rowItem, rowRestCol, mergedSpan); + exceed = exceed || itemExceed; + + tmpRow.push(item); rows.push(tmpRow); rowRestCol = mergedColumn; tmpRow = []; } }); - return rows; + return [rows, exceed]; } const useRow = (mergedColumn: number, items: InternalDescriptionsItemType[]) => { - const rows = useMemo(() => getCalcRows(items, mergedColumn), [items, mergedColumn]); + const [rows, exceed] = useMemo(() => getCalcRows(items, mergedColumn), [items, mergedColumn]); + + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + + warning( + !exceed, + 'Descriptions', + 'usage', + 'Sum of column `span` in a line not match `column` of Descriptions.', + ); + } return rows; }; diff --git a/components/divider/index.tsx b/components/divider/index.tsx index 5140bd3aa6..2b9b8df589 100644 --- a/components/divider/index.tsx +++ b/components/divider/index.tsx @@ -1,8 +1,8 @@ -import classNames from 'classnames'; import * as React from 'react'; -import warning from '../_util/warning'; -import { ConfigContext } from '../config-provider'; +import classNames from 'classnames'; +import { devUseWarning } from '../_util/warning'; +import { ConfigContext } from '../config-provider'; import useStyle from './style'; export interface DividerProps { @@ -76,9 +76,12 @@ const Divider: React.FC = (props) => { // Warning children not work in vertical mode if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( !children || type !== 'vertical', 'Divider', + 'usage', '`children` not working in `vertical` mode.', ); } diff --git a/components/drawer/index.tsx b/components/drawer/index.tsx index 6141e06e3a..6e2b0460a1 100644 --- a/components/drawer/index.tsx +++ b/components/drawer/index.tsx @@ -1,20 +1,20 @@ +import * as React from 'react'; import classNames from 'classnames'; import type { DrawerProps as RcDrawerProps } from 'rc-drawer'; import RcDrawer from 'rc-drawer'; import type { Placement } from 'rc-drawer/lib/Drawer'; import type { CSSMotionProps } from 'rc-motion'; -import * as React from 'react'; + import { getTransitionName } from '../_util/motion'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import { NoFormStyle } from '../form/context'; -import type { DrawerPanelProps } from './DrawerPanel'; -import DrawerPanel from './DrawerPanel'; - // CSSINJS import { NoCompactStyle } from '../space/Compact'; -import useStyle from './style'; import { usePanelRef } from '../watermark/context'; +import type { DrawerPanelProps } from './DrawerPanel'; +import DrawerPanel from './DrawerPanel'; +import useStyle from './style'; const SizeTypes = ['default', 'large'] as const; type sizeType = typeof SizeTypes[number]; @@ -88,6 +88,8 @@ const Drawer: React.FC & { // ========================== Warning =========================== if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + [ ['visible', 'open'], ['afterVisibleChange', 'afterOpenChange'], @@ -95,6 +97,7 @@ const Drawer: React.FC & { warning( !(deprecatedName in props), 'Drawer', + 'deprecated', `\`${deprecatedName}\` is deprecated, please use \`${newName}\` instead.`, ); }); @@ -103,6 +106,7 @@ const Drawer: React.FC & { warning( false, 'Drawer', + 'breaking', '`style` is replaced by `rootStyle` in v5. Please check that `position: absolute` is necessary.', ); } diff --git a/components/dropdown/dropdown.tsx b/components/dropdown/dropdown.tsx index dc42474c04..acc93e5e77 100644 --- a/components/dropdown/dropdown.tsx +++ b/components/dropdown/dropdown.tsx @@ -11,7 +11,7 @@ import type { AdjustOverflow } from '../_util/placements'; import getPlacements from '../_util/placements'; import genPurePanel from '../_util/PurePanel'; import { cloneElement } from '../_util/reactNode'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import type { MenuProps } from '../menu'; import Menu from '../menu'; @@ -109,6 +109,8 @@ const Dropdown: CompoundedComponent = (props) => { } = React.useContext(ConfigContext); // Warning for deprecated usage + const warning = devUseWarning(); + if (process.env.NODE_ENV !== 'production') { [ ['visible', 'open'], @@ -117,6 +119,7 @@ const Dropdown: CompoundedComponent = (props) => { warning( !(deprecatedName in props), 'Dropdown', + 'deprecated', `\`${deprecatedName}\` is deprecated which will be removed in next major version, please use \`${newName}\` instead.`, ); }); @@ -124,6 +127,7 @@ const Dropdown: CompoundedComponent = (props) => { warning( !('overlay' in props), 'Dropdown', + 'deprecated', '`overlay` is deprecated. Please use `menu` instead.', ); } @@ -146,19 +150,23 @@ const Dropdown: CompoundedComponent = (props) => { } if (placement.includes('Center')) { - const newPlacement = placement.slice(0, placement.indexOf('Center')) as DropdownPlacement; - warning( - !placement.includes('Center'), - 'Dropdown', - `You are using '${placement}' placement in Dropdown, which is deprecated. Try to use '${newPlacement}' instead.`, - ); - return newPlacement; + return placement.slice(0, placement.indexOf('Center')) as DropdownPlacement; } return placement as DropdownPlacement; }, [placement, direction]); if (process.env.NODE_ENV !== 'production') { + if (placement.includes('Center')) { + const newPlacement = placement.slice(0, placement.indexOf('Center')) as DropdownPlacement; + warning( + !placement.includes('Center'), + 'Dropdown', + 'deprecated', + `You are using '${placement}' placement in Dropdown, which is deprecated. Try to use '${newPlacement}' instead.`, + ); + } + [ ['visible', 'open'], ['onVisibleChange', 'onOpenChange'], @@ -166,6 +174,7 @@ const Dropdown: CompoundedComponent = (props) => { warning( !(deprecatedName in props), 'Dropdown', + 'deprecated', `\`${deprecatedName}\` is deprecated, please use \`${newName}\` instead.`, ); }); @@ -258,6 +267,7 @@ const Dropdown: CompoundedComponent = (props) => { warning( !mode || mode === 'vertical', 'Dropdown', + 'usage', `mode="${mode}" is not supported for Dropdown's Menu.`, ); }} diff --git a/components/float-button/FloatButton.tsx b/components/float-button/FloatButton.tsx index 61ab00677d..06720b0e12 100644 --- a/components/float-button/FloatButton.tsx +++ b/components/float-button/FloatButton.tsx @@ -1,13 +1,14 @@ +import React, { useContext, useMemo } from 'react'; import classNames from 'classnames'; import omit from 'rc-util/lib/omit'; -import React, { useContext, useMemo } from 'react'; -import warning from '../_util/warning'; + +import { devUseWarning } from '../_util/warning'; import Badge from '../badge'; import type { ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider'; import Tooltip from '../tooltip'; -import Content from './FloatButtonContent'; import FloatButtonGroupContext from './context'; +import Content from './FloatButtonContent'; import type { CompoundedComponent, FloatButtonBadgeProps, @@ -84,9 +85,12 @@ const FloatButton: React.ForwardRefRenderFunction< } if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( !(shape === 'circle' && description), 'FloatButton', + 'usage', 'supported only when `shape` is `square`. Due to narrow space for text, short sentence is recommended.', ); } diff --git a/components/float-button/FloatButtonGroup.tsx b/components/float-button/FloatButtonGroup.tsx index e14a2ccb74..471257aa13 100644 --- a/components/float-button/FloatButtonGroup.tsx +++ b/components/float-button/FloatButtonGroup.tsx @@ -1,14 +1,15 @@ +import React, { memo, useCallback, useContext, useEffect, useMemo, useRef } from 'react'; import CloseOutlined from '@ant-design/icons/CloseOutlined'; import FileTextOutlined from '@ant-design/icons/FileTextOutlined'; import classNames from 'classnames'; import CSSMotion from 'rc-motion'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; -import React, { memo, useCallback, useContext, useEffect, useMemo, useRef } from 'react'; -import warning from '../_util/warning'; + +import { devUseWarning } from '../_util/warning'; import type { ConfigConsumerProps } from '../config-provider'; import { ConfigContext } from '../config-provider'; -import FloatButton, { floatButtonPrefixCls } from './FloatButton'; import { FloatButtonGroupProvider } from './context'; +import FloatButton, { floatButtonPrefixCls } from './FloatButton'; import type { FloatButtonGroupProps } from './interface'; import useStyle from './style'; @@ -93,9 +94,12 @@ const FloatButtonGroup: React.FC = (props) => { // =================== Warning ===================== if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( !('open' in props) || !!trigger, 'FloatButton.Group', + 'usage', '`open` need to be used together with `trigger`', ); } diff --git a/components/form/FormItem/index.tsx b/components/form/FormItem/index.tsx index 2fdbbe9cfc..2fac0110dd 100644 --- a/components/form/FormItem/index.tsx +++ b/components/form/FormItem/index.tsx @@ -2,12 +2,12 @@ import * as React from 'react'; import classNames from 'classnames'; import { Field, FieldContext, ListContext } from 'rc-field-form'; import type { FieldProps } from 'rc-field-form/lib/Field'; -import type { Meta, NamePath } from 'rc-field-form/lib/interface'; +import type { Meta } from 'rc-field-form/lib/interface'; import useState from 'rc-util/lib/hooks/useState'; import { supportRef } from 'rc-util/lib/ref'; import { cloneElement, isValidElement } from '../../_util/reactNode'; -import warning from '../../_util/warning'; +import { devUseWarning } from '../../_util/warning'; import { ConfigContext } from '../../config-provider'; import { FormContext, NoStyleItemContext } from '../context'; import type { FormInstance } from '../Form'; @@ -80,13 +80,6 @@ export interface FormItemProps fieldKey?: React.Key | React.Key[]; } -function hasValidName(name?: NamePath): Boolean { - if (name === null) { - warning(false, 'Form.Item', '`null` is passed as `name` property'); - } - return !(name === undefined || name === null); -} - function genEmptyMeta(): Meta { return { errors: [], @@ -128,13 +121,20 @@ function InternalFormItem(props: FormItemProps): React.Rea const mergedValidateTrigger = validateTrigger !== undefined ? validateTrigger : contextValidateTrigger; - const hasName = hasValidName(name); + const hasName = !(name === undefined || name === null); const prefixCls = getPrefixCls('form', customizePrefixCls); // Style const [wrapSSR, hashId] = useStyle(prefixCls); + // ========================= Warn ========================= + const warning = devUseWarning(); + + if (process.env.NODE_ENV !== 'production') { + warning(name !== null, 'Form.Item', 'usage', '`null` is passed as `name` property'); + } + // ========================= MISC ========================= // Get `noStyle` required info const listContext = React.useContext(ListContext); @@ -308,12 +308,14 @@ function InternalFormItem(props: FormItemProps): React.Rea warning( !(shouldUpdate && dependencies), 'Form.Item', + 'usage', "`shouldUpdate` and `dependencies` shouldn't be used together. See https://u.ant.design/form-deps.", ); if (Array.isArray(mergedChildren) && hasName) { warning( false, 'Form.Item', + 'usage', 'A `Form.Item` with a `name` prop must have a single child element. For information on how to render more complex form items, see https://u.ant.design/complex-form-item.', ); childNode = mergedChildren; @@ -321,23 +323,27 @@ function InternalFormItem(props: FormItemProps): React.Rea warning( !!(shouldUpdate || dependencies), 'Form.Item', + 'usage', 'A `Form.Item` with a render function must have either `shouldUpdate` or `dependencies`.', ); warning( !hasName, 'Form.Item', + 'usage', 'A `Form.Item` with a render function cannot be a field, and thus cannot have a `name` prop.', ); } else if (dependencies && !isRenderProps && !hasName) { warning( false, 'Form.Item', + 'usage', 'Must set `name` or use a render function when `dependencies` is set.', ); } else if (isValidElement(mergedChildren)) { warning( mergedChildren.props.defaultValue === undefined, 'Form.Item', + 'usage', '`defaultValue` will not work on controlled Field. You should use `initialValues` of Form instead.', ); @@ -404,6 +410,7 @@ function InternalFormItem(props: FormItemProps): React.Rea warning( !mergedName.length, 'Form.Item', + 'usage', '`name` is only used for validate React element. If you are using Form.Item as layout display, please remove `name` instead.', ); childNode = mergedChildren as React.ReactNode; diff --git a/components/form/FormList.tsx b/components/form/FormList.tsx index 55160ca9ea..25bdf06b92 100644 --- a/components/form/FormList.tsx +++ b/components/form/FormList.tsx @@ -1,7 +1,8 @@ +import * as React from 'react'; import { List } from 'rc-field-form'; import type { StoreValue, ValidatorRule } from 'rc-field-form/lib/interface'; -import * as React from 'react'; -import warning from '../_util/warning'; + +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import { FormItemPrefixContext } from './context'; @@ -35,12 +36,17 @@ const FormList: React.FC = ({ children, ...props }) => { - warning( - typeof props.name === 'number' || - (Array.isArray(props.name) ? !!props.name.length : !!props.name), - 'Form.List', - 'Miss `name` prop.', - ); + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + + warning( + typeof props.name === 'number' || + (Array.isArray(props.name) ? !!props.name.length : !!props.name), + 'Form.List', + 'usage', + 'Miss `name` prop.', + ); + } const { getPrefixCls } = React.useContext(ConfigContext); const prefixCls = getPrefixCls('form', customizePrefixCls); diff --git a/components/form/hooks/useFormItemStatus.ts b/components/form/hooks/useFormItemStatus.ts index dc6291c5df..803e32c367 100644 --- a/components/form/hooks/useFormItemStatus.ts +++ b/components/form/hooks/useFormItemStatus.ts @@ -1,7 +1,8 @@ import { useContext } from 'react'; import type { ValidateStatus } from 'antd/es/form/FormItem'; + +import { devUseWarning } from '../../_util/warning'; import { FormItemInputContext } from '../context'; -import warning from '../../_util/warning'; type UseFormItemStatus = () => { status?: ValidateStatus; @@ -12,11 +13,16 @@ type UseFormItemStatus = () => { const useFormItemStatus: UseFormItemStatus = () => { const { status, errors = [], warnings = [] } = useContext(FormItemInputContext); - warning( - status !== undefined, - 'Form.Item', - 'Form.Item.useStatus should be used under Form.Item component. For more information: https://u.ant.design/form-item-usestatus', - ); + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + + warning( + status !== undefined, + 'Form.Item', + 'usage', + 'Form.Item.useStatus should be used under Form.Item component. For more information: https://u.ant.design/form-item-usestatus', + ); + } return { status, errors, warnings }; }; diff --git a/components/form/hooks/useFormWarning.ts b/components/form/hooks/useFormWarning.ts index 4f3d69fe26..188ff422b2 100644 --- a/components/form/hooks/useFormWarning.ts +++ b/components/form/hooks/useFormWarning.ts @@ -1,15 +1,18 @@ import { useEffect } from 'react'; -import warning from '../../_util/warning'; + +import { devUseWarning } from '../../_util/warning'; import type { FormProps } from '../Form'; const names: Record = {}; export default function useFormWarning({ name }: FormProps) { + const warning = devUseWarning(); + useEffect(() => { if (name) { names[name] = (names[name] || 0) + 1; - warning(names[name] <= 1, 'Form', 'There exist multiple Form with same `name`.'); + warning(names[name] <= 1, 'Form', 'usage', 'There exist multiple Form with same `name`.'); return () => { names[name] -= 1; diff --git a/components/icon/index.ts b/components/icon/index.ts index d985d56650..b0042d9047 100755 --- a/components/icon/index.ts +++ b/components/icon/index.ts @@ -1,8 +1,10 @@ -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; const Icon: React.FC = () => { if (process.env.NODE_ENV !== 'production') { - warning(false, 'Icon', 'Empty Icon'); + const warning = devUseWarning(); + + warning(false, 'Icon', 'usage', 'Empty Icon'); } return null; }; diff --git a/components/input/Group.tsx b/components/input/Group.tsx index ee144395d5..96fc3e4cf6 100644 --- a/components/input/Group.tsx +++ b/components/input/Group.tsx @@ -1,7 +1,8 @@ -import classNames from 'classnames'; import * as React from 'react'; import { useContext, useMemo } from 'react'; -import warning from '../_util/warning'; +import classNames from 'classnames'; + +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import type { FormItemStatusContextProps } from '../form/context'; import { FormItemInputContext } from '../form/context'; @@ -49,9 +50,12 @@ const Group: React.FC = (props) => { ); if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( false, 'Input.Group', + 'deprecated', `'Input.Group' is deprecated. Please use 'Space.Compact' instead.`, ); } diff --git a/components/input/Input.tsx b/components/input/Input.tsx index c80c37661d..580fcef2c0 100644 --- a/components/input/Input.tsx +++ b/components/input/Input.tsx @@ -1,17 +1,18 @@ +import React, { forwardRef, useContext, useEffect, useRef } from 'react'; import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled'; import classNames from 'classnames'; import type { InputRef, InputProps as RcInputProps } from 'rc-input'; import RcInput from 'rc-input'; import type { BaseInputProps } from 'rc-input/lib/interface'; import { composeRef } from 'rc-util/lib/ref'; -import React, { forwardRef, useContext, useEffect, useRef } from 'react'; + import type { InputStatus } from '../_util/statusUtils'; import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import DisabledContext from '../config-provider/DisabledContext'; -import type { SizeType } from '../config-provider/SizeContext'; import useSize from '../config-provider/hooks/useSize'; +import type { SizeType } from '../config-provider/SizeContext'; import { FormItemInputContext, NoFormStyle } from '../form/context'; import { NoCompactStyle, useCompactItemContext } from '../space/Compact'; import useRemovePasswordTimeout from './hooks/useRemovePasswordTimeout'; @@ -112,16 +113,24 @@ const Input = forwardRef((props, ref) => { // ===================== Focus warning ===================== const inputHasPrefixSuffix = hasPrefixSuffix(props) || !!hasFeedback; const prevHasPrefixSuffix = useRef(inputHasPrefixSuffix); - useEffect(() => { - if (inputHasPrefixSuffix && !prevHasPrefixSuffix.current) { - warning( - document.activeElement === inputRef.current?.input, - 'Input', - `When Input is focused, dynamic add or remove prefix / suffix will make it lose focus caused by dom structure change. Read more: https://ant.design/components/input/#FAQ`, - ); - } - prevHasPrefixSuffix.current = inputHasPrefixSuffix; - }, [inputHasPrefixSuffix]); + + /* eslint-disable react-hooks/rules-of-hooks */ + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + + useEffect(() => { + if (inputHasPrefixSuffix && !prevHasPrefixSuffix.current) { + warning( + document.activeElement === inputRef.current?.input, + 'Input', + 'usage', + `When Input is focused, dynamic add or remove prefix / suffix will make it lose focus caused by dom structure change. Read more: https://ant.design/components/input/#FAQ`, + ); + } + prevHasPrefixSuffix.current = inputHasPrefixSuffix; + }, [inputHasPrefixSuffix]); + } + /* eslint-enable */ // ===================== Remove Password value ===================== const removePasswordTimeout = useRemovePasswordTimeout(inputRef, true); diff --git a/components/locale/index.tsx b/components/locale/index.tsx index b37de4f6bb..12ce67cc7d 100644 --- a/components/locale/index.tsx +++ b/components/locale/index.tsx @@ -1,6 +1,7 @@ -import type { ValidateMessages } from 'rc-field-form/lib/interface'; import * as React from 'react'; -import warning from '../_util/warning'; +import type { ValidateMessages } from 'rc-field-form/lib/interface'; + +import { devUseWarning } from '../_util/warning'; import type { PickerLocale as DatePickerLocale } from '../date-picker/generatePicker'; import type { TransferLocale as TransferLocaleForEmpty } from '../empty'; import type { ModalLocale } from '../modal/locale'; @@ -68,9 +69,12 @@ const LocaleProvider: React.FC = (props) => { const { locale = {} as Locale, children, _ANT_MARK__ } = props; if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( _ANT_MARK__ === ANT_MARK, 'LocaleProvider', + 'deprecated', '`LocaleProvider` is deprecated. Please use `locale` with `ConfigProvider` instead: http://u.ant.design/locale', ); } diff --git a/components/mentions/index.tsx b/components/mentions/index.tsx index a05c0c818c..d37572cfb2 100644 --- a/components/mentions/index.tsx +++ b/components/mentions/index.tsx @@ -1,3 +1,5 @@ +// eslint-disable-next-line import/no-named-as-default +import * as React from 'react'; import classNames from 'classnames'; import RcMentions from 'rc-mentions'; import type { @@ -6,17 +8,15 @@ import type { MentionsRef as RcMentionsRef, } from 'rc-mentions/lib/Mentions'; import { composeRef } from 'rc-util/lib/ref'; -// eslint-disable-next-line import/no-named-as-default -import * as React from 'react'; + import genPurePanel from '../_util/PurePanel'; import type { InputStatus } from '../_util/statusUtils'; import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import DefaultRenderEmpty from '../config-provider/defaultRenderEmpty'; import { FormItemInputContext } from '../form/context'; import Spin from '../spin'; - import useStyle from './style'; export const { Option } = RcMentions; @@ -88,9 +88,12 @@ const InternalMentions: React.ForwardRefRenderFunction { @@ -70,23 +71,30 @@ const InternalMenu = forwardRef((props, ref) => { const mergedChildren = useItems(items) || children; // ======================== Warning ========================== - warning( - !('inlineCollapsed' in props && mode !== 'inline'), - 'Menu', - '`inlineCollapsed` should only be used when `mode` is inline.', - ); + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); - warning( - !(props.siderCollapsed !== undefined && 'inlineCollapsed' in props), - 'Menu', - '`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.', - ); + warning( + !('inlineCollapsed' in props && mode !== 'inline'), + 'Menu', + 'usage', + '`inlineCollapsed` should only be used when `mode` is inline.', + ); - warning( - 'items' in props && !children, - 'Menu', - '`children` will be removed in next major version. Please use `items` instead.', - ); + warning( + !(props.siderCollapsed !== undefined && 'inlineCollapsed' in props), + 'Menu', + 'usage', + '`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.', + ); + + warning( + 'items' in props && !children, + 'Menu', + 'deprecated', + '`children` will be removed in next major version. Please use `items` instead.', + ); + } overrideObj.validator?.({ mode }); diff --git a/components/message/useMessage.tsx b/components/message/useMessage.tsx index c49e4a6e26..69f2bdb78d 100644 --- a/components/message/useMessage.tsx +++ b/components/message/useMessage.tsx @@ -1,12 +1,14 @@ +import * as React from 'react'; +import type { FC, PropsWithChildren } from 'react'; import CloseOutlined from '@ant-design/icons/CloseOutlined'; import classNames from 'classnames'; import { NotificationProvider, useNotification as useRcNotification } from 'rc-notification'; import type { NotificationAPI } from 'rc-notification/lib'; -import * as React from 'react'; -import warning from '../_util/warning'; +import type { NotificationConfig as RcNotificationConfig } from 'rc-notification/lib/useNotification'; + +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import type { ComponentStyleConfig } from '../config-provider/context'; -import { PureContent } from './PurePanel'; import type { ArgsProps, ConfigOptions, @@ -15,10 +17,9 @@ import type { NoticeType, TypeOpen, } from './interface'; +import { PureContent } from './PurePanel'; import useStyle from './style'; import { getMotion, wrapPromiseFn } from './util'; -import type { FC, PropsWithChildren } from 'react'; -import type { NotificationConfig as RcNotificationConfig } from 'rc-notification/lib/useNotification'; const DEFAULT_OFFSET = 8; const DEFAULT_DURATION = 3; @@ -122,6 +123,8 @@ export function useInternalMessage( ): readonly [MessageInstance, React.ReactElement] { const holderRef = React.useRef(null); + const warning = devUseWarning(); + // ================================ API ================================ const wrapAPI = React.useMemo(() => { // Wrap with notification content @@ -137,6 +140,7 @@ export function useInternalMessage( warning( false, 'Message', + 'usage', 'You are calling notice in render which will break in React 18 concurrent mode. Please trigger in effect instead.', ); diff --git a/components/modal/ConfirmDialog.tsx b/components/modal/ConfirmDialog.tsx index 8b038f642e..1cc0a9c7f7 100644 --- a/components/modal/ConfirmDialog.tsx +++ b/components/modal/ConfirmDialog.tsx @@ -6,7 +6,7 @@ import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled'; import classNames from 'classnames'; import { getTransitionName } from '../_util/motion'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import type { ThemeConfig } from '../config-provider'; import ConfigProvider from '../config-provider'; import { useLocale } from '../locale'; @@ -61,11 +61,16 @@ export function ConfirmContent( ...resetProps } = props; - warning( - !(typeof icon === 'string' && icon.length > 2), - 'Modal', - `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`, - ); + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + + warning( + !(typeof icon === 'string' && icon.length > 2), + 'Modal', + 'breaking', + `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`, + ); + } // Icon let mergedIcon: React.ReactNode = icon; @@ -185,9 +190,12 @@ const ConfirmDialog: React.FC = (props) => { } = props; if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + warning( visible === undefined, 'Modal', + 'deprecated', `\`visible\` is deprecated, please use \`open\` instead.`, ); } diff --git a/components/modal/Modal.tsx b/components/modal/Modal.tsx index 6bb5ad1ef7..5ddd75acff 100644 --- a/components/modal/Modal.tsx +++ b/components/modal/Modal.tsx @@ -6,7 +6,7 @@ import Dialog from 'rc-dialog'; import useClosable from '../_util/hooks/useClosable'; import { getTransitionName } from '../_util/motion'; import { canUseDocElement } from '../_util/styleChecker'; -import warning from '../_util/warning'; +import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import { NoFormStyle } from '../form/context'; import { NoCompactStyle } from '../space/Compact'; @@ -54,11 +54,16 @@ const Modal: React.FC = (props) => { onOk?.(e); }; - warning( - !('visible' in props), - 'Modal', - `\`visible\` will be removed in next major version, please use \`open\` instead.`, - ); + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning(); + + warning( + !('visible' in props), + 'Modal', + 'deprecated', + `\`visible\` will be removed in next major version, please use \`open\` instead.`, + ); + } const { prefixCls: customizePrefixCls, @@ -90,10 +95,6 @@ const Modal: React.FC = (props) => { [`${prefixCls}-wrap-rtl`]: direction === 'rtl', }); - if (process.env.NODE_ENV !== 'production') { - warning(!('visible' in props), 'Modal', '`visible` is deprecated, please use `open` instead.'); - } - const dialogFooter = footer !== null && (