Merge pull request #48524 from ant-design/master

chore: merge master into feature
This commit is contained in:
lijianan 2024-04-18 10:11:58 +08:00 committed by GitHub
commit 27c942a00b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 3609 additions and 2495 deletions

View File

@ -1,39 +1,27 @@
import React from 'react';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { Col, Row, Tooltip } from 'antd';
import { Col, Row, Tooltip, Card, Typography } from 'antd';
import { createStyles } from 'antd-style';
import useLocale from '../../../hooks/useLocale';
const useStyle = createStyles(({ token, css }) => {
const { boxShadowSecondary } = token;
const { Paragraph } = Typography;
return {
card: css`
const useStyle = createStyles(({ token, css }) => ({
card: css`
position: relative;
display: flex;
flex-direction: column;
height: 100%;
color: inherit;
list-style: none;
border: 1px solid ${token.colorSplit};
border-radius: ${token.borderRadiusXS}px;
cursor: pointer;
transition: box-shadow ${token.motionDurationSlow};
overflow: hidden;
&:hover {
box-shadow: ${boxShadowSecondary};
color: inherit;
.ant-card-cover {
overflow: hidden;
}
`,
image: css`
width: calc(100% + 2px);
max-width: none;
height: 184px;
margin: -1px -1px 0;
object-fit: cover;
`,
badge: css`
img {
transition: all ${token.motionDurationSlow} ease-out;
}
&:hover img {
transform: scale(1.3);
`,
badge: css`
position: absolute;
top: 8px;
right: 8px;
@ -42,25 +30,12 @@ const useStyle = createStyles(({ token, css }) => {
font-size: ${token.fontSizeSM}px;
line-height: 1;
background: rgba(0, 0, 0, 0.65);
border-radius: ${token.borderRadiusXS}px;
border-radius: ${token.borderRadiusLG}px;
box-shadow: 0 0 2px rgba(255, 255, 255, 0.2);
display: inline-flex;
column-gap: ${token.paddingXXS}px;
`,
title: css`
margin: ${token.margin}px ${token.marginMD}px ${token.marginXS}px;
opacity: 0.85;
font-size: ${token.fontSizeXL}px;
line-height: 28px;
`,
description: css`
margin: 0 ${token.marginMD}px ${token.marginMD}px;
opacity: 0.65;
font-size: ${token.fontSizeXL}px;
line-height: 22px;
`,
};
});
}));
export type Resource = {
title: string;
@ -91,38 +66,33 @@ const ResourceCard: React.FC<ResourceCardProps> = ({ resource }) => {
const { styles } = useStyle();
const [locale] = useLocale(locales);
const { title: titleStr, description, cover, src, official } = resource;
const { title, description, cover, src, official } = resource;
let coverColor: string | null = null;
let title: string = titleStr;
const titleMatch = titleStr.match(/(.*)(#[\dA-Fa-f]{6})/);
if (titleMatch) {
title = titleMatch[1].trim();
// eslint-disable-next-line prefer-destructuring
coverColor = titleMatch[2];
}
const badge = official ? (
<div className={styles.badge}>{locale.official}</div>
) : (
<Tooltip title={locale.thirdPartDesc}>
<div className={styles.badge}>
<ExclamationCircleOutlined />
{locale.thirdPart}
</div>
</Tooltip>
);
return (
<Col xs={24} sm={12} md={8} lg={6} style={{ padding: 12 }}>
<Col xs={24} sm={12} md={8} lg={6}>
<a className={styles.card} target="_blank" href={src} rel="noreferrer">
<img
className={styles.image}
src={cover}
alt={title}
style={coverColor ? { backgroundColor: coverColor } : {}}
/>
{official ? (
<div className={styles.badge}>{locale.official}</div>
) : (
<Tooltip title={locale.thirdPartDesc}>
<div className={styles.badge}>
<ExclamationCircleOutlined />
{locale.thirdPart}
</div>
</Tooltip>
)}
<p className={styles?.title}>{title}</p>
<p className={styles.description}>{description}</p>
<Card hoverable className={styles.card} cover={<img src={cover} alt={title} />}>
<Card.Meta
title={title}
description={
<Paragraph style={{ marginBottom: 0 }} ellipsis={{ rows: 1 }} title={description}>
{description}
</Paragraph>
}
/>
{badge}
</Card>
</a>
</Col>
);
@ -133,7 +103,7 @@ export type ResourceCardsProps = {
};
const ResourceCards: React.FC<ResourceCardsProps> = ({ resources }) => (
<Row style={{ margin: '-12px -12px 0 -12px' }}>
<Row gutter={[24, 24]}>
{resources.map((item) => (
<ResourceCard resource={item} key={item?.title} />
))}

View File

@ -1,11 +1,12 @@
import { createHash } from 'crypto';
import fs from 'fs';
import path from 'path';
import { createHash } from 'crypto';
import createEmotionServer from '@emotion/server/create-instance';
import chalk from 'chalk';
import type { IApi, IRoute } from 'dumi';
import ReactTechStack from 'dumi/dist/techStacks/react';
import chalk from 'chalk';
import sylvanas from 'sylvanas';
import createEmotionServer from '@emotion/server/create-instance';
import localPackage from '../../package.json';
function extractEmotionStyle(html: string) {
@ -53,13 +54,65 @@ class AntdReactTechStack extends ReactTechStack {
if (md) {
// extract description & css style from md file
const description = md.match(
new RegExp(`(?:^|\\n)## ${locale}([^]+?)(\\n## [a-z]|\\n\`\`\`|\\n<style>|$)`),
)?.[1];
const style = md.match(/\r?\n(?:```css|<style>)\r?\n([^]+?)\r?\n(?:```|<\/style>)/)?.[1];
const blocks: Record<string, string> = {};
props.description ??= description?.trim();
props.style ??= style;
const lines = md.split('\n');
let blockName = '';
let cacheList: string[] = [];
// Get block name
const getBlockName = (text: string) => {
if (text.startsWith('## ')) {
return text.replace('## ', '').trim();
}
if (text.startsWith('```css') || text.startsWith('<style>')) {
return 'style';
}
return null;
};
// Fill block content
const fillBlock = (name: string, lineList: string[]) => {
if (lineList.length) {
let fullText: string;
if (name === 'style') {
fullText = lineList
.join('\n')
.replace(/<\/?style>/g, '')
.replace(/```(\s*css)/g, '');
} else {
fullText = lineList.slice(1).join('\n');
}
blocks[name] = fullText;
}
};
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Mark as new block
const nextBlockName = getBlockName(line);
if (nextBlockName) {
fillBlock(blockName, cacheList);
// Next Block
blockName = nextBlockName;
cacheList = [line];
} else {
cacheList.push(line);
}
}
// Last block
fillBlock(blockName, cacheList);
props.description = blocks[locale];
props.style = blocks.style;
}
}

View File

@ -6,7 +6,7 @@ Your pull requests will be merged after one of the collaborators approve.
Thank you!
-->
[[中文版模板 / Chinese template](https://github.com/ant-design/ant-design/blob/master/.github/PULL_REQUEST_TEMPLATE/pr_cn.md?plain=1)]
[中文版模板 / Chinese template](https://github.com/ant-design/ant-design/blob/master/.github/PULL_REQUEST_TEMPLATE/pr_cn.md?plain=1)
### 🤔 This is a ...

View File

@ -6,7 +6,7 @@
请确保填写以下 pull request 的信息,谢谢!~
-->
[[English Template / 英文模板](https://github.com/ant-design/ant-design/blob/master/.github/PULL_REQUEST_TEMPLATE.md?plain=1)]
[English Template / 英文模板](https://github.com/ant-design/ant-design/blob/master/.github/PULL_REQUEST_TEMPLATE.md?plain=1)
### 🤔 这个变动的性质是?

View File

@ -5,6 +5,7 @@ on:
types: [opened, synchronize]
permissions:
issues: write
contents: read
jobs:

View File

@ -287,6 +287,7 @@ jobs:
NODE_OPTIONS: "--max_old_space_size=4096"
CI: 1
# Artifact build files
- uses: actions/upload-artifact@v4
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
with:
@ -297,6 +298,17 @@ jobs:
es
lib
- name: zip builds
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
env:
ALI_OSS_AK_ID: ${{ secrets.ALI_OSS_AK_ID }}
ALI_OSS_AK_SECRET: ${{ secrets.ALI_OSS_AK_SECRET }}
HEAD_SHA: ${{ github.sha }}
run: |
zip -r oss-artifacts.zip dist locale es lib
echo "🤖 Uploading"
node scripts/visual-regression/upload.js ./oss-artifacts.zip --ref=$HEAD_SHA
compiled-module-test:
name: module test
runs-on: ubuntu-latest

1
.gitignore vendored
View File

@ -45,6 +45,7 @@ components/**/*.jsx
/.history
*.tmp
artifacts.zip
oss-artifacts.zip
server/
# Docs templates

View File

@ -1,9 +1,7 @@
const config = {
plugins: [
'remark-preset-lint-recommended',
['remark-lint-list-item-indent', 'space'],
['remark-lint-no-literal-urls', false],
['remark-lint-no-undefined-references', false],
['remark-lint-no-undefined-references', { allow: [' ', /RFC/] }],
],
};

View File

@ -307,7 +307,7 @@ const genCardStyle: GenerateStyle<CardToken> = (token): CSSObject => {
[`${componentCls}-body`]: {
padding: cardPaddingBase,
borderRadius: ` 0 0 ${unit(token.borderRadiusLG)} ${unit(token.borderRadiusLG)}`,
borderRadius: `0 0 ${unit(token.borderRadiusLG)} ${unit(token.borderRadiusLG)}`,
...clearFix(),
},

View File

@ -124,7 +124,9 @@ export const genBaseStyle: GenerateStyle<CollapseToken> = (token) => {
[`${componentCls}-arrow`]: {
...resetIcon(),
fontSize: fontSizeIcon,
// when `transform: rotate()` is applied to icon's root element
transition: `transform ${motionDurationSlow}`,
// when `transform: rotate()` is applied to icon's child element
svg: {
transition: `transform ${motionDurationSlow}`,
},
@ -231,7 +233,7 @@ export const genBaseStyle: GenerateStyle<CollapseToken> = (token) => {
const genArrowStyle: GenerateStyle<CollapseToken> = (token) => {
const { componentCls } = token;
const fixedSelector = `> ${componentCls}-item > ${componentCls}-header ${componentCls}-arrow svg`;
const fixedSelector = `> ${componentCls}-item > ${componentCls}-header ${componentCls}-arrow`;
return {
[`${componentCls}-rtl`]: {

View File

@ -698,4 +698,30 @@ describe('ColorPicker', () => {
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
).toBeFalsy();
});
describe('default clearValue should be changed', () => {
const Demo = () => {
const [color, setColor] = useState<string>('');
useEffect(() => {
setColor('#1677ff');
}, []);
return <ColorPicker value={color} allowClear />;
};
it('normal', () => {
const { container } = render(<Demo />);
expect(container.querySelector('.ant-color-picker-clear')).toBeFalsy();
});
it('strict', () => {
const { container } = render(
<React.StrictMode>
<Demo />
</React.StrictMode>,
);
expect(container.querySelector('.ant-color-picker-clear')).toBeFalsy();
});
});
});

View File

@ -4,6 +4,8 @@ import type { Color } from '../color';
import type { ColorValueType } from '../interface';
import { generateColor } from '../util';
const INIT_COLOR_REF = {} as ColorValueType;
function hasValue(value?: ColorValueType) {
return value !== undefined;
}
@ -33,7 +35,14 @@ const useColorState = (
prevColor.current = color;
};
const prevValue = useRef<ColorValueType | undefined>(INIT_COLOR_REF);
useEffect(() => {
// `useEffect` will be executed twice in strict mode even if the deps are the same
// So we compare the value manually to avoid unnecessary update
if (prevValue.current === value) {
return;
}
prevValue.current = value;
if (hasValue(value)) {
const newColor = generateColor(value || '');
if (prevColor.current.cleared === true) {

View File

@ -42620,6 +42620,601 @@ exports[`renders components/date-picker/demo/multiple-debug.tsx extend context c
<div
class="ant-flex ant-flex-align-stretch ant-flex-gap-small ant-flex-vertical"
>
<div
class="ant-picker ant-picker-multiple ant-picker-outlined"
>
<div
class="ant-picker-selector"
>
<div
class="ant-picker-selection-overflow"
/>
<span
class="ant-picker-selection-placeholder"
>
Bamboo
</span>
</div>
<input
class="ant-picker-multiple-input"
readonly=""
value=""
/>
<span
class="ant-picker-suffix"
>
<span
aria-label="calendar"
class="anticon anticon-calendar"
role="img"
>
<svg
aria-hidden="true"
data-icon="calendar"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zM184 392V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136H184z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-picker-dropdown ant-slide-up-appear ant-slide-up-appear-prepare ant-slide-up ant-picker-dropdown-placement-bottomLeft"
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; box-sizing: border-box;"
>
<div
class="ant-picker-panel-container ant-picker-date-panel-container"
style="margin-left: 0px; margin-right: auto;"
tabindex="-1"
>
<div
class="ant-picker-panel-layout"
>
<div>
<div
class="ant-picker-panel"
tabindex="0"
>
<div
class="ant-picker-date-panel"
>
<div
class="ant-picker-header"
>
<button
class="ant-picker-header-super-prev-btn"
tabindex="-1"
type="button"
>
<span
class="ant-picker-super-prev-icon"
/>
</button>
<button
class="ant-picker-header-prev-btn"
tabindex="-1"
type="button"
>
<span
class="ant-picker-prev-icon"
/>
</button>
<div
class="ant-picker-header-view"
>
<button
class="ant-picker-month-btn"
tabindex="-1"
type="button"
>
Nov
</button>
<button
class="ant-picker-year-btn"
tabindex="-1"
type="button"
>
2016
</button>
</div>
<button
class="ant-picker-header-next-btn"
tabindex="-1"
type="button"
>
<span
class="ant-picker-next-icon"
/>
</button>
<button
class="ant-picker-header-super-next-btn"
tabindex="-1"
type="button"
>
<span
class="ant-picker-super-next-icon"
/>
</button>
</div>
<div
class="ant-picker-body"
>
<table
class="ant-picker-content"
>
<thead>
<tr>
<th>
Su
</th>
<th>
Mo
</th>
<th>
Tu
</th>
<th>
We
</th>
<th>
Th
</th>
<th>
Fr
</th>
<th>
Sa
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="ant-picker-cell"
title="2016-10-30"
>
<div
class="ant-picker-cell-inner"
>
30
</div>
</td>
<td
class="ant-picker-cell"
title="2016-10-31"
>
<div
class="ant-picker-cell-inner"
>
31
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-01"
>
<div
class="ant-picker-cell-inner"
>
1
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-02"
>
<div
class="ant-picker-cell-inner"
>
2
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-03"
>
<div
class="ant-picker-cell-inner"
>
3
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-04"
>
<div
class="ant-picker-cell-inner"
>
4
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-05"
>
<div
class="ant-picker-cell-inner"
>
5
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-06"
>
<div
class="ant-picker-cell-inner"
>
6
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-07"
>
<div
class="ant-picker-cell-inner"
>
7
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-08"
>
<div
class="ant-picker-cell-inner"
>
8
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-09"
>
<div
class="ant-picker-cell-inner"
>
9
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-10"
>
<div
class="ant-picker-cell-inner"
>
10
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-11"
>
<div
class="ant-picker-cell-inner"
>
11
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-12"
>
<div
class="ant-picker-cell-inner"
>
12
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-13"
>
<div
class="ant-picker-cell-inner"
>
13
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-14"
>
<div
class="ant-picker-cell-inner"
>
14
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-15"
>
<div
class="ant-picker-cell-inner"
>
15
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-16"
>
<div
class="ant-picker-cell-inner"
>
16
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-17"
>
<div
class="ant-picker-cell-inner"
>
17
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-18"
>
<div
class="ant-picker-cell-inner"
>
18
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-19"
>
<div
class="ant-picker-cell-inner"
>
19
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-20"
>
<div
class="ant-picker-cell-inner"
>
20
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-21"
>
<div
class="ant-picker-cell-inner"
>
21
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view ant-picker-cell-today"
title="2016-11-22"
>
<div
class="ant-picker-cell-inner"
>
22
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-23"
>
<div
class="ant-picker-cell-inner"
>
23
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-24"
>
<div
class="ant-picker-cell-inner"
>
24
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-25"
>
<div
class="ant-picker-cell-inner"
>
25
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-26"
>
<div
class="ant-picker-cell-inner"
>
26
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-27"
>
<div
class="ant-picker-cell-inner"
>
27
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-28"
>
<div
class="ant-picker-cell-inner"
>
28
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-29"
>
<div
class="ant-picker-cell-inner"
>
29
</div>
</td>
<td
class="ant-picker-cell ant-picker-cell-in-view"
title="2016-11-30"
>
<div
class="ant-picker-cell-inner"
>
30
</div>
</td>
<td
class="ant-picker-cell"
title="2016-12-01"
>
<div
class="ant-picker-cell-inner"
>
1
</div>
</td>
<td
class="ant-picker-cell"
title="2016-12-02"
>
<div
class="ant-picker-cell-inner"
>
2
</div>
</td>
<td
class="ant-picker-cell"
title="2016-12-03"
>
<div
class="ant-picker-cell-inner"
>
3
</div>
</td>
</tr>
<tr>
<td
class="ant-picker-cell"
title="2016-12-04"
>
<div
class="ant-picker-cell-inner"
>
4
</div>
</td>
<td
class="ant-picker-cell"
title="2016-12-05"
>
<div
class="ant-picker-cell-inner"
>
5
</div>
</td>
<td
class="ant-picker-cell"
title="2016-12-06"
>
<div
class="ant-picker-cell-inner"
>
6
</div>
</td>
<td
class="ant-picker-cell"
title="2016-12-07"
>
<div
class="ant-picker-cell-inner"
>
7
</div>
</td>
<td
class="ant-picker-cell"
title="2016-12-08"
>
<div
class="ant-picker-cell-inner"
>
8
</div>
</td>
<td
class="ant-picker-cell"
title="2016-12-09"
>
<div
class="ant-picker-cell-inner"
>
9
</div>
</td>
<td
class="ant-picker-cell"
title="2016-12-10"
>
<div
class="ant-picker-cell-inner"
>
10
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-picker ant-picker-multiple ant-picker-small ant-picker-outlined"
>

View File

@ -3315,6 +3315,50 @@ exports[`renders components/date-picker/demo/multiple-debug.tsx correctly 1`] =
<div
class="ant-flex ant-flex-align-stretch ant-flex-gap-small ant-flex-vertical"
>
<div
class="ant-picker ant-picker-multiple ant-picker-outlined"
>
<div
class="ant-picker-selector"
>
<div
class="ant-picker-selection-overflow"
/>
<span
class="ant-picker-selection-placeholder"
>
Bamboo
</span>
</div>
<input
class="ant-picker-multiple-input"
readonly=""
value=""
/>
<span
class="ant-picker-suffix"
>
<span
aria-label="calendar"
class="anticon anticon-calendar"
role="img"
>
<svg
aria-hidden="true"
data-icon="calendar"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zM184 392V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136H184z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-picker ant-picker-multiple ant-picker-small ant-picker-outlined"
>

View File

@ -6,6 +6,7 @@ const defaultValue = new Array(10).fill(0).map((_, index) => dayjs('2000-01-01')
const App: React.FC = () => (
<Flex vertical gap="small">
<DatePicker multiple placeholder="Bamboo" />
<DatePicker multiple defaultValue={defaultValue} size="small" />
<DatePicker multiple defaultValue={defaultValue} />
<DatePicker multiple defaultValue={defaultValue} size="large" />

View File

@ -65,15 +65,33 @@ const genPickerMultipleStyle: GenerateStyle<PickerToken> = (token) => {
{
[`${componentCls}${componentCls}-multiple`]: {
width: '100%',
cursor: 'text',
// ==================== Selector =====================
[`${componentCls}-selector`]: {
flex: 'auto',
padding: 0,
position: 'relative',
'&:after': {
margin: 0,
},
// ================== placeholder ==================
[`${componentCls}-selection-placeholder`]: {
position: 'absolute',
top: '50%',
insetInlineStart: token.inputPaddingHorizontalBase,
insetInlineEnd: 0,
transform: 'translateY(-50%)',
transition: `all ${token.motionDurationSlow}`,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
flex: 1,
color: token.colorTextPlaceholder,
pointerEvents: 'none',
},
},
// ===================== Overflow ====================

View File

@ -562,7 +562,7 @@ export const genPanelStyle = (token: SharedPickerToken): CSSObject => {
'&::-webkit-scrollbar-thumb': {
backgroundColor: token.colorTextTertiary,
borderRadius: 4,
borderRadius: token.borderRadiusSM,
},
// For Firefox

View File

@ -1,111 +1,197 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders components/dropdown/demo/arrow.tsx correctly 1`] = `
Array [
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
<div
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<span>
bottomLeft
</span>
</button>,
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
style="flex-wrap:wrap"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
bottomLeft
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
bottom
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
bottomRight
</span>
</button>
</div>
</div>
</div>
<div
class="ant-space-item"
>
<span>
bottom
</span>
</button>,
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
bottomRight
</span>
</button>,
<br />,
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
topLeft
</span>
</button>,
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
top
</span>
</button>,
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
topRight
</span>
</button>,
]
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
style="flex-wrap:wrap"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
topLeft
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
top
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
topRight
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`renders components/dropdown/demo/arrow-center.tsx correctly 1`] = `
Array [
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
<div
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<span>
bottomLeft
</span>
</button>,
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
style="flex-wrap:wrap"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
bottomLeft
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
bottom
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
bottomRight
</span>
</button>
</div>
</div>
</div>
<div
class="ant-space-item"
>
<span>
bottom
</span>
</button>,
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
bottomRight
</span>
</button>,
<br />,
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
topLeft
</span>
</button>,
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
top
</span>
</button>,
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
topRight
</span>
</button>,
]
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
style="flex-wrap:wrap"
>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
topLeft
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
top
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-default ant-dropdown-trigger"
type="button"
>
<span>
topRight
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`renders components/dropdown/demo/basic.tsx correctly 1`] = `

View File

@ -5,15 +5,3 @@
## en-US
By specifying `arrow` prop with `{ pointAtCenter: true }`, the arrow will point to the center of the target element.
```css
#components-dropdown-demo-arrow-center .ant-btn {
margin-right: 8px;
margin-bottom: 8px;
}
.ant-row-rtl #components-dropdown-demo-arrow-center .ant-btn {
margin-right: 0;
margin-bottom: 8px;
margin-left: 8px;
}
```

View File

@ -1,6 +1,6 @@
import React from 'react';
import type { MenuProps } from 'antd';
import { Button, Dropdown } from 'antd';
import { Button, Dropdown, Space } from 'antd';
const items: MenuProps['items'] = [
{
@ -30,27 +30,30 @@ const items: MenuProps['items'] = [
];
const App: React.FC = () => (
<>
<Dropdown menu={{ items }} placement="bottomLeft" arrow={{ pointAtCenter: true }}>
<Button>bottomLeft</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="bottom" arrow={{ pointAtCenter: true }}>
<Button>bottom</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="bottomRight" arrow={{ pointAtCenter: true }}>
<Button>bottomRight</Button>
</Dropdown>
<br />
<Dropdown menu={{ items }} placement="topLeft" arrow={{ pointAtCenter: true }}>
<Button>topLeft</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="top" arrow={{ pointAtCenter: true }}>
<Button>top</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="topRight" arrow={{ pointAtCenter: true }}>
<Button>topRight</Button>
</Dropdown>
</>
<Space direction="vertical">
<Space wrap>
<Dropdown menu={{ items }} placement="bottomLeft" arrow={{ pointAtCenter: true }}>
<Button>bottomLeft</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="bottom" arrow={{ pointAtCenter: true }}>
<Button>bottom</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="bottomRight" arrow={{ pointAtCenter: true }}>
<Button>bottomRight</Button>
</Dropdown>
</Space>
<Space wrap>
<Dropdown menu={{ items }} placement="topLeft" arrow={{ pointAtCenter: true }}>
<Button>topLeft</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="top" arrow={{ pointAtCenter: true }}>
<Button>top</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="topRight" arrow={{ pointAtCenter: true }}>
<Button>topRight</Button>
</Dropdown>
</Space>
</Space>
);
export default App;

View File

@ -5,15 +5,3 @@
## en-US
You could display an arrow.
```css
#components-dropdown-demo-arrow .ant-btn {
margin-right: 8px;
margin-bottom: 8px;
}
.ant-row-rtl #components-dropdown-demo-arrow .ant-btn {
margin-right: 0;
margin-bottom: 8px;
margin-left: 8px;
}
```

View File

@ -1,6 +1,6 @@
import React from 'react';
import type { MenuProps } from 'antd';
import { Button, Dropdown } from 'antd';
import { Button, Dropdown, Space } from 'antd';
const items: MenuProps['items'] = [
{
@ -30,27 +30,30 @@ const items: MenuProps['items'] = [
];
const App: React.FC = () => (
<>
<Dropdown menu={{ items }} placement="bottomLeft" arrow>
<Button>bottomLeft</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="bottom" arrow>
<Button>bottom</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="bottomRight" arrow>
<Button>bottomRight</Button>
</Dropdown>
<br />
<Dropdown menu={{ items }} placement="topLeft" arrow>
<Button>topLeft</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="top" arrow>
<Button>top</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="topRight" arrow>
<Button>topRight</Button>
</Dropdown>
</>
<Space direction="vertical">
<Space wrap>
<Dropdown menu={{ items }} placement="bottomLeft" arrow>
<Button>bottomLeft</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="bottom" arrow>
<Button>bottom</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="bottomRight" arrow>
<Button>bottomRight</Button>
</Dropdown>
</Space>
<Space wrap>
<Dropdown menu={{ items }} placement="topLeft" arrow>
<Button>topLeft</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="top" arrow>
<Button>top</Button>
</Dropdown>
<Dropdown menu={{ items }} placement="topRight" arrow>
<Button>topRight</Button>
</Dropdown>
</Space>
</Space>
);
export default App;

View File

@ -427,7 +427,7 @@ function InternalFormItem<Values = any>(props: FormItemProps<Values>): React.Rea
childNode = mergedChildren(context as any);
} else {
warning(
!mergedName.length,
!mergedName.length || !!noStyle,
'usage',
'`name` is only used for validate React element. If you are using Form.Item as layout display, please remove `name` instead.',
);

View File

@ -584,6 +584,15 @@ describe('Form', () => {
);
});
it('No warning when use noStyle and children is empty', () => {
render(
<Form>
<Form.Item name="noWarning" noStyle />
</Form>,
);
expect(errorSpy).not.toHaveBeenCalled();
});
it('dynamic change required', async () => {
const { container } = render(
<Form>

View File

@ -148032,7 +148032,7 @@ exports[`Locale Provider should display the text as id 1`] = `
type="button"
>
<span>
OK
Oke
</span>
</button>
</div>
@ -148089,7 +148089,7 @@ exports[`Locale Provider should display the text as id 1`] = `
class="ant-transfer-list-header-selected"
>
0
item
data
</span>
<span
class="ant-transfer-list-header-title"
@ -148129,7 +148129,7 @@ exports[`Locale Provider should display the text as id 1`] = `
</span>
<input
class="ant-input"
placeholder="Cari"
placeholder="Cari di sini"
type="text"
value=""
/>
@ -148323,7 +148323,7 @@ exports[`Locale Provider should display the text as id 1`] = `
class="ant-transfer-list-header-selected"
>
0
item
data
</span>
<span
class="ant-transfer-list-header-title"
@ -148363,7 +148363,7 @@ exports[`Locale Provider should display the text as id 1`] = `
</span>
<input
class="ant-input"
placeholder="Cari"
placeholder="Cari di sini"
type="text"
value=""
/>
@ -149622,7 +149622,7 @@ exports[`Locale Provider should display the text as id 1`] = `
type="button"
>
<span>
OK
Oke
</span>
</button>
</div>

View File

@ -1,3 +1,4 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/id_ID';
import type { Locale } from '.';
@ -5,45 +6,143 @@ import Calendar from '../calendar/locale/id_ID';
import DatePicker from '../date-picker/locale/id_ID';
import TimePicker from '../time-picker/locale/id_ID';
const typeTemplate = '${label} tidak valid ${type}';
const localeValues: Locale = {
locale: 'id',
Pagination,
DatePicker,
TimePicker,
Calendar,
global: {
placeholder: 'Silahkan pilih',
},
Table: {
filterTitle: 'Saring',
filterConfirm: 'OK',
filterReset: 'Hapus',
selectAll: 'Pilih semua di halaman ini',
selectInvert: 'Balikkan pilihan di halaman ini',
filterTitle: 'Menu filter',
filterConfirm: 'Oke',
filterReset: 'Reset',
filterEmptyText: 'Tidak ada filter',
filterCheckall: 'Pilih semua item',
filterSearchPlaceholder: 'Cari di filter',
emptyText: 'Tidak ada data',
selectAll: 'Pilih halaman saat ini',
selectInvert: 'Balikkan halaman saat ini',
selectNone: 'Hapus semua data',
selectionAll: 'Pilih semua data',
sortTitle: 'Urutkan',
expand: 'Perluas baris',
collapse: 'Perkecil baris',
triggerDesc: 'Klik untuk mengurutkan secara menurun',
triggerAsc: 'Klik untuk mengurutkan secara menaik',
cancelSort: 'Klik untuk membatalkan pengurutan',
},
Tour: {
Next: 'Selanjutnya',
Previous: 'Sebelumnya',
Finish: 'Selesai',
},
Modal: {
okText: 'OK',
okText: 'Oke',
cancelText: 'Batal',
justOkText: 'OK',
justOkText: 'Oke',
},
Popconfirm: {
okText: 'OK',
okText: 'Oke',
cancelText: 'Batal',
},
Transfer: {
titles: ['', ''],
searchPlaceholder: 'Cari',
itemUnit: 'item',
itemsUnit: 'item',
searchPlaceholder: 'Cari di sini',
itemUnit: 'data',
itemsUnit: 'data',
remove: 'Hapus',
selectCurrent: 'Pilih halaman saat ini',
removeCurrent: 'Hapus halaman saat ini',
selectAll: 'Pilih semua data',
removeAll: 'Hapus semua data',
selectInvert: 'Balikkan halaman saat ini',
},
Upload: {
uploading: 'Mengunggah...',
removeFile: 'Hapus file',
uploadError: 'Kesalahan pengunggahan',
previewFile: 'File pratinjau',
downloadFile: 'Unduh berkas',
previewFile: 'Pratinjau file',
downloadFile: 'Unduh file',
},
Empty: {
description: 'Tidak ada data',
},
Icon: {
icon: 'ikon',
},
Text: {
edit: 'Ubah',
copy: 'Salin',
copied: 'Disalin',
expand: 'Perluas',
collapse: 'Perkecil',
},
Form: {
optional: '(optional)',
defaultValidateMessages: {
default: 'Kesalahan validasi untuk ${label}',
required: 'Tolong masukkan ${label}',
enum: '${label} harus menjadi salah satu dari [${enum}]',
whitespace: '${label} tidak boleh berupa karakter kosong',
date: {
format: '${label} format tanggal tidak valid',
parse: '${label} tidak dapat diubah menjadi tanggal',
invalid: '${label} adalah tanggal yang tidak valid',
},
types: {
string: typeTemplate,
method: typeTemplate,
array: typeTemplate,
object: typeTemplate,
number: typeTemplate,
date: typeTemplate,
boolean: typeTemplate,
integer: typeTemplate,
float: typeTemplate,
regexp: typeTemplate,
email: typeTemplate,
url: typeTemplate,
hex: typeTemplate,
},
string: {
len: '${label} harus berupa ${len} karakter',
min: '${label} harus minimal ${min} karakter',
max: '${label} harus maksimal ${max} karakter',
range: '${label} harus diantara ${min}-${max} karakter',
},
number: {
len: '${label} harus sama dengan ${len}',
min: '${label} harus minimal ${min}',
max: '${label} harus maksimal ${max}',
range: '${label} harus di antara ${min}-${max}',
},
array: {
len: 'Harus ${len} ${label}',
min: 'Minimal ${min} ${label}',
max: 'Maksimal ${max} ${label}',
range: 'Jumlah ${label} harus di antara ${min}-${max}',
},
pattern: {
mismatch: '${label} tidak sesuai dengan pola ${pattern}',
},
},
},
Image: {
preview: 'Pratinjau',
},
QRCode: {
expired: 'Kode QR sudah habis masa berlakunya',
refresh: 'Segarkan',
scanned: 'Dipindai',
},
ColorPicker: {
presetEmpty: 'Kosong',
},
};
export default localeValues;

View File

@ -128,6 +128,7 @@ const getVerticalStyle: GenerateStyle<MenuToken> = (token) => {
`border-color ${motionDurationSlow}`,
`background ${motionDurationSlow}`,
`padding ${motionDurationMid} ${motionEaseOut}`,
`padding-inline calc(50% - ${unit(token.calc(fontSizeLG).div(2).equal())} - ${unit(itemMarginInline)})`,
].join(','),
[`> ${componentCls}-title-content`]: {

View File

@ -28,6 +28,15 @@ describe('Popover', () => {
expect(container.querySelector('.ant-popover-inner-content')).toBeTruthy();
});
it('should support defaultOpen', () => {
const { container } = render(
<Popover title="code" defaultOpen>
<span>show me your code</span>
</Popover>,
);
expect(container.querySelector('.ant-popover')).toBeTruthy();
});
it('shows content for render functions', () => {
const renderTitle = () => 'some-title';
const renderContent = () => 'some-content';

View File

@ -60,6 +60,7 @@ const InternalPopover = React.forwardRef<TooltipRef, PopoverProps>((props, ref)
const overlayCls = classNames(overlayClassName, hashId, cssVarCls);
const [open, setOpen] = useMergedState(false, {
value: props.open ?? props.visible,
defaultValue: props.defaultOpen ?? props.defaultVisible,
});
const settingOpen = (

View File

@ -68,4 +68,12 @@ describe('Switch', () => {
it('have static property for type detecting', () => {
expect(Switch.__ANT_SWITCH).toBeTruthy();
});
it('inner element have min-height', () => {
const { container, rerender } = render(<Switch unCheckedChildren="0" size="small" />);
expect(container.querySelector('.ant-switch-inner-unchecked')).toHaveStyle('min-height: 16px');
rerender(<Switch unCheckedChildren="0" />);
expect(container.querySelector('.ant-switch-inner-unchecked')).toHaveStyle('min-height: 22px');
});
});

View File

@ -109,6 +109,11 @@ const genSwitchSmallStyle: GenerateStyle<SwitchToken, CSSObject> = (token) => {
[`${componentCls}-inner`]: {
paddingInlineStart: innerMaxMarginSM,
paddingInlineEnd: innerMinMarginSM,
[`${switchInnerCls}-checked, ${switchInnerCls}-unchecked`]: {
minHeight: trackHeightSM,
},
[`${switchInnerCls}-checked`]: {
marginInlineStart: `calc(-100% + ${trackPaddingCalc} - ${innerMaxMarginCalc})`,
marginInlineEnd: `calc(100% - ${trackPaddingCalc} + ${innerMaxMarginCalc})`,
@ -269,6 +274,7 @@ const genSwitchInnerStyle: GenerateStyle<SwitchToken, CSSObject> = (token) => {
fontSize: token.fontSizeSM,
transition: `margin-inline-start ${token.switchDuration} ease-in-out, margin-inline-end ${token.switchDuration} ease-in-out`,
pointerEvents: 'none',
minHeight: trackHeight,
},
[`${switchInnerCls}-checked`]: {

View File

@ -2,6 +2,8 @@ import AbstractCalculator from './calculator';
const CALC_UNIT = 'CALC_UNIT';
const regexp = new RegExp(CALC_UNIT, 'g');
function unit(value: string | number) {
if (typeof value === 'number') {
return `${value}${CALC_UNIT}`;
@ -77,7 +79,6 @@ export default class CSSCalculator extends AbstractCalculator {
equal(options?: { unit?: boolean }): string {
const { unit: cssUnit = true } = options || {};
const regexp = new RegExp(`${CALC_UNIT}`, 'g');
this.result = this.result.replace(regexp, cssUnit ? 'px' : '');
if (typeof this.lowPriority !== 'undefined') {
return `calc(${this.result})`;

View File

@ -5,9 +5,3 @@
## en-US
Customize render list with Table component.
```css
#components-transfer-demo-table-transfer .ant-table td {
background: transparent;
}
```

View File

@ -123,7 +123,7 @@ Do it step by step:
3. Add the language support for [rc-pagination](https://github.com/react-component/pagination), for example [this](https://github.com/react-component/pagination/blob/master/src/locale/en_US.js).
4. Wait for `rc-picker` and `rc-pagination` to release the new version containing the above.
5. Update the `rc-picker` and `rc-pagination` versions in `antd` and add the remaining other necessary content for the language. for example [Azerbaijani PR](https://github.com/ant-design/ant-design/pull/21387).
6. Add a test case for the language in [index.test.tsx](https://github.com/ant-design/ant-design/blob/master/components/locale-provider/__tests__/index.test.tsx).
6. Add a test case for the language in [index.test.tsx](https://github.com/ant-design/ant-design/blob/master/components/locale/__tests__/index.test.tsx).
7. update snapshots, you may also need to delete `node_modules`, lock files (`yarn.lock` or `package-lock.json`) and reinstall at first.
```bash

View File

@ -122,7 +122,7 @@ return (
3. 为 [rc-pagination](https://github.com/react-component/pagination) 添加对应语言,参考 [这个](https://github.com/react-component/pagination/blob/master/src/locale/en_US.js)。
4. 等待 `rc-picker``rc-pagination` 发布含上述内容的最低版本。
5. 参考 [阿塞拜疆语的 PR](https://github.com/ant-design/ant-design/pull/21387) 向 `antd` 发起 PR完善对应语言的其他内容和更新 `rc-picker``rc-pagination` 版本。
6. 在 [index.test.tsx](https://github.com/ant-design/ant-design/blob/master/components/locale-provider/__tests__/index.test.tsx) 添加该语言的测试用例。
6. 在 [index.test.tsx](https://github.com/ant-design/ant-design/blob/master/components/locale/__tests__/index.test.tsx) 添加该语言的测试用例。
7. 更新 snapshot在这之前或许你还需要先删除 `node_modules` 和 lock 文件 `yarn.lock` or `package-lock.json` 并全新安装。
```bash

View File

@ -49,7 +49,7 @@
"build": "npm run compile && NODE_OPTIONS='--max-old-space-size=4096' npm run dist",
"changelog": "npm run lint:changelog && tsx scripts/print-changelog.ts",
"check-commit": "tsx scripts/check-commit.ts",
"clean": "antd-tools run clean && rm -rf es lib coverage locale dist report.html artifacts.zip",
"clean": "antd-tools run clean && rm -rf es lib coverage locale dist report.html artifacts.zip oss-artifacts.zip",
"clean:lockfiles": "rm -rf package-lock.json yarn.lock",
"precompile": "npm run prestart",
"compile": "npm run clean && antd-tools run compile",
@ -145,7 +145,7 @@
"rc-motion": "^2.9.0",
"rc-notification": "~5.4.0",
"rc-pagination": "~4.0.4",
"rc-picker": "~4.3.2",
"rc-picker": "~4.4.1",
"rc-progress": "~4.0.0",
"rc-rate": "~2.12.0",
"rc-resize-observer": "^1.4.0",
@ -171,7 +171,7 @@
"@ant-design/tools": "^18.0.2",
"@antv/g6": "^4.8.24",
"@babel/eslint-plugin": "^7.23.5",
"@biomejs/biome": "^1.6.4",
"@biomejs/biome": "^1.7.0",
"@codesandbox/sandpack-react": "^2.13.8",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
@ -214,18 +214,19 @@
"@types/pngjs": "^6.0.4",
"@types/prismjs": "^1.26.3",
"@types/progress": "^2.0.7",
"@types/qs": "^6.9.14",
"@types/react": "^18.2.78",
"@types/qs": "^6.9.15",
"@types/react": "^18.2.79",
"@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^18.2.25",
"@types/react-highlight-words": "^0.16.7",
"@types/react-resizable": "^3.0.7",
"@types/semver": "^7.5.8",
"@types/spinnies": "^0.5.3",
"@types/tar": "^6.1.12",
"@types/throttle-debounce": "^5.0.2",
"@types/warning": "^3.0.3",
"@typescript-eslint/eslint-plugin": "^7.6.0",
"@typescript-eslint/parser": "^7.6.0",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"adm-zip": "^0.5.12",
"ali-oss": "^6.20.0",
"antd-img-crop": "^4.21.0",
@ -266,7 +267,7 @@
"husky": "^9.0.11",
"identity-obj-proxy": "^3.0.0",
"immer": "^10.0.4",
"inquirer": "^9.2.18",
"inquirer": "^9.2.19",
"is-ci": "^3.0.1",
"isomorphic-fetch": "^3.0.0",
"jest": "^29.7.0",
@ -319,15 +320,16 @@
"remark": "^15.0.1",
"remark-cli": "^12.0.0",
"remark-gfm": "^4.0.0",
"remark-lint": "^9.1.2",
"remark-lint-no-undefined-references": "^4.2.1",
"remark-preset-lint-recommended": "^6.1.3",
"remark-lint": "^10.0.0",
"remark-lint-no-undefined-references": "^5.0.0",
"remark-preset-lint-recommended": "^7.0.0",
"remark-rehype": "^11.1.0",
"runes2": "^1.1.4",
"semver": "^7.6.0",
"sharp": "^0.33.3",
"simple-git": "^3.24.0",
"size-limit": "^11.1.2",
"spinnies": "^0.5.1",
"stylelint": "^16.3.1",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-standard": "^36.0.0",

View File

@ -1,18 +1,65 @@
/* eslint-disable camelcase */
/* eslint-disable camelcase, no-async-promise-executor */
import fs from 'node:fs';
import runScript from '@npmcli/run-script';
import { Octokit } from '@octokit/rest';
import AdmZip from 'adm-zip';
import axios from 'axios';
import chalk from 'chalk';
import cliProgress from 'cli-progress';
import ora from 'ora';
import Spinnies from 'spinnies';
import checkRepo from './check-repo';
const { Notification: Notifier } = require('node-notifier');
const simpleGit = require('simple-git');
const blockStatus = ['failure', 'cancelled', 'timed_out'] as const;
const spinner = { interval: 80, frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] };
const spinnies = new Spinnies({ spinner });
let spinniesId = 0;
// `spinnies` 为按条目进度,需要做简单的封装变成接近 `ora` 的形态
const showMessage = (
message: string,
status?: 'succeed' | 'fail' | 'spinning' | 'non-spinnable' | 'stopped' | true,
uniqueTitle?: string,
) => {
if (!status) {
spinnies.add(`info-${spinniesId}`, {
text: message,
status: 'non-spinnable',
});
spinniesId += 1;
} else {
const mergedId = uniqueTitle || `msg-${spinniesId}`;
let mergedMessage = uniqueTitle ? `${uniqueTitle} ${message}` : message;
// `spinnies` 对中文支持有 bug长度会按中文一半计算。我们翻个倍修复一下。
mergedMessage = `${mergedMessage}${' '.repeat(mergedMessage.length)}`;
const existSpinner = spinnies.pick(mergedId);
if (!existSpinner) {
spinnies.add(mergedId, {
text: '',
});
}
if (status === 'succeed' || status === 'fail' || status === 'stopped') {
spinnies.update(mergedId, {
text: mergedMessage,
status,
});
spinniesId += 1;
} else {
spinnies.update(mergedId, {
text: mergedMessage,
status: status === true ? 'spinning' : status,
});
}
}
};
process.on('SIGINT', () => {
process.exit(1);
});
@ -38,136 +85,193 @@ const emojify = (status: string = '') => {
return `${emoji || ''} ${(status || '').padEnd(15)}`;
};
async function downloadArtifact(url: string, filepath: string) {
const bar = new cliProgress.SingleBar(
{
format: ` 下载中 [${chalk.cyan(
'{bar}',
)}] {percentage}% | : {eta}s | {value}/{total}`,
},
cliProgress.Presets.rect,
);
bar.start(1, 0);
const toMB = (bytes: number) => (bytes / 1024 / 1024).toFixed(2);
async function downloadArtifact(msgKey: string, url: string, filepath: string, token?: string) {
const headers: Record<string, string> = {};
if (token) {
headers.Authorization = `token ${token}`;
}
const response = await axios.get(url, {
headers: {
Authorization: `token ${process.env.GITHUB_ACCESS_TOKEN}`,
},
headers,
responseType: 'arraybuffer',
onDownloadProgress: (progressEvent) => {
bar.setTotal(progressEvent.total || 0);
bar.update(progressEvent.loaded);
const { loaded, total = 0 } = progressEvent;
showMessage(
`下载进度 ${toMB(loaded)}MB/${toMB(total)}MB (${((loaded / total) * 100).toFixed(2)}%)`,
true,
msgKey,
);
},
});
fs.writeFileSync(filepath, Buffer.from(response.data));
return filepath;
}
const runPrePublish = async () => {
await checkRepo();
const spinner = ora();
spinner.info(chalk.black.bgGreenBright('本次发布将跳过本地 CI 检查,远程 CI 通过后方可发布'));
showMessage(chalk.black.bgGreenBright('本次发布将跳过本地 CI 检查,远程 CI 通过后方可发布'));
const git = simpleGit();
const octokit = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
const { current: currentBranch } = await git.branch();
spinner.start(`正在拉取远程分支 ${currentBranch}`);
showMessage(`正在拉取远程分支 ${currentBranch}`, true);
await git.pull('origin', currentBranch);
spinner.succeed(`成功拉取远程分支 ${currentBranch}`);
spinner.start(`正在推送本地分支 ${currentBranch}`);
showMessage(`成功拉取远程分支 ${currentBranch}`, 'succeed');
showMessage(`正在推送本地分支 ${currentBranch}`, true);
await git.push('origin', currentBranch);
spinner.succeed(`成功推送远程分支 ${currentBranch}`);
spinner.succeed(`已经和远程分支保持同步 ${currentBranch}`);
showMessage(`成功推送远程分支 ${currentBranch}`, 'succeed');
showMessage(`已经和远程分支保持同步 ${currentBranch}`, 'succeed');
const { latest } = await git.log();
spinner.succeed(`找到本地最新 commit:`);
spinner.info(chalk.cyan(` hash: ${latest.hash}`));
spinner.info(chalk.cyan(` date: ${latest.date}`));
spinner.info(chalk.cyan(` message: ${latest.message}`));
spinner.info(chalk.cyan(` author_name: ${latest.author_name}`));
const sha = process.env.TARGET_SHA || latest.hash;
showMessage(`找到本地最新 commit:`, 'succeed');
showMessage(chalk.cyan(` hash: ${sha}`));
showMessage(chalk.cyan(` date: ${latest.date}`));
showMessage(chalk.cyan(` message: ${latest.message}`));
showMessage(chalk.cyan(` author_name: ${latest.author_name}`));
const owner = 'ant-design';
const repo = 'ant-design';
spinner.start(`开始检查远程分支 ${currentBranch} 的 CI 状态`);
showMessage(`开始检查远程分支 ${currentBranch} 的 CI 状态`, true);
const failureUrlList: string[] = [];
const {
data: { check_runs },
} = await octokit.checks.listForRef({
owner,
repo,
ref: latest.hash,
ref: sha,
});
spinner.succeed(`远程分支 CI 状态:`);
showMessage(`远程分支 CI 状态(${check_runs.length})`, 'succeed');
check_runs.forEach((run) => {
spinner.info(
` ${run.name.padEnd(36)} ${emojify(run.status)} ${emojify(run.conclusion || '')}`,
);
showMessage(` ${run.name.padEnd(36)} ${emojify(run.status)} ${emojify(run.conclusion || '')}`);
if (blockStatus.some((status) => run.conclusion === status)) {
failureUrlList.push(run.html_url!);
}
});
const conclusions = check_runs.map((run) => run.conclusion);
if (
conclusions.includes('failure') ||
conclusions.includes('cancelled') ||
conclusions.includes('timed_out')
) {
spinner.fail(chalk.bgRedBright('远程分支 CI 执行异常,无法继续发布,请尝试修复或重试'));
spinner.info(` 点此查看状态https://github.com/${owner}/${repo}/commit/${latest.hash}`);
if (blockStatus.some((status) => conclusions.includes(status))) {
showMessage(chalk.bgRedBright('远程分支 CI 执行异常,无法继续发布,请尝试修复或重试'), 'fail');
showMessage(` 点此查看状态https://github.com/${owner}/${repo}/commit/${sha}`);
failureUrlList.forEach((url) => {
showMessage(` - ${url}`);
});
process.exit(1);
}
const statuses = check_runs.map((run) => run.status);
if (check_runs.length < 1 || statuses.includes('queued') || statuses.includes('in_progress')) {
spinner.fail(chalk.bgRedBright('远程分支 CI 还在执行中,请稍候再试'));
spinner.info(` 点此查看状态https://github.com/${owner}/${repo}/commit/${latest.hash}`);
showMessage(chalk.bgRedBright('远程分支 CI 还在执行中,请稍候再试'), 'fail');
showMessage(` 点此查看状态https://github.com/${owner}/${repo}/commit/${sha}`);
process.exit(1);
}
spinner.succeed(`远程分支 CI 已通过`);
showMessage(`远程分支 CI 已通过`, 'succeed');
// clean up
await runScript({ event: 'clean', path: '.', stdio: 'inherit' });
spinner.succeed(`成功清理构建产物目录`);
spinner.start(`开始查找远程分支构建产物`);
const {
data: { workflow_runs },
} = await octokit.rest.actions.listWorkflowRunsForRepo({
owner,
repo,
head_sha: latest.hash,
per_page: 100,
exclude_pull_requests: true,
event: 'push',
status: 'completed',
conclusion: 'success',
head_branch: currentBranch,
showMessage(`成功清理构建产物目录`, 'succeed');
// 从 github artifact 中下载产物
const downloadArtifactPromise = Promise.resolve().then(async () => {
showMessage('开始查找远程分支构建产物', true, '[Github]');
const {
data: { workflow_runs },
} = await octokit.rest.actions.listWorkflowRunsForRepo({
owner,
repo,
head_sha: sha,
per_page: 100,
exclude_pull_requests: true,
event: 'push',
status: 'completed',
conclusion: 'success',
head_branch: currentBranch,
});
const testWorkflowRun = workflow_runs.find((run) => run.name === '✅ test');
if (!testWorkflowRun) {
throw new Error('找不到远程构建工作流');
}
const {
data: { artifacts },
} = await octokit.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: testWorkflowRun?.id || 0,
});
const artifact = artifacts.find((item) => item.name === 'build artifacts');
if (!artifact) {
throw new Error('找不到远程构建产物');
}
showMessage(`准备从远程分支下载构建产物`, true, '[Github]');
const { url } = await octokit.rest.actions.downloadArtifact.endpoint({
owner,
repo,
artifact_id: artifact.id,
archive_format: 'zip',
});
// 返回下载后的文件路径
return downloadArtifact('[Github]', url, 'artifacts.zip', process.env.GITHUB_ACCESS_TOKEN);
});
const testWorkflowRun = workflow_runs.find((run) => run.name === '✅ test');
if (!testWorkflowRun) {
spinner.fail(chalk.bgRedBright('找不到远程构建工作流'));
downloadArtifactPromise
.then(() => {
showMessage(`成功下载构建产物`, 'succeed', '[Github]');
})
.catch((e: Error) => {
showMessage(chalk.bgRedBright(e.message), 'fail', '[Github]');
});
// 从 OSS 下载产物
const downloadOSSPromise = Promise.resolve().then(async () => {
const url = `https://antd-visual-diff.oss-cn-shanghai.aliyuncs.com/${sha}/oss-artifacts.zip`;
showMessage(`准备从远程 OSS 下载构建产物`, true, '[OSS]');
// 返回下载后的文件路径
return downloadArtifact('[OSS]', url, 'oss-artifacts.zip');
});
downloadOSSPromise
.then(() => {
showMessage(`成功下载构建产物`, 'succeed', '[OSS]');
})
.catch((e: Error) => {
showMessage(chalk.bgRedBright(e.message), 'fail', '[OSS]');
});
// 任意一个完成,则完成
let firstArtifactFile: string;
try {
// @ts-ignore
firstArtifactFile = await Promise.any([downloadArtifactPromise, downloadOSSPromise]);
} catch (error) {
showMessage(
chalk.bgRedBright(`下载失败,请确认你当前 ${sha.slice(0, 6)} 位于 master 分支中`),
'fail',
);
process.exit(1);
}
const {
data: { artifacts },
} = await octokit.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: testWorkflowRun?.id || 0,
});
const artifact = artifacts.find((item) => item.name === 'build artifacts');
if (!artifact) {
spinner.fail(chalk.bgRedBright('找不到远程构建产物'));
process.exit(1);
}
spinner.info(`准备从远程分支下载构建产物`);
const { url } = await octokit.rest.actions.downloadArtifact.endpoint({
owner,
repo,
artifact_id: artifact.id,
archive_format: 'zip',
});
await downloadArtifact(url, 'artifacts.zip');
spinner.info();
spinner.succeed(`成功从远程分支下载构建产物`);
showMessage(`成功从远程分支下载构建产物`, 'succeed');
// unzip
spinner.start(`正在解压构建产物`);
const zip = new AdmZip('artifacts.zip');
showMessage(`正在解压构建产物`, true);
const zip = new AdmZip(firstArtifactFile);
zip.extractAllTo('./', true);
spinner.succeed(`成功解压构建产物`);
showMessage(`成功解压构建产物`, 'succeed');
await runScript({ event: 'test:dekko', path: '.', stdio: 'inherit' });
await runScript({ event: 'test:package-diff', path: '.', stdio: 'inherit' });
spinner.succeed(`文件检查通过,准备发布!`);
showMessage(`文件检查通过,准备发布!`, 'succeed');
new Notifier().notify({
title: '✅ 准备发布到 npm',

View File

@ -4,8 +4,8 @@ import React from 'react';
// eslint-disable-next-line import/no-unresolved
import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
import dayjs from 'dayjs';
import fse from 'fs-extra';
import { globSync } from 'glob';
import { configureToMatchImageSnapshot } from 'jest-image-snapshot';
import { JSDOM } from 'jsdom';
import MockDate from 'mockdate';
import ReactDOMServer from 'react-dom/server';
@ -17,12 +17,8 @@ import { TriggerMockContext } from './demoTestContext';
jest.mock('../../components/grid/hooks/useBreakpoint', () => () => ({}));
const toMatchImageSnapshot = configureToMatchImageSnapshot({
customSnapshotsDir: `${process.cwd()}/imageSnapshots`,
customDiffDir: `${process.cwd()}/imageDiffSnapshots`,
});
expect.extend({ toMatchImageSnapshot });
const snapshotPath = path.join(process.cwd(), 'imageSnapshots');
fse.ensureDirSync(snapshotPath);
const themes = {
default: theme.defaultAlgorithm,
@ -45,7 +41,7 @@ export default function imageTest(
let doc: Document;
let container: HTMLDivElement;
beforeAll(() => {
beforeAll(async () => {
const dom = new JSDOM('<!DOCTYPE html><body></body></p>', {
url: 'http://localhost/',
});
@ -103,6 +99,8 @@ export default function imageTest(
// Fill window
fillWindowEnv(win);
await page.setRequestInterception(true);
});
beforeEach(() => {
@ -112,8 +110,8 @@ export default function imageTest(
function test(name: string, suffix: string, themedComponent: React.ReactElement) {
it(name, async () => {
await jestPuppeteer.resetPage();
await page.setRequestInterception(true);
await page.setViewport({ width: 800, height: 600 });
const onRequestHandle = (request: any) => {
if (['image'].includes(request.resourceType())) {
request.abort();
@ -212,9 +210,7 @@ export default function imageTest(
fullPage: !options.onlyViewport,
});
expect(image).toMatchImageSnapshot({
customSnapshotIdentifier: `${identifier}${suffix}`,
});
await fse.writeFile(path.join(snapshotPath, `${identifier}${suffix}.png`), image);
MockDate.reset();
page.off('request', onRequestHandle);