diff --git a/.stylelintrc.json b/.stylelintrc.json index 89e5943dab..81a0a2c283 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -28,6 +28,7 @@ ] } ], + "import-notation": null, "no-descending-specificity": null, "no-invalid-position-at-import-rule": null, "declaration-empty-line-before": null, diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index c74a2569fa..33fbc8771b 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -15,6 +15,64 @@ timeline: true --- +## 4.24.1 + +`2022-11-04` + +- 🐞 Revert [#38160](https://github.com/ant-design/ant-design/pull/38160) to fix Table render `column.title` not as expect. [#38383](https://github.com/ant-design/ant-design/pull/38383) +- 💄 Alert add `@alert-padding-vertical` & `@alert-padding-horizontal` less variables. [#38369](https://github.com/ant-design/ant-design/pull/38369) [@imoctopus](https://github.com/imoctopus) +- 🐞 Fix Steps with customize Step `status` not work. [#38319](https://github.com/ant-design/ant-design/pull/38319) [@heiyu4585](https://github.com/heiyu4585) +- 🐞 Fix Popconfirm icon color get polluted. [#38355](https://github.com/ant-design/ant-design/pull/38355) +- 🐞 Fix Anchor missing dot style. [#38338](https://github.com/ant-design/ant-design/pull/38338) [@li-jia-nan](https://github.com/li-jia-nan) +- 🐞 Fix DatePicker missing `popupClassName` definition. [#38325](https://github.com/ant-design/ant-design/pull/38325) [@Cedong.Lee](https://github.com/Cedong.Lee) + +## 4.24.0 + +`2022-11-01` + +- 🔥 Add new component Space.Compact used to replace Input.Group and Button.Group. [#37652](https://github.com/ant-design/ant-design/pull/37652) [@foryuki](https://github.com/foryuki) +- 🆕 The `disabled` property on components inside a Form will now take precedence over the `disabled` property of the Form. [#37628](https://github.com/ant-design/ant-design/pull/37628) [@kiner-tang](https://github.com/kiner-tang) +- 🆕 Add `text` config for editable Typograph, support enabling ellipsis and editable at the same time. [#37761](https://github.com/ant-design/ant-design/pull/37761) [@zheeeng](https://github.com/zheeeng) +- 🆕 Row `align` and `justify` support reponsive value. [#37860](https://github.com/ant-design/ant-design/pull/37860) [@kiner-tang](https://github.com/kiner-tang) +- 🆕 Image add `preview.scaleStep` prop to adjust the magnitude of zoom in and out and set the default `scaleOffset` to 0.5. [#37340](https://github.com/ant-design/ant-design/pull/37340) [@coldice945](https://github.com/coldice945) +- 🆕 Steps support `items`. [#37531](https://github.com/ant-design/ant-design/pull/37531) [@heiyu4585](https://github.com/heiyu4585) +- 🆕 Collapse supports `collapsible="icon"` to collapse by clicking icon. [#37566](https://github.com/ant-design/ant-design/pull/37566) [@Sheepeer](https://github.com/Sheepeer) +- 🆕 Input.Password supports `visibilityToggle={{ visible, onVisibleChange }}` so that you can manually control password display and hide. [#38216](https://github.com/ant-design/ant-design/pull/38216) [@MrHeer](https://github.com/MrHeer) +- 🆕 Breadcrumb added the `menu` property. [#37885](https://github.com/ant-design/ant-design/pull/37885) [@JarvisArt](https://github.com/JarvisArt) +- 🆕 Dropdown added the `menu` `dropdownRender` property, and deprecated the `overlay` property. [#37885](https://github.com/ant-design/ant-design/pull/37885) [@JarvisArt](https://github.com/JarvisArt) +- Table + - 🆕 Table `filterDropdown` add close in argument to `close` filter dropdown only. [#37745](https://github.com/ant-design/ant-design/pull/37745) [@kiner-tang](https://github.com/kiner-tang) + - 🐞 Fix Table `aria-label` contains `[object Object]`. [#38160](https://github.com/ant-design/ant-design/pull/38160) [@kiner-tang](https://github.com/kiner-tang) +- 🐞 Fix Tabs component not reading the `getPopupContainer` property of ConfigProvider. [#38238](https://github.com/ant-design/ant-design/pull/38238) [@ZH-seven](https://github.com/ZH-seven) +- 🐞 Fix Tooltip is broken when used in a disabled Menu.Item. [#38273](https://github.com/ant-design/ant-design/pull/38273) +- 🐞 Fix the issue of miscalculated transform-origin for Tooltip with `placement` values like `topRight` or `bottomLeft`. [#38159](https://github.com/ant-design/ant-design/pull/38159) [@strear](https://github.com/strear) +- 🐞 TimePicker remove redundant warning about using `popupClassName`. [#38190](https://github.com/ant-design/ant-design/pull/38190) [@kiner-tang](https://github.com/kiner-tang) +- 🐞 Fix nest Drawer with default `open` append document order issue. [#37767](https://github.com/ant-design/ant-design/pull/37767) [#37790](https://github.com/ant-design/ant-design/pull/37790) +- 🐞 Fix issue where numbers were not displayed when Badge set both `color` and `count`. [#37609](https://github.com/ant-design/ant-design/pull/37609) [@kiner-tang](https://github.com/kiner-tang) +- 🐞 Fix Progress zoom behavior in Safari. [#38301](https://github.com/ant-design/ant-design/pull/38301) +- Modal + - 🐞 Fix Modal animation flush issue in React 18. [#38275](https://github.com/ant-design/ant-design/pull/38275) + - 🐞 Fix Modal.method() not focus trigger after close. [#38275](https://github.com/ant-design/ant-design/pull/38275) +- Transfer + - 🐞 Fix Transfer titles is undefined. [#38182](https://github.com/ant-design/ant-design/pull/38182) [@kiner-tang](https://github.com/kiner-tang) + - 🛠 Remove Transfer `defaultprops` spelling. [#38164](https://github.com/ant-design/ant-design/pull/38164) [#38154](https://github.com/ant-design/ant-design/pull/38154) [@li-jia-nan](https://github.com/li-jia-nan) +- 🛠 Refactor Anchor to Function Component, some methods of obtaining `ref` and calling internal instance methods will invalid.. [#38265](https://github.com/ant-design/ant-design/pull/38265) [#37957](https://github.com/ant-design/ant-design/pull/37957) [@li-jia-nan](https://github.com/li-jia-nan) [@RexSkz](https://github.com/RexSkz) +- 🛠 The layout of the Dropdown.Button component is implemented using Space.Compact instead. [#38090](https://github.com/ant-design/ant-design/pull/38090) [@foryuki](https://github.com/foryuki) +- 🛠 Optimize the internal implementation of DirectoryTree Typography component. [#38184](https://github.com/ant-design/ant-design/pull/38184) [#38181](https://github.com/ant-design/ant-design/pull/38181) [@holazz](https://github.com/holazz) [#37716](https://github.com/ant-design/ant-design/pull/37716) [@zheeeng](https://github.com/zheeeng) +- 💄 Fix TextArea custom border style not working when allowClear is enable. [#38101](https://github.com/ant-design/ant-design/pull/38101) +- 💄 Fix Popconfirm style issue when icon={null}, now an additional span tag will be wrapped around the icon element. [#37384](https://github.com/ant-design/ant-design/pull/37384) [@edc-hui](https://github.com/edc-hui) +- 💄 Fix Menu highlight style in compact mode. [#38223](https://github.com/ant-design/ant-design/pull/38223) [@messaooudi](https://github.com/messaooudi) +- Carousel + - 💄 Enlarge Carousel dots hover area for better experience. [#38257](https://github.com/ant-design/ant-design/pull/38257) + - 💄 Fix Carousel dots margin style not being reset. [#38100](https://github.com/ant-design/ant-design/pull/38100) +- TypeScript + - 🤖 Mentions additionally exports MentionsRef. [#38028](https://github.com/ant-design/ant-design/pull/38028) [@simonwong](https://github.com/simonwong) +- 🌐 Localization + - Add Transfer `titles` property internationalization configuration. [#38168](https://github.com/ant-design/ant-design/pull/38168) [@kiner-tang](https://github.com/kiner-tang) + - Fix default Empty description text. [#38127](https://github.com/ant-design/ant-design/pull/38127) [@HelloBojack](https://github.com/HelloBojack) + - 🇮🇹 Add missing `it_IT` locale. [#38108](https://github.com/ant-design/ant-design/pull/38108) [@ernestfolch](https://github.com/ernestfolch) + - 🇫🇷 Add missing `fr_FR` locale. [#38072](https://github.com/ant-design/ant-design/pull/38072) [@smwhr](https://github.com/smwhr) + ## 4.23.6 `2022-10-17` diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 4066f6f2de..13eb8edb46 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -15,6 +15,64 @@ timeline: true --- +## 4.24.1 + +`2022-11-04` + +- 🐞 回滚 [#38160](https://github.com/ant-design/ant-design/pull/38160) 以修复 Table 的 `column.title` 渲染不正确的问题。[#38383](https://github.com/ant-design/ant-design/pull/38383) +- 💄 Alert 增加 `@alert-padding-vertical` 和 `@alert-padding-horizontal` 变量。[#38369](https://github.com/ant-design/ant-design/pull/38369) [@imoctopus](https://github.com/imoctopus) +- 🐞 修复 Steps 中 用户配置的 Step `status` 优先级被覆盖的问题。[#38319](https://github.com/ant-design/ant-design/pull/38319) [@heiyu4585](https://github.com/heiyu4585) +- 🐞 修复 Popconfirm 图标颜色会被污染的问题。[#38355](https://github.com/ant-design/ant-design/pull/38355) +- 🐞 修复 Anchor 组件圆点样式丢失的问题。[#38338](https://github.com/ant-design/ant-design/pull/38338) [@li-jia-nan](https://github.com/li-jia-nan) +- 🐞 修复 DatePicker `popupClassName` 定义丢失的问题。[#38325](https://github.com/ant-design/ant-design/pull/38325) [@Cedong.Lee](https://github.com/Cedong.Lee) + +## 4.24.0 + +`2022-11-01` + +- 🔥 新增组件 Space.Compact 用以替代 Input.Group 和 Button.Group 组件。[#37652](https://github.com/ant-design/ant-design/pull/37652) [@foryuki](https://github.com/foryuki) +- 🆕 Form 内组件上的 `disabled` 属性现在将优先于 Form 的 `disabled` 属性。[#37628](https://github.com/ant-design/ant-design/pull/37628) [@kiner-tang](https://github.com/kiner-tang) +- 🆕 Typograph 增加 `text` 配置,支持同时开启省略与编辑模式时的使用。[#37761](https://github.com/ant-design/ant-design/pull/37761) [@zheeeng](https://github.com/zheeeng) +- 🆕 Row 组件的 `align` 和 `justify` 属性支持设置响应式的值。[#37860](https://github.com/ant-design/ant-design/pull/37860) [@kiner-tang](https://github.com/kiner-tang) +- 🆕 Image 增加 `preview.scaleStep` 属性调整放大缩小的幅度,并将默认的 `scaleOffset` 设置为 0.5。[#37340](https://github.com/ant-design/ant-design/pull/37340) [@coldice945](https://github.com/coldice945) +- 🆕 Steps 新增支持 `items`。[#37531](https://github.com/ant-design/ant-design/pull/37531) [@heiyu4585](https://github.com/heiyu4585) +- 🆕 Collapse 新增 `collapsible="icon"` 从而支持点击图标展开收起。[#37566](https://github.com/ant-design/ant-design/pull/37566) [@Sheepeer](https://github.com/Sheepeer) +- 🆕 Input.Password 支持 `visibilityToggle={{ visible, onVisibleChange }}` 从而可以手动控制密码显隐。[#38216](https://github.com/ant-design/ant-design/pull/38216) [@MrHeer](https://github.com/MrHeer) +- 🆕 Breadcrumb 新增 `menu` 属性。[#37885](https://github.com/ant-design/ant-design/pull/37885) [@JarvisArt](https://github.com/JarvisArt) +- 🆕 Dropdown 新增 `menu` `dropdownRender` 属性,并废弃了 `overlay` 属性。[#37885](https://github.com/ant-design/ant-design/pull/37885) [@JarvisArt](https://github.com/JarvisArt) +- Table + - 🆕 Table `filterDropdown` 新增一个 `close` 参数对象用于关闭筛选菜单。[#37745](https://github.com/ant-design/ant-design/pull/37745) [@kiner-tang](https://github.com/kiner-tang) + - 🐞 修复 Table 组件 `aria-label` 出现 `[object Object]` 的问题。[#38160](https://github.com/ant-design/ant-design/pull/38160) [@kiner-tang](https://github.com/kiner-tang) +- 🐞 修复 Tabs 组件没有读取 ConfigProvider 的 `getPopupContainer` 属性的问题。[#38238](https://github.com/ant-design/ant-design/pull/38238) [@ZH-seven](https://github.com/ZH-seven) +- 🐞 修复一个在 Menu.Item `disabled` 内使用 Tooltip 不生效的问题。[#38273](https://github.com/ant-design/ant-design/pull/38273) +- 🐞 修复 Tooltip 在 `placement` 值为 `topRight` 或 `bottomLeft` 时动画原点计算错误的问题。[#38159](https://github.com/ant-design/ant-design/pull/38159) [@strear](https://github.com/strear) +- 🐞 TimePicker 移除使用了 `popupClassName` 冗余警告。[#38190](https://github.com/ant-design/ant-design/pull/38190) [@kiner-tang](https://github.com/kiner-tang) +- 🐞 修复嵌套 Drawer 在默认都设置 `open` 时,添加至 document 顺序出错的问题。[#37767](https://github.com/ant-design/ant-design/pull/37767) [#37790](https://github.com/ant-design/ant-design/pull/37790) +- 🐞 修复 Badge 同时设置 `color` 和 `count` 时,数字不展示的问题。[#37609](https://github.com/ant-design/ant-design/pull/37609) [@kiner-tang](https://github.com/kiner-tang) +- 🐞 修复 Progress 在 Safari 下缩放异常的问题。[#38301](https://github.com/ant-design/ant-design/pull/38301) +- Modal + - 🐞 修复在 React 18 下 Modal 动画闪烁的问题。[#38275](https://github.com/ant-design/ant-design/pull/38275) + - 🐞 修复 Modal.method() 关闭时默认没有聚焦触发元素的问题。[#38275](https://github.com/ant-design/ant-design/pull/38275) +- Transfer + - 🐞 修复 Transfer `titles` 为空时报错的问题。[#38182](https://github.com/ant-design/ant-design/pull/38182) [@kiner-tang](https://github.com/kiner-tang) + - 🛠 移除 Transfer `defaultprops` 写法。[#38164](https://github.com/ant-design/ant-design/pull/38164) [#38154](https://github.com/ant-design/ant-design/pull/38154) [@li-jia-nan](https://github.com/li-jia-nan) +- 🛠 重构 Anchor 为 Function Component,之前一些获取 `ref` 并调用内部实例方法的写法都会失效。[#38265](https://github.com/ant-design/ant-design/pull/38265) [#37957](https://github.com/ant-design/ant-design/pull/37957) [@li-jia-nan](https://github.com/li-jia-nan) [@RexSkz](https://github.com/RexSkz) +- 🛠 Dropdown.Button 改用 Space.Compact 实现。[#38090](https://github.com/ant-design/ant-design/pull/38090) [@foryuki](https://github.com/foryuki) +- 🛠 优化 DirectoryTree Typography 组件的内部实现。[#38184](https://github.com/ant-design/ant-design/pull/38184) [#38181](https://github.com/ant-design/ant-design/pull/38181) [@holazz](https://github.com/holazz) [#37716](https://github.com/ant-design/ant-design/pull/37716) [@zheeeng](https://github.com/zheeeng) +- 💄 修复 TextArea 开启 `allowClear` 时自定义 border 样式无法生效的问题。[#38101](https://github.com/ant-design/ant-design/pull/38101) +- 💄 修复 Popconfirm 设置 `icon={null}` 的时 `title` padding 仍然存在的问题,现在 icon 元素外会额外包裹一个 span 标签。[#37384](https://github.com/ant-design/ant-design/pull/37384) [@edc-hui](https://github.com/edc-hui) +- 💄 修复 Menu 在紧凑模式下的高亮条样式。[#38223](https://github.com/ant-design/ant-design/pull/38223) [@messaooudi](https://github.com/messaooudi) +- Carousel + - 💄 扩大 Carousel 切换点的鼠标响应范围,优化切换体验。[#38257](https://github.com/ant-design/ant-design/pull/38257) + - 💄 修复 Carousel `dots` 样式未被正确 reset 的问题。[#38100](https://github.com/ant-design/ant-design/pull/38100) +- TypeScript + - 🤖 Mentions 额外导出 MentionsRef。[#38028](https://github.com/ant-design/ant-design/pull/38028) [@simonwong](https://github.com/simonwong) +- 🌐 国际化 + - 添加 Transfer `titles` 属性国际化配置。[#38168](https://github.com/ant-design/ant-design/pull/38168) [@kiner-tang](https://github.com/kiner-tang) + - 修正默认 Empty 描述文案。[#38127](https://github.com/ant-design/ant-design/pull/38127) [@HelloBojack](https://github.com/HelloBojack) + - 🇮🇹 补全 `it_IT` 文案。[#38108](https://github.com/ant-design/ant-design/pull/38108) [@ernestfolch](https://github.com/ernestfolch) + - 🇫🇷 补全 `fr_FR` 文案。[#38072](https://github.com/ant-design/ant-design/pull/38072) [@smwhr](https://github.com/smwhr) + ## 4.23.6 `2022-10-17` diff --git a/components/__tests__/__snapshots__/index.test.ts.snap b/components/__tests__/__snapshots__/index.test.ts.snap index 6f3eff4e71..740ef46f1f 100644 --- a/components/__tests__/__snapshots__/index.test.ts.snap +++ b/components/__tests__/__snapshots__/index.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`antd exports modules correctly 1`] = ` -Array [ +[ "Affix", "Alert", "Anchor", diff --git a/components/_util/ActionButton.tsx b/components/_util/ActionButton.tsx index adc57edf9c..433cd68883 100644 --- a/components/_util/ActionButton.tsx +++ b/components/_util/ActionButton.tsx @@ -22,7 +22,7 @@ function isThenable(thing?: PromiseLike): boolean { const ActionButton: React.FC = props => { const clickedRef = React.useRef(false); - const ref = React.useRef(); + const ref = React.useRef(null); const [loading, setLoading] = useState(false); const { close } = props; const onInternalClose = (...args: any[]) => { @@ -30,10 +30,11 @@ const ActionButton: React.FC = props => { }; React.useEffect(() => { - let timeoutId: any; + let timeoutId: NodeJS.Timer | null = null; if (props.autoFocus) { - const $this = ref.current as HTMLInputElement; - timeoutId = setTimeout(() => $this.focus()); + timeoutId = setTimeout(() => { + ref.current?.focus(); + }); } return () => { if (timeoutId) { diff --git a/components/_util/wave/index.tsx b/components/_util/wave/index.tsx index fc4ffeba20..5e03290e43 100644 --- a/components/_util/wave/index.tsx +++ b/components/_util/wave/index.tsx @@ -40,8 +40,10 @@ function isNotGrey(color: string) { function isValidWaveColor(color: string) { return ( color && + color !== '#fff' && color !== '#ffffff' && color !== 'rgb(255, 255, 255)' && + color !== 'rgba(255, 255, 255, 1)' && isNotGrey(color) && !/rgba\((?:\d*, ){3}0\)/.test(color) && // any transparent rgba color color !== 'transparent' diff --git a/components/affix/__tests__/Affix.test.tsx b/components/affix/__tests__/Affix.test.tsx index 0315eec15a..c60c1244fa 100644 --- a/components/affix/__tests__/Affix.test.tsx +++ b/components/affix/__tests__/Affix.test.tsx @@ -15,6 +15,7 @@ class AffixMounter extends React.Component<{ onTestUpdatePosition?(): void; onChange?: () => void; getInstance?: (inst: InternalAffixClass) => void; + style?: React.CSSProperties; }> { private container: HTMLDivElement; @@ -201,6 +202,7 @@ describe('Affix Render', () => { expect(getObserverEntities()).toHaveLength(1); expect(getObserverEntities()[0].target).toBe(window); }); + it('check position change before measure', async () => { const { container } = render( <> @@ -216,6 +218,35 @@ describe('Affix Render', () => { await movePlaceholder(1000); expect(container.querySelector('.ant-affix')).toBeTruthy(); }); + + it('do not measure when hidden', async () => { + let affixInstance: InternalAffixClass | null = null; + + const { rerender } = render( + { + affixInstance = inst; + }} + offsetBottom={0} + />, + ); + await waitFakeTimer(); + const firstAffixStyle = affixInstance!.state.affixStyle; + + rerender( + { + affixInstance = inst; + }} + offsetBottom={0} + style={{ display: 'none' }} + />, + ); + await waitFakeTimer(); + const secondAffixStyle = affixInstance!.state.affixStyle; + + expect(firstAffixStyle).toEqual(secondAffixStyle); + }); }); describe('updatePosition when size changed', () => { diff --git a/components/affix/index.tsx b/components/affix/index.tsx index b16995a2e4..df871dfd84 100644 --- a/components/affix/index.tsx +++ b/components/affix/index.tsx @@ -171,6 +171,15 @@ class Affix extends React.Component { const fixedTop = getFixedTop(placeholderReact, targetRect, offsetTop); const fixedBottom = getFixedBottom(placeholderReact, targetRect, offsetBottom); + if ( + placeholderReact.top === 0 && + placeholderReact.left === 0 && + placeholderReact.width === 0 && + placeholderReact.height === 0 + ) { + return; + } + if (fixedTop !== undefined) { newState.affixStyle = { position: 'fixed', diff --git a/components/alert/ErrorBoundary.tsx b/components/alert/ErrorBoundary.tsx index b3b42da520..a5e2e7eec9 100644 --- a/components/alert/ErrorBoundary.tsx +++ b/components/alert/ErrorBoundary.tsx @@ -7,15 +7,14 @@ interface ErrorBoundaryProps { children?: React.ReactNode; } -export default class ErrorBoundary extends React.Component< - ErrorBoundaryProps, - { - error?: Error | null; - info: { - componentStack?: string; - }; - } -> { +interface ErrorBoundaryStates { + error?: Error | null; + info?: { + componentStack?: string; + }; +} + +class ErrorBoundary extends React.Component { state = { error: undefined, info: { @@ -41,3 +40,5 @@ export default class ErrorBoundary extends React.Component< return children; } } + +export default ErrorBoundary; diff --git a/components/anchor/Anchor.tsx b/components/anchor/Anchor.tsx index da322cbcc1..2dd362a736 100644 --- a/components/anchor/Anchor.tsx +++ b/components/anchor/Anchor.tsx @@ -1,5 +1,4 @@ import classNames from 'classnames'; -import memoizeOne from 'memoize-one'; import addEventListener from 'rc-util/lib/Dom/addEventListener'; import * as React from 'react'; import Affix from '../affix'; @@ -91,87 +90,69 @@ export interface AntAnchor { ) => void; } -class Anchor extends React.Component { - static contextType = ConfigContext; +const AnchorContent: React.FC = props => { + const { + rootClassName, + anchorPrefixCls: prefixCls, + className = '', + style, + offsetTop, + affix = true, + showInkInFixed = false, + children, + bounds, + targetOffset, + onClick, + onChange, + getContainer, + getCurrentAnchor, + } = props; - state = { - activeLink: null, - }; + const [links, setLinks] = React.useState([]); + const [activeLink, setActiveLink] = React.useState(null); + const activeLinkRef = React.useRef(activeLink); - context: ConfigConsumerProps; + const wrapperRef = React.useRef(null); + const spanLinkNode = React.useRef(null); + const animating = React.useRef(false); - private wrapperRef = React.createRef(); + const { direction, getTargetContainer } = React.useContext(ConfigContext); - private inkNode: HTMLSpanElement; + const getCurrentContainer = getContainer ?? getTargetContainer ?? getDefaultContainer; - // scroll scope's container - private scrollContainer: HTMLElement | Window; + const dependencyListItem: React.DependencyList[number] = JSON.stringify(links); - private links: string[] = []; - - private scrollEvent: ReturnType; - - private animating: boolean; - - private prefixCls?: string; - - // Context - registerLink: AntAnchor['registerLink'] = link => { - if (!this.links.includes(link)) { - this.links.push(link); - } - }; - - unregisterLink: AntAnchor['unregisterLink'] = link => { - const index = this.links.indexOf(link); - if (index !== -1) { - this.links.splice(index, 1); - } - }; - - getContainer = () => { - const { getTargetContainer } = this.context; - const { getContainer } = this.props; - - const getFunc = getContainer ?? getTargetContainer ?? getDefaultContainer; - - return getFunc(); - }; - - componentDidMount() { - this.scrollContainer = this.getContainer(); - this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll); - this.handleScroll(); - } - - componentDidUpdate() { - const { getCurrentAnchor } = this.props; - const { activeLink } = this.state; - if (this.scrollEvent) { - const currentContainer = this.getContainer(); - if (this.scrollContainer !== currentContainer) { - this.scrollContainer = currentContainer; - this.scrollEvent.remove(); - this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll); - this.handleScroll(); + const registerLink = React.useCallback( + link => { + if (!links.includes(link)) { + setLinks(prev => [...prev, link]); } - } - if (typeof getCurrentAnchor === 'function') { - this.setCurrentActiveLink(getCurrentAnchor(activeLink || ''), false); - } - this.updateInk(); - } + }, + [dependencyListItem], + ); - componentWillUnmount() { - if (this.scrollEvent) { - this.scrollEvent.remove(); - } - } + const unregisterLink = React.useCallback( + link => { + if (links.includes(link)) { + setLinks(prev => prev.filter(i => i !== link)); + } + }, + [dependencyListItem], + ); - getCurrentAnchor(offsetTop = 0, bounds = 5): string { - const linkSections: Array
= []; - const container = this.getContainer(); - this.links.forEach(link => { + const updateInk = () => { + const linkNode = wrapperRef.current?.querySelector( + `.${prefixCls}-link-title-active`, + ); + if (linkNode && spanLinkNode.current) { + spanLinkNode.current.style.top = `${linkNode.offsetTop + linkNode.clientHeight / 2 - 4.5}px`; + } + }; + + const getInternalCurrentAnchor = (_links: string[], _offsetTop = 0, _bounds = 5): string => { + const linkSections: Section[] = []; + const container = getCurrentContainer(); + _links.forEach(link => { const sharpLinkMatch = sharpMatcherRegx.exec(link?.toString()); if (!sharpLinkMatch) { return; @@ -179,7 +160,7 @@ class Anchor extends React.Component { - const { offsetTop, targetOffset } = this.props; - - this.setCurrentActiveLink(link); - const container = this.getContainer(); - const scrollTop = getScroll(container, true); - const sharpLinkMatch = sharpMatcherRegx.exec(link); - if (!sharpLinkMatch) { - return; - } - const targetElement = document.getElementById(sharpLinkMatch[1]); - if (!targetElement) { - return; - } - - const eleOffsetTop = getOffsetTop(targetElement, container); - let y = scrollTop + eleOffsetTop; - y -= targetOffset !== undefined ? targetOffset : offsetTop || 0; - this.animating = true; - - scrollTo(y, { - callback: () => { - this.animating = false; - }, - getContainer: this.getContainer, - }); }; - saveInkNode = (node: HTMLSpanElement) => { - this.inkNode = node; - }; - - setCurrentActiveLink = (link: string, triggerChange = true) => { - const { activeLink } = this.state; - const { onChange, getCurrentAnchor } = this.props; - if (activeLink === link) { + const setCurrentActiveLink = (link: string) => { + if (activeLinkRef.current === link) { return; } + // https://github.com/ant-design/ant-design/issues/30584 - this.setState({ - activeLink: typeof getCurrentAnchor === 'function' ? getCurrentAnchor(link) : link, - }); - if (triggerChange) { - onChange?.(link); - } + const newLink = typeof getCurrentAnchor === 'function' ? getCurrentAnchor(link) : link; + setActiveLink(newLink); + activeLinkRef.current = newLink; + + // onChange should respect the original link (which may caused by + // window scroll or user click), not the new link + onChange?.(link); }; - handleScroll = () => { - if (this.animating) { + const handleScroll = React.useCallback(() => { + if (animating.current) { return; } - const { offsetTop, bounds, targetOffset } = this.props; - const currentActiveLink = this.getCurrentAnchor( + if (typeof getCurrentAnchor === 'function') { + return; + } + const currentActiveLink = getInternalCurrentAnchor( + links, targetOffset !== undefined ? targetOffset : offsetTop || 0, bounds, ); - this.setCurrentActiveLink(currentActiveLink); - }; + setCurrentActiveLink(currentActiveLink); + }, [dependencyListItem, targetOffset, offsetTop]); - updateInk = () => { - const { prefixCls, wrapperRef } = this; - const anchorNode = wrapperRef.current; - const linkNode = anchorNode?.querySelector(`.${prefixCls}-link-title-active`); - if (linkNode) { - this.inkNode.style.top = `${linkNode.offsetTop + linkNode.clientHeight / 2 - 4.5}px`; - } - }; + const handleScrollTo = React.useCallback<(link: string) => void>( + link => { + setCurrentActiveLink(link); + const container = getCurrentContainer(); + const scrollTop = getScroll(container, true); + const sharpLinkMatch = sharpMatcherRegx.exec(link); + if (!sharpLinkMatch) { + return; + } + const targetElement = document.getElementById(sharpLinkMatch[1]); + if (!targetElement) { + return; + } - getMemoizedContextValue = memoizeOne( - (link: AntAnchor['activeLink'], onClickFn: AnchorProps['onClick']): AntAnchor => ({ - registerLink: this.registerLink, - unregisterLink: this.unregisterLink, - scrollTo: this.handleScrollTo, - activeLink: link, - onClick: onClickFn, - }), + const eleOffsetTop = getOffsetTop(targetElement, container); + let y = scrollTop + eleOffsetTop; + y -= targetOffset !== undefined ? targetOffset : offsetTop || 0; + animating.current = true; + scrollTo(y, { + getContainer: getCurrentContainer, + callback() { + animating.current = false; + }, + }); + }, + [targetOffset, offsetTop], ); - render() { - const { direction } = this.context; - const { - anchorPrefixCls: prefixCls, - className = '', - style, - offsetTop, - affix = true, - showInkInFixed = false, - children, - onClick, - rootClassName, - } = this.props; - const { activeLink } = this.state; + const inkClass = classNames( + { + [`${prefixCls}-ink-ball-visible`]: activeLink, + }, + `${prefixCls}-ink-ball`, + ); - // To support old version react. - // Have to add prefixCls on the instance. - // https://github.com/facebook/react/issues/12397 - this.prefixCls = prefixCls; + const wrapperClass = classNames( + rootClassName, + `${prefixCls}-wrapper`, + { + [`${prefixCls}-rtl`]: direction === 'rtl', + }, + className, + ); - const inkClass = classNames(`${prefixCls}-ink-ball`, { - visible: activeLink, - }); + const anchorClass = classNames(prefixCls, { + [`${prefixCls}-fixed`]: !affix && !showInkInFixed, + }); - const wrapperClass = classNames( - rootClassName, - `${prefixCls}-wrapper`, - { - [`${prefixCls}-rtl`]: direction === 'rtl', - }, - className, - ); + const wrapperStyle: React.CSSProperties = { + maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh', + ...style, + }; - const anchorClass = classNames(prefixCls, { - [`${prefixCls}-fixed`]: !affix && !showInkInFixed, - }); - - const wrapperStyle: React.CSSProperties = { - maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh', - ...style, - }; - - const anchorContent = ( -
-
-
- -
- {children} + const anchorContent = ( +
+
+
+
+ {children}
- ); +
+ ); - const contextValue = this.getMemoizedContextValue(activeLink, onClick); + React.useEffect(() => { + const scrollContainer = getCurrentContainer(); + const scrollEvent = addEventListener(scrollContainer, 'scroll', handleScroll); + handleScroll(); + return () => { + scrollEvent?.remove(); + }; + }, [dependencyListItem]); - return ( - - {affix ? ( - - {anchorContent} - - ) : ( - anchorContent - )} - - ); - } -} + React.useEffect(() => { + if (typeof getCurrentAnchor === 'function') { + setCurrentActiveLink(getCurrentAnchor(activeLinkRef.current || '')); + } + }, [getCurrentAnchor]); -// just use in test -export type InternalAnchorClass = Anchor; + React.useEffect(() => { + updateInk(); + }, [getCurrentAnchor, dependencyListItem, activeLink]); -const AnchorFC = React.forwardRef((props, ref) => { + const memoizedContextValue = React.useMemo( + () => ({ + registerLink, + unregisterLink, + scrollTo: handleScrollTo, + activeLink, + onClick, + }), + [activeLink, onClick, handleScrollTo], + ); + + return ( + + {affix ? ( + + {anchorContent} + + ) : ( + anchorContent + )} + + ); +}; + +const Anchor: React.FC = props => { const { prefixCls: customizePrefixCls } = props; - const { getPrefixCls } = React.useContext(ConfigContext); + const { getPrefixCls } = React.useContext(ConfigContext); const anchorPrefixCls = getPrefixCls('anchor', customizePrefixCls); const [wrapSSR, hashId] = useStyle(anchorPrefixCls); - const anchorProps: InternalAnchorProps = { - ...props, + return wrapSSR( + , + ); +}; - anchorPrefixCls, - rootClassName: hashId, - }; - - return wrapSSR(); -}); - -export default AnchorFC; +export default Anchor; diff --git a/components/anchor/AnchorLink.tsx b/components/anchor/AnchorLink.tsx index a43345b2af..11c2c71744 100644 --- a/components/anchor/AnchorLink.tsx +++ b/components/anchor/AnchorLink.tsx @@ -26,7 +26,7 @@ const AnchorLink: React.FC = props => { return () => { unregisterLink?.(href); }; - }, [href]); + }, [href, registerLink, unregisterLink]); const handleClick = (e: React.MouseEvent) => { onClick?.(e, { title, href }); diff --git a/components/anchor/__tests__/Anchor.test.tsx b/components/anchor/__tests__/Anchor.test.tsx index f501f27da0..aa47e56c3c 100644 --- a/components/anchor/__tests__/Anchor.test.tsx +++ b/components/anchor/__tests__/Anchor.test.tsx @@ -1,20 +1,9 @@ import React from 'react'; import Anchor from '..'; import { fireEvent, render, waitFakeTimer } from '../../../tests/utils'; -import type { InternalAnchorClass } from '../Anchor'; const { Link } = Anchor; -function createGetContainer(id: string) { - return () => { - const container = document.getElementById(id); - if (container == null) { - throw new Error(); - } - return container; - }; -} - function createDiv() { const root = document.createElement('div'); document.body.appendChild(root); @@ -57,122 +46,77 @@ describe('Anchor Render', () => { getClientRectsMock.mockRestore(); }); - it('Anchor render perfectly', () => { + it('renders correctly', () => { const hash = getHashUrl(); - let anchorInstance: InternalAnchorClass; const { container } = render( - { - anchorInstance = node as InternalAnchorClass; - }} - > + , ); - - fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); - anchorInstance!.handleScroll(); - expect(anchorInstance!.state).not.toBe(null); + expect(container.querySelector(`a[href="#${hash}"]`)).not.toBe(null); }); - it('Anchor render perfectly for complete href - click', async () => { + it('actives the target when clicking a link', async () => { const hash = getHashUrl(); - let anchorInstance: InternalAnchorClass; const { container } = render( - { - anchorInstance = node as InternalAnchorClass; - }} - > + , ); - fireEvent.click(container.querySelector(`a[href="http://www.example.com/#${hash}"]`)!); + const link = container.querySelector(`a[href="http://www.example.com/#${hash}"]`)!; + fireEvent.click(link); await waitFakeTimer(); - expect(anchorInstance!.state!.activeLink).toBe(`http://www.example.com/#${hash}`); + expect(link.classList).toContain('ant-anchor-link-title-active'); }); - it('Anchor render perfectly for complete href - hash router', async () => { + it('scrolls the page when clicking a link', async () => { const root = createDiv(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); render(
Q1
, { container: root }); - let anchorInstance: InternalAnchorClass; - render( - { - anchorInstance = node as InternalAnchorClass; - }} - > + const { container } = render( + , ); - anchorInstance!.handleScrollTo('/#/faq?locale=en#Q1'); + const link = container.querySelector(`a[href="/#/faq?locale=en#Q1"]`)!; + fireEvent.click(link); await waitFakeTimer(); - expect(anchorInstance!.state.activeLink).toBe('/#/faq?locale=en#Q1'); expect(scrollToSpy).toHaveBeenCalled(); }); - it('Anchor render perfectly for complete href - scroll', async () => { - const hash = getHashUrl(); + it('handleScroll should not be triggered when scrolling caused by clicking a link', async () => { + const hash1 = getHashUrl(); + const hash2 = getHashUrl(); const root = createDiv(); - render(
Hello
, { container: root }); - let anchorInstance: InternalAnchorClass; + const onChange = jest.fn(); render( - { - anchorInstance = node as InternalAnchorClass; - }} - > - +
+
Hello
+
World
+
, + { container: root }, + ); + const { container } = render( + + + , ); - anchorInstance!.handleScroll(); + onChange.mockClear(); + + const link = container.querySelector(`a[href="#${hash2}"]`)!; + // this will trigger 1 onChange + fireEvent.click(link); + // smooth scroll caused by clicking needs time to finish. + // we scroll the window before it finish, the scroll listener should not be triggered, + fireEvent.scroll(window); + await waitFakeTimer(); - expect(anchorInstance!.state!.activeLink).toBe(`http://www.example.com/#${hash}`); + // if the scroll listener is triggered, we will get 2 onChange, now we expect only 1. + expect(onChange).toHaveBeenCalledTimes(1); }); - it('Anchor render perfectly for complete href - scrollTo', async () => { - const hash = getHashUrl(); - const scrollToSpy = jest.spyOn(window, 'scrollTo'); - const root = createDiv(); - render(
Hello
, { container: root }); - let anchorInstance: InternalAnchorClass; - render( - { - anchorInstance = node as InternalAnchorClass; - }} - > - - , - ); - - anchorInstance!.handleScrollTo(`##${hash}`); - await waitFakeTimer(); - expect(anchorInstance!.state.activeLink).toBe(`##${hash}`); - const calls = scrollToSpy.mock.calls.length; - expect(scrollToSpy.mock.calls.length).toBe(calls); - }); - - it('should remove listener when unmount', async () => { - const hash = getHashUrl(); - let anchorInstance: InternalAnchorClass; - const { unmount } = render( - { - anchorInstance = node as InternalAnchorClass; - }} - > - - , - ); - - const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove'); - unmount(); - expect(removeListenerSpy).toHaveBeenCalled(); - }); - - it('should unregister link when unmount children', () => { + it('should update DOM when children are unmounted', () => { const hash = getHashUrl(); const { container, rerender } = render( @@ -187,32 +131,94 @@ describe('Anchor Render', () => { expect(container.querySelector('.ant-anchor-link-title')).toBeFalsy(); }); - it('should update links when link href update', async () => { + it('should update DOM when link href is changed', async () => { const hash = getHashUrl(); - let anchorInstance: InternalAnchorClass; function AnchorUpdate({ href }: { href: string }) { return ( - { - anchorInstance = node as InternalAnchorClass; - }} - > + ); } - const { rerender } = render(); + const { container, rerender } = render(); - if (anchorInstance! == null) { - throw new Error('anchorInstance should not be null'); - } - - expect((anchorInstance as any)!.links).toEqual([`#${hash}`]); + expect(container.querySelector(`a[href="#${hash}"]`)).toBeTruthy(); rerender(); - expect((anchorInstance as any)!.links).toEqual([`#${hash}_1`]); + expect(container.querySelector(`a[href="#${hash}_1"]`)).toBeTruthy(); }); - it('Anchor onClick event', () => { + it('targetOffset prop', async () => { + const hash = getHashUrl(); + + const scrollToSpy = jest.spyOn(window, 'scrollTo'); + const root = createDiv(); + render(

Hello

, { container: root }); + const { container, rerender } = render( + + + , + ); + + const setProps = (props: Record) => + rerender( + + + , + ); + + fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); + await waitFakeTimer(); + expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000); + + setProps({ offsetTop: 100 }); + + fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); + await waitFakeTimer(); + expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900); + + setProps({ targetOffset: 200 }); + + fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); + await waitFakeTimer(); + expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); + }); + + // https://github.com/ant-design/ant-design/issues/31941 + it('targetOffset prop when contain spaces', async () => { + const hash = `${getHashUrl()} s p a c e s`; + + const scrollToSpy = jest.spyOn(window, 'scrollTo'); + const root = createDiv(); + render(

Hello

, { container: root }); + const { container, rerender } = render( + + + , + ); + + const setProps = (props: Record) => + rerender( + + + , + ); + + fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); + await waitFakeTimer(); + expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000); + + setProps({ offsetTop: 100 }); + fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); + await waitFakeTimer(); + expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900); + + setProps({ targetOffset: 200 }); + fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); + await waitFakeTimer(); + expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); + }); + + it('onClick event', () => { const hash = getHashUrl(); let event; let link; @@ -226,297 +232,23 @@ describe('Anchor Render', () => { const href = `#${hash}`; const title = hash; - let anchorInstance: InternalAnchorClass; const { container } = render( - { - anchorInstance = node as InternalAnchorClass; - }} - > + , ); fireEvent.click(container.querySelector(`a[href="${href}"]`)!); - anchorInstance!.handleScroll(); expect(event).not.toBe(undefined); expect(link).toEqual({ href, title }); }); - it('Different function returns the same DOM', async () => { - const hash = getHashUrl(); - const root = createDiv(); - render(
Hello
, { container: root }); - const getContainerA = createGetContainer(hash); - const getContainerB = createGetContainer(hash); - let anchorInstance: InternalAnchorClass; - const { rerender } = render( - { - anchorInstance = node as InternalAnchorClass; - }} - > - - , - ); - - const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove'); - await waitFakeTimer(); - rerender( - - - , - ); - expect(removeListenerSpy).not.toHaveBeenCalled(); - }); - - it('Different function returns different DOM', async () => { - const hash1 = getHashUrl(); - const hash2 = getHashUrl(); - const root = createDiv(); - render( -
-
Hello
-
World
-
, - { container: root }, - ); - const getContainerA = createGetContainer(hash1); - const getContainerB = createGetContainer(hash2); - let anchorInstance: InternalAnchorClass; - const { rerender } = render( - { - anchorInstance = node as InternalAnchorClass; - }} - > - - - , - ); - - const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove'); - expect(removeListenerSpy).not.toHaveBeenCalled(); - await waitFakeTimer(); - rerender( - - - - , - ); - expect(removeListenerSpy).toHaveBeenCalled(); - }); - - it('Same function returns the same DOM', () => { - const hash = getHashUrl(); - const root = createDiv(); - render(
Hello
, { container: root }); - const getContainer = createGetContainer(hash); - let anchorInstance: InternalAnchorClass; - const { container } = render( - { - anchorInstance = node as InternalAnchorClass; - }} - > - - , - ); - - fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); - - anchorInstance!.handleScroll(); - expect(anchorInstance!.state).not.toBe(null); - }); - - it('Same function returns different DOM', async () => { - const hash1 = getHashUrl(); - const hash2 = getHashUrl(); - const root = createDiv(); - render( -
-
Hello
-
World
-
, - { container: root }, - ); - const holdContainer = { - container: document.getElementById(hash1), - }; - const getContainer = () => { - if (holdContainer.container == null) { - throw new Error('container should not be null'); - } - return holdContainer.container; - }; - let anchorInstance: InternalAnchorClass; - const { rerender } = render( - { - anchorInstance = node as InternalAnchorClass; - }} - > - - - , - ); - const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove'); - expect(removeListenerSpy).not.toHaveBeenCalled(); - await waitFakeTimer(); - holdContainer.container = document.getElementById(hash2); - rerender( - - - - , - ); - expect(removeListenerSpy).toHaveBeenCalled(); - }); - - it('Anchor targetOffset prop', async () => { - const hash = getHashUrl(); - let dateNowMock: jest.SpyInstance; - - function dataNowMockFn() { - let start = 0; - - const handler = () => { - start += 1000; - return start; - }; - - return jest.spyOn(Date, 'now').mockImplementation(handler); - } - - dateNowMock = dataNowMockFn(); - - const scrollToSpy = jest.spyOn(window, 'scrollTo'); - const root = createDiv(); - render(

Hello

, { container: root }); - let anchorInstance: InternalAnchorClass; - const { rerender } = render( - { - anchorInstance = node as InternalAnchorClass; - }} - > - - , - ); - - const setProps = (props: Record) => - rerender( - { - anchorInstance = node as InternalAnchorClass; - }} - {...props} - > - - , - ); - - anchorInstance!.handleScrollTo(`#${hash}`); - await waitFakeTimer(); - expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000); - dateNowMock = dataNowMockFn(); - - setProps({ offsetTop: 100 }); - - anchorInstance!.handleScrollTo(`#${hash}`); - await waitFakeTimer(); - expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900); - dateNowMock = dataNowMockFn(); - - setProps({ targetOffset: 200 }); - - anchorInstance!.handleScrollTo(`#${hash}`); - await waitFakeTimer(); - expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); - - dateNowMock.mockRestore(); - }); - - // https://github.com/ant-design/ant-design/issues/31941 - it('Anchor targetOffset prop when contain spaces', async () => { - const hash = `${getHashUrl()} s p a c e s`; - let dateNowMock: jest.SpyInstance; - - function dataNowMockFn() { - let start = 0; - - const handler = () => { - start += 1000; - return start; - }; - - return jest.spyOn(Date, 'now').mockImplementation(handler); - } - - dateNowMock = dataNowMockFn(); - - const scrollToSpy = jest.spyOn(window, 'scrollTo'); - const root = createDiv(); - render(

Hello

, { container: root }); - let anchorInstance: InternalAnchorClass; - const { rerender } = render( - { - anchorInstance = node as InternalAnchorClass; - }} - > - - , - ); - - const setProps = (props: Record) => - rerender( - { - anchorInstance = node as InternalAnchorClass; - }} - {...props} - > - - , - ); - - anchorInstance!.handleScrollTo(`#${hash}`); - await waitFakeTimer(); - expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000); - dateNowMock = dataNowMockFn(); - - setProps({ offsetTop: 100 }); - anchorInstance!.handleScrollTo(`#${hash}`); - await waitFakeTimer(); - expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900); - dateNowMock = dataNowMockFn(); - - setProps({ targetOffset: 200 }); - anchorInstance!.handleScrollTo(`#${hash}`); - await waitFakeTimer(); - expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); - - dateNowMock.mockRestore(); - }); - - it('Anchor onChange prop', async () => { + it('onChange event', () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const onChange = jest.fn(); - let anchorInstance: InternalAnchorClass; - render( - { - anchorInstance = node as InternalAnchorClass; - }} - > + const { container } = render( + , @@ -526,89 +258,57 @@ describe('Anchor Render', () => { ); expect(onChange).toHaveBeenCalledTimes(1); - anchorInstance!.handleScrollTo(hash2); + fireEvent.click(container.querySelector(`a[href="#${hash2}"]`)!); expect(onChange).toHaveBeenCalledTimes(2); - expect(onChange).toHaveBeenCalledWith(hash2); + expect(onChange).toHaveBeenLastCalledWith(`#${hash2}`); }); - it('invalid hash', async () => { - let anchorInstance: InternalAnchorClass; + it('handles invalid hash correctly', () => { const { container } = render( - { - anchorInstance = node as InternalAnchorClass; - }} - > + , ); - fireEvent.click(container.querySelector(`a[href="notexsited"]`)!); - - anchorInstance!.handleScrollTo('notexsited'); - expect(anchorInstance!.state).not.toBe(null); + const link = container.querySelector(`a[href="notexsited"]`)!; + fireEvent.click(link); + expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe('title'); }); it('test edge case when getBoundingClientRect return zero size', async () => { getBoundingClientRectMock.mockReturnValue({ width: 0, height: 0, top: 1000 } as DOMRect); const hash = getHashUrl(); - let dateNowMock: jest.SpyInstance; - - function dataNowMockFn() { - let start = 0; - - const handler = () => { - start += 1000; - return start; - }; - - return jest.spyOn(Date, 'now').mockImplementation(handler); - } - - dateNowMock = dataNowMockFn(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); const root = createDiv(); render(

Hello

, { container: root }); - let anchorInstance: InternalAnchorClass; - const { rerender } = render( - { - anchorInstance = node as InternalAnchorClass; - }} - > + const { container, rerender } = render( + , ); const setProps = (props: Record) => rerender( - { - anchorInstance = node as InternalAnchorClass; - }} - {...props} - > + , ); - anchorInstance!.handleScrollTo(`#${hash}`); + + fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000); - dateNowMock = dataNowMockFn(); setProps({ offsetTop: 100 }); - anchorInstance!.handleScrollTo(`#${hash}`); + fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900); - dateNowMock = dataNowMockFn(); setProps({ targetOffset: 200 }); - anchorInstance!.handleScrollTo(`#${hash}`); + fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); - dateNowMock.mockRestore(); getBoundingClientRectMock.mockReturnValue({ width: 100, height: 100, @@ -618,102 +318,61 @@ describe('Anchor Render', () => { it('test edge case when container is not windows', async () => { const hash = getHashUrl(); - let dateNowMock: jest.SpyInstance; - - function dataNowMockFn() { - let start = 0; - - const handler = () => { - start += 1000; - return start; - }; - - return jest.spyOn(Date, 'now').mockImplementation(handler); - } - - dateNowMock = dataNowMockFn(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); const root = createDiv(); render(

Hello

, { container: root }); - let anchorInstance: InternalAnchorClass; - const { rerender } = render( - document.body} - ref={node => { - anchorInstance = node as InternalAnchorClass; - }} - > + const { container, rerender } = render( + document.body}> , ); const setProps = (props: Record) => rerender( - document.body} - ref={node => { - anchorInstance = node as InternalAnchorClass; - }} - {...props} - > + document.body} {...props}> , ); - anchorInstance!.handleScrollTo(`#${hash}`); + + fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); - dateNowMock = dataNowMockFn(); setProps({ offsetTop: 100 }); - anchorInstance!.handleScrollTo(`#${hash}`); + fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); - dateNowMock = dataNowMockFn(); + setProps({ targetOffset: 200 }); - anchorInstance!.handleScrollTo(`#${hash}`); + fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); - - dateNowMock.mockRestore(); }); describe('getCurrentAnchor', () => { - it('Anchor getCurrentAnchor prop', () => { + it('getCurrentAnchor prop', () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const getCurrentAnchor = () => `#${hash2}`; - let anchorInstance: InternalAnchorClass; - render( - { - anchorInstance = node as InternalAnchorClass; - }} - > + const { container } = render( + , ); - expect(anchorInstance!.state.activeLink).toBe(`#${hash2}`); + expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe(hash2); }); // https://github.com/ant-design/ant-design/issues/30584 - it('should trigger onChange when have getCurrentAnchor', async () => { + it('should trigger onChange when have getCurrentAnchor', () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const onChange = jest.fn(); - let anchorInstance: InternalAnchorClass; - render( - hash1} - ref={node => { - anchorInstance = node as InternalAnchorClass; - }} - > + const { container } = render( + hash1}> , @@ -723,13 +382,13 @@ describe('Anchor Render', () => { ); expect(onChange).toHaveBeenCalledTimes(1); - anchorInstance!.handleScrollTo(hash2); + fireEvent.click(container.querySelector(`a[href="#${hash2}"]`)!); expect(onChange).toHaveBeenCalledTimes(2); - expect(onChange).toHaveBeenCalledWith(hash2); + expect(onChange).toHaveBeenLastCalledWith(`#${hash2}`); }); // https://github.com/ant-design/ant-design/issues/34784 - it('getCurrentAnchor have default link as argument', async () => { + it('getCurrentAnchor have default link as argument', () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const getCurrentAnchor = jest.fn(); @@ -747,7 +406,7 @@ describe('Anchor Render', () => { }); // https://github.com/ant-design/ant-design/issues/37627 - it('should update anchorLink when component is rerender', async () => { + it('should update active link when getCurrentAnchor changes', async () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const Demo: React.FC<{ current: string }> = ({ current }) => ( @@ -761,7 +420,8 @@ describe('Anchor Render', () => { rerender(); expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe(hash2); }); - it('should correct render when href is null', () => { + + it('should render correctly when href is null', () => { expect(() => { render( diff --git a/components/anchor/__tests__/cached-context.test.tsx b/components/anchor/__tests__/cached-context.test.tsx index db9bf8fd70..8db4f3e27c 100644 --- a/components/anchor/__tests__/cached-context.test.tsx +++ b/components/anchor/__tests__/cached-context.test.tsx @@ -1,53 +1,40 @@ -import React, { memo, useContext, useRef, useState } from 'react'; -import { fireEvent, getNodeText, render } from '../../../tests/utils'; +import React, { memo, useContext } from 'react'; +import { fireEvent, pureRender } from '../../../tests/utils'; import Anchor from '../Anchor'; import AnchorContext from '../context'; -// we use'memo' here in order to only render inner component while context changed. -const CacheInner = memo(() => { - const countRef = useRef(0); - countRef.current++; - // subscribe anchor context - useContext(AnchorContext); - return ( -
- Child Rendering Count: {countRef.current} -
- ); -}); +let innerCount = 0; +let outerCount = 0; -const CacheOuter = () => { - // We use 'useState' here in order to trigger parent component rendering. - const [count, setCount] = useState(1); - const handleClick = () => { - setCount(count + 1); - }; - // During each rendering phase, the cached context value returned from method 'Anchor#getMemoizedContextValue' will take effect. - // So 'CacheInner' component won't rerender. - return ( -
- - Parent Rendering Count: {count} - - - -
- ); +const handleClick = () => { + outerCount++; }; -it("Rendering on Anchor without changed AnchorContext won't trigger rendering on child component.", () => { - const { container } = render(); - const childCount = getNodeText(container.querySelector('#child_count')!); - - fireEvent.click(container.querySelector('#parent_btn')!); - - expect(getNodeText(container.querySelector('#parent_count')!)).toBe('2'); - // child component won't rerender - expect(getNodeText(container.querySelector('#child_count')!)).toBe(childCount); - fireEvent.click(container.querySelector('#parent_btn')!); - expect(getNodeText(container.querySelector('#parent_count')!)).toBe('3'); - // child component won't rerender - expect(getNodeText(container.querySelector('#child_count')!)).toBe(childCount); +// we use'memo' here in order to only render inner component while context changed. +const CacheInner: React.FC = memo(() => { + innerCount++; + // subscribe locale context + useContext(AnchorContext); + return null; +}); + +const CacheOuter: React.FC = memo(() => ( + <> + + + + + +)); + +it("Rendering on Anchor without changed won't trigger rendering on child component.", () => { + const { container, unmount } = pureRender(); + expect(outerCount).toBe(0); + expect(innerCount).toBe(2); + fireEvent.click(container.querySelector('#parent_btn')!); + expect(outerCount).toBe(1); + expect(innerCount).toBe(2); + unmount(); }); diff --git a/components/anchor/index.en-US.md b/components/anchor/index.en-US.md index 5a47de33f2..a90ae289eb 100644 --- a/components/anchor/index.en-US.md +++ b/components/anchor/index.en-US.md @@ -12,6 +12,10 @@ Hyperlinks to scroll on one page. For displaying anchor hyperlinks on page and jumping between them. +> Notes for developers +> +> After version `4.24.0`, we rewrite Anchor use FC, Some methods of obtaining `ref` and calling internal instance methods will invalid. + ## API ### Anchor Props diff --git a/components/anchor/index.zh-CN.md b/components/anchor/index.zh-CN.md index fae28c613d..d07c39381d 100644 --- a/components/anchor/index.zh-CN.md +++ b/components/anchor/index.zh-CN.md @@ -13,6 +13,10 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_1-C1JwsC/Anchor.svg 需要展现当前页面上可供跳转的锚点链接,以及快速在锚点之间跳转。 +> 开发者注意事项: +> +> 自 `4.24.0` 起,由于组件从 class 重构成 FC,之前一些获取 `ref` 并调用内部实例方法的写法都会失效 + ## API ### Anchor Props diff --git a/components/anchor/style/index.tsx b/components/anchor/style/index.tsx index 1cc5c48360..67467ff0fd 100644 --- a/components/anchor/style/index.tsx +++ b/components/anchor/style/index.tsx @@ -65,7 +65,7 @@ const genSharedAnchorStyle: GenerateStyle = (token): CSSObject => { transform: 'translateX(-50%)', transition: `top ${token.motionDurationSlow} ease-in-out`, - '&.visible': { + [`&.${componentCls}-anchor-ink-ball-visible`]: { display: 'inline-block', }, }, diff --git a/components/auto-complete/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/auto-complete/__tests__/__snapshots__/demo-extend.test.ts.snap index dd536e77f8..3cc0cfc505 100644 --- a/components/auto-complete/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/auto-complete/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -701,7 +701,7 @@ exports[`renders ./components/auto-complete/demo/form-debug.md extend context co
- No Data + No data
@@ -849,7 +849,7 @@ exports[`renders ./components/auto-complete/demo/form-debug.md extend context co
- No Data + No data
@@ -1137,7 +1137,7 @@ exports[`renders ./components/auto-complete/demo/form-debug.md extend context co
- No Data + No data
@@ -1354,7 +1354,7 @@ exports[`renders ./components/auto-complete/demo/form-debug.md extend context co
- No Data + No data
@@ -1580,7 +1580,7 @@ exports[`renders ./components/auto-complete/demo/form-debug.md extend context co
- No Data + No data
diff --git a/components/back-top/index.tsx b/components/back-top/index.tsx index 060a685d75..b67aca5ad7 100644 --- a/components/back-top/index.tsx +++ b/components/back-top/index.tsx @@ -58,7 +58,7 @@ const BackTop: React.FC = props => { }); const ref = React.createRef(); - const scrollEvent = React.useRef(); + const scrollEvent = React.useRef>(null); const getDefaultTarget = () => ref.current && ref.current.ownerDocument ? ref.current.ownerDocument : window; diff --git a/components/badge/index.tsx b/components/badge/index.tsx index 8601d3a243..a62029e6a0 100644 --- a/components/badge/index.tsx +++ b/components/badge/index.tsx @@ -69,7 +69,7 @@ const Badge: CompoundedComponent = ({ const isZero = numberedDisplayCount === '0' || numberedDisplayCount === 0; - const ignoreCount = count === null || (count !== null && isZero); + const ignoreCount = count === null || isZero; const hasStatus = ((status !== null && status !== undefined) || (color !== null && color !== undefined)) && diff --git a/components/breadcrumb/BreadcrumbItem.tsx b/components/breadcrumb/BreadcrumbItem.tsx index 12521f738e..9a4290e943 100644 --- a/components/breadcrumb/BreadcrumbItem.tsx +++ b/components/breadcrumb/BreadcrumbItem.tsx @@ -1,6 +1,7 @@ import DownOutlined from '@ant-design/icons/DownOutlined'; import * as React from 'react'; +import warning from '../_util/warning'; import { ConfigContext } from '../config-provider'; import type { DropdownProps } from '../dropdown/dropdown'; import Dropdown from '../dropdown/dropdown'; @@ -9,30 +10,47 @@ export interface BreadcrumbItemProps { prefixCls?: string; separator?: React.ReactNode; href?: string; - overlay?: DropdownProps['overlay']; + menu?: DropdownProps['menu']; dropdownProps?: DropdownProps; onClick?: React.MouseEventHandler; className?: string; children?: React.ReactNode; + + // Deprecated + /** @deprecated Please use `menu` instead */ + overlay?: DropdownProps['overlay']; } interface BreadcrumbItemInterface extends React.FC { __ANT_BREADCRUMB_ITEM: boolean; } -const BreadcrumbItem: BreadcrumbItemInterface = ({ - prefixCls: customizePrefixCls, - separator = '/', - children, - overlay, - dropdownProps, - ...restProps -}) => { +const BreadcrumbItem: BreadcrumbItemInterface = props => { + const { + prefixCls: customizePrefixCls, + separator = '/', + children, + menu, + overlay, + dropdownProps, + ...restProps + } = props; + const { getPrefixCls } = React.useContext(ConfigContext); const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls); + + // Warning for deprecated usage + if (process.env.NODE_ENV !== 'production') { + warning( + !('overlay' in props), + 'Breadcrumb.Item', + '`overlay` is deprecated. Please use `menu` instead.', + ); + } + /** If overlay is have Wrap a Dropdown */ const renderBreadcrumbNode = (breadcrumbItem: React.ReactNode) => { - if (overlay) { + if (menu || overlay) { return ( - + {breadcrumbItem} diff --git a/components/breadcrumb/__tests__/Breadcrumb.test.tsx b/components/breadcrumb/__tests__/Breadcrumb.test.tsx index a1855c3cfd..97090180a8 100644 --- a/components/breadcrumb/__tests__/Breadcrumb.test.tsx +++ b/components/breadcrumb/__tests__/Breadcrumb.test.tsx @@ -33,6 +33,19 @@ describe('Breadcrumb', () => { ); }); + it('overlay deprecation warning', () => { + render( + + menu}> + General + + , + ); + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: [antd: Breadcrumb.Item] `overlay` is deprecated. Please use `menu` instead.', + ); + }); + // https://github.com/ant-design/ant-design/issues/5015 it('should allow Breadcrumb.Item is null or undefined', () => { const { asFragment } = render( diff --git a/components/breadcrumb/demo/overlay.md b/components/breadcrumb/demo/overlay.md index 840f765c4a..f024b947d7 100644 --- a/components/breadcrumb/demo/overlay.md +++ b/components/breadcrumb/demo/overlay.md @@ -14,39 +14,35 @@ title: Breadcrumbs support drop down menu. ```tsx -import { Breadcrumb, Menu } from 'antd'; +import { Breadcrumb } from 'antd'; import React from 'react'; -const menu = ( - - General - - ), - }, - { - key: '2', - label: ( - - Layout - - ), - }, - { - key: '3', - label: ( - - Navigation - - ), - }, - ]} - /> -); +const items = [ + { + key: '1', + label: ( + + General + + ), + }, + { + key: '2', + label: ( + + Layout + + ), + }, + { + key: '3', + label: ( + + Navigation + + ), + }, +]; const App: React.FC = () => ( @@ -54,7 +50,7 @@ const App: React.FC = () => ( Component - + General Button diff --git a/components/breadcrumb/index.en-US.md b/components/breadcrumb/index.en-US.md index 742eb462ca..007492642e 100644 --- a/components/breadcrumb/index.en-US.md +++ b/components/breadcrumb/index.en-US.md @@ -13,6 +13,39 @@ A breadcrumb displays the current location within a hierarchy. It allows going b - When you need to inform the user of where they are. - When the user may need to navigate back to a higher level. +### Usage upgrade after 4.24.0 + +```__react +import Alert from '../alert'; +ReactDOM.render(, mountNode); +``` + +```jsx +// works when >=4.24.0, recommended ✅ +const items = [ + { label: 'item 1', key: 'item-1' }, // remember to pass the key prop + { label: 'item 2', key: 'item-2' }, +]; +return ( + + Ant Design + +); + +// works when <4.24.0, deprecated when >=4.24.0 🙅🏻‍♀️ +const menu = ( + + item 1 + item 2 + +); +return ( + + Ant Design + +); +``` + ## API ### Breadcrumb @@ -31,14 +64,14 @@ A breadcrumb displays the current location within a hierarchy. It allows going b | className | The additional css class | string | - | | | dropdownProps | The dropdown props | [Dropdown](/components/dropdown) | - | | | href | Target of hyperlink | string | - | | -| overlay | The dropdown menu | [Menu](/components/menu) \| () => Menu | - | | +| menu | The menu props | [MenuProps](/components/menu/#API) | - | 4.24.0 | | onClick | Set the handler to handle click event | (e:MouseEvent) => void | - | | ### Breadcrumb.Separator -| Property | Description | Type | Default | Version | -| --- | --- | --- | --- | --- | -| children | Custom separator | ReactNode | `/` | | +| Property | Description | Type | Default | Version | +| -------- | ---------------- | --------- | ------- | ------- | +| children | Custom separator | ReactNode | `/` | | > When using `Breadcrumb.Separator`,its parent component must be set to `separator=""`, otherwise the default separator of the parent component will appear. diff --git a/components/breadcrumb/index.zh-CN.md b/components/breadcrumb/index.zh-CN.md index cdda39bf5e..7409161a3c 100644 --- a/components/breadcrumb/index.zh-CN.md +++ b/components/breadcrumb/index.zh-CN.md @@ -14,6 +14,39 @@ cover: https://gw.alipayobjects.com/zos/alicdn/9Ltop8JwH/Breadcrumb.svg - 当需要告知用户『你在哪里』时; - 当需要向上导航的功能时。 +### 4.24.0 用法升级 + +```__react +import Alert from '../alert'; +ReactDOM.render(, mountNode); +``` + +```jsx +// >=4.24.0 可用,推荐的写法 ✅ +const items = [ + { label: '菜单项一', key: 'item-1' }, // 菜单项务必填写 key + { label: '菜单项二', key: 'item-2' }, +]; +return ( + + Ant Design + +); + +// <4.24.0 可用,>=4.24.0 时不推荐 🙅🏻‍♀️ +const menu = ( + + 菜单项一 + 菜单项二 + +); +return ( + + Ant Design + +); +``` + ## API ### Breadcrumb @@ -27,19 +60,19 @@ cover: https://gw.alipayobjects.com/zos/alicdn/9Ltop8JwH/Breadcrumb.svg ### Breadcrumb.Item -| 参数 | 说明 | 类型 | 默认值 | 版本 | -| --- | --- | --- | --- | --- | -| className | 自定义类名 | string | - | | -| dropdownProps | 弹出下拉菜单的自定义配置 | [Dropdown](/components/dropdown) | - | | -| href | 链接的目的地 | string | - | | -| overlay | 下拉菜单的内容 | [Menu](/components/menu) \| () => Menu | - | | -| onClick | 单击事件 | (e:MouseEvent) => void | - | | +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| ------------- | ------------------------ | ---------------------------------- | ------ | ------ | +| className | 自定义类名 | string | - | | +| dropdownProps | 弹出下拉菜单的自定义配置 | [Dropdown](/components/dropdown) | - | | +| href | 链接的目的地 | string | - | | +| menu | 菜单配置项 | [MenuProps](/components/menu/#API) | - | 4.24.0 | +| onClick | 单击事件 | (e:MouseEvent) => void | - | | ### Breadcrumb.Separator -| 参数 | 说明 | 类型 | 默认值 | 版本 | -| --- | --- | --- | --- | --- | -| children | 要显示的分隔符 | ReactNode | `/` | | +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| -------- | -------------- | --------- | ------ | ---- | +| children | 要显示的分隔符 | ReactNode | `/` | | > 注意:在使用 `Breadcrumb.Separator` 时,其父组件的分隔符必须设置为 `separator=""`,否则会出现父组件默认的分隔符。 diff --git a/components/button/demo/multiple.md b/components/button/demo/multiple.md index b9fa8e2e3f..5d79c1e4e1 100644 --- a/components/button/demo/multiple.md +++ b/components/button/demo/multiple.md @@ -15,38 +15,33 @@ If you need several buttons, we recommend that you use 1 primary button + n seco ```tsx import type { MenuProps } from 'antd'; -import { Button, Dropdown, Menu } from 'antd'; +import { Button, Dropdown } from 'antd'; import React from 'react'; const onMenuClick: MenuProps['onClick'] = e => { console.log('click', e); }; -const menu = ( - -); +const items = [ + { + key: '1', + label: '1st item', + }, + { + key: '2', + label: '2nd item', + }, + { + key: '3', + label: '3rd item', + }, +]; const App: React.FC = () => ( <> - Actions + Actions ); diff --git a/components/carousel/__tests__/index.test.tsx b/components/carousel/__tests__/index.test.tsx index 3865f8f94f..f980f862d1 100644 --- a/components/carousel/__tests__/index.test.tsx +++ b/components/carousel/__tests__/index.test.tsx @@ -57,7 +57,6 @@ describe('Carousel', () => { }); it('should trigger autoPlay after window resize', async () => { - jest.useRealTimers(); const ref = React.createRef(); render( diff --git a/components/carousel/style/index.tsx b/components/carousel/style/index.tsx index 2972ec9fa5..4acd077583 100644 --- a/components/carousel/style/index.tsx +++ b/components/carousel/style/index.tsx @@ -15,9 +15,11 @@ interface CarouselToken extends FullToken<'Carousel'> { } const genCarouselStyle: GenerateStyle = token => { - const { componentCls, antCls, carouselArrowSize, carouselDotOffset, carouselDotInline } = token; + const { componentCls, antCls, carouselArrowSize, carouselDotOffset, marginXXS } = token; const arrowOffset = -carouselArrowSize * 1.25; + const carouselDotMargin = marginXXS; + return { [componentCls]: { ...resetComponent(token), @@ -196,7 +198,7 @@ const genCarouselStyle: GenerateStyle = token => { boxSizing: 'content-box', width: token.dotWidth, height: token.dotHeight, - marginInline: carouselDotInline, + marginInline: carouselDotMargin, padding: 0, textAlign: 'center', textIndent: -999, @@ -204,6 +206,7 @@ const genCarouselStyle: GenerateStyle = token => { transition: `all ${token.motionDurationSlow}`, button: { + position: 'relative', display: 'block', width: '100%', height: token.dotHeight, @@ -221,6 +224,12 @@ const genCarouselStyle: GenerateStyle = token => { '&: hover, &:focus': { opacity: 0.75, }, + + '&::after': { + position: 'absolute', + inset: -carouselDotMargin, + content: '""', + }, }, '&.slick-active': { @@ -242,7 +251,7 @@ const genCarouselStyle: GenerateStyle = token => { }; const genCarouselVerticalStyle: GenerateStyle = token => { - const { componentCls, carouselDotOffset } = token; + const { componentCls, carouselDotOffset, marginXXS } = token; const reverseSizeOfDot = { width: token.dotHeight, @@ -273,7 +282,7 @@ const genCarouselVerticalStyle: GenerateStyle = token => { li: { // reverse width and height in vertical situation ...reverseSizeOfDot, - margin: '4px 2px', + margin: `${marginXXS}px 0`, verticalAlign: 'baseline', button: reverseSizeOfDot, @@ -321,11 +330,10 @@ const genCarouselRtlStyle: GenerateStyle = token => { export default genComponentStyleHook( 'Carousel', token => { - const { controlHeightLG, controlHeightSM, dotHeight } = token; + const { controlHeightLG, controlHeightSM } = token; const carouselToken = mergeToken(token, { carouselArrowSize: controlHeightLG / 2, carouselDotOffset: controlHeightSM / 2, - carouselDotInline: dotHeight, }); return [ diff --git a/components/cascader/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/cascader/__tests__/__snapshots__/demo-extend.test.ts.snap index 96c793b248..c683f3b041 100644 --- a/components/cascader/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/cascader/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -3149,7 +3149,7 @@ exports[`renders ./components/cascader/demo/status.md extend context correctly 1
- No Data + No data
@@ -3299,7 +3299,7 @@ exports[`renders ./components/cascader/demo/status.md extend context correctly 1
- No Data + No data
diff --git a/components/cascader/__tests__/__snapshots__/index.test.tsx.snap b/components/cascader/__tests__/__snapshots__/index.test.tsx.snap index 75c663557b..d3290c6e86 100644 --- a/components/cascader/__tests__/__snapshots__/index.test.tsx.snap +++ b/components/cascader/__tests__/__snapshots__/index.test.tsx.snap @@ -1617,9 +1617,9 @@ exports[`Cascader rtl render component should be rendered correctly in RTL direc `; -exports[`Cascader should highlight keyword and filter when search in Cascader 1`] = `"
  • Zhejiang / Hangzhou / West Lake
  • Jiangsu / Nanjing / Zhong Hua Men
"`; +exports[`Cascader should highlight keyword and filter when search in Cascader 1`] = `"
"`; -exports[`Cascader should highlight keyword and filter when search in Cascader with same field name of label and value 1`] = `"
  • Zhejiang / Hangzhou / West Lake
  • Zhejiang / Hangzhou / Xia Sha
"`; +exports[`Cascader should highlight keyword and filter when search in Cascader with same field name of label and value 1`] = `"
"`; exports[`Cascader should render not found content 1`] = `
- No Data + No data
@@ -1760,7 +1760,7 @@ exports[`Cascader should show not found content when options.length is 0 1`] = `
- No Data + No data
diff --git a/components/config-provider/__tests__/__snapshots__/components.test.tsx.snap b/components/config-provider/__tests__/__snapshots__/components.test.tsx.snap index 2105a05950..5b4395a138 100644 --- a/components/config-provider/__tests__/__snapshots__/components.test.tsx.snap +++ b/components/config-provider/__tests__/__snapshots__/components.test.tsx.snap @@ -15033,7 +15033,7 @@ exports[`ConfigProvider components Empty configProvider 1`] = `
- No Data + No data
`; @@ -15108,7 +15108,7 @@ exports[`ConfigProvider components Empty configProvider componentDisabled 1`] =
- No Data + No data
`; @@ -15183,7 +15183,7 @@ exports[`ConfigProvider components Empty configProvider componentSize large 1`]
- No Data + No data
`; @@ -15258,7 +15258,7 @@ exports[`ConfigProvider components Empty configProvider componentSize middle 1`]
- No Data + No data
`; @@ -15333,7 +15333,7 @@ exports[`ConfigProvider components Empty configProvider virtual and dropdownMatc
- No Data + No data
`; @@ -15408,7 +15408,7 @@ exports[`ConfigProvider components Empty normal 1`] = `
- No Data + No data
`; @@ -15483,7 +15483,7 @@ exports[`ConfigProvider components Empty prefixCls 1`] = `
- No Data + No data
`; @@ -18863,7 +18863,6 @@ exports[`ConfigProvider components Pagination configProvider 1`] = `
                            • @@ -26873,7 +26859,7 @@ exports[`ConfigProvider components Table configProvider 1`] = `
                              - No Data + No data
@@ -26916,7 +26902,7 @@ exports[`ConfigProvider components Table configProvider componentDisabled 1`] = > @@ -27180,7 +27166,7 @@ exports[`ConfigProvider components Table configProvider componentDisabled 1`] =
- No Data + No data
@@ -27223,7 +27209,7 @@ exports[`ConfigProvider components Table configProvider componentSize large 1`] > @@ -27485,7 +27471,7 @@ exports[`ConfigProvider components Table configProvider componentSize large 1`]
- No Data + No data
@@ -27528,7 +27514,7 @@ exports[`ConfigProvider components Table configProvider componentSize middle 1`] > @@ -27790,7 +27776,7 @@ exports[`ConfigProvider components Table configProvider componentSize middle 1`]
- No Data + No data
@@ -27833,7 +27819,7 @@ exports[`ConfigProvider components Table configProvider virtual and dropdownMatc > @@ -28095,7 +28081,7 @@ exports[`ConfigProvider components Table configProvider virtual and dropdownMatc
- No Data + No data
@@ -28138,7 +28124,7 @@ exports[`ConfigProvider components Table normal 1`] = ` > @@ -28400,7 +28386,7 @@ exports[`ConfigProvider components Table normal 1`] = `
- No Data + No data
@@ -28443,7 +28429,7 @@ exports[`ConfigProvider components Table prefixCls 1`] = ` > @@ -28705,7 +28691,7 @@ exports[`ConfigProvider components Table prefixCls 1`] = `
- No Data + No data
@@ -38481,7 +38467,7 @@ exports[`ConfigProvider components Transfer configProvider 1`] = `
- No Data + No data
@@ -38640,7 +38626,7 @@ exports[`ConfigProvider components Transfer configProvider 1`] = `
- No Data + No data
@@ -38753,7 +38739,7 @@ exports[`ConfigProvider components Transfer configProvider componentDisabled 1`]
- No Data + No data
@@ -38913,7 +38899,7 @@ exports[`ConfigProvider components Transfer configProvider componentDisabled 1`]
- No Data + No data
@@ -39025,7 +39011,7 @@ exports[`ConfigProvider components Transfer configProvider componentSize large 1
- No Data + No data
@@ -39184,7 +39170,7 @@ exports[`ConfigProvider components Transfer configProvider componentSize large 1
- No Data + No data
@@ -39296,7 +39282,7 @@ exports[`ConfigProvider components Transfer configProvider componentSize middle
- No Data + No data
@@ -39455,7 +39441,7 @@ exports[`ConfigProvider components Transfer configProvider componentSize middle
- No Data + No data
@@ -39567,7 +39553,7 @@ exports[`ConfigProvider components Transfer configProvider virtual and dropdownM
- No Data + No data
@@ -39726,7 +39712,7 @@ exports[`ConfigProvider components Transfer configProvider virtual and dropdownM
- No Data + No data
@@ -39838,7 +39824,7 @@ exports[`ConfigProvider components Transfer normal 1`] = `
- No Data + No data
@@ -39997,7 +39983,7 @@ exports[`ConfigProvider components Transfer normal 1`] = `
- No Data + No data
@@ -40109,7 +40095,7 @@ exports[`ConfigProvider components Transfer prefixCls 1`] = `
- No Data + No data
@@ -40268,7 +40254,7 @@ exports[`ConfigProvider components Transfer prefixCls 1`] = `
- No Data + No data
diff --git a/components/config-provider/__tests__/components.test.tsx b/components/config-provider/__tests__/components.test.tsx index f1b19b23c2..08764d1456 100644 --- a/components/config-provider/__tests__/components.test.tsx +++ b/components/config-provider/__tests__/components.test.tsx @@ -274,19 +274,7 @@ describe('ConfigProvider', () => { testPair('Drawer', props => ); // Dropdown - testPair('Dropdown', props => { - const menu = ( - - Bamboo - - ); - - return ( - - Light - - ); - }); + testPair('Dropdown', props => Light); // Form testPair('Form', props => ( diff --git a/components/config-provider/demo/theme.md b/components/config-provider/demo/theme.md index 7596737879..c6d4a3ced9 100644 --- a/components/config-provider/demo/theme.md +++ b/components/config-provider/demo/theme.md @@ -312,21 +312,19 @@ const App: React.FC = () => { {/* Dropdown */} - } + menu={{ + items: [ + { + key: '1', + label: '1st menu item', + }, + { + key: '2', + label: 'a danger item', + danger: true, + }, + ], + }} > e.preventDefault()}> diff --git a/components/date-picker/__tests__/type.test.tsx b/components/date-picker/__tests__/type.test.tsx index 0600c706e1..f47f6ff5ad 100644 --- a/components/date-picker/__tests__/type.test.tsx +++ b/components/date-picker/__tests__/type.test.tsx @@ -58,4 +58,11 @@ describe('DatePicker.typescript', () => { ); expect(datePicker).toBeTruthy(); }); + + it('DatePicker and RangePicker supports popupClassName', () => { + const datePicker = ; + expect(datePicker).toBeTruthy(); + const rangePicker = ; + expect(rangePicker).toBeTruthy(); + }); }); diff --git a/components/date-picker/generatePicker/generateRangePicker.tsx b/components/date-picker/generatePicker/generateRangePicker.tsx index 2517ff9819..da5ea9daa7 100644 --- a/components/date-picker/generatePicker/generateRangePicker.tsx +++ b/components/date-picker/generatePicker/generateRangePicker.tsx @@ -23,18 +23,20 @@ import warning from '../../_util/warning'; import useStyle from '../style'; -export default function generateRangePicker( - generateConfig: GenerateConfig, -): PickerComponentClass> { +export default function generateRangePicker(generateConfig: GenerateConfig) { type InternalRangePickerProps = RangePickerProps & {}; + type DateRangePickerProps = RangePickerProps & { + /** + * @deprecated `dropdownClassName` is deprecated which will be removed in next major + * version.Please use `popupClassName` instead. + */ + dropdownClassName?: string; + popupClassName?: string; + }; const RangePicker = forwardRef< InternalRangePickerProps | CommonPickerMethods, - RangePickerProps & { - popupClassName?: string; - /** @deprecated Please use `popupClassName` instead */ - dropdownClassName?: string; - } + DateRangePickerProps >((props, ref) => { const { prefixCls: customizePrefixCls, @@ -154,5 +156,5 @@ export default function generateRangePicker( ); }); - return RangePicker as unknown as PickerComponentClass>; + return RangePicker as unknown as PickerComponentClass; } diff --git a/components/date-picker/generatePicker/generateSinglePicker.tsx b/components/date-picker/generatePicker/generateSinglePicker.tsx index 222fcca89a..fbbca8f1ae 100644 --- a/components/date-picker/generatePicker/generateSinglePicker.tsx +++ b/components/date-picker/generatePicker/generateSinglePicker.tsx @@ -8,7 +8,7 @@ import type { PickerMode } from 'rc-picker/lib/interface'; import * as React from 'react'; import { forwardRef, useContext, useImperativeHandle } from 'react'; import { useCompactItemContext } from '../../space/Compact'; -import type { PickerDateProps, PickerProps, PickerTimeProps } from '.'; +import type { PickerProps, PickerTimeProps } from '.'; import { Components, getTimeProps } from '.'; import { ConfigContext } from '../../config-provider'; import DisabledContext from '../../config-provider/DisabledContext'; @@ -25,13 +25,14 @@ import type { CommonPickerMethods, DatePickRef, PickerComponentClass } from './i import useStyle from '../style'; export default function generatePicker(generateConfig: GenerateConfig) { - type DatePickerProps = PickerProps & { + type CustomPickerProps = { status?: InputStatus; hashId?: string; popupClassName?: string; - /** @deprecated Please use `popupClassName` instead */ - dropdownClassName?: string; }; + type DatePickerProps = PickerProps & CustomPickerProps; + type TimePickerProps = PickerTimeProps & CustomPickerProps; + function getPicker( picker?: PickerMode, displayName?: string, @@ -178,14 +179,11 @@ export default function generatePicker(generateConfig: GenerateConfig< } const DatePicker = getPicker(); - const WeekPicker = getPicker, 'picker'>>('week', 'WeekPicker'); - const MonthPicker = getPicker, 'picker'>>('month', 'MonthPicker'); - const YearPicker = getPicker, 'picker'>>('year', 'YearPicker'); - const TimePicker = getPicker, 'picker'>>('time', 'TimePicker'); - const QuarterPicker = getPicker, 'picker'>>( - 'quarter', - 'QuarterPicker', - ); + const WeekPicker = getPicker>('week', 'WeekPicker'); + const MonthPicker = getPicker>('month', 'MonthPicker'); + const YearPicker = getPicker>('year', 'YearPicker'); + const TimePicker = getPicker>('time', 'TimePicker'); + const QuarterPicker = getPicker>('quarter', 'QuarterPicker'); return { DatePicker, WeekPicker, MonthPicker, YearPicker, TimePicker, QuarterPicker }; } diff --git a/components/drawer/style/index.tsx b/components/drawer/style/index.tsx index f8374b4dc6..e8a7c69d25 100644 --- a/components/drawer/style/index.tsx +++ b/components/drawer/style/index.tsx @@ -153,7 +153,7 @@ const genDrawerStyle: GenerateStyle = (token: DrawerToken) => { }, [`${componentCls}-extra`]: { - flex: 0, + flex: 'none', }, [`${componentCls}-close`]: { diff --git a/components/dropdown/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/dropdown/__tests__/__snapshots__/demo-extend.test.ts.snap index c7b60e1f0f..83138dc534 100644 --- a/components/dropdown/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/dropdown/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -3081,6 +3081,594 @@ Array [ ] `; +exports[`renders ./components/dropdown/demo/custom-dropdown.md extend context correctly 1`] = ` +Array [ + +
+
+ Hover me +
+
+ + + +
+
+
, +
+
+
@@ -1000,7 +1000,7 @@ exports[`renders ./components/empty/demo/config-provider.md extend context corre
- No Data + No data
@@ -1101,7 +1101,7 @@ exports[`renders ./components/empty/demo/config-provider.md extend context corre
- No Data + No data
@@ -1171,7 +1171,7 @@ exports[`renders ./components/empty/demo/config-provider.md extend context corre
- No Data + No data
@@ -1335,7 +1335,7 @@ exports[`renders ./components/empty/demo/simple.md extend context correctly 1`]
- No Data + No data
`; diff --git a/components/empty/__tests__/__snapshots__/demo.test.ts.snap b/components/empty/__tests__/__snapshots__/demo.test.ts.snap index d016717300..24088bec53 100644 --- a/components/empty/__tests__/__snapshots__/demo.test.ts.snap +++ b/components/empty/__tests__/__snapshots__/demo.test.ts.snap @@ -70,7 +70,7 @@ exports[`renders ./components/empty/demo/basic.md correctly 1`] = `
- No Data + No data
`; @@ -389,7 +389,7 @@ exports[`renders ./components/empty/demo/config-provider.md correctly 1`] = `
- No Data + No data
@@ -549,7 +549,7 @@ exports[`renders ./components/empty/demo/config-provider.md correctly 1`] = `
- No Data + No data
@@ -650,7 +650,7 @@ exports[`renders ./components/empty/demo/config-provider.md correctly 1`] = `
- No Data + No data
@@ -720,7 +720,7 @@ exports[`renders ./components/empty/demo/config-provider.md correctly 1`] = `
- No Data + No data
@@ -884,7 +884,7 @@ exports[`renders ./components/empty/demo/simple.md correctly 1`] = `
- No Data + No data
`; diff --git a/components/empty/__tests__/__snapshots__/index.test.tsx.snap b/components/empty/__tests__/__snapshots__/index.test.tsx.snap index 2631ee80b8..c7b65a0e16 100644 --- a/components/empty/__tests__/__snapshots__/index.test.tsx.snap +++ b/components/empty/__tests__/__snapshots__/index.test.tsx.snap @@ -70,7 +70,7 @@ exports[`Empty rtl render component should be rendered correctly in RTL directio
- No Data + No data
`; @@ -145,7 +145,7 @@ exports[`Empty should render in RTL direction 1`] = `
- No Data + No data
`; diff --git a/components/form/FormItem/index.tsx b/components/form/FormItem/index.tsx index edb587ce0e..19c0751e13 100644 --- a/components/form/FormItem/index.tsx +++ b/components/form/FormItem/index.tsx @@ -284,27 +284,31 @@ function InternalFormItem(props: FormItemProps): React.Rea warning( !(shouldUpdate && dependencies), 'Form.Item', - "`shouldUpdate` and `dependencies` shouldn't be used together. See https://ant.design/components/form/#dependencies.", + "`shouldUpdate` and `dependencies` shouldn't be used together. See https://u.ant.design/#form-deps.", ); if (Array.isArray(children) && hasName) { - warning(false, 'Form.Item', '`children` is array of render props cannot have `name`.'); + warning( + false, + 'Form.Item', + '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 = children; } else if (isRenderProps && (!(shouldUpdate || dependencies) || hasName)) { warning( !!(shouldUpdate || dependencies), 'Form.Item', - '`children` of render props only work with `shouldUpdate` or `dependencies`.', + 'A `Form.Item` with a render function must have either `shouldUpdate` or `dependencies`.', ); warning( !hasName, 'Form.Item', - "Do not use `name` with `children` of render props since it's not a field.", + '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', - 'Must set `name` or use render props when `dependencies` is set.', + 'Must set `name` or use a render function when `dependencies` is set.', ); } else if (isValidElement(children)) { warning( diff --git a/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap index 3c4b2e540e..adccf820cd 100644 --- a/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -5900,7 +5900,7 @@ exports[`renders ./components/form/demo/dynamic-form-items-complex.md extend con
@@ -2150,7 +2150,6 @@ exports[`renders ./components/list/demo/vertical.md extend context correctly 1`] >
  • - No Data + No data @@ -2150,7 +2150,6 @@ exports[`renders ./components/list/demo/vertical.md correctly 1`] = ` >
    • - No Data + No data diff --git a/components/list/__tests__/__snapshots__/index.test.tsx.snap b/components/list/__tests__/__snapshots__/index.test.tsx.snap index 4747f8d3a2..7b504e5421 100644 --- a/components/list/__tests__/__snapshots__/index.test.tsx.snap +++ b/components/list/__tests__/__snapshots__/index.test.tsx.snap @@ -55,7 +55,7 @@ exports[`List rtl render component should be rendered correctly in RTL direction
      - No Data + No data
      diff --git a/components/list/__tests__/__snapshots__/pagination.test.tsx.snap b/components/list/__tests__/__snapshots__/pagination.test.tsx.snap index eb87255890..15562b1dac 100644 --- a/components/list/__tests__/__snapshots__/pagination.test.tsx.snap +++ b/components/list/__tests__/__snapshots__/pagination.test.tsx.snap @@ -31,7 +31,6 @@ exports[`List.pagination renders pagination correctly 1`] = ` >
          • - No Data + No data @@ -10899,7 +10897,7 @@ exports[`Locale Provider should display the text as az 1`] = `
            - No Data + No data
            @@ -11970,7 +11968,7 @@ exports[`Locale Provider should display the text as az 1`] = `
            - No Data + No data
            @@ -12090,7 +12088,6 @@ exports[`Locale Provider should display the text as bg 1`] = `
                            • - No Data + No data
            @@ -56835,7 +56824,7 @@ exports[`Locale Provider should display the text as en 1`] = `
            - No Data + No data
            @@ -57906,7 +57895,7 @@ exports[`Locale Provider should display the text as en 1`] = `
            - No Data + No data
            @@ -58026,7 +58015,6 @@ exports[`Locale Provider should display the text as en-gb 1`] = `
                                                        • - No Data + No data
            @@ -174225,7 +174191,7 @@ exports[`Locale Provider should display the text as kn 1`] = `
            - No Data + No data
            @@ -175296,7 +175262,7 @@ exports[`Locale Provider should display the text as kn 1`] = `
            - No Data + No data
            @@ -175416,7 +175382,6 @@ exports[`Locale Provider should display the text as ko 1`] = `
                                                                            • ( )); + it("Rendering on LocaleProvider won't trigger rendering on child component.", () => { - const { container } = pureRender(); + const { container, unmount } = pureRender(); expect(outerCount).toBe(0); expect(innerCount).toBe(1); fireEvent.click(container.querySelector('#parent_btn')!); expect(outerCount).toBe(1); expect(innerCount).toBe(1); + unmount(); }); diff --git a/components/locale-provider/__tests__/config.test.tsx b/components/locale-provider/__tests__/config.test.tsx index b43c4505c8..886db210d8 100644 --- a/components/locale-provider/__tests__/config.test.tsx +++ b/components/locale-provider/__tests__/config.test.tsx @@ -1,7 +1,6 @@ import React, { useEffect } from 'react'; -import { act } from 'react-dom/test-utils'; import { Modal } from '../..'; -import { sleep, render, fireEvent } from '../../../tests/utils'; +import { waitFakeTimer, render, fireEvent } from '../../../tests/utils'; import ConfigProvider from '../../config-provider'; import zhCN from '../zh_CN'; @@ -46,16 +45,10 @@ describe('Locale Provider demo', () => { const { container } = render(); fireEvent.click(container.querySelector('.about')!); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); fireEvent.click(container.querySelector('.dashboard')!); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect(document.body.querySelectorAll('.ant-btn-primary span')[0]?.textContent).toBe('确 定'); Modal.destroyAll(); diff --git a/components/locale/en_US.tsx b/components/locale/en_US.tsx index e05ece5f76..4b87557341 100644 --- a/components/locale/en_US.tsx +++ b/components/locale/en_US.tsx @@ -69,7 +69,7 @@ const localeValues: Locale = { downloadFile: 'Download file', }, Empty: { - description: 'No Data', + description: 'No data', }, Icon: { icon: 'icon', diff --git a/components/menu/__tests__/cached-context.test.tsx b/components/menu/__tests__/cached-context.test.tsx index 682e82ed6c..99f49fb324 100644 --- a/components/menu/__tests__/cached-context.test.tsx +++ b/components/menu/__tests__/cached-context.test.tsx @@ -1,55 +1,42 @@ -import React, { memo, useContext, useRef, useState } from 'react'; +import React, { memo, useContext } from 'react'; import Menu from '../index'; import MenuContext from '../MenuContext'; -import { render, fireEvent } from '../../../tests/utils'; +import { fireEvent, pureRender } from '../../../tests/utils'; -// we use'memo' here in order to only render inner component while context changed. -const CacheInner = memo(() => { - const countRef = useRef(0); - countRef.current++; - // subscribe anchor context - useContext(MenuContext); - return ( -
                                                                              - Child Rendering Count: {countRef.current} -
                                                                              - ); -}); +let innerCount = 0; +let outerCount = 0; -const CacheOuter = () => { - // We use 'useState' here in order to trigger parent component rendering. - const [count, setCount] = useState(1); - const handleClick = () => { - setCount(count + 1); - }; - // During each rendering phase, the cached context value returned from method 'Menu#getMemoizedContextValue' will take effect. - // So 'CacheInner' component won't rerender. - return ( -
                                                                              - - Parent Rendering Count: {count} - - - - - -
                                                                              - ); +const handleClick = () => { + outerCount++; }; +// we use'memo' here in order to only render inner component while context changed. +const CacheInner: React.FC = memo(() => { + innerCount++; + // subscribe locale context + useContext(MenuContext); + return null; +}); + +const CacheOuter: React.FC = memo(() => ( + <> + + + + + + + +)); + it("Rendering on Menu without changed MenuContext won't trigger rendering on child component.", () => { - const { container, unmount } = render(); - const childCount = container.querySelector('#child_count')?.textContent; + const { container, unmount } = pureRender(); + expect(outerCount).toBe(0); + expect(innerCount).toBe(1); fireEvent.click(container.querySelector('#parent_btn')!); - expect(container.querySelector('#parent_count')?.textContent).toBe('2'); - // child component won't rerender - expect(container.querySelector('#child_count')?.textContent).toBe(childCount); - fireEvent.click(container.querySelector('#parent_btn')!); - expect(container.querySelector('#parent_count')?.textContent).toBe('3'); - // child component won't rerender - expect(container.querySelector('#child_count')?.textContent).toBe(childCount); - // in order to depress warning "Warning: An update to Menu inside a test was not wrapped in act(...)." + expect(outerCount).toBe(1); + expect(innerCount).toBe(1); unmount(); }); diff --git a/components/menu/style/index.tsx b/components/menu/style/index.tsx index 34acc22dea..b7a86a68c4 100644 --- a/components/menu/style/index.tsx +++ b/components/menu/style/index.tsx @@ -261,7 +261,6 @@ const getBaseStyle: GenerateStyle = token => { a: { color: 'inherit !important', - pointerEvents: 'none', }, [`> ${componentCls}-submenu-title`]: { diff --git a/components/modal/__tests__/confirm.test.tsx b/components/modal/__tests__/confirm.test.tsx index e696b9fe1d..62d8bc93f5 100644 --- a/components/modal/__tests__/confirm.test.tsx +++ b/components/modal/__tests__/confirm.test.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import TestUtils from 'react-dom/test-utils'; import type { ModalFuncProps } from '..'; import Modal from '..'; -import { sleep, act } from '../../../tests/utils'; +import { waitFakeTimer, act } from '../../../tests/utils'; import ConfigProvider from '../../config-provider'; import type { ModalFunc } from '../confirm'; import destroyFns from '../destroyFns'; @@ -56,16 +56,21 @@ describe('Modal.confirm triggers callbacks correctly', () => { }; /* eslint-enable */ + beforeAll(() => { + jest.useFakeTimers(); + }); + afterEach(async () => { - jest.clearAllTimers(); errorSpy.mockReset(); Modal.destroyAll(); - await sleep(); + await waitFakeTimer(); document.body.innerHTML = ''; + jest.clearAllTimers(); }); afterAll(() => { + jest.useRealTimers(); errorSpy.mockRestore(); }); @@ -73,41 +78,32 @@ describe('Modal.confirm triggers callbacks correctly', () => { return document.body.querySelectorAll(className); } - function open(args?: ModalFuncProps) { - jest.useFakeTimers(); + async function open(args?: ModalFuncProps) { confirm({ title: 'Want to delete these items?', content: 'some descriptions', ...args, }); - act(() => { - jest.runAllTimers(); - }); - jest.useRealTimers(); + + await waitFakeTimer(); } - it('should not render title when title not defined', () => { - jest.useFakeTimers(); + it('should not render title when title not defined', async () => { confirm({ content: 'some descriptions', }); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect(document.querySelector('.ant-modal-confirm-title')).toBe(null); - jest.useRealTimers(); }); it('trigger onCancel once when click on cancel button', async () => { const onCancel = jest.fn(); const onOk = jest.fn(); - open({ + await open({ onCancel, onOk, }); - // first Modal - await sleep(); $$('.ant-btn')[0].click(); expect(onCancel.mock.calls.length).toBe(1); expect(onOk.mock.calls.length).toBe(0); @@ -116,21 +112,18 @@ describe('Modal.confirm triggers callbacks correctly', () => { it('trigger onOk once when click on ok button', async () => { const onCancel = jest.fn(); const onOk = jest.fn(); - open({ + await open({ onCancel, onOk, }); - // second Modal - await sleep(); $$('.ant-btn-primary')[0].click(); expect(onCancel.mock.calls.length).toBe(0); expect(onOk.mock.calls.length).toBe(1); }); it('should allow Modal.confirm without onCancel been set', async () => { - open(); - await sleep(); + await open(); // Third Modal $$('.ant-btn')[0].click(); @@ -138,17 +131,14 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); it('should allow Modal.confirm without onOk been set', async () => { - open(); + await open(); // Fourth Modal - await sleep(); $$('.ant-btn-primary')[0].click(); expect(errorSpy).not.toHaveBeenCalled(); }); it('should close confirm modal when press ESC', async () => { - jest.useFakeTimers(); - jest.clearAllTimers(); const onCancel = jest.fn(); Modal.confirm({ title: 'title', @@ -156,100 +146,68 @@ describe('Modal.confirm triggers callbacks correctly', () => { onCancel, }); - act(() => { - jest.runAllTimers(); - }); - await sleep(); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-confirm`)).toHaveLength(1); TestUtils.Simulate.keyDown($$('.ant-modal')[0], { keyCode: KeyCode.ESC, }); - act(() => { - jest.runAllTimers(); - }); - await sleep(0); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(0); expect($$(`.ant-modal-confirm-confirm`)).toHaveLength(0); expect(onCancel).toHaveBeenCalledTimes(1); - jest.useRealTimers(); }); it('should not hide confirm when onOk return Promise.resolve', async () => { - open({ + await open({ onOk: () => Promise.resolve(''), }); - await sleep(); $$('.ant-btn-primary')[0].click(); expect($$('.ant-modal-confirm')).toHaveLength(1); }); it('should emit error when onOk return Promise.reject', async () => { const error = new Error('something wrong'); - open({ + await open({ onOk: () => Promise.reject(error), }); - await sleep(); $$('.ant-btn-primary')[0].click(); // wait promise - await sleep(); + await waitFakeTimer(); expect(errorSpy).toHaveBeenCalledWith(error); }); it('shows animation when close', async () => { - open(); - - jest.useFakeTimers(); - act(() => { - jest.runAllTimers(); - }); - await sleep(); - act(() => { - jest.runAllTimers(); - }); + await open(); expect($$('.ant-modal-confirm')).toHaveLength(1); - await sleep(); + await waitFakeTimer(); + $$('.ant-btn')[0].click(); - act(() => { - jest.runAllTimers(); - }); - await sleep(); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect($$('.ant-modal-confirm')).toHaveLength(0); - jest.useRealTimers(); }); it('ok only', async () => { - open({ okCancel: false }); - await sleep(); + await open({ okCancel: false }); expect($$('.ant-btn')).toHaveLength(1); expect($$('.ant-btn')[0].innerHTML).toContain('OK'); }); it('allows extra props on buttons', async () => { - open({ + await open({ okButtonProps: { disabled: true }, cancelButtonProps: { 'data-test': 'baz' } as ModalFuncProps['cancelButtonProps'], }); - await sleep(); expect($$('.ant-btn')).toHaveLength(2); expect(($$('.ant-btn')[0].attributes as any)['data-test'].value).toBe('baz'); expect(($$('.ant-btn')[1] as HTMLButtonElement).disabled).toBe(true); @@ -258,29 +216,18 @@ describe('Modal.confirm triggers callbacks correctly', () => { describe('should close modals when click confirm button', () => { (['info', 'success', 'warning', 'error'] as const).forEach(type => { it(type, async () => { - jest.useFakeTimers(); Modal[type]?.({ title: 'title', content: 'content' }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - jest.runAllTimers(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); $$('.ant-btn')[0].click(); - await act(async () => { - jest.runAllTimers(); - await sleep(); - jest.runAllTimers(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0); - jest.useRealTimers(); }); }); }); it('should close confirm modal when click cancel button', async () => { - jest.useFakeTimers(); const onCancel = jest.fn(); Modal.confirm({ // test legacy visible @@ -289,22 +236,15 @@ describe('Modal.confirm triggers callbacks correctly', () => { content: 'content', onCancel, }); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-confirm`)).toHaveLength(1); $$('.ant-btn')[0].click(); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-confirm`)).toHaveLength(0); expect(onCancel).toHaveBeenCalledTimes(1); - jest.useRealTimers(); }); it('should close confirm modal when click close button', async () => { - jest.useFakeTimers(); const onCancel = jest.fn(); Modal.confirm({ title: 'title', @@ -313,42 +253,28 @@ describe('Modal.confirm triggers callbacks correctly', () => { closeIcon: 'X', onCancel, }); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-close`)).toHaveLength(1); $$('.ant-btn')[0].click(); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-close`)).toHaveLength(0); expect(onCancel).toHaveBeenCalledTimes(1); - jest.useRealTimers(); }); describe('should not close modals when click confirm button when onOk has argument', () => { (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach(type => { it(type, async () => { - jest.useFakeTimers(); Modal[type]?.({ title: 'title', content: 'content', onOk: _ => null, // eslint-disable-line no-unused-vars }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); $$('.ant-btn-primary')[0].click(); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); - jest.useRealTimers(); }); }); }); @@ -356,15 +282,11 @@ describe('Modal.confirm triggers callbacks correctly', () => { describe('could be update by new config', () => { (['info', 'success', 'warning', 'error'] as const).forEach(type => { it(type, async () => { - jest.useFakeTimers(); const instance = Modal[type]?.({ title: 'title', content: 'content', }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); expect($$('.ant-modal-confirm-title')[0].innerHTML).toBe('title'); expect($$('.ant-modal-confirm-content')[0].innerHTML).toBe('content'); @@ -373,33 +295,25 @@ describe('Modal.confirm triggers callbacks correctly', () => { content: 'new content', }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); expect($$('.ant-modal-confirm-title')[0].innerHTML).toBe('new title'); expect($$('.ant-modal-confirm-content')[0].innerHTML).toBe('new content'); instance.destroy(); - act(() => { - jest.runAllTimers(); - }); - jest.useRealTimers(); + await waitFakeTimer(); + expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0); }); }); }); describe('could be update by call function', () => { (['info', 'success', 'warning', 'error'] as const).forEach(type => { - it(type, () => { - jest.useFakeTimers(); + it(type, async () => { const instance = Modal[type]?.({ title: 'title', okButtonProps: { loading: true, style: { color: 'red' } }, }); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); expect($$('.ant-modal-confirm-title')[0].innerHTML).toBe('title'); expect($$('.ant-modal-confirm-btns .ant-btn-primary')[0].classList).toContain( @@ -413,9 +327,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { loading: false, }, })); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); expect($$('.ant-modal-confirm-title')[0].innerHTML).toBe('title'); expect($$('.ant-modal-confirm-btns .ant-btn-primary')[0].classList).not.toContain( @@ -423,42 +335,31 @@ describe('Modal.confirm triggers callbacks correctly', () => { ); expect($$('.ant-modal-confirm-btns .ant-btn-primary')[0].style.color).toBe('red'); instance.destroy(); - act(() => { - jest.runAllTimers(); - }); - jest.useRealTimers(); + + await waitFakeTimer(); + expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0); }); }); }); describe('could be destroy', () => { (['info', 'success', 'warning', 'error'] as const).forEach(type => { - jest.useFakeTimers(); it(type, async () => { const instance = Modal[type]?.({ title: 'title', content: 'content', }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); instance.destroy(); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0); }); - jest.useRealTimers(); }); }); it('could be Modal.destroyAll', async () => { - jest.useFakeTimers(); - // Show (['info', 'success', 'warning', 'error'] as const).forEach(type => { Modal[type]?.({ @@ -467,10 +368,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); ['info', 'success', 'warning', 'error'].forEach(type => { expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); @@ -479,39 +377,31 @@ describe('Modal.confirm triggers callbacks correctly', () => { // Destroy Modal.destroyAll(); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); ['info', 'success', 'warning', 'error'].forEach(type => { expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0); }); - jest.useRealTimers(); }); it('prefixCls', async () => { - open({ prefixCls: 'custom-modal' }); + await open({ prefixCls: 'custom-modal' }); - await sleep(); expect($$('.custom-modal-mask')).toHaveLength(1); expect($$('.custom-modal-wrap')).toHaveLength(1); expect($$('.custom-modal-confirm')).toHaveLength(1); expect($$('.custom-modal-confirm-body-wrapper')).toHaveLength(1); }); - it('should be Modal.confirm without mask', () => { - open({ mask: false }); + it('should be Modal.confirm without mask', async () => { + await open({ mask: false }); expect($$('.ant-modal-mask')).toHaveLength(0); }); - it('destroyFns should reduce when instance.destroy', () => { - jest.useFakeTimers(); - + it('destroyFns should reduce when instance.destroy', async () => { Modal.destroyAll(); // clear destroyFns - act(() => { - jest.runAllTimers(); - }); + + await waitFakeTimer(); const instances: ReturnType[] = []; (['info', 'success', 'warning', 'error'] as const).forEach(type => { @@ -537,22 +427,16 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); expect(destroyFns.length).toBe(length - index - 1); }); - - jest.useRealTimers(); }); it('should warning when pass a string as icon props', async () => { - jest.useFakeTimers(); const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); confirm({ content: 'some descriptions', icon: 'ab', }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect(warnSpy).not.toHaveBeenCalled(); confirm({ @@ -560,16 +444,12 @@ describe('Modal.confirm triggers callbacks correctly', () => { icon: 'question', }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect(warnSpy).toHaveBeenCalledWith( `Warning: [antd: Modal] \`icon\` is using ReactNode instead of string naming in v4. Please check \`question\` at https://ant.design/components/icon`, ); warnSpy.mockRestore(); - jest.useRealTimers(); }); it('icon can be null to hide icon', async () => { @@ -580,10 +460,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { icon: null, }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); // We check icon is not exist in the body expect(document.querySelector('.ant-modal-confirm-body')!.children).toHaveLength(2); @@ -596,9 +473,8 @@ describe('Modal.confirm triggers callbacks correctly', () => { it('ok button should trigger onOk once when click it many times quickly', async () => { const onOk = jest.fn(); - open({ onOk }); + await open({ onOk }); - await sleep(); $$('.ant-btn-primary')[0].click(); $$('.ant-btn-primary')[0].click(); expect(onOk).toHaveBeenCalledTimes(1); @@ -607,7 +483,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { // https://github.com/ant-design/ant-design/issues/23358 it('ok button should trigger onOk multiple times when onOk has close argument', async () => { const onOk = jest.fn(); - open({ + await open({ onOk(close?: any) { onOk(); // @ts-ignore @@ -615,7 +491,6 @@ describe('Modal.confirm triggers callbacks correctly', () => { }, }); - await sleep(); $$('.ant-btn-primary')[0].click(); $$('.ant-btn-primary')[0].click(); $$('.ant-btn-primary')[0].click(); @@ -623,28 +498,21 @@ describe('Modal.confirm triggers callbacks correctly', () => { }); it('should be able to global config rootPrefixCls', async () => { - jest.useFakeTimers(); ConfigProvider.config({ prefixCls: 'my', iconPrefixCls: 'bamboo' }); confirm({ title: 'title', icon: }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect(document.querySelectorAll('.ant-btn').length).toBe(0); expect(document.querySelectorAll('.my-btn').length).toBe(2); expect(document.querySelectorAll('.bamboo-smile').length).toBe(1); expect(document.querySelectorAll('.my-modal-confirm').length).toBe(1); ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: undefined }); - jest.useRealTimers(); }); it('should be able to config rootPrefixCls', async () => { resetWarned(); - jest.useFakeTimers(); - Modal.config({ rootPrefixCls: 'my', }); @@ -656,10 +524,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { title: 'title', }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect(document.querySelectorAll('.ant-btn').length).toBe(0); expect(document.querySelectorAll('.my-btn').length).toBe(2); @@ -671,10 +536,7 @@ describe('Modal.confirm triggers callbacks correctly', () => { title: 'title', }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect(document.querySelectorAll('.ant-btn').length).toBe(0); expect(document.querySelectorAll('.my-btn').length).toBe(2); @@ -684,40 +546,36 @@ describe('Modal.confirm triggers callbacks correctly', () => { Modal.config({ rootPrefixCls: '', }); - jest.useRealTimers(); }); it('trigger afterClose once when click on cancel button', async () => { const afterClose = jest.fn(); - open({ + await open({ afterClose, }); // first Modal - await sleep(); $$('.ant-btn')[0].click(); expect(afterClose).not.toHaveBeenCalled(); - await sleep(500); + await waitFakeTimer(500); expect(afterClose).toHaveBeenCalled(); }); it('trigger afterClose once when click on ok button', async () => { const afterClose = jest.fn(); - open({ + await open({ afterClose, }); // second Modal - await sleep(); $$('.ant-btn-primary')[0].click(); expect(afterClose).not.toHaveBeenCalled(); - await sleep(500); + await waitFakeTimer(500); expect(afterClose).toHaveBeenCalled(); }); it('bodyStyle', async () => { - open({ bodyStyle: { width: 500 } }); + await open({ bodyStyle: { width: 500 } }); - await sleep(); const { width } = $$('.ant-modal-body')[0].style; expect(width).toBe('500px'); }); @@ -725,7 +583,6 @@ describe('Modal.confirm triggers callbacks correctly', () => { describe('the callback close should be a method when onCancel has a close parameter', () => { (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach(type => { it(`click the close icon to trigger ${type} onCancel`, async () => { - jest.useFakeTimers(); const mock = jest.fn(); Modal[type]?.({ @@ -733,29 +590,20 @@ describe('Modal.confirm triggers callbacks correctly', () => { onCancel: close => mock(close), }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); $$('.ant-modal-close')[0].click(); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0); expect(mock).toHaveBeenCalledWith(expect.any(Function)); - - jest.useRealTimers(); }); }); (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach(type => { it(`press ESC to trigger ${type} onCancel`, async () => { - jest.useFakeTimers(); const mock = jest.fn(); Modal[type]?.({ @@ -763,37 +611,22 @@ describe('Modal.confirm triggers callbacks correctly', () => { onCancel: close => mock(close), }); - act(() => { - jest.runAllTimers(); - }); - await sleep(); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); TestUtils.Simulate.keyDown($$('.ant-modal')[0], { keyCode: KeyCode.ESC, }); - act(() => { - jest.runAllTimers(); - }); - await sleep(0); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(0); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0); expect(mock).toHaveBeenCalledWith(expect.any(Function)); - - jest.useRealTimers(); }); }); (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach(type => { it(`click the mask to trigger ${type} onCancel`, async () => { - jest.useFakeTimers(); const mock = jest.fn(); Modal[type]?.({ @@ -801,104 +634,73 @@ describe('Modal.confirm triggers callbacks correctly', () => { onCancel: close => mock(close), }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$('.ant-modal-mask')).toHaveLength(1); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); $$('.ant-modal-wrap')[0].click(); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0); expect(mock).toHaveBeenCalledWith(expect.any(Function)); - - jest.useRealTimers(); }); }); }); it('confirm modal click Cancel button close callback is a function', async () => { - jest.useFakeTimers(); const mock = jest.fn(); Modal.confirm({ onCancel: close => mock(close), }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); $$('.ant-modal-confirm-btns > .ant-btn')[0].click(); - - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect(mock).toHaveBeenCalledWith(expect.any(Function)); - - jest.useRealTimers(); }); it('close can close modal when onCancel has a close parameter', async () => { - jest.useFakeTimers(); - Modal.confirm({ onCancel: close => close(), }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$('.ant-modal-confirm-confirm')).toHaveLength(1); $$('.ant-modal-confirm-btns > .ant-btn')[0].click(); - - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$('.ant-modal-confirm-confirm')).toHaveLength(0); - - jest.useRealTimers(); }); // https://github.com/ant-design/ant-design/issues/37461 it('Update should closable', async () => { + resetWarned(); jest.useFakeTimers(); + const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - Modal.confirm({}).update({ - open: true, + const modal = Modal.confirm({}); + + modal.update({ + visible: true, }); - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$('.ant-modal-confirm-confirm')).toHaveLength(1); $$('.ant-modal-confirm-btns > .ant-btn')[0].click(); - - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); expect($$('.ant-modal-confirm-confirm')).toHaveLength(0); jest.useRealTimers(); + errSpy.mockRestore(); }); }); diff --git a/components/modal/__tests__/hook.test.tsx b/components/modal/__tests__/hook.test.tsx index 2cb7a43c7b..e4db711ec1 100644 --- a/components/modal/__tests__/hook.test.tsx +++ b/components/modal/__tests__/hook.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import TestUtils, { act } from 'react-dom/test-utils'; import Modal from '..'; -import { fireEvent, render, sleep } from '../../../tests/utils'; +import { fireEvent, render, waitFakeTimer } from '../../../tests/utils'; import Button from '../../button'; import ConfigProvider from '../../config-provider'; import Input from '../../input'; @@ -200,13 +200,11 @@ describe('Modal.hook', () => { jest.useFakeTimers(); const clear = async function clear() { - await act(async () => { - jest.runAllTimers(); - await sleep(); - }); + await waitFakeTimer(); }; const mockFn = jest.fn(); + const Demo = () => { const [modal, contextHolder] = Modal.useModal(); diff --git a/components/modal/style/index.tsx b/components/modal/style/index.tsx index 0e58721323..cea122b409 100644 --- a/components/modal/style/index.tsx +++ b/components/modal/style/index.tsx @@ -348,6 +348,11 @@ const genModalConfirmStyle: GenerateStyle = token => { [`${confirmComponentCls}-success ${confirmComponentCls}-body > ${token.iconCls}`]: { color: token.colorSuccess, }, + + // https://github.com/ant-design/ant-design/issues/37329 + [`${componentCls}-zoom-leave ${componentCls}-btns`]: { + pointerEvents: 'none', + }, }; }; diff --git a/components/pagination/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/pagination/__tests__/__snapshots__/demo-extend.test.ts.snap index bc1913a326..0d441e2e82 100644 --- a/components/pagination/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/pagination/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -3,7 +3,6 @@ exports[`renders ./components/pagination/demo/all.md extend context correctly 1`] = `
                                                                              • ,
                                                                                • ,
                                                                                  • ,
                                                                                    • ,
                                                                                      • ,
                                                                                        • ,
                                                                                          • ,
                                                                                            • ,
                                                                                              • ,
                                                                                                • ,
                                                                                                  • ,
                                                                                                    • ,
                                                                                                      • { + it('should support showTotal in simple mode', () => { + const { container } = render( + `${range[0]}-${range[1]} of ${total} items`} + />, + ); + expect(container?.querySelector('.ant-pagination-total-text')).toHaveTextContent( + '1-10 of 100 items', + ); + }); +}); diff --git a/components/popconfirm/__tests__/__snapshots__/index.test.tsx.snap b/components/popconfirm/__tests__/__snapshots__/index.test.tsx.snap index 0def59782f..a2f125ceb4 100644 --- a/components/popconfirm/__tests__/__snapshots__/index.test.tsx.snap +++ b/components/popconfirm/__tests__/__snapshots__/index.test.tsx.snap @@ -2,8 +2,8 @@ exports[`Popconfirm rtl render component should be rendered correctly in RTL direction 1`] = ``; -exports[`Popconfirm should show overlay when trigger is clicked 1`] = `"
                                                                                                        code
                                                                                                        "`; +exports[`Popconfirm should show overlay when trigger is clicked 1`] = `"
                                                                                                        "`; -exports[`Popconfirm should show overlay when trigger is clicked 2`] = `"
                                                                                                        code
                                                                                                        "`; +exports[`Popconfirm should show overlay when trigger is clicked 2`] = `"
                                                                                                        "`; -exports[`Popconfirm shows content for render functions 1`] = `"
                                                                                                        some-title
                                                                                                        "`; +exports[`Popconfirm shows content for render functions 1`] = `"
                                                                                                        "`; diff --git a/components/popover/__tests__/__snapshots__/index.test.tsx.snap b/components/popover/__tests__/__snapshots__/index.test.tsx.snap index 02f804ef41..048e36abfd 100644 --- a/components/popover/__tests__/__snapshots__/index.test.tsx.snap +++ b/components/popover/__tests__/__snapshots__/index.test.tsx.snap @@ -41,4 +41,4 @@ Array [ ] `; -exports[`Popover shows content for render functions 1`] = `"
                                                                                                        some-title
                                                                                                        some-content
                                                                                                        "`; +exports[`Popover shows content for render functions 1`] = `"
                                                                                                        "`; diff --git a/components/progress/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/progress/__tests__/__snapshots__/demo-extend.test.ts.snap index bf33954c87..b0077fff80 100644 --- a/components/progress/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/progress/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -12,36 +12,37 @@ Array [ >
                                                                                                        @@ -396,36 +401,37 @@ Array [ >
                                                                                                        @@ -367,36 +372,37 @@ Array [ > - No Data + No data
            @@ -7795,7 +7795,7 @@ exports[`renders ./components/select/demo/status.md extend context correctly 1`]
            - No Data + No data
            diff --git a/components/space/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/space/__tests__/__snapshots__/demo-extend.test.ts.snap index 18cb7208b9..15ff35297c 100644 --- a/components/space/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/space/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -10371,7 +10371,7 @@ exports[`renders ./components/space/demo/compact-debug.md extend context correct
            - No Data + No data
            diff --git a/components/statistic/__tests__/index.test.tsx b/components/statistic/__tests__/index.test.tsx index 75dac8c092..bd022f193b 100644 --- a/components/statistic/__tests__/index.test.tsx +++ b/components/statistic/__tests__/index.test.tsx @@ -4,7 +4,7 @@ import React from 'react'; import Statistic from '..'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; -import { fireEvent, render, sleep } from '../../../tests/utils'; +import { fireEvent, render, waitFakeTimer } from '../../../tests/utils'; import { formatTimeStr } from '../utils'; describe('Statistic', () => { @@ -102,15 +102,18 @@ describe('Statistic', () => { }); it('time going', async () => { + jest.useFakeTimers(); const now = Date.now() + 1000; const onFinish = jest.fn(); const { unmount } = render(); - await sleep(10); + await waitFakeTimer(10); unmount(); expect(onFinish).not.toHaveBeenCalled(); + jest.clearAllTimers(); + jest.useRealTimers(); }); it('responses hover events', () => { @@ -139,6 +142,7 @@ describe('Statistic', () => { describe('time onchange', () => { it("called if time has't passed", async () => { + jest.useFakeTimers(); const deadline = Date.now() + 10 * 1000; let remainingTime; @@ -147,8 +151,10 @@ describe('Statistic', () => { }; render(); // container.update(); - await sleep(100); + await waitFakeTimer(100); expect(remainingTime).toBeGreaterThan(0); + jest.clearAllTimers(); + jest.useRealTimers(); }); }); @@ -162,12 +168,14 @@ describe('Statistic', () => { }); it('called if finished', async () => { + jest.useFakeTimers(); const now = Date.now() + 10; const onFinish = jest.fn(); render(); - MockDate.set(dayjs('2019-11-28 00:00:00').valueOf()); - await sleep(100); + await waitFakeTimer(); expect(onFinish).toHaveBeenCalled(); + jest.clearAllTimers(); + jest.useRealTimers(); }); }); }); diff --git a/components/steps/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/steps/__tests__/__snapshots__/demo-extend.test.ts.snap index 4683ebd12f..73345d4236 100644 --- a/components/steps/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/steps/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -1207,36 +1207,37 @@ Array [ > { @@ -33,6 +33,46 @@ describe('Steps', () => { expect(container.firstChild).toMatchSnapshot(); }); + it('items out of render function', () => { + const items = [ + { + title: '已完成', + }, + { + title: '进行中', + }, + { + title: '待运行', + description: 'Hello World!', + }, + { + title: '待运行', + }, + ]; + const ControlSteps = () => { + const [current, setCurrent] = React.useState(0); + return ( + { + // eslint-disable-next-line no-console + console.log('Change:', val); + setCurrent(val); + }} + items={items} + /> + ); + }; + const { container } = render(); + expect( + container.querySelectorAll('.ant-steps-item')[1].classList.contains('ant-steps-item-process'), + ).toBe(false); + fireEvent.click(screen.getByText(/进行中/i)); + expect( + container.querySelectorAll('.ant-steps-item')[1].classList.contains('ant-steps-item-process'), + ).toBe(true); + }); + it('should render correct when use Step', () => { const { container } = render( diff --git a/components/steps/demo/deprecated.md b/components/steps/demo/deprecated.md index 381c0d11d7..1b9857de3b 100644 --- a/components/steps/demo/deprecated.md +++ b/components/steps/demo/deprecated.md @@ -2,7 +2,7 @@ order: -1 title: zh-CN: 基本用法(废弃的语法糖) - en-US: Basic + en-US: Basic (deprecated syntactic sugar) version: < 4.24.0 --- diff --git a/components/steps/index.en-US.md b/components/steps/index.en-US.md index 8ff25a2a98..8a89614846 100644 --- a/components/steps/index.en-US.md +++ b/components/steps/index.en-US.md @@ -12,7 +12,12 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/UZYqMizXHaj/Steps.svg When a given task is complicated or has a certain sequence in the series of subtasks, we can decompose it into several steps to make things easier. -## API +### Usage upgrade after 4.24.0 + +```__react +import Alert from '../alert'; +ReactDOM.render(, mountNode); +``` ```jsx // works when >=4.24.0, recommended ✅ @@ -27,6 +32,8 @@ return ; ; ``` +## API + ### Steps The whole of the step bar. diff --git a/components/steps/index.zh-CN.md b/components/steps/index.zh-CN.md index 58c77e0d0d..9f7bfcf6de 100644 --- a/components/steps/index.zh-CN.md +++ b/components/steps/index.zh-CN.md @@ -33,6 +33,8 @@ return ; ; ``` +## API + ### Steps 整体步骤条。 diff --git a/components/switch/__tests__/index.test.tsx b/components/switch/__tests__/index.test.tsx index f5b9835864..21870ef3cf 100644 --- a/components/switch/__tests__/index.test.tsx +++ b/components/switch/__tests__/index.test.tsx @@ -3,7 +3,7 @@ import Switch from '..'; import focusTest from '../../../tests/shared/focusTest'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; -import { sleep, fireEvent, render } from '../../../tests/utils'; +import { waitFakeTimer, fireEvent, render } from '../../../tests/utils'; import { resetWarned } from '../../_util/warning'; describe('Switch', () => { @@ -12,10 +12,13 @@ describe('Switch', () => { rtlTest(Switch); it('should has click wave effect', async () => { + jest.useFakeTimers(); const { container } = render(); fireEvent.click(container.querySelector('.ant-switch')!); - await sleep(0); + await waitFakeTimer(); expect(container.querySelector('button')!.getAttribute('ant-click-animating')).toBe('true'); + jest.clearAllTimers(); + jest.useRealTimers(); }); it('warning if set `value`', () => { diff --git a/components/switch/__tests__/wave.test.tsx b/components/switch/__tests__/wave.test.tsx index 6d18ac3f74..deb8333927 100644 --- a/components/switch/__tests__/wave.test.tsx +++ b/components/switch/__tests__/wave.test.tsx @@ -1,17 +1,18 @@ import React from 'react'; import Switch from '..'; -import { sleep, render, fireEvent } from '../../../tests/utils'; +import { waitFakeTimer, render, fireEvent } from '../../../tests/utils'; describe('click wave effect', () => { async function click(container: HTMLElement) { fireEvent.click(container.querySelector('.ant-switch')!); container.querySelector('.ant-switch')!.dispatchEvent(new Event('transitionstart')); - await sleep(20); + await waitFakeTimer(); container.querySelector('.ant-switch')!.dispatchEvent(new Event('animationend')); - await sleep(20); + await waitFakeTimer(); } it('should have click wave effect', async () => { + jest.useFakeTimers(); const { container } = render(); await click(container); await click(container); @@ -23,5 +24,7 @@ describe('click wave effect', () => { const event = new Event('animationend'); Object.assign(event, { animationName: 'fadeEffect' }); container.querySelector('.ant-switch')!.dispatchEvent(event); + jest.clearAllTimers(); + jest.useRealTimers(); }); }); diff --git a/components/table/__tests__/Table.sorter.test.tsx b/components/table/__tests__/Table.sorter.test.tsx index 8b0d93c5c5..6238c4f38e 100644 --- a/components/table/__tests__/Table.sorter.test.tsx +++ b/components/table/__tests__/Table.sorter.test.tsx @@ -105,33 +105,9 @@ describe('Table.sorter', () => { fireEvent.click(container.querySelector('.ant-table-column-sorters')!); expect(getNameColumn()?.getAttribute('aria-sort')).toEqual(null); - expect(getNameColumn()?.getAttribute('aria-label')).toEqual('Name sortable'); - }); - - it('aria-label should be use the first text content in element when title is ReactElement', () => { - const { container } = render( - createTable( - { - sortDirections: ['descend', 'ascend'], - }, - { - title: ( - - Name - kiner - - ), - defaultSortOrder: 'descend', - }, - ), + expect(getNameColumn()?.getAttribute('aria-label')).toEqual( + "this column's title is Name,this column is sortable", ); - - const getNameColumn = () => container.querySelector('th'); - fireEvent.click(container.querySelector('.ant-table-column-sorters')!); - expect(getNameColumn()?.getAttribute('aria-sort')).toEqual('ascending'); - expect(getNameColumn()?.getAttribute('aria-label')).toEqual(null); - fireEvent.click(container.querySelector('.ant-table-column-sorters')!); - expect(getNameColumn()?.getAttribute('aria-label')).toEqual('Name sortable'); }); it('sort records', () => { diff --git a/components/table/__tests__/Table.test.tsx b/components/table/__tests__/Table.test.tsx index 1d8531c826..8caff8c6d5 100644 --- a/components/table/__tests__/Table.test.tsx +++ b/components/table/__tests__/Table.test.tsx @@ -286,4 +286,71 @@ describe('Table', () => { render(); expect(warnSpy).not.toHaveBeenCalled(); }); + + // https://github.com/ant-design/ant-design/issues/38371 + it('should render title', () => { + const columns = [ + { + title: ( +
            + name + Jason +
            + ), + key: 'name', + sorter: true, + }, + { + title: ( +
            + +
            + ), + key: 'name', + sorter: true, + }, + { + title: () => ( +
            + age + 20 +
            + ), + key: 'name', + sorter: true, + }, + { + title: () => 'color', + key: 'name', + sorter: true, + }, + { + title: 'sex', + key: 'name', + sorter: true, + }, + ]; + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('title should support ReactNode', () => { + const { container } = render( +
            + CurrentUser + + ), + dataIndex: 'name', + }, + ]} + dataSource={[]} + />, + ); + + expect(container.querySelector('thead th')).toMatchSnapshot(); + }); }); diff --git a/components/table/__tests__/__snapshots__/Table.expand.test.tsx.snap b/components/table/__tests__/__snapshots__/Table.expand.test.tsx.snap index bc97b122e2..9ff96c971c 100644 --- a/components/table/__tests__/__snapshots__/Table.expand.test.tsx.snap +++ b/components/table/__tests__/__snapshots__/Table.expand.test.tsx.snap @@ -82,7 +82,6 @@ exports[`Table.expand click to expand 1`] = `
                  • - No Data + No data diff --git a/components/table/__tests__/__snapshots__/Table.rowSelection.test.tsx.snap b/components/table/__tests__/__snapshots__/Table.rowSelection.test.tsx.snap index 984c18e549..39f96c95d8 100644 --- a/components/table/__tests__/__snapshots__/Table.rowSelection.test.tsx.snap +++ b/components/table/__tests__/__snapshots__/Table.rowSelection.test.tsx.snap @@ -53,6 +53,7 @@ exports[`Table.rowSelection fix expand on th left when selection column fixed on class="ant-checkbox" >
            +`; diff --git a/components/table/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/table/__tests__/__snapshots__/demo-extend.test.ts.snap index 8ec70f17cb..8874cab5e3 100644 --- a/components/table/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/table/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -35,7 +35,7 @@ exports[`renders ./components/table/demo/ajax.md extend context correctly 1`] = >
            @@ -180,7 +180,6 @@ exports[`Table.sorter should support defaultOrder in Column 1`] = `
            • - No Data + No data @@ -203,3 +203,437 @@ exports[`Table rtl render component should be rendered correctly in RTL directio `; + +exports[`Table should render title 1`] = ` +
              +
              +
              +
              +
              +
              +
              + + + + + + + + + + + + + + + + +
              +
              + +
              + + name + + + Jason + +
              +
              + + + + + + + + + + +
              +
              +
              + +
              + +
              +
              + + + + + + + + + + +
              +
              +
              + +
              + + age + + + 20 + +
              +
              + + + + + + + + + + +
              +
              +
              + + color + + + + + + + + + + + +
              +
              +
              + + sex + + + + + + + + + + + +
              +
              +
              +
              + + + + + + + + + +
              +
              + No data +
              +
              +
              +
              +
              +
              +
              +
              +
              +
              +`; + +exports[`Table title should support ReactNode 1`] = ` +
            +
            + + Current + + + User + +
            +
            @@ -404,7 +404,7 @@ exports[`renders ./components/table/demo/ajax.md extend context correctly 1`] =
            - No Data + No data
            @@ -661,7 +661,6 @@ exports[`renders ./components/table/demo/basic.md extend context correctly 1`] =
            @@ -1855,7 +1852,6 @@ exports[`renders ./components/table/demo/custom-filter-panel.md extend context c
              • @@ -3382,7 +3378,7 @@ Array [ Name
            @@ -3693,7 +3689,7 @@ Array [ @@ -4834,7 +4830,6 @@ Array [
            @@ -8097,7 +8087,6 @@ exports[`renders ./components/table/demo/filter-in-tree.md extend context correc
            @@ -8918,7 +8907,6 @@ exports[`renders ./components/table/demo/filter-search.md extend context correct
            @@ -14308,7 +14292,6 @@ exports[`renders ./components/table/demo/head.md extend context correctly 1`] =
            @@ -14860,7 +14842,7 @@ exports[`renders ./components/table/demo/multiple-sorter.md extend context corre @@ -14945,7 +14927,7 @@ exports[`renders ./components/table/demo/multiple-sorter.md extend context corre @@ -15141,7 +15123,6 @@ exports[`renders ./components/table/demo/multiple-sorter.md extend context corre
                      • @@ -19721,7 +19698,6 @@ exports[`renders ./components/table/demo/order-column.md extend context correctl
            @@ -21607,7 +21580,6 @@ exports[`renders ./components/table/demo/resizable-column.md extend context corr
              • @@ -22127,7 +22099,6 @@ exports[`renders ./components/table/demo/row-selection.md extend context correct
                • @@ -22699,7 +22671,6 @@ exports[`renders ./components/table/demo/row-selection-and-operation.md extend c
                  • @@ -23619,7 +23591,6 @@ exports[`renders ./components/table/demo/row-selection-custom.md extend context
                    • @@ -24032,7 +24004,6 @@ exports[`renders ./components/table/demo/row-selection-custom-debug.md extend co
                            • @@ -26700,7 +26669,6 @@ Array [
                              • @@ -27419,7 +27388,6 @@ Array [
            @@ -192,7 +192,7 @@ exports[`renders ./components/table/demo/ajax.md correctly 1`] = `
            - No Data + No data
            @@ -449,7 +449,6 @@ exports[`renders ./components/table/demo/basic.md correctly 1`] = `
            @@ -1325,7 +1322,6 @@ exports[`renders ./components/table/demo/custom-filter-panel.md correctly 1`] =
              • @@ -2852,7 +2848,7 @@ Array [ Name
            @@ -2951,7 +2947,7 @@ Array [ @@ -4068,7 +4064,6 @@ Array [
            @@ -6119,7 +6109,6 @@ exports[`renders ./components/table/demo/filter-in-tree.md correctly 1`] = `
            @@ -6466,7 +6455,6 @@ exports[`renders ./components/table/demo/filter-search.md correctly 1`] = `
            @@ -10675,7 +10659,6 @@ exports[`renders ./components/table/demo/head.md correctly 1`] = `
            @@ -11203,7 +11185,7 @@ exports[`renders ./components/table/demo/multiple-sorter.md correctly 1`] = ` @@ -11264,7 +11246,7 @@ exports[`renders ./components/table/demo/multiple-sorter.md correctly 1`] = ` @@ -11436,7 +11418,6 @@ exports[`renders ./components/table/demo/multiple-sorter.md correctly 1`] = `
                      • @@ -14739,7 +14716,6 @@ exports[`renders ./components/table/demo/order-column.md correctly 1`] = `
            @@ -16153,7 +16126,6 @@ exports[`renders ./components/table/demo/resizable-column.md correctly 1`] = `
              • @@ -16673,7 +16645,6 @@ exports[`renders ./components/table/demo/row-selection.md correctly 1`] = `
                • @@ -17245,7 +17217,6 @@ exports[`renders ./components/table/demo/row-selection-and-operation.md correctl
                  • @@ -17871,7 +17843,6 @@ exports[`renders ./components/table/demo/row-selection-custom.md correctly 1`] =
                    • @@ -18284,7 +18256,6 @@ exports[`renders ./components/table/demo/row-selection-custom-debug.md correctly
                            • @@ -20836,7 +20805,6 @@ Array [
                              • @@ -21555,7 +21524,6 @@ Array [
                                • - No Data + No data @@ -484,7 +484,7 @@ exports[`Table renders empty table with fixed columns should work 1`] = `
                                  - No Data + No data
                                  @@ -645,7 +645,7 @@ exports[`Table renders empty table without emptyText when loading 1`] = `
                                  - No Data + No data
                                  diff --git a/components/table/demo/custom-filter-panel.md b/components/table/demo/custom-filter-panel.md index f7f5058a76..be69bd5f2b 100644 --- a/components/table/demo/custom-filter-panel.md +++ b/components/table/demo/custom-filter-panel.md @@ -84,7 +84,7 @@ const App: React.FC = () => { const getColumnSearchProps = (dataIndex: DataIndex): ColumnType => ({ filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( -
                                  +
                                  e.stopPropagation()}> -); +const items = [ + { key: '1', label: 'Action 1' }, + { key: '2', label: 'Action 2' }, +]; const App: React.FC = () => { const createExpandedRowRender = (bordered: boolean) => () => { @@ -70,7 +66,7 @@ const App: React.FC = () => { Pause Stop - + More diff --git a/components/table/demo/nested-table.md b/components/table/demo/nested-table.md index 718eb2508e..29179c438e 100644 --- a/components/table/demo/nested-table.md +++ b/components/table/demo/nested-table.md @@ -16,7 +16,7 @@ Showing more detailed info of every row. ```tsx import { DownOutlined } from '@ant-design/icons'; import type { TableColumnsType } from 'antd'; -import { Badge, Dropdown, Menu, Space, Table } from 'antd'; +import { Badge, Dropdown, Space, Table } from 'antd'; import React from 'react'; interface DataType { @@ -36,14 +36,10 @@ interface ExpandedDataType { upgradeNum: string; } -const menu = ( - -); +const items = [ + { key: '1', label: 'Action 1' }, + { key: '2', label: 'Action 2' }, +]; const App: React.FC = () => { const expandedRowRender = () => { @@ -69,7 +65,7 @@ const App: React.FC = () => { Pause Stop - + More diff --git a/components/table/demo/resizable-column.md b/components/table/demo/resizable-column.md index 4c30d204ff..369f74f344 100644 --- a/components/table/demo/resizable-column.md +++ b/components/table/demo/resizable-column.md @@ -114,7 +114,7 @@ const App: React.FC = () => { }, ]; - const handleResize = + const handleResize: Function = (index: number) => (_: React.SyntheticEvent, { size }: ResizeCallbackData) => { const newColumns = [...columns]; diff --git a/components/table/demo/virtual-list.md b/components/table/demo/virtual-list.md index bd8fd8335e..3d0c3f81ee 100644 --- a/components/table/demo/virtual-list.md +++ b/components/table/demo/virtual-list.md @@ -15,12 +15,13 @@ Integrate virtual scroll with `react-window` to achieve a high performance table ```tsx import { Table } from 'antd'; +import type { TableProps } from 'antd'; import classNames from 'classnames'; import ResizeObserver from 'rc-resize-observer'; import React, { useEffect, useRef, useState } from 'react'; import { VariableSizeGrid as Grid } from 'react-window'; -const VirtualTable = (props: Parameters[0]) => { +const VirtualTable = (props: TableProps) => { const { columns, scroll } = props; const [tableWidth, setTableWidth] = useState(0); diff --git a/components/table/hooks/useFilter/FilterDropdown.tsx b/components/table/hooks/useFilter/FilterDropdown.tsx index 189876af2d..301cac48b4 100644 --- a/components/table/hooks/useFilter/FilterDropdown.tsx +++ b/components/table/hooks/useFilter/FilterDropdown.tsx @@ -473,7 +473,7 @@ function FilterDropdown(props: FilterDropdownProps) { dropdownContent = {dropdownContent}; } - const menu = ( + const menu = () => ( {dropdownContent} @@ -494,7 +494,7 @@ function FilterDropdown(props: FilterDropdownProps) {
                                  {children} ( if (selectionType !== 'radio') { let customizeSelections: React.ReactNode; if (mergedSelections) { - const menu = ( - { - const { key, text, onSelect: onSelectionClick } = selection; + const menu = { + getPopupContainer, + items: mergedSelections.map((selection, index) => { + const { key, text, onSelect: onSelectionClick } = selection; - return { - key: key || index, - onClick: () => { - onSelectionClick?.(recordKeys); - }, - label: text, - }; - })} - /> - ); + return { + key: key || index, + onClick: () => { + onSelectionClick?.(recordKeys); + }, + label: text, + }; + }), + }; customizeSelections = (
                                  - + @@ -466,6 +463,7 @@ export default function useSelection( } onChange={onSelectAllChange} disabled={flattedData.length === 0 || allDisabled} + aria-label={customizeSelections ? 'Custom selection' : 'Select all'} skipGroup /> {customizeSelections} diff --git a/components/table/hooks/useSorter.tsx b/components/table/hooks/useSorter.tsx index 48f9807f14..125d7930b5 100644 --- a/components/table/hooks/useSorter.tsx +++ b/components/table/hooks/useSorter.tsx @@ -17,7 +17,7 @@ import type { TableLocale, TransformColumns, } from '../interface'; -import { getColumnKey, getColumnPos, renderColumnTitle } from '../util'; +import { getColumnKey, getColumnPos, renderColumnTitle, safeColumnTitle } from '../util'; const ASCEND = 'ascend'; const DESCEND = 'descend'; @@ -205,16 +205,21 @@ function injectSorter( } }; + const renderTitle = safeColumnTitle(column.title, {}); + const displayTitle = renderTitle?.toString(); + // Inform the screen-reader so it can tell the visually impaired user which column is sorted if (sorterOrder) { cell['aria-sort'] = sorterOrder === 'ascend' ? 'ascending' : 'descending'; } else { - cell['aria-label'] = `${renderColumnTitle(column.title, {})} sortable`; + cell['aria-label'] = `${ + displayTitle ? `this column's title is ${displayTitle},` : '' + }this column is sortable`; } cell.className = classNames(cell.className, `${prefixCls}-column-has-sorters`); cell.tabIndex = 0; if (column.ellipsis) { - cell.title = (renderColumnTitle(column.title, {}) ?? '').toString(); + cell.title = (renderTitle ?? '').toString(); } return cell; }, diff --git a/components/table/util.ts b/components/table/util.ts index e6394f186e..9870cf2cc2 100644 --- a/components/table/util.ts +++ b/components/table/util.ts @@ -1,5 +1,4 @@ /* eslint-disable import/prefer-default-export */ -import React from 'react'; import type { ColumnTitle, ColumnTitleProps, ColumnType, Key } from './interface'; export function getColumnKey(column: ColumnType, defaultKey: string): Key { @@ -17,22 +16,6 @@ export function getColumnPos(index: number, pos?: string) { return pos ? `${pos}-${index}` : `${index}`; } -/** - * Get first text content in Element - * - * @param node - * @returns - */ -function getElementFirstTextContent(node: React.ReactElement): string { - if (!node || !node.props || !node.props.children) return ''; - if (typeof node.props.children === 'string') return node.props.children; - return ( - node.props.children?.map?.((item: React.ReactElement) => - getElementFirstTextContent(item), - )?.[0] || '' - ); -} - export function renderColumnTitle( title: ColumnTitle, props: ColumnTitleProps, @@ -40,12 +23,23 @@ export function renderColumnTitle( if (typeof title === 'function') { return title(props); } - // fix: #38155 - if (React.isValidElement(title)) { - // if title is a React Element, we should get first text content as result, - // if there has not text content in React Element, return origin title - return getElementFirstTextContent(title) || title; - } return title; } + +/** + * Safe get column title + * + * Should filter [object Object] + * + * @param title + * @returns + */ +export function safeColumnTitle( + title: ColumnTitle, + props: ColumnTitleProps, +) { + const res = renderColumnTitle(title, props); + if (Object.prototype.toString.call(res) === '[object Object]') return ''; + return res; +} diff --git a/components/tooltip/__tests__/tooltip.test.tsx b/components/tooltip/__tests__/tooltip.test.tsx index 779aaf82e9..2787caeb46 100644 --- a/components/tooltip/__tests__/tooltip.test.tsx +++ b/components/tooltip/__tests__/tooltip.test.tsx @@ -4,7 +4,7 @@ import type { TooltipPlacement } from '..'; import Tooltip from '..'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; -import { fireEvent, render, sleep, waitFor, act } from '../../../tests/utils'; +import { fireEvent, render, waitFakeTimer, waitFor, act } from '../../../tests/utils'; import Button from '../../button'; import DatePicker from '../../date-picker'; import Input from '../../input'; @@ -240,6 +240,7 @@ describe('Tooltip', () => { }); it('should works for date picker', async () => { + jest.useFakeTimers(); const onOpenChange = jest.fn(); const ref = React.createRef(); @@ -253,19 +254,22 @@ describe('Tooltip', () => { const picker = container.getElementsByClassName('ant-picker')[0]; fireEvent.mouseEnter(picker); - await sleep(100); + await waitFakeTimer(); expect(onOpenChange).toHaveBeenCalledWith(true); expect(ref.current?.props.visible).toBe(true); expect(container.querySelector('.ant-tooltip-open')).not.toBeNull(); fireEvent.mouseLeave(picker); - await sleep(100); + await waitFakeTimer(); expect(onOpenChange).toHaveBeenCalledWith(false); expect(ref.current?.props.visible).toBe(false); expect(container.querySelector('.ant-tooltip-open')).toBeNull(); + jest.clearAllTimers(); + jest.useRealTimers(); }); it('should works for input group', async () => { + jest.useFakeTimers(); const onOpenChange = jest.fn(); const ref = React.createRef(); const { container } = render( @@ -280,16 +284,18 @@ describe('Tooltip', () => { expect(container.getElementsByClassName('ant-input-group')).toHaveLength(1); const inputGroup = container.getElementsByClassName('ant-input-group')[0]; fireEvent.mouseEnter(inputGroup); - await sleep(100); + await waitFakeTimer(); expect(onOpenChange).toHaveBeenCalledWith(true); expect(ref.current.props.visible).toBe(true); expect(container.querySelector('.ant-tooltip-open')).not.toBeNull(); fireEvent.mouseLeave(inputGroup); - await sleep(100); + await waitFakeTimer(); expect(onOpenChange).toHaveBeenCalledWith(false); expect(ref.current.props.visible).toBe(false); expect(container.querySelector('.ant-tooltip-open')).toBeNull(); + jest.clearAllTimers(); + jest.useRealTimers(); }); // https://github.com/ant-design/ant-design/issues/20891 @@ -321,6 +327,17 @@ describe('Tooltip', () => { }); describe('support other placement when mouse enter', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); const placementList = [ 'top', 'left', @@ -345,7 +362,7 @@ describe('Tooltip', () => { expect(container.getElementsByTagName('span')).toHaveLength(1); const element = container.getElementsByTagName('span')[0]; fireEvent.mouseEnter(element); - await sleep(500); + await waitFakeTimer(); await waitFor(() => { expect(document.querySelector(`.ant-tooltip-placement-${placement}`)).not.toBeNull(); }); @@ -369,7 +386,7 @@ describe('Tooltip', () => { ); const button = container.getElementsByTagName('span')[0]; fireEvent.mouseEnter(button); - await sleep(600); + await waitFakeTimer(); expect(document.querySelector('.ant-tooltip')).not.toBeNull(); }); diff --git a/components/transfer/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/transfer/__tests__/__snapshots__/demo-extend.test.ts.snap index cbac345923..d2c57e9076 100644 --- a/components/transfer/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/transfer/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -300,7 +300,7 @@ exports[`renders ./components/transfer/demo/advanced.md extend context correctly
                                  - No Data + No data
                                  @@ -675,7 +675,7 @@ exports[`renders ./components/transfer/demo/advanced.md extend context correctly
                                  - No Data + No data
                                  @@ -1876,7 +1876,7 @@ exports[`renders ./components/transfer/demo/custom-item.md extend context correc
                                  - No Data + No data
                                  @@ -2166,7 +2166,7 @@ exports[`renders ./components/transfer/demo/custom-item.md extend context correc
                                  - No Data + No data
                                  @@ -3134,7 +3134,7 @@ Array [
                                  - No Data + No data
                                  @@ -3463,7 +3463,7 @@ Array [
                                  - No Data + No data
                                  @@ -4753,7 +4753,7 @@ exports[`renders ./components/transfer/demo/search.md extend context correctly 1
                                  - No Data + No data
                                  @@ -5108,7 +5108,7 @@ exports[`renders ./components/transfer/demo/search.md extend context correctly 1
                                  - No Data + No data
                                  @@ -5357,7 +5357,7 @@ exports[`renders ./components/transfer/demo/status.md extend context correctly 1
                                  - No Data + No data
                                  @@ -5646,7 +5646,7 @@ exports[`renders ./components/transfer/demo/status.md extend context correctly 1
                                  - No Data + No data
                                  @@ -5955,7 +5955,7 @@ exports[`renders ./components/transfer/demo/status.md extend context correctly 1
                                  - No Data + No data
                                  @@ -6310,7 +6310,7 @@ exports[`renders ./components/transfer/demo/status.md extend context correctly 1
                                  - No Data + No data
                                  @@ -6556,6 +6556,7 @@ Array [ class="ant-checkbox" > @@ -7026,7 +7027,6 @@ Array [
                                  • @@ -7607,7 +7608,6 @@ Array [
                                    • - No Data + No data diff --git a/components/transfer/__tests__/__snapshots__/demo.test.ts.snap b/components/transfer/__tests__/__snapshots__/demo.test.ts.snap index dc485f6ada..2220dddd2d 100644 --- a/components/transfer/__tests__/__snapshots__/demo.test.ts.snap +++ b/components/transfer/__tests__/__snapshots__/demo.test.ts.snap @@ -171,7 +171,7 @@ exports[`renders ./components/transfer/demo/advanced.md correctly 1`] = `
                                      - No Data + No data
                                      @@ -417,7 +417,7 @@ exports[`renders ./components/transfer/demo/advanced.md correctly 1`] = `
                                      - No Data + No data
                                      @@ -1231,7 +1231,7 @@ exports[`renders ./components/transfer/demo/custom-item.md correctly 1`] = `
                                      - No Data + No data
                                      @@ -1392,7 +1392,7 @@ exports[`renders ./components/transfer/demo/custom-item.md correctly 1`] = `
                                      - No Data + No data
                                      @@ -1918,7 +1918,7 @@ Array [
                                      - No Data + No data
                                      @@ -2063,7 +2063,7 @@ Array [
                                      - No Data + No data
                                      @@ -3021,7 +3021,7 @@ exports[`renders ./components/transfer/demo/search.md correctly 1`] = `
                                      - No Data + No data
                                      @@ -3247,7 +3247,7 @@ exports[`renders ./components/transfer/demo/search.md correctly 1`] = `
                                      - No Data + No data
                                      @@ -3367,7 +3367,7 @@ exports[`renders ./components/transfer/demo/status.md correctly 1`] = `
                                      - No Data + No data
                                      @@ -3527,7 +3527,7 @@ exports[`renders ./components/transfer/demo/status.md correctly 1`] = `
                                      - No Data + No data
                                      @@ -3707,7 +3707,7 @@ exports[`renders ./components/transfer/demo/status.md correctly 1`] = `
                                      - No Data + No data
                                      @@ -3933,7 +3933,7 @@ exports[`renders ./components/transfer/demo/status.md correctly 1`] = `
                                      - No Data + No data
                                      @@ -4050,6 +4050,7 @@ Array [ class="ant-checkbox" > @@ -4520,7 +4521,6 @@ Array [
                                      • @@ -4972,7 +4973,6 @@ Array [
                                        • - No Data + No data diff --git a/components/transfer/__tests__/__snapshots__/index.test.tsx.snap b/components/transfer/__tests__/__snapshots__/index.test.tsx.snap index 55446513d6..a703e87735 100644 --- a/components/transfer/__tests__/__snapshots__/index.test.tsx.snap +++ b/components/transfer/__tests__/__snapshots__/index.test.tsx.snap @@ -103,7 +103,7 @@ exports[`Transfer rtl render component should be rendered correctly in RTL direc
                                          - No Data + No data
                                          @@ -262,7 +262,7 @@ exports[`Transfer rtl render component should be rendered correctly in RTL direc
                                          - No Data + No data
                                          @@ -1025,7 +1025,7 @@ exports[`Transfer should support render value and label in item 1`] = `
                                          - No Data + No data
                                          @@ -1274,7 +1274,7 @@ exports[`immutable data dataSource is frozen 1`] = `
                                          - No Data + No data
                                          diff --git a/components/transfer/list.tsx b/components/transfer/list.tsx index 725ef49dc4..31f58871c9 100644 --- a/components/transfer/list.tsx +++ b/components/transfer/list.tsx @@ -4,7 +4,7 @@ import omit from 'rc-util/lib/omit'; import * as React from 'react'; import Checkbox from '../checkbox'; import Dropdown from '../dropdown'; -import Menu from '../menu'; +import type { MenuProps } from '../menu'; import { isValidElement } from '../_util/reactNode'; import type { KeyWiseTransferItem, @@ -358,9 +358,9 @@ export default class TransferList< !pagination && this.getCheckBox({ filteredItems, onItemSelectAll, disabled, prefixCls }); - let menu: React.ReactElement | null = null; + let items: MenuProps['items']; if (showRemove) { - const items = [ + items = [ /* Remove Current Page */ pagination ? { @@ -384,10 +384,8 @@ export default class TransferList< label: removeAll, }, ].filter(item => item); - - menu = ; } else { - const items = [ + items = [ { key: 'selectAll', onClick: () => { @@ -437,12 +435,10 @@ export default class TransferList< label: selectInvert, }, ]; - - menu = ; } const dropdown = ( - + ); diff --git a/components/tree-select/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/tree-select/__tests__/__snapshots__/demo-extend.test.ts.snap index 9be176a457..abd25920ae 100644 --- a/components/tree-select/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/tree-select/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -1848,7 +1848,7 @@ exports[`renders ./components/tree-select/demo/status.md extend context correctl
                                          - No Data + No data
                                          @@ -1984,7 +1984,7 @@ exports[`renders ./components/tree-select/demo/status.md extend context correctl
                                          - No Data + No data
                                          diff --git a/components/tree/Tree.tsx b/components/tree/Tree.tsx index b1b682b319..0aa9b4c848 100644 --- a/components/tree/Tree.tsx +++ b/components/tree/Tree.tsx @@ -1,12 +1,11 @@ import HolderOutlined from '@ant-design/icons/HolderOutlined'; import classNames from 'classnames'; import type { BasicDataNode, TreeProps as RcTreeProps } from 'rc-tree'; -import RcTree, { TreeNode } from 'rc-tree'; +import RcTree from 'rc-tree'; import type { DataNode, Key } from 'rc-tree/lib/interface'; import * as React from 'react'; import { ConfigContext } from '../config-provider'; import initCollapseMotion from '../_util/motion'; -import DirectoryTree from './DirectoryTree'; import dropIndicatorRender from './utils/dropIndicator'; import renderSwitcherIcon from './utils/iconUtil'; @@ -156,13 +155,6 @@ export interface TreeProps blockNode?: boolean; } -type CompoundedComponent = (( - props: React.PropsWithChildren> & { ref?: React.Ref }, -) => React.ReactElement) & { - TreeNode: typeof TreeNode; - DirectoryTree: typeof DirectoryTree; -}; - const Tree = React.forwardRef((props, ref) => { const { getPrefixCls, direction, virtual } = React.useContext(ConfigContext); const { @@ -253,10 +245,6 @@ const Tree = React.forwardRef((props, ref) => { {children} , ); -}) as unknown as CompoundedComponent; - -Tree.TreeNode = TreeNode; - -Tree.DirectoryTree = DirectoryTree; +}); export default Tree; diff --git a/components/tree/demo/search.md b/components/tree/demo/search.md index 7353ef3841..7ca6b0aa4b 100644 --- a/components/tree/demo/search.md +++ b/components/tree/demo/search.md @@ -81,7 +81,7 @@ const App: React.FC = () => { const [searchValue, setSearchValue] = useState(''); const [autoExpandParent, setAutoExpandParent] = useState(true); - const onExpand = (newExpandedKeys: string[]) => { + const onExpand = (newExpandedKeys: React.Key[]) => { setExpandedKeys(newExpandedKeys); setAutoExpandParent(false); }; diff --git a/components/tree/index.tsx b/components/tree/index.tsx index e822168412..32c2f8e914 100644 --- a/components/tree/index.tsx +++ b/components/tree/index.tsx @@ -1,6 +1,14 @@ -import Tree from './Tree'; +import type RcTree from 'rc-tree'; +import { TreeNode } from 'rc-tree'; +import type { BasicDataNode } from 'rc-tree'; +import type { DataNode } from 'rc-tree/lib/interface'; -export { DataNode, EventDataNode } from 'rc-tree/lib/interface'; +import type { TreeProps } from './Tree'; +import TreePure from './Tree'; +import DirectoryTree from './DirectoryTree'; + +export { DataNode }; +export { EventDataNode } from 'rc-tree/lib/interface'; export { DirectoryTreeProps, ExpandAction as DirectoryTreeExpandAction } from './DirectoryTree'; export { AntdTreeNodeAttribute, @@ -13,4 +21,15 @@ export { TreeProps, } from './Tree'; +type CompoundedComponent = (( + props: React.PropsWithChildren> & { ref?: React.Ref }, +) => React.ReactElement) & { + TreeNode: typeof TreeNode; + DirectoryTree: typeof DirectoryTree; +}; + +const Tree = TreePure as unknown as CompoundedComponent; +Tree.DirectoryTree = DirectoryTree; +Tree.TreeNode = TreeNode; + export default Tree; diff --git a/components/typography/__tests__/ellipsis.test.tsx b/components/typography/__tests__/ellipsis.test.tsx index ba8634ca0f..2855572951 100644 --- a/components/typography/__tests__/ellipsis.test.tsx +++ b/components/typography/__tests__/ellipsis.test.tsx @@ -1,7 +1,7 @@ import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; import React from 'react'; import { act } from 'react-dom/test-utils'; -import { fireEvent, render, sleep, triggerResize, waitFor } from '../../../tests/utils'; +import { fireEvent, render, waitFakeTimer, triggerResize, waitFor } from '../../../tests/utils'; import type { EllipsisConfig } from '../Base'; import Base from '../Base'; // eslint-disable-next-line no-unused-vars @@ -20,6 +20,7 @@ describe('Typography.Ellipsis', () => { let computeSpy: jest.SpyInstance; beforeAll(() => { + jest.useFakeTimers(); mockRectSpy = spyElementPrototypes(HTMLElement, { offsetHeight: { get() { @@ -54,6 +55,7 @@ describe('Typography.Ellipsis', () => { }); afterAll(() => { + jest.useRealTimers(); errorSpy.mockRestore(); mockRectSpy.mockRestore(); computeSpy.mockRestore(); @@ -72,7 +74,7 @@ describe('Typography.Ellipsis', () => { ); triggerResize(ref.current); - await sleep(20); + await waitFakeTimer(); expect(container.firstChild?.textContent).toEqual('Bamboo is Little ...'); expect(onEllipsis).toHaveBeenCalledWith(true); @@ -135,7 +137,7 @@ describe('Typography.Ellipsis', () => { ); triggerResize(ref.current); - await sleep(20); + await waitFakeTimer(); expect(wrapper.firstChild?.textContent).toEqual('Ant Design, a des...'); const ellipsisSpans = wrapper.querySelectorAll('span[aria-hidden]'); @@ -155,7 +157,7 @@ describe('Typography.Ellipsis', () => { ); triggerResize(ref.current); - await sleep(20); + await waitFakeTimer(); expect(wrapper.querySelector('p')?.textContent).toEqual('Bamboo is...--suffix'); unmount(); @@ -175,7 +177,7 @@ describe('Typography.Ellipsis', () => { ); triggerResize(ref.current); - await sleep(20); + await waitFakeTimer(); expect(wrapper.querySelector('p')?.textContent).toEqual( '...--The information is very important', @@ -215,7 +217,7 @@ describe('Typography.Ellipsis', () => { ); triggerResize(ref.current); - await sleep(20); + await waitFakeTimer(); expect(wrapper.textContent).toEqual('Bamboo is Little...'); }); @@ -327,7 +329,7 @@ describe('Typography.Ellipsis', () => { , ); triggerResize(ref.current); - await sleep(20); + await waitFakeTimer(); return wrapper; } @@ -424,7 +426,7 @@ describe('Typography.Ellipsis', () => { , ); triggerResize(ref.current); - await sleep(20); + await waitFakeTimer(); fireEvent.mouseEnter(container.firstChild!); await waitFor(() => { diff --git a/components/upload/Upload.tsx b/components/upload/Upload.tsx index 8d426b673d..4d222b820b 100644 --- a/components/upload/Upload.tsx +++ b/components/upload/Upload.tsx @@ -62,7 +62,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr const [dragState, setDragState] = React.useState('drop'); - const upload = React.useRef(); + const upload = React.useRef(null); warning( 'fileList' in props || !('value' in props), @@ -278,7 +278,7 @@ const InternalUpload: React.ForwardRefRenderFunction = (pr item.status = 'removed'; } }); - upload.current?.abort(currentFile); + upload.current?.abort(currentFile as RcFile); onInternalChange(currentFile, removedFileList); } diff --git a/components/upload/UploadList/ListItem.tsx b/components/upload/UploadList/ListItem.tsx index 09b90743f8..e5706755a3 100644 --- a/components/upload/UploadList/ListItem.tsx +++ b/components/upload/UploadList/ListItem.tsx @@ -82,14 +82,15 @@ const ListItem = React.forwardRef( // Delay to show the progress bar const [showProgress, setShowProgress] = React.useState(false); - const progressRafRef = React.useRef(); + const progressRafRef = React.useRef(null); React.useEffect(() => { progressRafRef.current = setTimeout(() => { setShowProgress(true); }, 300); - return () => { - window.clearTimeout(progressRafRef.current); + if (progressRafRef.current) { + clearTimeout(progressRafRef.current); + } }; }, []); diff --git a/components/upload/__tests__/upload.test.tsx b/components/upload/__tests__/upload.test.tsx index 2fa01d823b..5a073b5e0e 100644 --- a/components/upload/__tests__/upload.test.tsx +++ b/components/upload/__tests__/upload.test.tsx @@ -2,12 +2,12 @@ import produce from 'immer'; import { cloneDeep } from 'lodash'; import type { UploadRequestOption } from 'rc-upload/lib/interface'; -import React from 'react'; +import React, { createRef } from 'react'; import type { RcFile, UploadFile, UploadProps } from '..'; import Upload from '..'; import mountTest from '../../../tests/shared/mountTest'; import rtlTest from '../../../tests/shared/rtlTest'; -import { fireEvent, render, sleep, act } from '../../../tests/utils'; +import { fireEvent, render, waitFakeTimer, act } from '../../../tests/utils'; import Form from '../../form'; import { resetWarned } from '../../_util/warning'; import { getFileItem, isImageUrl, removeFileItem } from '../utils'; @@ -19,8 +19,17 @@ describe('Upload', () => { mountTest(Upload); rtlTest(Upload); + beforeAll(() => { + jest.useFakeTimers(); + }); beforeEach(() => setup()); - afterEach(() => teardown()); + afterAll(() => { + jest.useRealTimers(); + }); + afterEach(() => { + jest.clearAllTimers(); + return teardown(); + }); // Mock for rc-util raf window.requestAnimationFrame = callback => window.setTimeout(callback, 16); @@ -29,16 +38,18 @@ describe('Upload', () => { // https://github.com/react-component/upload/issues/36 it('should get refs inside Upload in componentDidMount', () => { - let ref: React.ReactInstance; + let ref: React.RefObject; class App extends React.Component { + inputRef = createRef(); + componentDidMount() { - ref = this.refs.input; + ref = this.inputRef; } render() { return ( - + ); } @@ -48,7 +59,6 @@ describe('Upload', () => { }); it('return promise in beforeUpload', async () => { - jest.useFakeTimers(); const data = jest.fn(); const done = jest.fn(); const props: UploadProps = { @@ -74,22 +84,11 @@ describe('Upload', () => { fireEvent.change(wrapper.querySelector('input')!, { target: { files: [{ file: 'foo.png' }] }, }); - act(() => { - jest.runAllTimers(); - }); - await act(async () => { - for (let i = 0; i < 4; i += 1) { - // eslint-disable-next-line no-await-in-loop - await Promise.resolve(); - } - }); + await waitFakeTimer(); expect(done).toHaveBeenCalled(); - - jest.useRealTimers(); }); it('beforeUpload can be falsy', async () => { - jest.useFakeTimers(); const done = jest.fn(); const props: UploadProps = { action: 'http://upload.com', @@ -110,18 +109,11 @@ describe('Upload', () => { fireEvent.change(wrapper.querySelector('input')!, { target: { files: [{ file: 'foo.png' }] }, }); - await act(async () => { - for (let i = 0; i < 4; i += 1) { - // eslint-disable-next-line no-await-in-loop - await Promise.resolve(); - } - }); + await waitFakeTimer(); expect(done).toHaveBeenCalled(); - jest.useRealTimers(); }); it('upload promise return file in beforeUpload', async () => { - jest.useFakeTimers(); const done = jest.fn(); const data = jest.fn(); const props: UploadProps = { @@ -153,18 +145,9 @@ describe('Upload', () => { fireEvent.change(wrapper.querySelector('input')!, { target: { files: [{ file: 'foo.png' }] }, }); - act(() => { - jest.runAllTimers(); - }); - await act(async () => { - for (let i = 0; i < 4; i += 1) { - // eslint-disable-next-line no-await-in-loop - await Promise.resolve(); - } - }); + await waitFakeTimer(); expect(done).toHaveBeenCalled(); - jest.useRealTimers(); }); it('should not stop upload when return value of beforeUpload is false', done => { @@ -298,8 +281,7 @@ describe('Upload', () => { expect(wrapper.querySelectorAll('input#upload').length).toBe(0); }); - it('should be controlled by fileList', () => { - jest.useFakeTimers(); + it('should be controlled by fileList', async () => { const fileList = [ { uid: '-1', @@ -312,11 +294,8 @@ describe('Upload', () => { const { rerender } = render(); expect(ref.current.fileList).toEqual([]); rerender(); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect(ref.current.fileList).toEqual(fileList); - jest.useRealTimers(); }); it('should be able to get uid at first', () => { @@ -451,7 +430,7 @@ describe('Upload', () => { expect(linkNode?.getAttribute('rel')).toBe('noopener'); }); - it('should not stop remove when return value of onRemove is false', done => { + it('should stop remove when return value of onRemove is false', async () => { const mockRemove = jest.fn(() => false); const props: UploadProps = { onRemove: mockRemove, @@ -469,12 +448,11 @@ describe('Upload', () => { fireEvent.click(wrapper.querySelector('div.ant-upload-list-item .anticon-delete')!); - setTimeout(() => { - expect(mockRemove).toHaveBeenCalled(); - expect(props.fileList).toHaveLength(1); - expect(props.fileList?.[0]?.status).toBe('done'); - done(); - }); + await waitFakeTimer(); + + expect(mockRemove).toHaveBeenCalled(); + expect(props.fileList).toHaveLength(1); + expect(props.fileList?.[0]?.status).toBe('done'); }); // https://github.com/ant-design/ant-design/issues/18902 @@ -504,13 +482,8 @@ describe('Upload', () => { ); fireEvent.click(container.querySelector('div.ant-upload-list-item .anticon-delete')!); - // uploadStart is a batch work which we need wait for react act - await act(async () => { - await Promise.resolve(); - }); - // Delay return true for remove - await sleep(100); + await waitFakeTimer(); await act(async () => { await removePromise(true); }); @@ -519,7 +492,7 @@ describe('Upload', () => { expect(file.status).toBe('removed'); }); - it('should not stop download when return use onDownload', done => { + it('should not stop download when return use onDownload', async () => { const mockRemove = jest.fn(() => false); const props: UploadProps = { onRemove: mockRemove, @@ -540,11 +513,9 @@ describe('Upload', () => { fireEvent.click(wrapper.querySelector('div.ant-upload-list-item .anticon-download')!); - setTimeout(() => { - expect(props.fileList).toHaveLength(1); - expect(props.fileList?.[0]?.status).toBe('done'); - done(); - }); + await waitFakeTimer(); + expect(props.fileList).toHaveLength(1); + expect(props.fileList?.[0]?.status).toBe('done'); }); // https://github.com/ant-design/ant-design/issues/14439 @@ -658,7 +629,6 @@ describe('Upload', () => { // https://github.com/ant-design/ant-design/issues/26427 it('should sync file list with control mode', async () => { - jest.useFakeTimers(); const done = jest.fn(); let callTimes = 0; @@ -718,21 +688,9 @@ describe('Upload', () => { target: { files: [{ file: 'foo.png' }] }, }); - await act(async () => { - for (let i = 0; i < 3; i += 1) { - // eslint-disable-next-line no-await-in-loop - await Promise.resolve(); - } - }); - act(() => { - jest.runAllTimers(); - }); - await act(async () => { - await Promise.resolve(); - }); + await waitFakeTimer(); expect(done).toHaveBeenCalled(); - jest.useRealTimers(); }); describe('maxCount', () => { @@ -764,7 +722,7 @@ describe('Upload', () => { }, }); - await sleep(20); + await waitFakeTimer(); expect(onChange.mock.calls[0][0].fileList).toHaveLength(1); expect(onChange.mock.calls[0][0].fileList[0]).toEqual( @@ -807,7 +765,7 @@ describe('Upload', () => { }, }); - await sleep(20); + await waitFakeTimer(); expect(onChange.mock.calls[0][0].fileList).toHaveLength(2); expect(onChange.mock.calls[0][0].fileList).toEqual([ @@ -850,7 +808,7 @@ describe('Upload', () => { }, }); - await sleep(); + await waitFakeTimer(); const { file } = onChange.mock.calls[0][0]; const clone = cloneDeep(file); @@ -875,6 +833,8 @@ describe('Upload', () => { }, ]; + const image = cloneDeep(fileList[0]); + const frozenFileList = fileList.map(Object.freeze); const { container: wrapper } = render( @@ -884,11 +844,10 @@ describe('Upload', () => { fireEvent.click(rmBtn[rmBtn.length - 1]); // Wait for Upload async remove - await act(async () => { - await sleep(); - }); - }); + await waitFakeTimer(); + expect(image).toEqual(frozenFileList[0]); + }); // https://github.com/ant-design/ant-design/issues/30390 // IE11 Does not support the File constructor it('should not break in IE if beforeUpload returns false', async () => { @@ -906,7 +865,7 @@ describe('Upload', () => { }); // React 18 is async now - await sleep(); + await waitFakeTimer(); expect(onChange.mock.calls[0][0].fileList).toHaveLength(1); spyIE.mockRestore(); @@ -914,8 +873,6 @@ describe('Upload', () => { // https://github.com/ant-design/ant-design/issues/33819 it('should show the animation of the upload children leaving when the upload children becomes null', async () => { - jest.useFakeTimers(); - const { container, rerender } = render( @@ -931,16 +888,12 @@ describe('Upload', () => { }); // Motion leave status change: start > active - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); fireEvent.animationEnd(container.querySelector('.ant-upload-select')!); expect(container.querySelector('.ant-upload-select')).not.toHaveClass( 'ant-upload-animate-inline-leave-start', ); - - jest.useRealTimers(); }); it(' should pass prefixCls', async () => { @@ -978,9 +931,8 @@ describe('Upload', () => { }); // React 18 is async now - await act(async () => { - await sleep(); - }); + await waitFakeTimer(); + onChange.mockReset(); // Processing diff --git a/components/upload/__tests__/uploadlist.test.tsx b/components/upload/__tests__/uploadlist.test.tsx index 3cd1995614..dd7fb0e80c 100644 --- a/components/upload/__tests__/uploadlist.test.tsx +++ b/components/upload/__tests__/uploadlist.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import Upload from '..'; -import { fireEvent, render, sleep, waitFor, act } from '../../../tests/utils'; +import { act, fireEvent, render, waitFakeTimer, waitFor } from '../../../tests/utils'; import Form from '../../form'; import UploadList from '../UploadList'; import { previewImage } from '../utils'; @@ -54,10 +54,15 @@ describe('Upload List', () => { // HTMLCanvasElement.prototype - beforeEach(() => setup()); + beforeEach(() => { + jest.useFakeTimers(); + return setup(); + }); afterEach(() => { teardown(); drawImageCallback = null; + jest.clearAllTimers(); + jest.useRealTimers(); }); let open: jest.MockInstance; @@ -109,8 +114,6 @@ describe('Upload List', () => { // https://github.com/ant-design/ant-design/issues/7269 it('should remove correct item when uid is 0', async () => { - jest.useFakeTimers(); - const list = [ { uid: '0', @@ -138,36 +141,22 @@ describe('Upload List', () => { ); // Upload use Promise to wait remove action. Let's wait this also. - await act(async () => { - for (let i = 0; i < 10; i += 1) { - // eslint-disable-next-line no-await-in-loop - await Promise.resolve(); - } - }); - - // Progress motion to active - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); // Progress motion to done // React 17 will reach deadline, so we need check if already done if (container.querySelector('.ant-upload-animate-leave-active')) { fireEvent.animationEnd(container.querySelector('.ant-upload-animate-leave-active')!); } - act(() => { - jest.runAllTimers(); - }); + + await waitFakeTimer(); expect(container.querySelectorAll('.ant-upload-list-item-container')).toHaveLength(1); unmount(); - - jest.useRealTimers(); }); it('should be uploading when upload a file', async () => { - jest.useFakeTimers(); const done = jest.fn(); let wrapper: ReturnType; let latestFileList: UploadFile[] | null = null; @@ -195,23 +184,14 @@ describe('Upload List', () => { fireEvent.change(wrapper.container.querySelector('input')!, { target: { files: [{ name: 'foo.png' }] }, }); - await act(async () => { - await Promise.resolve(); - await Promise.resolve(); - }); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect(done).toHaveBeenCalled(); wrapper.unmount(); - - jest.useRealTimers(); }); it('handle error', async () => { - jest.useFakeTimers(); const onChange = jest.fn(); const { @@ -231,14 +211,9 @@ describe('Upload List', () => { target: { files: [{ name: 'foo.png' }] }, }); - await act(async () => { - await Promise.resolve(); - }); + await waitFakeTimer(); // Wait twice since `errorRequest` also use timeout for mock - act(() => { - jest.runAllTimers(); - }); expect(onChange).toHaveBeenLastCalledWith( expect.objectContaining({ @@ -249,21 +224,15 @@ describe('Upload List', () => { fireEvent.animationEnd(wrapper.querySelector('.ant-upload-animate-appear-active')!); } - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect(wrapper.firstChild).toMatchSnapshot(); // Error message fireEvent.mouseEnter(wrapper.querySelector('.ant-upload-list-item')!); - act(() => { - jest.runAllTimers(); - }); - + await waitFakeTimer(); expect(baseElement.querySelector('.ant-tooltip')).not.toHaveClass('.ant-tooltip-hidden'); - jest.useRealTimers(); unmount(); }); @@ -285,7 +254,7 @@ describe('Upload List', () => { target: { files: [{ name: 'foo.png' }] }, }); - await sleep(); + await waitFakeTimer(); expect(ref.current.fileList.length).toBe(fileList.length + 1); expect(handleChange.mock.calls[0][0].fileList).toHaveLength(3); @@ -381,8 +350,8 @@ describe('Upload List', () => { expect(handleRemove).toHaveBeenCalledWith(fileList[0]); fireEvent.click(wrapper.querySelectorAll('.anticon-delete')[1]); expect(handleRemove).toHaveBeenCalledWith(fileList[1]); - await sleep(); - expect(handleChange.mock.calls.length).toBe(2); + await waitFakeTimer(); + expect(handleChange).toHaveBeenCalledTimes(2); unmount(); }); @@ -409,6 +378,7 @@ describe('Upload List', () => { , ); fireEvent.click(wrapper.querySelectorAll('.anticon-download')[0]); + expect(handleDownload).toHaveBeenCalled(); unmount(); }); @@ -467,7 +437,7 @@ describe('Upload List', () => { , ); - await sleep(); + await waitFakeTimer(); expect(ref.current.fileList[2].thumbUrl).not.toBe(undefined); expect(onDrawImage).toHaveBeenCalled(); @@ -559,8 +529,6 @@ describe('Upload List', () => { }); it('not crash when uploading not provides percent', async () => { - jest.useFakeTimers(); - const { unmount } = render( { />, ); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); unmount(); - - jest.useRealTimers(); }); it('should support showRemoveIcon and showPreviewIcon', () => { @@ -640,8 +604,8 @@ describe('Upload List', () => { fireEvent.click(wrapper.querySelectorAll('.custom-delete')[1]); expect(handleRemove).toHaveBeenCalledWith(fileList[1]); expect(myClick).toHaveBeenCalled(); - await sleep(); - expect(handleChange.mock.calls.length).toBe(2); + await waitFakeTimer(); + expect(handleChange).toHaveBeenCalledTimes(2); unmount(); }); @@ -718,11 +682,9 @@ describe('Upload List', () => { rules={[ { required: true, - validator(_, value, callback) { + async validator(_, value) { if (!value || value.length === 0) { - callback('file required'); - } else { - callback(); + throw new Error('file required'); } }, }, @@ -739,7 +701,7 @@ describe('Upload List', () => { const { container, unmount } = render(); fireEvent.submit(container.querySelector('form')!); - await sleep(); + await waitFakeTimer(); expect(formRef!.getFieldError(['file'])).toEqual(['file required']); fireEvent.change(container.querySelector('input')!, { @@ -747,7 +709,7 @@ describe('Upload List', () => { }); fireEvent.submit(container.querySelector('form')!); - await sleep(); + await waitFakeTimer(); expect(formRef!.getFieldError(['file'])).toEqual([]); unmount(); @@ -795,7 +757,8 @@ describe('Upload List', () => { target: { files: [{ name: 'foo.png' }] }, }); - await sleep(); + await waitFakeTimer(); + expect(wrapper.querySelector('.ant-upload-list-item-thumbnail')?.getAttribute('href')).toBe( null, ); @@ -1005,7 +968,7 @@ describe('Upload List', () => { , ); expect(previewFile).toHaveBeenCalledWith(file.originFileObj); - await sleep(100); + await waitFakeTimer(); expect( wrapper.querySelector('.ant-upload-list-item-thumbnail img')?.getAttribute('src'), @@ -1039,7 +1002,6 @@ describe('Upload List', () => { ); const imgNode = wrapper.querySelectorAll('.ant-upload-list-item-thumbnail img'); expect(imgNode.length).toBe(2); - unmount(); }); it('should render when custom imageUrl return true', () => { @@ -1056,7 +1018,6 @@ describe('Upload List', () => { const imgNode = wrapper.querySelectorAll('.ant-upload-list-item-thumbnail img'); expect(isImageUrl).toHaveBeenCalled(); expect(imgNode.length).toBe(3); - unmount(); }); it('should not render when custom imageUrl return false', () => { @@ -1073,31 +1034,11 @@ describe('Upload List', () => { const imgNode = wrapper.querySelectorAll('.ant-upload-list-item-thumbnail img'); expect(isImageUrl).toHaveBeenCalled(); expect(imgNode.length).toBe(0); - unmount(); }); }); - describe('thumbUrl support for non-image', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - const nonImageFile = new File([''], 'foo.7z', { type: 'application/x-7z-compressed' }); - - /** Wait for a long promise since `rc-util` internal has at least 3 promise wait */ - async function waitPromise() { - /* eslint-disable no-await-in-loop */ - for (let i = 0; i < 10; i += 1) { - await Promise.resolve(); - } - /* eslint-enable */ - } - it('should render when upload non-image file and configure thumbUrl in onChange', async () => { const thumbUrl = 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'; @@ -1140,20 +1081,12 @@ describe('Upload List', () => { }); // Wait for `rc-upload` process file - await waitPromise(); - - // Wait for mock request finish request - act(() => { - jest.runAllTimers(); - }); - + await waitFakeTimer(); // Basic called times expect(onChange).toHaveBeenCalled(); // Check for images - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); const afterImgNode = wrapper.container.querySelectorAll( '.ant-upload-list-item-thumbnail img', ); @@ -1162,29 +1095,25 @@ describe('Upload List', () => { wrapper.unmount(); }); - it('should not render when upload non-image file without thumbUrl in onChange', done => { + it('should not render when upload non-image file without thumbUrl in onChange', async () => { (global as any).testName = 'should not render when upload non-image file without thumbUrl in onChange'; let wrapper: ReturnType; - const onChange: UploadProps['onChange'] = async ({ fileList: files }) => { - wrapper.rerender( - - - , - ); - - await sleep(); - const imgNode = wrapper.container.querySelectorAll('.ant-upload-list-item-thumbnail img'); - expect(imgNode.length).toBe(0); - - done(); - }; + const onChange = jest.fn[]>( + ({ fileList: files }) => { + wrapper.rerender( + + + , + ); + }, + ); wrapper = render( { fireEvent.change(wrapper.container.querySelector('input')!, { target: { files: [nonImageFile] }, }); + + await waitFakeTimer(); + expect(onChange).toHaveBeenCalled(); + expect(wrapper.container.querySelectorAll('.ant-upload-list-item-thumbnail img').length).toBe( + 0, + ); }); }); it('[deprecated] should support transformFile', done => { + jest.useRealTimers(); let wrapper: ReturnType; let lastFile: UploadFile; @@ -1281,8 +1217,6 @@ describe('Upload List', () => { // https://github.com/ant-design/ant-design/issues/26536 it('multiple file upload should keep the internal fileList async', async () => { - jest.useFakeTimers(); - const uploadRef = React.createRef(); const MyUpload: React.FC = () => { @@ -1320,14 +1254,10 @@ describe('Upload List', () => { expect(uploadRef.current.fileList).toHaveLength(fileNames.length); - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect(uploadRef.current.fileList).toHaveLength(fileNames.length); unmount(); - - jest.useRealTimers(); }); it('itemRender', () => { @@ -1384,13 +1314,11 @@ describe('Upload List', () => { const beforeUpload = jest.fn(() => Upload.LIST_IGNORE); const { container: wrapper, unmount } = render(); - await act(() => { - fireEvent.change(wrapper.querySelector('input')!, { - target: { files: [{ file: 'foo.png' }] }, - }); + fireEvent.change(wrapper.querySelector('input')!, { + target: { files: [{ file: 'foo.png' }] }, }); - await sleep(); + await waitFakeTimer(); expect(beforeUpload).toHaveBeenCalled(); expect(wrapper.querySelectorAll('.ant-upload-list-text-container')).toHaveLength(0); @@ -1585,8 +1513,6 @@ describe('Upload List', () => { // https://github.com/ant-design/ant-design/issues/36286 it('remove should keep origin className', async () => { - jest.useFakeTimers(); - const onChange = jest.fn(); const list = [ { @@ -1605,15 +1531,7 @@ describe('Upload List', () => { fireEvent.click(container.querySelector('.anticon-delete')!); - // Wait for Upload sync - for (let i = 0; i < 10; i += 1) { - // eslint-disable-next-line no-await-in-loop - await Promise.resolve(); - } - - act(() => { - jest.runAllTimers(); - }); + await waitFakeTimer(); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ @@ -1622,7 +1540,5 @@ describe('Upload List', () => { ); expect(container.querySelector('.ant-upload-list-item-error')).toBeTruthy(); - - jest.useRealTimers(); }); }); diff --git a/docs/react/recommendation.en-US.md b/docs/react/recommendation.en-US.md index d43f075d69..b76aecc892 100644 --- a/docs/react/recommendation.en-US.md +++ b/docs/react/recommendation.en-US.md @@ -16,7 +16,7 @@ title: Third-Party Libraries | Code Editor | [react-codemirror2](https://github.com/scniro/react-codemirror2) [react-monaco-editor](https://github.com/superRaytin/react-monaco-editor) | | Rich Text Editor | [react-quill](https://github.com/zenoamaro/react-quill) [braft-editor](https://github.com/margox/braft-editor) | | JSON Viewer | [react-json-view](https://github.com/mac-s-g/react-json-view) | -| Color Picker | [react-color](http://casesandberg.github.io/react-color/) | +| Color Picker | [react-colorful](https://github.com/omgovich/react-colorful) [react-color](http://casesandberg.github.io/react-color/) | | Media Query | [react-responsive](https://github.com/contra/react-responsive) [react-media](https://github.com/ReactTraining/react-media) | | Copy to clipboard | [react-copy-to-clipboard](https://github.com/nkbt/react-copy-to-clipboard) | | Document head manager | [react-helmet](https://github.com/nfl/react-helmet) [react-helmet-async](https://github.com/staylor/react-helmet-async) | diff --git a/docs/react/recommendation.zh-CN.md b/docs/react/recommendation.zh-CN.md index 597b8162fe..835c57fabb 100644 --- a/docs/react/recommendation.zh-CN.md +++ b/docs/react/recommendation.zh-CN.md @@ -16,7 +16,7 @@ title: 社区精选组件 | 代码编辑器 | [react-codemirror2](https://github.com/scniro/react-codemirror2) [react-monaco-editor](https://github.com/superRaytin/react-monaco-editor) | | 富文本编辑器 | [react-quill](https://github.com/zenoamaro/react-quill) [braft-editor](https://github.com/margox/braft-editor) | | JSON 显示器 | [react-json-view](https://github.com/mac-s-g/react-json-view) | -| 拾色器 | [react-color](http://casesandberg.github.io/react-color/) | +| 拾色器 | [react-colorful](https://github.com/omgovich/react-colorful) [react-color](http://casesandberg.github.io/react-color/) | | 响应式 | [react-responsive](https://github.com/contra/react-responsive) [react-media](https://github.com/ReactTraining/react-media) | | 复制到剪贴板 | [react-copy-to-clipboard](https://github.com/nkbt/react-copy-to-clipboard) | | 页面 meta 属性 | [react-helmet](https://github.com/nfl/react-helmet) [react-helmet-async](https://github.com/staylor/react-helmet-async) | diff --git a/package.json b/package.json index 897e4bd933..2f5c7dbc84 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,8 @@ "version": "node ./scripts/generate-version", "install-react-16": "npm i --no-save --legacy-peer-deps react@16 react-dom@16", "install-react-17": "npm i --no-save --legacy-peer-deps react@17 react-dom@17", - "install-react-18": "npm i --no-save --legacy-peer-deps react@18 react-dom@18 @testing-library/react@13" + "install-react-18": "npm i --no-save --legacy-peer-deps react@18 react-dom@18 @testing-library/react@13", + "fix-memory-limit": "cross-env LIMIT=10240 increase-memory-limit" }, "browserslist": [ "> 0.5%", @@ -119,30 +120,29 @@ "copy-to-clipboard": "^3.2.0", "dayjs": "^1.11.1", "lodash": "^4.17.21", - "memoize-one": "^6.0.0", "rc-cascader": "~3.7.0", "rc-checkbox": "~2.3.0", "rc-collapse": "~3.4.2", - "rc-dialog": "~9.0.0", + "rc-dialog": "~9.0.2", "rc-drawer": "~6.0.0", "rc-dropdown": "~4.0.0", "rc-field-form": "~1.27.0", - "rc-image": "~5.9.0", - "rc-input": "~0.1.3", + "rc-image": "~5.10.2", + "rc-input": "~0.1.4", "rc-input-number": "~7.3.9", "rc-mentions": "~1.10.0", "rc-menu": "~9.6.3", "rc-motion": "^2.6.1", "rc-notification": "~5.0.0-alpha.9", - "rc-pagination": "~3.1.17", + "rc-pagination": "~3.2.0", "rc-picker": "~3.0.0-4", - "rc-progress": "~3.3.2", + "rc-progress": "~3.4.1", "rc-rate": "~2.9.0", "rc-resize-observer": "^1.2.0", "rc-segmented": "~2.1.0", "rc-select": "~14.1.13", "rc-slider": "~10.0.0", - "rc-steps": "~5.0.0-alpha.0", + "rc-steps": "~5.0.0-alpha.2", "rc-switch": "~4.0.0", "rc-table": "~7.26.0", "rc-tabs": "~12.2.0", @@ -175,7 +175,7 @@ "@types/jest-image-snapshot": "^5.1.0", "@types/jquery": "^3.5.14", "@types/lodash": "^4.14.139", - "@types/puppeteer": "^5.4.0", + "@types/puppeteer": "^7.0.4", "@types/qs": "^6.9.7", "@types/react": "^18.0.0", "@types/react-color": "^3.0.1", @@ -232,14 +232,15 @@ "identity-obj-proxy": "^3.0.0", "immer": "^9.0.1", "immutability-helper": "^3.0.0", + "increase-memory-limit": "^1.0.7", "inquirer": "^9.1.2", "intersection-observer": "^0.12.0", "isomorphic-fetch": "^3.0.0", - "jest": "^28.0.3", - "jest-axe": "^6.0.0", + "jest": "^29.0.0", + "jest-axe": "^7.0.0", "jest-environment-jsdom": "^29.0.1", "jest-environment-node": "^29.0.0", - "jest-image-snapshot": "^5.1.0", + "jest-image-snapshot": "^6.0.0", "jest-puppeteer": "^6.0.0", "jquery": "^3.4.1", "jsdom": "^20.0.0", @@ -257,7 +258,7 @@ "qs": "^6.10.1", "rc-footer": "^0.6.6", "rc-tween-one": "^3.0.3", - "rc-virtual-list": "^3.4.2", + "rc-virtual-list": "^3.4.11", "react": "^17.0.0", "react-color": "^2.17.3", "react-copy-to-clipboard": "^5.0.1", @@ -289,7 +290,7 @@ "stylelint": "^14.9.0", "stylelint-config-prettier": "^9.0.2", "stylelint-config-rational-order": "^0.1.2", - "stylelint-config-standard": "^28.0.0", + "stylelint-config-standard": "^29.0.0", "stylelint-declaration-block-no-ignored-properties": "^2.1.0", "stylelint-order": "^5.0.0", "theme-switcher": "^1.0.2", diff --git a/scripts/post-script.js b/scripts/post-script.js index ca5ca5de69..2fa2bd4c1b 100644 --- a/scripts/post-script.js +++ b/scripts/post-script.js @@ -18,6 +18,7 @@ const DEPRECIATED_VERSION = { 'https://github.com/ant-design/ant-design/issues/37929', 'https://github.com/ant-design/ant-design/issues/37931', ], + '4.24.0': ['https://github.com/ant-design/ant-design/issues/38371'], }; function matchDeprecated(version) { diff --git a/site/theme/static/responsive.less b/site/theme/static/responsive.less index 2200450c54..d8af5f659e 100644 --- a/site/theme/static/responsive.less +++ b/site/theme/static/responsive.less @@ -23,6 +23,7 @@ .code-boxes-col-1-1 { float: none; width: 100%; + min-width: 100%; max-width: unset; } } diff --git a/site/theme/style/themes/default.less b/site/theme/style/themes/default.less index ecbdcbfb40..7321f9b20b 100644 --- a/site/theme/style/themes/default.less +++ b/site/theme/style/themes/default.less @@ -966,6 +966,8 @@ @alert-text-color: @text-color; @alert-close-color: @text-color-secondary; @alert-close-hover-color: @icon-color-hover; +@alert-padding-vertical: @padding-xs; +@alert-padding-horizontal: @padding-md - 1px; @alert-no-icon-padding-vertical: @padding-xs; @alert-with-description-no-icon-padding-vertical: @padding-md - 1px; @alert-with-description-padding-vertical: @padding-md - 1px; diff --git a/site/theme/style/themes/variable.less b/site/theme/style/themes/variable.less index 4f437e6006..2a7f995d07 100644 --- a/site/theme/style/themes/variable.less +++ b/site/theme/style/themes/variable.less @@ -1021,6 +1021,8 @@ @alert-text-color: @text-color; @alert-close-color: @text-color-secondary; @alert-close-hover-color: @icon-color-hover; +@alert-padding-vertical: @padding-xs; +@alert-padding-horizontal: @padding-md - 1px; @alert-no-icon-padding-vertical: @padding-xs; @alert-with-description-no-icon-padding-vertical: @padding-md - 1px; @alert-with-description-padding-vertical: @padding-md - 1px; diff --git a/site/theme/template/Content/MainContent.jsx b/site/theme/template/Content/MainContent.jsx index de528f7be4..bea871872a 100644 --- a/site/theme/template/Content/MainContent.jsx +++ b/site/theme/template/Content/MainContent.jsx @@ -295,17 +295,15 @@ class MainContent extends Component { const { intl: { formatMessage }, } = this.props; - return ( - this.changeThemeMode(key)} selectedKeys={[theme]}> - {[ - { type: 'default', text: formatMessage({ id: 'app.theme.switch.default' }) }, - { type: 'dark', text: formatMessage({ id: 'app.theme.switch.dark' }) }, - { type: 'compact', text: formatMessage({ id: 'app.theme.switch.compact' }) }, - ].map(({ type, text }) => ( - {text} - ))} - - ); + return { + onClick: ({ key }) => this.changeThemeMode(key), + selectedKeys: [theme], + items: [ + { key: 'default', label: formatMessage({ id: 'app.theme.switch.default' }) }, + { key: 'dark', label: formatMessage({ id: 'app.theme.switch.dark' }) }, + { key: 'compact', label: formatMessage({ id: 'app.theme.switch.compact' }) }, + ], + }; } flattenMenu(menu) { diff --git a/site/theme/template/Layout/Header/More.tsx b/site/theme/template/Layout/Header/More.tsx index 39e471f3af..8cb8e7939a 100644 --- a/site/theme/template/Layout/Header/More.tsx +++ b/site/theme/template/Layout/Header/More.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import type { MenuProps } from 'antd'; -import { Dropdown, Menu, Button } from 'antd'; +import { Dropdown, Button } from 'antd'; import { FormattedMessage } from 'react-intl'; import { DownOutlined } from '@ant-design/icons'; import type { SharedProps } from './interface'; @@ -84,10 +84,9 @@ export function getEcosystemGroup(): Exclude { } export default (props: SharedProps) => { - const menu = ; const downstyle = props.isRTL ? '-1px 2px 0 0' : '-1px 0 0 2px'; return ( - +