diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c5bf95428f..f2b6233dbf 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -61,3 +61,18 @@ Describe changes from the user side, and list all potential break changes or oth - [ ] Demo is updated/provided or not needed - [ ] TypeScript definition is updated/provided or not needed - [ ] Changelog is provided or not needed + +--- + + + +### 🚀 Summary + +copilot:summary + +### 🔍 Walkthrough + +copilot:walkthrough diff --git a/.github/PULL_REQUEST_TEMPLATE/pr_cn.md b/.github/PULL_REQUEST_TEMPLATE/pr_cn.md index 046c0c2f54..a72aa0c3ea 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pr_cn.md +++ b/.github/PULL_REQUEST_TEMPLATE/pr_cn.md @@ -61,3 +61,17 @@ - [ ] 代码演示已提供或无须提供 - [ ] TypeScript 定义已补充或无须补充 - [ ] Changelog 已提供或无须提供 + +--- + + + +### 🚀 概述 + +copilot:summary + +### 🔍 实现细节 + +copilot:walkthrough diff --git a/components/breadcrumb/index.en-US.md b/components/breadcrumb/index.en-US.md index 77a2b8f0d8..f1516a4e31 100644 --- a/components/breadcrumb/index.en-US.md +++ b/components/breadcrumb/index.en-US.md @@ -40,7 +40,7 @@ return ; react-router V6 Configuring the Separator Bread crumbs with drop down menu -Configuring the Separator +Configuring the Separator Independently Debug Routes ## API diff --git a/components/breadcrumb/index.zh-CN.md b/components/breadcrumb/index.zh-CN.md index da7c951623..7b94a3fc56 100644 --- a/components/breadcrumb/index.zh-CN.md +++ b/components/breadcrumb/index.zh-CN.md @@ -41,7 +41,7 @@ return ; react-router V6 分隔符 带下拉菜单的面包屑 -分隔符 +独立的分隔符 Debug Routes ## API diff --git a/components/cascader/index.tsx b/components/cascader/index.tsx index 861c17bc57..5d36df65bb 100644 --- a/components/cascader/index.tsx +++ b/components/cascader/index.tsx @@ -7,30 +7,31 @@ import type { DefaultOptionType, FieldNames, MultipleCascaderProps as RcMultipleCascaderProps, - ShowSearchType, SingleCascaderProps as RcSingleCascaderProps, + ShowSearchType, } from 'rc-cascader'; import RcCascader from 'rc-cascader'; +import type { Placement } from 'rc-select/lib/BaseSelect'; import omit from 'rc-util/lib/omit'; import * as React from 'react'; import { ConfigContext } from '../config-provider'; -import DefaultRenderEmpty from '../config-provider/defaultRenderEmpty'; import DisabledContext from '../config-provider/DisabledContext'; import type { SizeType } from '../config-provider/SizeContext'; import SizeContext from '../config-provider/SizeContext'; +import DefaultRenderEmpty from '../config-provider/defaultRenderEmpty'; import { useCompactItemContext } from '../space/Compact'; -import { FormItemInputContext } from '../form/context'; -import getIcons from '../select/utils/iconUtil'; import type { SelectCommonPlacement } from '../_util/motion'; import { getTransitionDirection, getTransitionName } from '../_util/motion'; import type { InputStatus } from '../_util/statusUtils'; import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'; import warning from '../_util/warning'; +import { FormItemInputContext } from '../form/context'; +import getIcons from '../select/utils/iconUtil'; +import genPurePanel from '../_util/PurePanel'; import useSelectStyle from '../select/style'; import useShowArrow from '../select/useShowArrow'; -import genPurePanel from '../_util/PurePanel'; import useStyle from './style'; // Align the design since we use `rc-select` in root. This help: @@ -85,7 +86,7 @@ const defaultSearchRender: ShowSearchType['render'] = (inputValue, path, prefixC optionList.push(' / '); } - let label = (node as any)[fieldNames.label!]; + let label = node[fieldNames.label!]; const type = typeof label; if (type === 'string' || type === 'number') { label = highlightKeyword(String(label), lower, prefixCls); @@ -266,12 +267,12 @@ const Cascader = React.forwardRef((props: CascaderProps, ref: React.Ref { + const memoPlacement = React.useMemo(() => { if (placement !== undefined) { return placement; } return isRtl ? 'bottomRight' : 'bottomLeft'; - }; + }, [placement, isRtl]); // ==================== Render ===================== const renderNode = ( @@ -295,7 +296,7 @@ const Cascader = React.forwardRef((props: CascaderProps, ref: React.Ref + `; diff --git a/components/checkbox/__tests__/__snapshots__/demo.test.tsx.snap b/components/checkbox/__tests__/__snapshots__/demo.test.tsx.snap index 760cb54d2e..a9f5caa101 100644 --- a/components/checkbox/__tests__/__snapshots__/demo.test.tsx.snap +++ b/components/checkbox/__tests__/__snapshots__/demo.test.tsx.snap @@ -383,6 +383,24 @@ exports[`renders components/checkbox/demo/debug-line.tsx correctly 1`] = ` + `; diff --git a/components/checkbox/demo/debug-line.tsx b/components/checkbox/demo/debug-line.tsx index aaf89daf17..fbbb99ecb5 100644 --- a/components/checkbox/demo/debug-line.tsx +++ b/components/checkbox/demo/debug-line.tsx @@ -1,5 +1,5 @@ +import { Checkbox, ConfigProvider, Radio, Space } from 'antd'; import React from 'react'; -import { Checkbox, Radio, Space } from 'antd'; const sharedStyle: React.CSSProperties = { border: '1px solid red', @@ -43,6 +43,16 @@ const App: React.FC = () => (
Bamboo
Little + + + Aligned + ); diff --git a/components/checkbox/style/index.ts b/components/checkbox/style/index.ts index 67404be944..8d42050eba 100644 --- a/components/checkbox/style/index.ts +++ b/components/checkbox/style/index.ts @@ -71,19 +71,26 @@ export const genCheckboxStyle: GenerateStyle = (token) => { [checkboxCls]: { ...resetComponent(token), - top: '0.2em', position: 'relative', whiteSpace: 'nowrap', lineHeight: 1, cursor: 'pointer', + alignSelf: 'start', + // https://github.com/ant-design/ant-design/issues/41564 + // Since `checkboxSize` is dynamic which should align with the text box, + // We need do calculation here for offset. + transform: `translate(0, ${ + (token.lineHeight * token.fontSize) / 2 - token.checkboxSize / 2 + }px)`, + // Wrapper > Checkbox > input [`${checkboxCls}-input`]: { position: 'absolute', // Since baseline align will get additional space offset, // we need to move input to top to make it align with text. // Ref: https://github.com/ant-design/ant-design/issues/38926#issuecomment-1486137799 - inset: `-0.2em 0`, + inset: 0, zIndex: 1, cursor: 'pointer', opacity: 0, diff --git a/components/collapse/Collapse.tsx b/components/collapse/Collapse.tsx index 403a2f4a27..0742b2908f 100644 --- a/components/collapse/Collapse.tsx +++ b/components/collapse/Collapse.tsx @@ -6,13 +6,13 @@ import * as React from 'react'; import toArray from 'rc-util/lib/Children/toArray'; import omit from 'rc-util/lib/omit'; -import { ConfigContext } from '../config-provider'; import initCollapseMotion from '../_util/motion'; import { cloneElement } from '../_util/reactNode'; import warning from '../_util/warning'; -import type { CollapsibleType } from './CollapsePanel'; +import { ConfigContext } from '../config-provider'; import type { SizeType } from '../config-provider/SizeContext'; import SizeContext from '../config-provider/SizeContext'; +import type { CollapsibleType } from './CollapsePanel'; import CollapsePanel from './CollapsePanel'; import useStyle from './style'; @@ -66,6 +66,8 @@ const Collapse = React.forwardRef((props, ref) => ghost, size: customizeSize, expandIconPosition = 'start', + children, + expandIcon, } = props; const mergedSize = customizeSize || size || 'middle'; @@ -89,7 +91,6 @@ const Collapse = React.forwardRef((props, ref) => }, [expandIconPosition]); const renderExpandIcon = (panelProps: PanelProps = {}) => { - const { expandIcon } = props; const icon = ( expandIcon ? ( expandIcon(panelProps) @@ -121,22 +122,23 @@ const Collapse = React.forwardRef((props, ref) => leavedClassName: `${prefixCls}-content-hidden`, }; - const getItems = () => { - const { children } = props; - return toArray(children).map((child: React.ReactElement, index: number) => { - if (child.props?.disabled) { - const key = child.key || String(index); - const { disabled, collapsible } = child.props; - const childProps: CollapseProps & { key: React.Key } = { - ...omit(child.props, ['disabled']), - key, - collapsible: collapsible ?? (disabled ? 'disabled' : undefined), - }; - return cloneElement(child, childProps); - } - return child; - }); - }; + const items = React.useMemo( + () => + toArray(children).map((child, index) => { + if (child.props?.disabled) { + const key = child.key ?? String(index); + const { disabled, collapsible } = child.props; + const childProps: CollapseProps & { key: React.Key } = { + ...omit(child.props, ['disabled']), + key, + collapsible: collapsible ?? (disabled ? 'disabled' : undefined), + }; + return cloneElement(child, childProps); + } + return child; + }), + [children], + ); return wrapSSR( ((props, ref) => prefixCls={prefixCls} className={collapseClassName} > - {getItems()} + {items} , ); }); diff --git a/components/drawer/DrawerPanel.tsx b/components/drawer/DrawerPanel.tsx index 725f68ce09..6448cd7d78 100644 --- a/components/drawer/DrawerPanel.tsx +++ b/components/drawer/DrawerPanel.tsx @@ -1,7 +1,7 @@ -import * as React from 'react'; import CloseOutlined from '@ant-design/icons/CloseOutlined'; -import type { DrawerProps as RCDrawerProps } from 'rc-drawer'; import classNames from 'classnames'; +import type { DrawerProps as RCDrawerProps } from 'rc-drawer'; +import * as React from 'react'; export interface DrawerPanelProps { prefixCls: string; @@ -22,18 +22,15 @@ export interface DrawerPanelProps { children?: React.ReactNode; } -export default function DrawerPanel(props: DrawerPanelProps) { +const DrawerPanel: React.FC = (props) => { const { prefixCls, - title, footer, extra, - closable = true, closeIcon = , onClose, - headerStyle, drawerStyle, bodyStyle, @@ -47,17 +44,16 @@ export default function DrawerPanel(props: DrawerPanelProps) { ); - function renderHeader() { + const headerNode = React.useMemo(() => { if (!title && !closable) { return null; } - return (
{closeIconNode} @@ -66,28 +62,29 @@ export default function DrawerPanel(props: DrawerPanelProps) { {extra &&
{extra}
}
); - } + }, [closable, closeIconNode, extra, headerStyle, prefixCls, title]); - function renderFooter() { + const footerNode = React.useMemo(() => { if (!footer) { return null; } - const footerClassName = `${prefixCls}-footer`; return (
{footer}
); - } + }, [footer, footerStyle, prefixCls]); return ( -
- {renderHeader()} +
+ {headerNode}
{children}
- {renderFooter()} + {footerNode}
); -} +}; + +export default DrawerPanel; diff --git a/components/dropdown/dropdown.tsx b/components/dropdown/dropdown.tsx index d50cf61f84..8639c7fc2e 100644 --- a/components/dropdown/dropdown.tsx +++ b/components/dropdown/dropdown.tsx @@ -5,19 +5,19 @@ import useEvent from 'rc-util/lib/hooks/useEvent'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; import omit from 'rc-util/lib/omit'; import * as React from 'react'; +import genPurePanel from '../_util/PurePanel'; +import type { AdjustOverflow } from '../_util/placements'; +import getPlacements from '../_util/placements'; +import { cloneElement } from '../_util/reactNode'; +import warning from '../_util/warning'; import { ConfigContext } from '../config-provider'; import type { MenuProps } from '../menu'; import Menu from '../menu'; import { OverrideProvider } from '../menu/OverrideContext'; import { NoCompactStyle } from '../space/Compact'; -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 theme from '../theme'; import DropdownButton from './dropdown-button'; import useStyle from './style'; -import theme from '../theme'; const Placements = [ 'topLeft', @@ -92,6 +92,29 @@ type CompoundedComponent = React.FC & { }; const Dropdown: CompoundedComponent = (props) => { + const { + menu, + arrow, + prefixCls: customizePrefixCls, + children, + trigger, + disabled, + dropdownRender, + getPopupContainer, + overlayClassName, + rootClassName, + open, + onOpenChange, + // Deprecated + visible, + onVisibleChange, + mouseEnterDelay = 0.15, + mouseLeaveDelay = 0.1, + autoAdjustOverflow = true, + placement = '', + overlay, + transitionName, + } = props; const { getPopupContainer: getContextPopupContainer, getPrefixCls, @@ -118,9 +141,9 @@ const Dropdown: CompoundedComponent = (props) => { ); } - const getTransitionName = () => { + const memoTransitionName = React.useMemo(() => { const rootPrefixCls = getPrefixCls(); - const { placement = '', transitionName } = props; + if (transitionName !== undefined) { return transitionName; } @@ -128,10 +151,9 @@ const Dropdown: CompoundedComponent = (props) => { return `${rootPrefixCls}-slide-down`; } return `${rootPrefixCls}-slide-up`; - }; + }, [getPrefixCls, placement, transitionName]); - const getPlacement = () => { - const { placement } = props; + const memoPlacement = React.useMemo(() => { if (!placement) { return direction === 'rtl' ? 'bottomRight' : 'bottomLeft'; } @@ -147,29 +169,7 @@ const Dropdown: CompoundedComponent = (props) => { } return placement; - }; - - const { - menu, - arrow, - prefixCls: customizePrefixCls, - children, - trigger, - disabled, - dropdownRender, - getPopupContainer, - overlayClassName, - rootClassName, - open, - onOpenChange, - - // Deprecated - visible, - onVisibleChange, - mouseEnterDelay = 0.15, - mouseLeaveDelay = 0.1, - autoAdjustOverflow = true, - } = props; + }, [placement, direction]); if (process.env.NODE_ENV !== 'production') { [ @@ -239,7 +239,6 @@ const Dropdown: CompoundedComponent = (props) => { const renderOverlay = () => { // rc-dropdown already can process the function of overlay, but we have check logic here. // So we need render the element to check and pass back to rc-dropdown. - const { overlay } = props; let overlayNode: React.ReactNode; if (menu?.items) { @@ -294,10 +293,10 @@ const Dropdown: CompoundedComponent = (props) => { overlayClassName={overlayClassNameCustomized} prefixCls={prefixCls} getPopupContainer={getPopupContainer || getContextPopupContainer} - transitionName={getTransitionName()} + transitionName={memoTransitionName} trigger={triggerActions} overlay={renderOverlay} - placement={getPlacement()} + placement={memoPlacement} onVisibleChange={onInnerOpenChange} > {dropdownTrigger} diff --git a/components/form/FormItem/ItemHolder.tsx b/components/form/FormItem/ItemHolder.tsx index 56256ff5ee..fd7d5a97e8 100644 --- a/components/form/FormItem/ItemHolder.tsx +++ b/components/form/FormItem/ItemHolder.tsx @@ -5,6 +5,7 @@ import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; import classNames from 'classnames'; import type { Meta } from 'rc-field-form/lib/interface'; import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; +import isVisible from 'rc-util/lib/Dom/isVisible'; import omit from 'rc-util/lib/omit'; import * as React from 'react'; import type { FormItemProps, ValidateStatus } from '.'; @@ -65,14 +66,17 @@ export default function ItemHolder(props: ItemHolderProps) { const debounceWarnings = useDebounce(warnings); const hasHelp = help !== undefined && help !== null; const hasError = !!(hasHelp || errors.length || warnings.length); + const isOnScreen = !!itemRef.current && isVisible(itemRef.current); const [marginBottom, setMarginBottom] = React.useState(null); useLayoutEffect(() => { if (hasError && itemRef.current) { + // The element must be part of the DOMTree to use getComputedStyle + // https://stackoverflow.com/questions/35360711/getcomputedstyle-returns-a-cssstyledeclaration-but-all-properties-are-empty-on-a const itemStyle = getComputedStyle(itemRef.current); setMarginBottom(parseInt(itemStyle.marginBottom, 10)); } - }, [hasError]); + }, [hasError, isOnScreen]); const onErrorVisibleChanged = (nextVisible: boolean) => { if (!nextVisible) { diff --git a/components/form/__tests__/index.test.tsx b/components/form/__tests__/index.test.tsx index d3ef046d0c..0112d3118f 100644 --- a/components/form/__tests__/index.test.tsx +++ b/components/form/__tests__/index.test.tsx @@ -1418,6 +1418,20 @@ describe('Form', () => { expect(container.querySelector('.drawer-select')?.className).not.toContain('status-error'); }); + it('should be set up correctly marginBottom', () => { + render( + +
+ + + +
+
, + ); + + expect(document.querySelector('.ant-form-item-margin-offset')).toBeTruthy(); + }); + it('Form.Item.useStatus should work', async () => { const { Item: { useStatus }, @@ -1545,6 +1559,7 @@ describe('Form', () => { marginBottom: -24, }); }); + it('form child components should be given priority to own disabled props when it in a disabled form', () => { const props = { name: 'file', diff --git a/components/mentions/index.tsx b/components/mentions/index.tsx index 9e34bdabe4..a40a8e23f4 100644 --- a/components/mentions/index.tsx +++ b/components/mentions/index.tsx @@ -8,14 +8,14 @@ import type { import { composeRef } from 'rc-util/lib/ref'; // eslint-disable-next-line import/no-named-as-default import * as React from 'react'; -import { ConfigContext } from '../config-provider'; -import DefaultRenderEmpty from '../config-provider/defaultRenderEmpty'; -import { FormItemInputContext } from '../form/context'; -import Spin from '../spin'; import genPurePanel from '../_util/PurePanel'; import type { InputStatus } from '../_util/statusUtils'; import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'; import warning 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'; @@ -81,7 +81,7 @@ const InternalMentions: React.ForwardRefRenderFunction { const [focused, setFocused] = React.useState(false); - const innerRef = React.useRef(); + const innerRef = React.useRef(null); const mergedRef = composeRef(ref, innerRef); // =================== Warning ===================== @@ -123,7 +123,7 @@ const InternalMentions: React.ForwardRefRenderFunction; }, [notFoundContent, renderEmpty]); - const getOptions = () => { + const mentionOptions = React.useMemo(() => { if (loading) { return (