chore: sync master into feature

This commit is contained in:
thinkasany 2025-04-28 09:50:45 +08:00
commit 2542d2424e
21 changed files with 267 additions and 193 deletions

View File

@ -9,26 +9,22 @@ updates:
directory: /
schedule:
interval: daily
time: "05:00"
timezone: Asia/Shanghai
groups:
rc-component-patch:
dependency-type: production
patterns:
- "rc-*"
- "@rc-component*"
update-types: [patch]
dependencies:
dependency-type: production
exclude-patterns:
- "rc-*"
- "@rc-component*"
update-types: [major, minor]
dev-dependencies:
dependency-type: development
update-types: [major]
ignore:
- dependency-name: "@ant-design/cssinjs"
- dependency-name: "rc-*"
- dependency-name: "@rc-component*"
- dependency-name: "@ant-design*"
- dependency-name: dayjs
versions: [1.x]
- package-ecosystem: github-actions
directory: /
schedule:

View File

@ -75,5 +75,6 @@
"https://github.com/ant-design/ant-design/issues/51420",
"https://github.com/ant-design/ant-design/issues/51430"
],
"5.22.6": ["https://github.com/ant-design/ant-design/issues/52124"]
"5.22.6": ["https://github.com/ant-design/ant-design/issues/52124"],
"5.24.8": ["https://github.com/ant-design/ant-design/issues/53652"]
}

View File

@ -8,7 +8,7 @@
[![CI status][github-action-image]][github-action-url] [![codecov][codecov-image]][codecov-url] [![NPM version][npm-image]][npm-url] [![NPM downloads][download-image]][download-url]
[![][bundlephobia-image]][bundlephobia-url] [![][jsdelivr-image]][jsdelivr-url] [![FOSSA Status][fossa-image]][fossa-url]
[![][bundlephobia-image]][bundlephobia-url] [![][jsdelivr-image]][jsdelivr-url] [![FOSSA Status][fossa-image]][fossa-url] [![DeepWiki][deepwiki-image]][deepwiki-url]
[![Follow Twitter][twitter-image]][twitter-url] [![Renovate status][renovate-image]][renovate-dashboard-url] [![][issues-helper-image]][issues-helper-url] [![dumi][dumi-image]][dumi-url] [![Issues need help][help-wanted-image]][help-wanted-url]
@ -43,6 +43,8 @@
[dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
[dumi-url]: https://github.com/umijs/dumi
[github-issues-url]: https://new-issue.ant.design
[deepwiki-url]: https://deepwiki.com/ant-design/ant-design
[deepwiki-image]: https://img.shields.io/badge/Chat%20with-DeepWiki%20🤖-20B2AA?style=flat-square
</div>
@ -191,8 +193,7 @@ $ npm start
<a href="https://openomy.app/github/ant-design/ant-design" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=ant-design/ant-design&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
</a>
请参考[贡献指南](https://ant.design/docs/react/contributing-cn).
@ -214,8 +215,6 @@ $ npm start
## Issue 赞助
我们使用 [Polar.sh](https://polar.sh/ant-design) 和 [Issuehunt](https://issuehunt.io/repos/3452688) 来推动您希望看到的针对 antd 的修复和改进,请查看我们的赞助列表:
<a href="https://polar.sh/ant-design"><img src="https://polar.sh/embed/fund-our-backlog.svg?org=ant-design" /></a>
我们使用 [Issuehunt](https://issuehunt.io/repos/3452688) 来推动您希望看到的针对 antd 的修复和改进,请查看我们的赞助列表:
[![Let's fund issues in this repository](https://raw.githubusercontent.com/BoostIO/issuehunt-materials/master/v1/issuehunt-button-v1.svg)](https://issuehunt.io/repos/34526884)

View File

@ -8,7 +8,7 @@ An enterprise-class UI design language and React UI library.
[![CI status][github-action-image]][github-action-url] [![codecov][codecov-image]][codecov-url] [![NPM version][npm-image]][npm-url] [![NPM downloads][download-image]][download-url]
[![][bundlephobia-image]][bundlephobia-url] [![][jsdelivr-image]][jsdelivr-url] [![FOSSA Status][fossa-image]][fossa-url]
[![][bundlephobia-image]][bundlephobia-url] [![][jsdelivr-image]][jsdelivr-url] [![FOSSA Status][fossa-image]][fossa-url] [![DeepWiki][deepwiki-image]][deepwiki-url]
[![Follow Twitter][twitter-image]][twitter-url] [![Renovate status][renovate-image]][renovate-dashboard-url] [![][issues-helper-image]][issues-helper-url] [![dumi][dumi-image]][dumi-url] [![Issues need help][help-wanted-image]][help-wanted-url]
@ -43,6 +43,8 @@ An enterprise-class UI design language and React UI library.
[dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
[dumi-url]: https://github.com/umijs/dumi
[github-issues-url]: https://new-issue.ant.design
[deepwiki-url]: https://deepwiki.com/ant-design/ant-design
[deepwiki-image]: https://img.shields.io/badge/Chat%20with-DeepWiki%20🤖-20B2AA?style=flat-square
</div>
@ -173,8 +175,7 @@ Open your browser and visit http://127.0.0.1:8001, see more at [Development](htt
<a href="https://openomy.app/github/ant-design/ant-design" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=ant-design/ant-design&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
</a>
Let's build a better antd together.
@ -184,8 +185,6 @@ For collaborators, adhere to our [Pull Request Principle](https://github.com/ant
## Issue funding
We use [Polar.sh](https://polar.sh/ant-design) and [Issuehunt](https://issuehunt.io/repos/3452688) to up-vote and promote specific features that you would like to see and implement. Check our backlog and help us:
<a href="https://polar.sh/ant-design"><img src="https://polar.sh/embed/fund-our-backlog.svg?org=ant-design" /></a>
We use [Issuehunt](https://issuehunt.io/repos/3452688) to up-vote and promote specific features that you would like to see and implement. Check our backlog and help us:
[![Let's fund issues in this repository](https://raw.githubusercontent.com/BoostIO/issuehunt-materials/master/v1/issuehunt-button-v1.svg)](https://issuehunt.io/repos/34526884)

View File

@ -238,7 +238,8 @@ const Affix = React.forwardRef<AffixRef, InternalAffixProps>((props, ref) => {
React.useEffect(() => {
addListeners();
}, [target, affixStyle, lastAffix]);
return () => removeListeners();
}, [target, affixStyle, lastAffix, offsetTop, offsetBottom]);
React.useEffect(() => {
updatePosition();

View File

@ -16,6 +16,11 @@ Before using icons, you need to install the [@ant-design/icons](https://github.c
<InstallDependencies npm='npm install @ant-design/icons@5.x --save' yarn='yarn add @ant-design/icons@5.x' pnpm='pnpm install @ant-design/icons@5.x --save' bun='bun add @ant-design/icons@5.x'></InstallDependencies>
<!-- prettier-ignore -->
:::warning{title=Tips}
Remember to use @ant-design/icons v5 with antd v5. See: [#53275](https://github.com/ant-design/ant-design/issues/53275#issuecomment-2747448317)
:::
## List of icons
<IconSearch></IconSearch>

View File

@ -11,21 +11,26 @@ demo:
cols: 2
---
## 使用方法
## 使用方法 {#how-to-use}
使用图标组件,你需要安装 [@ant-design/icons](https://github.com/ant-design/ant-design-icons) 图标组件包:
<InstallDependencies npm='npm install @ant-design/icons@5.x --save' yarn='yarn add @ant-design/icons@5.x' pnpm='pnpm install @ant-design/icons@5.x --save' bun='bun add @ant-design/icons@5.x'></InstallDependencies>
## 设计师专属
<!-- prettier-ignore -->
:::warning{title=温馨提示}
使用 antd v5 时, 请确保安装配套的 @ant-design/icons v5 版本。详见 [#53275](https://github.com/ant-design/ant-design/issues/53275#issuecomment-2747448317)
:::
## 设计师专属 {#designers-exclusive}
安装 [Kitchen Sketch 插件 💎](https://kitchen.alipay.com),就可以一键拖拽使用 Ant Design 和 Iconfont 的海量图标,还可以关联自有项目。
## 图标列表
## 图标列表 {#list-of-icons}
<IconSearch></IconSearch>
## 代码演示
## 代码演示 {#examples}
<!-- prettier-ignore -->
<code src="./demo/basic.tsx">基本用法</code>
@ -38,7 +43,7 @@ demo:
从 4.0 开始antd 不再内置 Icon 组件,请使用独立的包 `@ant-design/icons`
### 通用图标
### 通用图标 {#common-icon}
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
@ -58,7 +63,7 @@ import { StarOutlined, StarFilled, StarTwoTone } from '@ant-design/icons';
<StarTwoTone twoToneColor="#eb2f96" />
```
### 自定义 Icon
### 自定义 Icon {#custom-icon}
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
@ -67,7 +72,7 @@ import { StarOutlined, StarFilled, StarTwoTone } from '@ant-design/icons';
| spin | 是否有旋转动画 | boolean | false | |
| style | 设置图标的样式,例如 `fontSize``color` | CSSProperties | - | |
### 关于 SVG 图标
### 关于 SVG 图标 {#about-svg-icons}
`3.9.0` 之后,我们使用了 SVG 图标替换了原先的 font 图标,从而带来了以下优势:
@ -86,7 +91,7 @@ import { MessageOutlined } from '@ant-design/icons';
<MessageOutlined style={{ fontSize: '16px', color: '#08c' }} />;
```
### 双色图标主色
### 双色图标主色 {#set-two-tone-color}
对于双色图标,可以通过使用 `getTwoToneColor()``setTwoToneColor(colorString)` 来全局设置图标主色。
@ -97,7 +102,7 @@ setTwoToneColor('#eb2f96');
getTwoToneColor(); // #eb2f96
```
### 自定义 font 图标
### 自定义 font 图标 {#custom-font-icon}
`3.9.0` 之后,我们提供了一个 `createFromIconfontCN` 方法,方便开发者调用在 [iconfont.cn](http://iconfont.cn/) 上自行管理的图标。
@ -126,7 +131,7 @@ options 的配置项如下:
见 [iconfont.cn 使用帮助](http://iconfont.cn/help/detail?spm=a313x.7781069.1998910419.15&helptype=code) 查看如何生成 js 地址。
### 自定义 SVG 图标
### 自定义 SVG 图标 {#custom-svg-icon}
如果使用 `webpack`,可以通过配置 [@svgr/webpack](https://www.npmjs.com/package/@svgr/webpack) 来将 `svg` 图标作为 `React` 组件导入。`@svgr/webpack` 的 `options` 选项请参阅 [svgr 文档](https://github.com/smooth-code/svgr#options)。
@ -184,6 +189,6 @@ ReactDOM.createRoot(mountNode).render(<Icon component={MessageSvg} />);
| style | 计算后的 `svg` 元素样式 | CSSProperties | - | |
| width | `svg` 元素宽度 | string \| number | `1em` | |
## 主题变量Design Token
## 主题变量Design Token{#design-token}
<ComponentTokenTable component="Icon"></ComponentTokenTable>

View File

@ -3,35 +3,30 @@ import { Avatar, Divider, List, Skeleton } from 'antd';
import InfiniteScroll from 'react-infinite-scroll-component';
interface DataType {
gender: string;
name: {
title: string;
first: string;
last: string;
};
email: string;
picture: {
large: string;
medium: string;
thumbnail: string;
};
nat: string;
gender?: string;
name?: string;
email?: string;
avatar?: string;
id?: string;
}
const App: React.FC = () => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<DataType[]>([]);
const [page, setPage] = useState(1);
const loadMoreData = () => {
if (loading) {
return;
}
setLoading(true);
fetch('https://randomuser.me/api/?results=10&inc=name,gender,email,nat,picture&noinfo')
fetch(`https://660d2bd96ddfa2943b33731c.mockapi.io/api/users/?page=${page}&limit=10`)
.then((res) => res.json())
.then((body) => {
setData([...data, ...body.results]);
.then((res) => {
const results = Array.isArray(res) ? res : [];
setData([...data, ...results]);
setLoading(false);
setPage(page + 1);
})
.catch(() => {
setLoading(false);
@ -65,8 +60,8 @@ const App: React.FC = () => {
renderItem={(item) => (
<List.Item key={item.email}>
<List.Item.Meta
avatar={<Avatar src={item.picture.large} />}
title={<a href="https://ant.design">{item.name.last}</a>}
avatar={<Avatar src={item.avatar} />}
title={<a href="https://ant.design">{item.name}</a>}
description={item.email}
/>
<div>Content</div>

View File

@ -3,59 +3,51 @@ import { Avatar, Button, List, Skeleton } from 'antd';
interface DataType {
gender?: string;
name: {
title?: string;
first?: string;
last?: string;
};
name?: string;
email?: string;
picture: {
large?: string;
medium?: string;
thumbnail?: string;
};
nat?: string;
avatar?: string;
loading: boolean;
}
const count = 3;
const fakeDataUrl = `https://randomuser.me/api/?results=${count}&inc=name,gender,email,nat,picture&noinfo`;
const PAGE_SIZE = 3;
const App: React.FC = () => {
const [initLoading, setInitLoading] = useState(true);
const [loading, setLoading] = useState(false);
const [data, setData] = useState<DataType[]>([]);
const [list, setList] = useState<DataType[]>([]);
const [page, setPage] = useState(1);
const fetchData = (currentPage: number) => {
const fakeDataUrl = `https://660d2bd96ddfa2943b33731c.mockapi.io/api/users?page=${currentPage}&limit=${PAGE_SIZE}`;
return fetch(fakeDataUrl).then((res) => res.json());
};
useEffect(() => {
fetch(fakeDataUrl)
.then((res) => res.json())
.then((res) => {
setInitLoading(false);
setData(res.results);
setList(res.results);
});
fetchData(page).then((res) => {
const results = Array.isArray(res) ? res : [];
setInitLoading(false);
setData(results);
setList(results);
});
}, []);
const onLoadMore = () => {
setLoading(true);
setList(
data.concat(
Array.from({ length: count }).map(() => ({ loading: true, name: {}, picture: {} })),
),
);
fetch(fakeDataUrl)
.then((res) => res.json())
.then((res) => {
const newData = data.concat(res.results);
setData(newData);
setList(newData);
setLoading(false);
// Resetting window's offsetTop so as to display react-virtualized demo underfloor.
// In real scene, you can using public method of react-virtualized:
// https://stackoverflow.com/questions/46700726/how-to-use-public-method-updateposition-of-react-virtualized
window.dispatchEvent(new Event('resize'));
});
setList(data.concat(Array.from({ length: PAGE_SIZE }).map(() => ({ loading: true }))));
const nextPage = page + 1;
setPage(nextPage);
fetchData(nextPage).then((res) => {
const results = Array.isArray(res) ? res : [];
const newData = data.concat(results);
setData(newData);
setList(newData);
setLoading(false);
// Resetting window's offsetTop so as to display react-virtualized demo underfloor.
// In real scene, you can using public method of react-virtualized:
// https://stackoverflow.com/questions/46700726/how-to-use-public-method-updateposition-of-react-virtualized
window.dispatchEvent(new Event('resize'));
});
};
const loadMore =
@ -85,8 +77,8 @@ const App: React.FC = () => {
>
<Skeleton avatar title={false} loading={item.loading} active>
<List.Item.Meta
avatar={<Avatar src={item.picture.large} />}
title={<a href="https://ant.design">{item.name?.last}</a>}
avatar={<Avatar src={item.avatar} />}
title={<a href="https://ant.design">{item.name}</a>}
description="Ant Design, a design language for background applications, is refined by Ant UED Team"
/>
<div>content</div>

View File

@ -5,32 +5,26 @@ import VirtualList from 'rc-virtual-list';
interface UserItem {
email: string;
gender: string;
name: {
first: string;
last: string;
title: string;
};
nat: string;
picture: {
large: string;
medium: string;
thumbnail: string;
};
name: string;
avatar: string;
}
const fakeDataUrl =
'https://randomuser.me/api/?results=20&inc=name,gender,email,nat,picture&noinfo';
const ContainerHeight = 400;
const CONTAINER_HEIGHT = 400;
const PAGE_SIZE = 20;
const App: React.FC = () => {
const [data, setData] = useState<UserItem[]>([]);
const [page, setPage] = useState(1);
const appendData = (showMessage = true) => {
const fakeDataUrl = `https://660d2bd96ddfa2943b33731c.mockapi.io/api/users/?page=${page}&limit=${PAGE_SIZE}`;
fetch(fakeDataUrl)
.then((res) => res.json())
.then((body) => {
setData(data.concat(body.results));
showMessage && message.success(`${body.results.length} more items loaded!`);
const results = Array.isArray(body) ? body : [];
setData(data.concat(results));
setPage(page + 1);
showMessage && message.success(`${results.length} more items loaded!`);
});
};
@ -40,7 +34,9 @@ const App: React.FC = () => {
const onScroll = (e: React.UIEvent<HTMLElement, UIEvent>) => {
// Refer to: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#problems_and_solutions
if (Math.abs(e.currentTarget.scrollHeight - e.currentTarget.scrollTop - ContainerHeight) <= 1) {
if (
Math.abs(e.currentTarget.scrollHeight - e.currentTarget.scrollTop - CONTAINER_HEIGHT) <= 1
) {
appendData();
}
};
@ -49,7 +45,7 @@ const App: React.FC = () => {
<List>
<VirtualList
data={data}
height={ContainerHeight}
height={CONTAINER_HEIGHT}
itemHeight={47}
itemKey="email"
onScroll={onScroll}
@ -57,8 +53,8 @@ const App: React.FC = () => {
{(item: UserItem) => (
<List.Item key={item.email}>
<List.Item.Meta
avatar={<Avatar src={item.picture.large} />}
title={<a href="https://ant.design">{item.name.last}</a>}
avatar={<Avatar src={item.avatar} />}
title={<a href="https://ant.design">{item.name}</a>}
description={item.email}
/>
<div>Content</div>

View File

@ -14429,7 +14429,9 @@ exports[`renders components/select/demo/select-users.tsx extend context correctl
class="ant-select-item-empty"
id="rc_select_TEST_OR_SSR_list"
role="listbox"
/>
>
No results found
</div>
</div>
</div>
<span

View File

@ -1,5 +1,7 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('Select image', () => {
imageDemoTest('select');
imageDemoTest('select', {
mobile: ['basic.tsx'],
});
});

View File

@ -1,5 +1,5 @@
import React, { useMemo, useRef, useState } from 'react';
import { Select, Spin } from 'antd';
import { Select, Spin, Avatar } from 'antd';
import type { SelectProps } from 'antd';
import debounce from 'lodash/debounce';
@ -10,8 +10,13 @@ export interface DebounceSelectProps<ValueType = any>
}
function DebounceSelect<
ValueType extends { key?: string; label: React.ReactNode; value: string | number } = any,
>({ fetchOptions, debounceTimeout = 800, ...props }: DebounceSelectProps<ValueType>) {
ValueType extends {
key?: string;
label: React.ReactNode;
value: string | number;
avatar?: string;
} = any,
>({ fetchOptions, debounceTimeout = 300, ...props }: DebounceSelectProps<ValueType>) {
const [fetching, setFetching] = useState(false);
const [options, setOptions] = useState<ValueType[]>([]);
const fetchRef = useRef(0);
@ -42,9 +47,15 @@ function DebounceSelect<
labelInValue
filterOption={false}
onSearch={debounceFetcher}
notFoundContent={fetching ? <Spin size="small" /> : null}
notFoundContent={fetching ? <Spin size="small" /> : 'No results found'}
{...props}
options={options}
optionRender={(option) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
{option.data.avatar && <Avatar src={option.data.avatar} style={{ marginRight: 8 }} />}
{option.label}
</div>
)}
/>
);
}
@ -53,21 +64,21 @@ function DebounceSelect<
interface UserValue {
label: string;
value: string;
avatar?: string;
}
async function fetchUserList(username: string): Promise<UserValue[]> {
console.log('fetching user', username);
return fetch('https://randomuser.me/api/?results=5')
.then((response) => response.json())
.then((body) =>
body.results.map(
(user: { name: { first: string; last: string }; login: { username: string } }) => ({
label: `${user.name.first} ${user.name.last}`,
value: user.login.username,
}),
),
);
return fetch(`https://660d2bd96ddfa2943b33731c.mockapi.io/api/users/?search=${username}`)
.then((res) => res.json())
.then((res) => {
const results = Array.isArray(res) ? res : [];
return results.map((user) => ({
label: user.name,
value: user.id,
avatar: user.avatar,
}));
});
}
const App: React.FC = () => {

View File

@ -72,6 +72,14 @@ const getSearchInputWithoutBorderStyle: GenerateStyle<SelectToken, CSSObject> =
const genBaseStyle: GenerateStyle<SelectToken> = (token) => {
const { antCls, componentCls, inputPaddingHorizontalBase, iconCls } = token;
const hoverShowClearStyle: CSSObject = {
[`${componentCls}-clear`]: {
opacity: 1,
background: token.colorBgBase,
borderRadius: '50%',
},
};
return {
[componentCls]: {
...resetComponent(token),
@ -198,11 +206,8 @@ const genBaseStyle: GenerateStyle<SelectToken> = (token) => {
},
},
[`&:hover ${componentCls}-clear`]: {
opacity: 1,
background: token.colorBgBase,
borderRadius: '50%',
},
'@media(hover:none)': hoverShowClearStyle,
'&:hover': hoverShowClearStyle,
},
// ========================= Feedback ==========================

View File

@ -95,6 +95,7 @@ const SplitBar: React.FC<SplitBarProps> = (props) => {
const handleLazyEnd = useEvent(() => {
onOffsetUpdate(index, constrainedOffsetX, constrainedOffsetY, true);
setConstrainedOffset(0);
onOffsetEnd();
});
React.useEffect(() => {

View File

@ -122,6 +122,9 @@ describe('Splitter lazy', () => {
onResize.mockReset();
mockDrag(container.querySelector('.ant-splitter-bar-dragger')!, onResize, -1000);
expect(onResizeEnd).toHaveBeenCalledWith([30, 70]);
// mask should hide
expect(container.querySelector('.ant-splitter-mask')).toBeFalsy();
});
it('should work with touch events when lazy', async () => {

View File

@ -6,7 +6,7 @@
当使用 `rowSelection` 时,请设置 `rowSelection.preserveSelectedRowKeys` 属性以保留 `key`
**注意,此示例使用 [模拟接口](https://randomuser.me),展示数据可能不准确,请打开网络面板查看请求。**
**注意,此示例使用 [模拟接口](https://mocky.io),展示数据可能不准确,请打开网络面板查看请求。**
> 🛎️ 想要 3 分钟实现?试试 [ProTable](https://procomponents.ant.design/components/table)
@ -16,4 +16,4 @@ This example shows how to fetch and present data from a remote server, and how t
Setting `rowSelection.preserveSelectedRowKeys` to keep the `key` when enable selection.
**Note, this example use [Mock API](https://randomuser.me) that you can look up in Network Console.**
**Note, this example use [Mock API](https://mocky.io) that you can look up in Network Console.**

View File

@ -9,15 +9,10 @@ type ColumnsType<T extends object = object> = TableProps<T>['columns'];
type TablePaginationConfig = Exclude<GetProp<TableProps, 'pagination'>, boolean>;
interface DataType {
name: {
first: string;
last: string;
};
name: string;
gender: string;
email: string;
login: {
uuid: string;
};
id: string;
}
interface TableParams {
@ -32,7 +27,6 @@ const columns: ColumnsType<DataType> = [
title: 'Name',
dataIndex: 'name',
sorter: true,
render: (name) => `${name.first} ${name.last}`,
width: '20%',
},
{
@ -58,11 +52,38 @@ const toURLSearchParams = <T extends AnyObject>(record: T) => {
return params;
};
const getRandomuserParams = (params: TableParams) => ({
results: params.pagination?.pageSize,
page: params.pagination?.current,
...params,
});
const getRandomuserParams = (params: TableParams) => {
const { pagination, filters, sortField, sortOrder, ...restParams } = params;
const result: Record<string, any> = {};
// https://github.com/mockapi-io/docs/wiki/Code-examples#pagination
result.limit = pagination?.pageSize;
result.page = pagination?.current;
// https://github.com/mockapi-io/docs/wiki/Code-examples#filtering
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
result[key] = value;
}
});
}
// https://github.com/mockapi-io/docs/wiki/Code-examples#sorting
if (sortField) {
result.orderby = sortField;
result.order = sortOrder === 'ascend' ? 'asc' : 'desc';
}
// 处理其他参数
Object.entries(restParams).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
result[key] = value;
}
});
return result;
};
const App: React.FC = () => {
const [data, setData] = useState<DataType[]>();
@ -78,17 +99,17 @@ const App: React.FC = () => {
const fetchData = () => {
setLoading(true);
fetch(`https://randomuser.me/api?${params.toString()}`)
fetch(`https://660d2bd96ddfa2943b33731c.mockapi.io/api/users?${params.toString()}`)
.then((res) => res.json())
.then(({ results }) => {
setData(results);
.then((res) => {
setData(Array.isArray(res) ? res : []);
setLoading(false);
setTableParams({
...tableParams,
pagination: {
...tableParams.pagination,
total: 200,
// 200 is mock data, you should read it from server
total: 100,
// 100 is mock data, you should read it from server
// total: data.totalCount,
},
});
@ -120,7 +141,7 @@ const App: React.FC = () => {
return (
<Table<DataType>
columns={columns}
rowKey={(record) => record.login.uuid}
rowKey={(record) => record.id}
dataSource={data}
pagination={tableParams.pagination}
loading={loading}

View File

@ -1,6 +1,7 @@
// jest-puppeteer.config.js
module.exports = {
launch: {
headless: 'new',
ignoreDefaultArgs: ['--disable-extensions'],
args: [
// Required for Docker version of Puppeteer

View File

@ -289,7 +289,7 @@
"prettier": "^3.4.1",
"pretty-format": "^29.7.0",
"prismjs": "^1.29.0",
"puppeteer": "^24.0.0",
"puppeteer": "^24.7.1",
"rc-footer": "^0.6.8",
"rc-tween-one": "^3.0.6",
"rc-virtual-list": "^3.17.0",
@ -356,16 +356,13 @@
},
"pnpm": {
"overrides": {
"countup.js": "2.8.0",
"nwsapi": "2.2.20"
}
},
"overrides": {
"countup.js": "2.8.0",
"nwsapi": "2.2.20"
},
"resolutions": {
"countup.js": "2.8.0",
"nwsapi": "2.2.20"
}
}

View File

@ -8,7 +8,7 @@ import fse from 'fs-extra';
import { globSync } from 'glob';
import { JSDOM } from 'jsdom';
import MockDate from 'mockdate';
import type { HTTPRequest } from 'puppeteer';
import type { HTTPRequest, Viewport } from 'puppeteer';
import rcWarning from 'rc-util/lib/warning';
import ReactDOMServer from 'react-dom/server';
@ -32,6 +32,7 @@ interface ImageTestOptions {
onlyViewport?: boolean;
ssr?: boolean;
openTriggerClassName?: string;
mobile?: boolean;
}
// eslint-disable-next-line jest/no-export
@ -109,9 +110,14 @@ export default function imageTest(
container = doc.querySelector<HTMLDivElement>('#root')!;
});
function test(name: string, suffix: string, themedComponent: React.ReactElement) {
function test(name: string, suffix: string, themedComponent: React.ReactElement, mobile = false) {
it(name, async () => {
await page.setViewport({ width: 800, height: 600 });
const sharedViewportConfig: Partial<Viewport> = {
isMobile: mobile,
hasTouch: mobile,
};
await page.setViewport({ width: 800, height: 600, ...sharedViewportConfig });
const onRequestHandle = (request: HTTPRequest) => {
if (['image'].includes(request.resourceType())) {
@ -166,6 +172,11 @@ export default function imageTest(
unmount();
}
// Remove mobile css for hardcode since CI will always think as mobile
if (!mobile) {
styleStr = styleStr.replace(/@media\(hover:\s*none\)/g, '@media(hover:not-valid)');
}
if (openTriggerClassName) {
styleStr += `<style>
.${openTriggerClassName} {
@ -211,7 +222,7 @@ export default function imageTest(
Please consider using \`onlyViewport: ["filename.tsx"]\`, read more: https://github.com/ant-design/ant-design/pull/52053`,
);
await page.setViewport({ width: 800, height: bodyHeight });
await page.setViewport({ width: 800, height: bodyHeight, ...sharedViewportConfig });
}
const image = await page.screenshot({
@ -225,29 +236,35 @@ export default function imageTest(
});
}
Object.entries(themes).forEach(([key, algorithm]) => {
const configTheme = {
algorithm,
token: {
fontFamily: 'Arial',
},
};
if (!options.mobile) {
Object.entries(themes).forEach(([key, algorithm]) => {
const configTheme = {
algorithm,
token: {
fontFamily: 'Arial',
},
};
test(
`component image screenshot should correct ${key}`,
`.${key}`,
<div style={{ background: key === 'dark' ? '#000' : '', padding: `24px 12px` }} key={key}>
<ConfigProvider theme={configTheme}>{component}</ConfigProvider>
</div>,
);
test(
`[CSS Var] component image screenshot should correct ${key}`,
`.${key}.css-var`,
<div style={{ background: key === 'dark' ? '#000' : '', padding: `24px 12px` }} key={key}>
<ConfigProvider theme={{ ...configTheme, cssVar: true }}>{component}</ConfigProvider>
</div>,
);
});
test(
`component image screenshot should correct ${key}`,
`.${key}`,
<div style={{ background: key === 'dark' ? '#000' : '', padding: `24px 12px` }} key={key}>
<ConfigProvider theme={configTheme}>{component}</ConfigProvider>
</div>,
);
test(
`[CSS Var] component image screenshot should correct ${key}`,
`.${key}.css-var`,
<div style={{ background: key === 'dark' ? '#000' : '', padding: `24px 12px` }} key={key}>
<ConfigProvider theme={{ ...configTheme, cssVar: true }}>{component}</ConfigProvider>
</div>,
);
});
// Mobile Snapshot
} else {
test(identifier, `.mobile`, component, true);
}
}
type Options = {
@ -257,6 +274,7 @@ type Options = {
ssr?: boolean;
/** Open Trigger to check the popup render */
openTriggerClassName?: string;
mobile?: string[];
};
// eslint-disable-next-line jest/no-export
@ -266,25 +284,49 @@ export function imageDemoTest(component: string, options: Options = {}) {
(file) => !file.includes('_semantic'),
);
const mobileDemos: [file: string, node: any][] = [];
const getTestOption = (file: string) => ({
onlyViewport:
options.onlyViewport === true ||
(Array.isArray(options.onlyViewport) && options.onlyViewport.some((c) => file.endsWith(c))),
ssr: options.ssr,
openTriggerClassName: options.openTriggerClassName,
});
files.forEach((file) => {
if (Array.isArray(options.skip) && options.skip.some((c) => file.endsWith(c))) {
describeMethod = describe.skip;
} else {
describeMethod = describe;
}
describeMethod(`Test ${file} image`, () => {
let Demo = require(`../../${file}`).default;
if (typeof Demo === 'function') {
Demo = <Demo />;
}
imageTest(Demo, `${component}-${path.basename(file, '.tsx')}`, {
onlyViewport:
options.onlyViewport === true ||
(Array.isArray(options.onlyViewport) &&
options.onlyViewport.some((c) => file.endsWith(c))),
ssr: options.ssr,
openTriggerClassName: options.openTriggerClassName,
});
imageTest(Demo, `${component}-${path.basename(file, '.tsx')}`, getTestOption(file));
// Check if need mobile test
if ((options.mobile || []).some((c) => file.endsWith(c))) {
mobileDemos.push([file, Demo]);
}
});
});
if (mobileDemos.length) {
describeMethod(`Test mobile image`, () => {
beforeAll(async () => {
await jestPuppeteer.resetPage();
});
mobileDemos.forEach(([file, Demo]) => {
imageTest(Demo, `${component}-${path.basename(file, '.tsx')}`, {
...getTestOption(file),
mobile: true,
});
});
});
}
}