From 0f87c9d2596bb49721eac0f779745d5553fcf3b5 Mon Sep 17 00:00:00 2001 From: MadCcc <1075746765@qq.com> Date: Tue, 29 Aug 2023 16:44:13 +0800 Subject: [PATCH 1/5] fix: Watermark should not crash if content is empty (#44501) --- components/watermark/__tests__/index.test.tsx | 19 +++++++++++++++++++ components/watermark/useClips.ts | 4 +++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/components/watermark/__tests__/index.test.tsx b/components/watermark/__tests__/index.test.tsx index 8de5a7aaf21b..ef7182cdf3d0 100644 --- a/components/watermark/__tests__/index.test.tsx +++ b/components/watermark/__tests__/index.test.tsx @@ -16,10 +16,18 @@ describe('Watermark', () => { }); }); + beforeEach(() => { + jest.useFakeTimers(); + }); + afterAll(() => { mockSrcSet.mockRestore(); }); + afterEach(() => { + jest.useRealTimers(); + }); + it('The watermark should render successfully', () => { const { container } = render(); expect(container.querySelector('.watermark div')).toBeTruthy(); @@ -94,4 +102,15 @@ describe('Watermark', () => { await waitFor(() => expect(target).toBeTruthy()); expect(container).toMatchSnapshot(); }); + + it('should not crash if content is empty string', async () => { + const spy = jest.spyOn(CanvasRenderingContext2D.prototype, 'drawImage'); + render(); + await waitFakeTimer(); + expect(spy).not.toHaveBeenCalledWith(expect.anything(), 0, 0); + expect(spy).not.toHaveBeenCalledWith(expect.anything(), -0, 0); + expect(spy).not.toHaveBeenCalledWith(expect.anything(), -0, -0); + expect(spy).not.toHaveBeenCalledWith(expect.anything(), 0, -0); + spy.mockRestore(); + }); }); diff --git a/components/watermark/useClips.ts b/components/watermark/useClips.ts index 2392e67d6a88..c8c8843ab440 100644 --- a/components/watermark/useClips.ts +++ b/components/watermark/useClips.ts @@ -69,7 +69,9 @@ export default function useClips() { // Copy from `ctx` and rotate rCtx.translate(realMaxSize / 2, realMaxSize / 2); rCtx.rotate(angle); - rCtx.drawImage(canvas, -contentWidth / 2, -contentHeight / 2); + if (contentWidth > 0 && contentHeight > 0) { + rCtx.drawImage(canvas, -contentWidth / 2, -contentHeight / 2); + } // Get boundary of rotated text function getRotatePos(x: number, y: number) { From 9c494ab9101dd8fee2c22d33015073bbccba8e71 Mon Sep 17 00:00:00 2001 From: Konv Suu <2583695112@qq.com> Date: Tue, 29 Aug 2023 17:56:56 +0800 Subject: [PATCH 2/5] fix: transfer label height (#44471) --- components/transfer/style/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/transfer/style/index.ts b/components/transfer/style/index.ts index 54e01e3e0182..cb6ffd25e2f8 100644 --- a/components/transfer/style/index.ts +++ b/components/transfer/style/index.ts @@ -305,6 +305,11 @@ const genTransferListStyle: GenerateStyle = (token: TransferToken '&-footer': { borderTop: `${lineWidth}px ${lineType} ${colorSplit}`, }, + + // fix: https://github.com/ant-design/ant-design/issues/44489 + '&-checkbox': { + lineHeight: 1, + }, }; }; From 8d7dd801201f2d2af5b1b30b67a432eaeab8d90c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Tue, 29 Aug 2023 18:47:41 +0800 Subject: [PATCH 3/5] docs: Update batch of docs & demos (#44509) * docs: update select opt * docs: update form deps demo * docs: upload onChange * docs: update form nest Form.List demo --- .../__snapshots__/demo-extend.test.ts.snap | 455 ++++++++++-------- .../__snapshots__/demo.test.tsx.snap | 371 +++++++++----- components/form/demo/dep-debug.md | 7 - components/form/demo/dep-debug.tsx | 31 -- components/form/demo/dependencies.md | 7 + components/form/demo/dependencies.tsx | 57 +++ .../form/demo/dynamic-form-items-complex.md | 4 +- .../form/demo/dynamic-form-items-complex.tsx | 131 +++-- components/form/index.en-US.md | 6 +- components/form/index.zh-CN.md | 6 +- components/select/demo/search.tsx | 8 +- components/select/index.en-US.md | 4 +- components/select/index.zh-CN.md | 2 +- components/upload/index.en-US.md | 4 +- components/upload/index.zh-CN.md | 4 +- 15 files changed, 622 insertions(+), 475 deletions(-) delete mode 100644 components/form/demo/dep-debug.md delete mode 100644 components/form/demo/dep-debug.tsx create mode 100644 components/form/demo/dependencies.md create mode 100644 components/form/demo/dependencies.tsx diff --git a/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap index 58fc51ffbd40..1e18f3cceb2c 100644 --- a/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -2434,6 +2434,142 @@ exports[`renders components/form/demo/customized-form-controls.tsx extend contex exports[`renders components/form/demo/customized-form-controls.tsx extend context correctly 2`] = `[]`; +exports[`renders components/form/demo/dependencies.tsx extend context correctly 1`] = ` + + + + + + + + + + Try modify \`Password2\` and then modify \`Password\` + + + + + + + + Password + + + + + + + + + + + + + + + + Confirm Password + + + + + + + + + + + + + + Only Update when + + password2 + + updated: + + + {} + + + +`; + +exports[`renders components/form/demo/dependencies.tsx extend context correctly 2`] = `[]`; + exports[`renders components/form/demo/disabled.tsx extend context correctly 1`] = ` Array [ - - Area - + + Item 1 + + + + + + + + + + + Name + + + - - - - - - - - - Beijing - - - Shanghai - - - - - - - - - Beijing - - - - - - Shanghai - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - Add sights - - + + + + Add Sub Item + + + + + + - - - - - - - - - Submit - - - - - - + + + Add Item + + + + + { + "items": [ + {} + ] +} + + `; diff --git a/components/form/__tests__/__snapshots__/demo.test.tsx.snap b/components/form/__tests__/__snapshots__/demo.test.tsx.snap index 1b68f75d542e..9f8cd97d21a5 100644 --- a/components/form/__tests__/__snapshots__/demo.test.tsx.snap +++ b/components/form/__tests__/__snapshots__/demo.test.tsx.snap @@ -1777,6 +1777,140 @@ exports[`renders components/form/demo/customized-form-controls.tsx correctly 1`] `; +exports[`renders components/form/demo/dependencies.tsx correctly 1`] = ` + + + + + + + + + + Try modify \`Password2\` and then modify \`Password\` + + + + + + + + Password + + + + + + + + + + + + + + + + Confirm Password + + + + + + + + + + + + + + Only Update when + + password2 + + updated: + + + {} + + + +`; + exports[`renders components/form/demo/disabled.tsx correctly 1`] = ` Array [ - - Area - + + Item 1 + + + + + + + + + + + Name + + + - - - + - - - - - - - - - - - - - - - - + + + + - - - - - - - - Add sights - - + + + + Add Sub Item + + + + + + - - - - - - - - - Submit - - - - - - + + + Add Item + + + + + {} + + `; diff --git a/components/form/demo/dep-debug.md b/components/form/demo/dep-debug.md deleted file mode 100644 index 48ae23c246fc..000000000000 --- a/components/form/demo/dep-debug.md +++ /dev/null @@ -1,7 +0,0 @@ -## zh-CN - -Buggy! - -## en-US - -Buggy! diff --git a/components/form/demo/dep-debug.tsx b/components/form/demo/dep-debug.tsx deleted file mode 100644 index d554bb3111e2..000000000000 --- a/components/form/demo/dep-debug.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { Form, Input } from 'antd'; - -let acc = 0; - -const App: React.FC = () => { - const [form] = Form.useForm(); - return ( - - - { - () => acc++ - // return {JSON.stringify(form.getFieldsValue(), null, 2)}; - } - - - - - - - - - ); -}; - -export default App; diff --git a/components/form/demo/dependencies.md b/components/form/demo/dependencies.md new file mode 100644 index 000000000000..b902166f6b4e --- /dev/null +++ b/components/form/demo/dependencies.md @@ -0,0 +1,7 @@ +## zh-CN + +Form.Item 可以通过 `dependencies` 属性,设置关联字段。当关联字段的值发生变化时,会触发校验与更新。 + +## en-US + +Form.Item can set the associated field through the `dependencies` property. When the value of the associated field changes, the validation and update will be triggered. diff --git a/components/form/demo/dependencies.tsx b/components/form/demo/dependencies.tsx new file mode 100644 index 000000000000..b1f9b44bca26 --- /dev/null +++ b/components/form/demo/dependencies.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Alert, Form, Input, Typography } from 'antd'; + +const App: React.FC = () => { + const [form] = Form.useForm(); + return ( + + + + + + + + {/* Field */} + ({ + validator(_, value) { + if (!value || getFieldValue('password') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('The new password that you entered do not match!')); + }, + }), + ]} + > + + + + {/* Render Props */} + + {() => ( + + + Only Update when password2 updated: + + {JSON.stringify(form.getFieldsValue(), null, 2)} + + )} + + + ); +}; + +export default App; diff --git a/components/form/demo/dynamic-form-items-complex.md b/components/form/demo/dynamic-form-items-complex.md index fe335dccd794..43c1dd8e5f38 100644 --- a/components/form/demo/dynamic-form-items-complex.md +++ b/components/form/demo/dynamic-form-items-complex.md @@ -1,7 +1,7 @@ ## zh-CN -这个例子演示了一个表单中包含多个表单控件的情况。 +多个 Form.List 嵌套的使用场景。 ## en-US -This example demonstrates the case that a form contains multiple form controls. +Multiple Form.List nested usage scenarios. diff --git a/components/form/demo/dynamic-form-items-complex.tsx b/components/form/demo/dynamic-form-items-complex.tsx index 2de26286bb0a..e35434d3dc8b 100644 --- a/components/form/demo/dynamic-form-items-complex.tsx +++ b/components/form/demo/dynamic-form-items-complex.tsx @@ -1,96 +1,83 @@ import React from 'react'; -import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; -import { Button, Form, Input, Select, Space } from 'antd'; - -const { Option } = Select; - -const areas = [ - { label: 'Beijing', value: 'Beijing' }, - { label: 'Shanghai', value: 'Shanghai' }, -]; - -const sights = { - Beijing: ['Tiananmen', 'Great Wall'], - Shanghai: ['Oriental Pearl', 'The Bund'], -}; - -type SightsKeys = keyof typeof sights; +import { CloseOutlined } from '@ant-design/icons'; +import { Button, Card, Form, Input, Space, Typography } from 'antd'; const App: React.FC = () => { const [form] = Form.useForm(); - const onFinish = (values: any) => { - console.log('Received values of form:', values); - }; - - const handleChange = () => { - form.setFieldsValue({ sights: [] }); - }; - return ( - - - - + {(fields, { add, remove }) => ( - <> + {fields.map((field) => ( - - - prevValues.area !== curValues.area || prevValues.sights !== curValues.sights - } - > - {() => ( - - - {(sights[form.getFieldValue('area') as SightsKeys] || []).map((item) => ( - - {item} - - ))} - - - )} - - + { + remove(field.name); + }} + /> + } + > + - remove(field.name)} /> - + {/* Nest Form.List */} + + + {(subFields, subOpt) => ( + + {subFields.map((subField) => ( + + + + + + + + { + subOpt.remove(subField.name); + }} + /> + + ))} + subOpt.add()} block> + + Add Sub Item + + + )} + + + ))} - - add()} block icon={}> - Add sights - - - > + add()} block> + + Add Item + + )} - - - Submit - + + + {() => ( + + {JSON.stringify(form.getFieldsValue(), null, 2)} + + )} ); diff --git a/components/form/index.en-US.md b/components/form/index.en-US.md index 2b52c06c7dc0..c94951a69c74 100644 --- a/components/form/index.en-US.md +++ b/components/form/index.en-US.md @@ -46,9 +46,9 @@ High performance Form component with data scope management. Including data colle Handle Form Data Manually Customized Validation Dynamic Rules +Dependencies Other Form Controls Disabled Input Debug -Dep Debug label ellipsis Test col 24 usage Ref item @@ -153,12 +153,10 @@ After wrapped by `Form.Item` with `name` property, `value`(or other property def ### dependencies -Used when there are dependencies between fields. If a field has the `dependencies` prop, this field will automatically trigger updates and validations when upstream is updated. A common scenario is a user registration form with "password" and "confirm password" fields. The "Confirm Password" validation depends on the "Password" field. After setting `dependencies`, the "Password" field update will re-trigger the validation of "Check Password". You can refer [examples](#components-form-demo-register). +Used when there are dependencies between fields. If a field has the `dependencies` prop, this field will automatically trigger updates and validations when upstream is updated. A common scenario is a user registration form with "password" and "confirm password" fields. The "Confirm Password" validation depends on the "Password" field. After setting `dependencies`, the "Password" field update will re-trigger the validation of "Check Password". You can refer [examples](#components-form-demo-dependencies). `dependencies` shouldn't be used together with `shouldUpdate`, since it may result in conflicting update logic. -`dependencies` supports `Form.Item` with render props children since `4.5.0`. - ### shouldUpdate Form updates only the modified field-related components for performance optimization purposes by incremental update. In most cases, you only need to write code or do validation with the [`dependencies`](#dependencies) property. In some specific cases, such as when a new field option appears with a field value changed, or you just want to keep some area updating by form update, you can modify the update logic of Form.Item via the `shouldUpdate`. diff --git a/components/form/index.zh-CN.md b/components/form/index.zh-CN.md index f1c735c07897..a46c1327c4fb 100644 --- a/components/form/index.zh-CN.md +++ b/components/form/index.zh-CN.md @@ -47,9 +47,9 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA 自行处理表单数据 自定义校验 动态校验规则 +校验与更新依赖 校验其他组件 Disabled Input Debug -Dep Debug 测试 label 省略 测试特殊 col 24 用法 引用字段 @@ -154,12 +154,10 @@ const validateMessages = { ### dependencies -当字段间存在依赖关系时使用。如果一个字段设置了 `dependencies` 属性。那么它所依赖的字段更新时,该字段将自动触发更新与校验。一种常见的场景,就是注册用户表单的“密码”与“确认密码”字段。“确认密码”校验依赖于“密码”字段,设置 `dependencies` 后,“密码”字段更新会重新触发“校验密码”的校验逻辑。你可以参考[具体例子](#components-form-demo-register)。 +当字段间存在依赖关系时使用。如果一个字段设置了 `dependencies` 属性。那么它所依赖的字段更新时,该字段将自动触发更新与校验。一种常见的场景,就是注册用户表单的“密码”与“确认密码”字段。“确认密码”校验依赖于“密码”字段,设置 `dependencies` 后,“密码”字段更新会重新触发“校验密码”的校验逻辑。你可以参考[具体例子](#components-form-demo-dependencies)。 `dependencies` 不应和 `shouldUpdate` 一起使用,因为这可能带来更新逻辑的混乱。 -从 `4.5.0` 版本开始,`dependencies` 支持使用 render props 类型 children 的 `Form.Item`。 - ### shouldUpdate Form 通过增量更新方式,只更新被修改的字段相关组件以达到性能优化目的。大部分场景下,你只需要编写代码或者与 [`dependencies`](#dependencies) 属性配合校验即可。而在某些特定场景,例如修改某个字段值后出现新的字段选项、或者纯粹希望表单任意变化都对某一个区域进行渲染。你可以通过 `shouldUpdate` 修改 Form.Item 的更新逻辑。 diff --git a/components/select/demo/search.tsx b/components/select/demo/search.tsx index 511aeca2a6c5..ca580b1e0b85 100644 --- a/components/select/demo/search.tsx +++ b/components/select/demo/search.tsx @@ -9,6 +9,10 @@ const onSearch = (value: string) => { console.log('search:', value); }; +// Filter `option.label` match the user type `input` +const filterOption = (input: string, option: { label: string; value: string }) => + (option?.label ?? '').toLowerCase().includes(input.toLowerCase()); + const App: React.FC = () => ( ( optionFilterProp="children" onChange={onChange} onSearch={onSearch} - filterOption={(input, option) => - (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) - } + filterOption={filterOption} options={[ { value: 'jack', diff --git a/components/select/index.en-US.md b/components/select/index.en-US.md index d12000821709..94068a1f030c 100644 --- a/components/select/index.en-US.md +++ b/components/select/index.en-US.md @@ -14,7 +14,7 @@ Select component to select value from options. - A dropdown menu for displaying choices - an elegant alternative to the native `` element. - Utilizing [Radio](/components/radio/) is recommended when there are fewer total options (less than 5). -- You probably need [AutoComplete](/components/auto-complete/) if you're looking for an input box that can be typed or selected. +- You probably need [AutoComplete](/components/auto-complete/) if you're looking for an input box that can be typed or selected. ## Examples @@ -69,7 +69,7 @@ Common props ref:[Common props](/docs/react/common-props) | dropdownRender | Customize dropdown content | (originNode: ReactNode) => ReactNode | - | | | dropdownStyle | The style of dropdown menu | CSSProperties | - | | | fieldNames | Customize node label, value, options,groupLabel field name | object | { label: `label`, value: `value`, options: `options`, groupLabel: `label` } | 4.17.0 (`groupLabel` added in 5.6.0) | -| filterOption | If true, filter options by input, if function, filter options against it. The function will receive two arguments, `inputValue` and `option`, if the function returns `true`, the option will be included in the filtered set; Otherwise, it will be excluded | boolean \| function(inputValue, option) | true | | +| filterOption | If true, filter options by input, if function, filter options against it. The function will receive two arguments, `inputValue` and `option`, if the function returns `true`, the option will be included in the filtered set; Otherwise, it will be excluded. [Example](#select-demo-search) | boolean \| function(inputValue, option) | true | | | filterSort | Sort function for search options sorting, see [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)'s compareFunction | (optionA: Option, optionB: Option) => number | - | 4.9.0 | | getPopupContainer | Parent Node which the selector should be rendered to. Default to `body`. When position issues happen, try to modify it into scrollable content and position it relative. [Example](https://codesandbox.io/s/4j168r7jw0) | function(triggerNode) | () => document.body | | | labelInValue | Whether to embed label in value, turn the format of value from `string` to { value: string, label: ReactNode } | boolean | false | | diff --git a/components/select/index.zh-CN.md b/components/select/index.zh-CN.md index a6ce1e642220..4e4937f9c6fd 100644 --- a/components/select/index.zh-CN.md +++ b/components/select/index.zh-CN.md @@ -70,7 +70,7 @@ demo: | dropdownRender | 自定义下拉框内容 | (originNode: ReactNode) => ReactNode | - | | | dropdownStyle | 下拉菜单的 style 属性 | CSSProperties | - | | | fieldNames | 自定义节点 label、value、options、groupLabel 的字段 | object | { label: `label`, value: `value`, options: `options`, groupLabel: `label` } | 4.17.0(`groupLabel` 在 5.6.0 新增) | -| filterOption | 是否根据输入项进行筛选。当其为一个函数时,会接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 true,反之则返回 false | boolean \| function(inputValue, option) | true | | +| filterOption | 是否根据输入项进行筛选。当其为一个函数时,会接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 true,反之则返回 false。[示例](#select-demo-search) | boolean \| function(inputValue, option) | true | | | filterSort | 搜索时对筛选结果项的排序函数, 类似[Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)里的 compareFunction | (optionA: Option, optionB: Option) => number | - | 4.9.0 | | getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codesandbox.io/s/4j168r7jw0) | function(triggerNode) | () => document.body | | | labelInValue | 是否把每个选项的 label 包装到 value 中,会把 Select 的 value 类型从 `string` 变为 { value: string, label: ReactNode } 的格式 | boolean | false | | diff --git a/components/upload/index.en-US.md b/components/upload/index.en-US.md index 08928a434239..e7f01b70a6f0 100644 --- a/components/upload/index.en-US.md +++ b/components/upload/index.en-US.md @@ -72,7 +72,7 @@ Common props ref:[Common props](/docs/react/common-props) | progress | Custom progress bar | [ProgressProps](/components/progress/#api) (support `type="line"` only) | { strokeWidth: 2, showInfo: false } | 4.3.0 | | showUploadList | Whether to show default upload list, could be an object to specify `showPreviewIcon`, `showRemoveIcon`, `showDownloadIcon`, `removeIcon` and `downloadIcon` individually | boolean \| { showPreviewIcon?: boolean, showDownloadIcon?: boolean, showRemoveIcon?: boolean, previewIcon?: ReactNode \| (file: UploadFile) => ReactNode, removeIcon?: ReactNode \| (file: UploadFile) => ReactNode, downloadIcon?: ReactNode \| (file: UploadFile) => ReactNode } | true | function: 4.7.0 | | withCredentials | The ajax upload with cookie sent | boolean | false | | -| onChange | A callback function, can be executed when uploading state is changing, see [onChange](#onchange) | function | - | | +| onChange | A callback function, can be executed when uploading state is changing. It will trigger by every uploading phase. see [onChange](#onchange) | function | - | | | onDrop | A callback function executed when files are dragged and dropped into the upload area | (event: React.DragEvent) => void | - | 4.16.0 | | onDownload | Click the method to download the file, pass the method to perform the method logic, and do not pass the default jump to the new TAB | function(file): void | (Jump to new TAB) | | | onPreview | A callback function, will be executed when the file link or preview icon is clicked | function(file) | - | | @@ -94,7 +94,7 @@ Extends File with additional props. ### onChange -> The function will be called when uploading is in progress, completed, or failed. +> 💡 The function will be called when uploading is in progress, completed, or failed. When uploading state change, it returns: diff --git a/components/upload/index.zh-CN.md b/components/upload/index.zh-CN.md index 8d42a0ae82fe..8a2cd417cd2a 100644 --- a/components/upload/index.zh-CN.md +++ b/components/upload/index.zh-CN.md @@ -73,7 +73,7 @@ demo: | progress | 自定义进度条样式 | [ProgressProps](/components/progress-cn#api)(仅支持 `type="line"`) | { strokeWidth: 2, showInfo: false } | 4.3.0 | | showUploadList | 是否展示文件列表, 可设为一个对象,用于单独设定 `showPreviewIcon`, `showRemoveIcon`, `showDownloadIcon`, `removeIcon` 和 `downloadIcon` | boolean \| { showPreviewIcon?: boolean, showRemoveIcon?: boolean, showDownloadIcon?: boolean, previewIcon?: ReactNode \| (file: UploadFile) => ReactNode, removeIcon?: ReactNode \| (file: UploadFile) => ReactNode, downloadIcon?: ReactNode \| (file: UploadFile) => ReactNode } | true | function: 4.7.0 | | withCredentials | 上传请求时是否携带 cookie | boolean | false | | -| onChange | 上传文件改变时的回调,详见 [onChange](#onchange) | function | - | | +| onChange | 上传文件改变时的回调,上传每个阶段都会触发该事件。详见 [onChange](#onchange) | function | - | | | onDrop | 当文件被拖入上传区域时执行的回调功能 | (event: React.DragEvent) => void | - | 4.16.0 | | onDownload | 点击下载文件时的回调,如果没有指定,则默认跳转到文件 url 对应的标签页 | function(file): void | (跳转新标签页) | | | onPreview | 点击文件链接或预览图标时的回调 | function(file) | - | | @@ -95,7 +95,7 @@ demo: ### onChange -> 上传中、完成、失败都会调用这个函数。 +> 💡 上传中、完成、失败都会调用这个函数。 文件状态改变的回调,返回为: From 629efb7d5d47302403b1277a2bfce802839ac730 Mon Sep 17 00:00:00 2001 From: MadCcc <1075746765@qq.com> Date: Tue, 29 Aug 2023 19:31:12 +0800 Subject: [PATCH 4/5] perf: Notification style should be generated when shown (#44488) * fix: Notification style should be generated when shown * chore: bump rc-notification * chore: code clean * feat: update --- components/message/style/index.tsx | 3 -- components/message/useMessage.tsx | 32 +++++++++++++----- components/notification/style/index.tsx | 3 -- components/notification/useNotification.tsx | 37 +++++++++++++++------ package.json | 2 +- 5 files changed, 52 insertions(+), 25 deletions(-) diff --git a/components/message/style/index.tsx b/components/message/style/index.tsx index 794734729130..80cee4cbc5c9 100644 --- a/components/message/style/index.tsx +++ b/components/message/style/index.tsx @@ -201,7 +201,4 @@ export default genComponentStyleHook( token.paddingSM }px`, }), - { - clientOnly: true, - }, ); diff --git a/components/message/useMessage.tsx b/components/message/useMessage.tsx index 5fb3ed25b992..c49e4a6e26b6 100644 --- a/components/message/useMessage.tsx +++ b/components/message/useMessage.tsx @@ -1,6 +1,6 @@ import CloseOutlined from '@ant-design/icons/CloseOutlined'; import classNames from 'classnames'; -import { useNotification as useRcNotification } from 'rc-notification'; +import { NotificationProvider, useNotification as useRcNotification } from 'rc-notification'; import type { NotificationAPI } from 'rc-notification/lib'; import * as React from 'react'; import warning from '../_util/warning'; @@ -17,6 +17,8 @@ import type { } from './interface'; import useStyle from './style'; import { getMotion, wrapPromiseFn } from './util'; +import type { FC, PropsWithChildren } from 'react'; +import type { NotificationConfig as RcNotificationConfig } from 'rc-notification/lib/useNotification'; const DEFAULT_OFFSET = 8; const DEFAULT_DURATION = 3; @@ -30,10 +32,27 @@ type HolderProps = ConfigOptions & { interface HolderRef extends NotificationAPI { prefixCls: string; - hashId: string; message?: ComponentStyleConfig; } +const Wrapper: FC> = ({ children, prefixCls }) => { + const [, hashId] = useStyle(prefixCls); + return ( + + {children} + + ); +}; + +const renderNotifications: RcNotificationConfig['renderNotifications'] = ( + node, + { prefixCls, key }, +) => ( + + {node} + +); + const Holder = React.forwardRef((props, ref) => { const { top, @@ -49,8 +68,6 @@ const Holder = React.forwardRef((props, ref) => { const prefixCls = staticPrefixCls || getPrefixCls('message'); - const [, hashId] = useStyle(prefixCls); - // =============================== Style =============================== const getStyle = (): React.CSSProperties => ({ left: '50%', @@ -58,7 +75,7 @@ const Holder = React.forwardRef((props, ref) => { top: top ?? DEFAULT_OFFSET, }); - const getClassName = () => classNames(hashId, { [`${prefixCls}-rtl`]: rtl }); + const getClassName = () => classNames({ [`${prefixCls}-rtl`]: rtl }); // ============================== Motion =============================== const getNotificationMotion = () => getMotion(prefixCls, transitionName); @@ -82,13 +99,13 @@ const Holder = React.forwardRef((props, ref) => { getContainer: () => staticGetContainer?.() || getPopupContainer?.() || document.body, maxCount, onAllRemoved, + renderNotifications, }); // ================================ Ref ================================ React.useImperativeHandle(ref, () => ({ ...api, prefixCls, - hashId, message, })); @@ -128,7 +145,7 @@ export function useInternalMessage( return fakeResult; } - const { open: originOpen, prefixCls, hashId, message } = holderRef.current; + const { open: originOpen, prefixCls, message } = holderRef.current; const noticePrefixCls = `${prefixCls}-notice`; const { content, icon, type, key, className, style, onClose, ...restConfig } = config; @@ -151,7 +168,6 @@ export function useInternalMessage( placement: 'top', className: classNames( type && `${noticePrefixCls}-${type}`, - hashId, className, message?.className, ), diff --git a/components/notification/style/index.tsx b/components/notification/style/index.tsx index c4bfa12e5531..c14d08a802b3 100644 --- a/components/notification/style/index.tsx +++ b/components/notification/style/index.tsx @@ -300,7 +300,4 @@ export default genComponentStyleHook( zIndexPopup: token.zIndexPopupBase + 50, width: 384, }), - { - clientOnly: true, - }, ); diff --git a/components/notification/useNotification.tsx b/components/notification/useNotification.tsx index 505109b04e94..f9b045133833 100644 --- a/components/notification/useNotification.tsx +++ b/components/notification/useNotification.tsx @@ -1,7 +1,10 @@ import * as React from 'react'; import classNames from 'classnames'; -import { useNotification as useRcNotification } from 'rc-notification'; -import type { NotificationAPI } from 'rc-notification/lib'; +import { NotificationProvider, useNotification as useRcNotification } from 'rc-notification'; +import type { + NotificationAPI, + NotificationConfig as RcNotificationConfig, +} from 'rc-notification/lib'; import warning from '../_util/warning'; import { ConfigContext } from '../config-provider'; import type { ComponentStyleConfig } from '../config-provider/context'; @@ -14,6 +17,7 @@ import type { import { getCloseIcon, PureContent } from './PurePanel'; import useStyle from './style'; import { getMotion, getPlacementStyle } from './util'; +import type { FC, PropsWithChildren } from 'react'; const DEFAULT_OFFSET = 24; const DEFAULT_DURATION = 4.5; @@ -28,10 +32,27 @@ type HolderProps = NotificationConfig & { interface HolderRef extends NotificationAPI { prefixCls: string; - hashId: string; notification?: ComponentStyleConfig; } +const Wrapper: FC> = ({ children, prefixCls }) => { + const [, hashId] = useStyle(prefixCls); + return ( + + {children} + + ); +}; + +const renderNotifications: RcNotificationConfig['renderNotifications'] = ( + node, + { prefixCls, key }, +) => ( + + {node} + +); + const Holder = React.forwardRef((props, ref) => { const { top, @@ -50,10 +71,7 @@ const Holder = React.forwardRef((props, ref) => { const getStyle = (placement: NotificationPlacement): React.CSSProperties => getPlacementStyle(placement, top ?? DEFAULT_OFFSET, bottom ?? DEFAULT_OFFSET); - // Style - const [, hashId] = useStyle(prefixCls); - - const getClassName = () => classNames(hashId, { [`${prefixCls}-rtl`]: rtl }); + const getClassName = () => classNames({ [`${prefixCls}-rtl`]: rtl }); // ============================== Motion =============================== const getNotificationMotion = () => getMotion(prefixCls); @@ -70,13 +88,13 @@ const Holder = React.forwardRef((props, ref) => { getContainer: () => staticGetContainer?.() || getPopupContainer?.() || document.body, maxCount, onAllRemoved, + renderNotifications, }); // ================================ Ref ================================ React.useImperativeHandle(ref, () => ({ ...api, prefixCls, - hashId, notification, })); @@ -106,7 +124,7 @@ export function useInternalNotification( return; } - const { open: originOpen, prefixCls, hashId, notification } = holderRef.current; + const { open: originOpen, prefixCls, notification } = holderRef.current; const noticePrefixCls = `${prefixCls}-notice`; @@ -142,7 +160,6 @@ export function useInternalNotification( ), className: classNames( type && `${noticePrefixCls}-${type}`, - hashId, className, notification?.className, ), diff --git a/package.json b/package.json index 41bcc777eef2..73eb1a640ae9 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "rc-mentions": "~2.5.0", "rc-menu": "~9.10.0", "rc-motion": "^2.7.3", - "rc-notification": "~5.0.4", + "rc-notification": "~5.1.1", "rc-pagination": "~3.6.0", "rc-picker": "~3.13.0", "rc-progress": "~3.4.1", From acdf1153fa342a1f6ac4453c34932aa3831b8121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Tue, 29 Aug 2023 19:43:48 +0800 Subject: [PATCH 5/5] refactor: Extract Tag unused style (#44512) * refactor: tag status as extract style * chore: add key * chore: preset of it --- components/button/button.tsx | 4 +- components/tag/index.tsx | 11 ++- components/tag/style/index.ts | 98 ++++++------------- components/tag/style/presetCmp.ts | 33 +++++++ components/tag/style/statusCmp.ts | 43 ++++++++ .../theme/util/genComponentStyleHook.ts | 14 ++- 6 files changed, 128 insertions(+), 75 deletions(-) create mode 100644 components/tag/style/presetCmp.ts create mode 100644 components/tag/style/statusCmp.ts diff --git a/components/button/button.tsx b/components/button/button.tsx index 47c8b2bf50c4..02b741a74d77 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -25,7 +25,7 @@ import { isTwoCNChar, isUnBorderedButtonType, spaceChildren } from './buttonHelp import IconWrapper from './IconWrapper'; import LoadingIcon from './LoadingIcon'; import useStyle from './style'; -import CompactStyle from './style/compactCmp'; +import CompactCmp from './style/compactCmp'; export type LegacyButtonType = ButtonType | 'danger'; @@ -289,7 +289,7 @@ const InternalButton: React.ForwardRefRenderFunction< {kids} {/* Styles: compact */} - {compactItemClassnames && } + {compactItemClassnames && } ); diff --git a/components/tag/index.tsx b/components/tag/index.tsx index c4e2f890358d..d06a0bae1c81 100644 --- a/components/tag/index.tsx +++ b/components/tag/index.tsx @@ -1,8 +1,9 @@ 'use client'; +import * as React from 'react'; import CloseOutlined from '@ant-design/icons/CloseOutlined'; import classNames from 'classnames'; -import * as React from 'react'; + import type { PresetColorType, PresetStatusColorType } from '../_util/colors'; import { isPresetColor, isPresetStatusColor } from '../_util/colors'; import useClosable from '../_util/hooks/useClosable'; @@ -12,6 +13,8 @@ import Wave from '../_util/wave'; import { ConfigContext } from '../config-provider'; import CheckableTag from './CheckableTag'; import useStyle from './style'; +import PresetCmp from './style/presetCmp'; +import StatusCmp from './style/statusCmp'; export type { CheckableTagProps } from './CheckableTag'; @@ -69,7 +72,9 @@ const InternalTag: React.ForwardRefRenderFunction = ( } }, [props.visible]); - const isInternalColor = isPresetColor(color) || isPresetStatusColor(color); + const isPreset = isPresetColor(color); + const isStatus = isPresetStatusColor(color); + const isInternalColor = isPreset || isStatus; const tagStyle: React.CSSProperties = { backgroundColor: color && !isInternalColor ? color : undefined, @@ -140,6 +145,8 @@ const InternalTag: React.ForwardRefRenderFunction = ( {kids} {mergedCloseIcon} + {isPreset && } + {isStatus && } ); diff --git a/components/tag/style/index.ts b/components/tag/style/index.ts index ec39b35268b9..c54b28f1f8d8 100644 --- a/components/tag/style/index.ts +++ b/components/tag/style/index.ts @@ -1,9 +1,11 @@ -import type { CSSInterpolation } from '@ant-design/cssinjs'; import type React from 'react'; -import capitalize from '../../_util/capitalize'; +import type { CSSInterpolation } from '@ant-design/cssinjs'; + import { resetComponent } from '../../style'; +import type { GlobalToken } from '../../theme'; import type { FullToken } from '../../theme/internal'; -import { genComponentStyleHook, genPresetColor, mergeToken } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import type { GenStyleFn } from '../../theme/util/genComponentStyleHook'; export interface ComponentToken { /** @@ -18,7 +20,7 @@ export interface ComponentToken { defaultColor: string; } -interface TagToken extends FullToken<'Tag'> { +export interface TagToken extends FullToken<'Tag'> { tagFontSize: number; tagLineHeight: React.CSSProperties['lineHeight']; tagIconSize: number; @@ -28,44 +30,6 @@ interface TagToken extends FullToken<'Tag'> { // ============================== Styles ============================== -type CssVariableType = 'Success' | 'Info' | 'Error' | 'Warning'; - -const genTagStatusStyle = ( - token: TagToken, - status: 'success' | 'processing' | 'error' | 'warning', - cssVariableType: CssVariableType, -): CSSInterpolation => { - const capitalizedCssVariableType = capitalize(cssVariableType); - return { - [`${token.componentCls}-${status}`]: { - color: token[`color${cssVariableType}`], - background: token[`color${capitalizedCssVariableType}Bg`], - borderColor: token[`color${capitalizedCssVariableType}Border`], - [`&${token.componentCls}-borderless`]: { - borderColor: 'transparent', - }, - }, - }; -}; - -const genPresetStyle = (token: TagToken) => - genPresetColor(token, (colorKey, { textColor, lightBorderColor, lightColor, darkColor }) => ({ - [`${token.componentCls}-${colorKey}`]: { - color: textColor, - background: lightColor, - borderColor: lightBorderColor, - // Inverse color - '&-inverse': { - color: token.colorTextLightSolid, - background: darkColor, - borderColor: darkColor, - }, - [`&${token.componentCls}-borderless`]: { - borderColor: 'transparent', - }, - }, - })); - const genBaseStyle = (token: TagToken): CSSInterpolation => { const { paddingXXS, lineWidth, tagPaddingHorizontal, componentCls } = token; const paddingInline = tagPaddingHorizontal - lineWidth; @@ -162,33 +126,33 @@ const genBaseStyle = (token: TagToken): CSSInterpolation => { }; // ============================== Export ============================== +export const prepareToken: (token: Parameters>[0]) => TagToken = (token) => { + const { lineWidth, fontSizeIcon } = token; + + const tagFontSize = token.fontSizeSM; + const tagLineHeight = `${token.lineHeightSM * tagFontSize}px`; + + const tagToken = mergeToken(token, { + tagFontSize, + tagLineHeight, + tagIconSize: fontSizeIcon - 2 * lineWidth, // Tag icon is much smaller + tagPaddingHorizontal: 8, // Fixed padding. + tagBorderlessBg: token.colorFillTertiary, + }); + return tagToken; +}; + +export const prepareCommonToken: (token: GlobalToken) => ComponentToken = (token) => ({ + defaultBg: token.colorFillQuaternary, + defaultColor: token.colorText, +}); + export default genComponentStyleHook( 'Tag', (token) => { - const { lineWidth, fontSizeIcon } = token; - - const tagFontSize = token.fontSizeSM; - const tagLineHeight = `${token.lineHeightSM * tagFontSize}px`; - - const tagToken = mergeToken(token, { - tagFontSize, - tagLineHeight, - tagIconSize: fontSizeIcon - 2 * lineWidth, // Tag icon is much smaller - tagPaddingHorizontal: 8, // Fixed padding. - tagBorderlessBg: token.colorFillTertiary, - }); - - return [ - genBaseStyle(tagToken), - genPresetStyle(tagToken), - genTagStatusStyle(tagToken, 'success', 'Success'), - genTagStatusStyle(tagToken, 'processing', 'Info'), - genTagStatusStyle(tagToken, 'error', 'Error'), - genTagStatusStyle(tagToken, 'warning', 'Warning'), - ]; + const tagToken = prepareToken(token); + + return genBaseStyle(tagToken); }, - (token) => ({ - defaultBg: token.colorFillQuaternary, - defaultColor: token.colorText, - }), + prepareCommonToken, ); diff --git a/components/tag/style/presetCmp.ts b/components/tag/style/presetCmp.ts new file mode 100644 index 000000000000..fe796b0d45e2 --- /dev/null +++ b/components/tag/style/presetCmp.ts @@ -0,0 +1,33 @@ +// Style as status component +import { prepareCommonToken, prepareToken, type TagToken } from '.'; +import { genPresetColor, genSubStyleComponent } from '../../theme/internal'; + +// ============================== Preset ============================== +const genPresetStyle = (token: TagToken) => + genPresetColor(token, (colorKey, { textColor, lightBorderColor, lightColor, darkColor }) => ({ + [`${token.componentCls}-${colorKey}`]: { + color: textColor, + background: lightColor, + borderColor: lightBorderColor, + // Inverse color + '&-inverse': { + color: token.colorTextLightSolid, + background: darkColor, + borderColor: darkColor, + }, + [`&${token.componentCls}-borderless`]: { + borderColor: 'transparent', + }, + }, + })); + +// ============================== Export ============================== +export default genSubStyleComponent( + ['Tag', 'preset'], + (token) => { + const tagToken = prepareToken(token); + + return genPresetStyle(tagToken); + }, + prepareCommonToken, +); diff --git a/components/tag/style/statusCmp.ts b/components/tag/style/statusCmp.ts new file mode 100644 index 000000000000..f8d2704d4006 --- /dev/null +++ b/components/tag/style/statusCmp.ts @@ -0,0 +1,43 @@ +// Style as status component +import type { CSSInterpolation } from '@ant-design/cssinjs'; + +import { prepareCommonToken, prepareToken, type TagToken } from '.'; +import capitalize from '../../_util/capitalize'; +import { genSubStyleComponent } from '../../theme/internal'; + +// ============================== Status ============================== +type CssVariableType = 'Success' | 'Info' | 'Error' | 'Warning'; + +const genTagStatusStyle = ( + token: TagToken, + status: 'success' | 'processing' | 'error' | 'warning', + cssVariableType: CssVariableType, +): CSSInterpolation => { + const capitalizedCssVariableType = capitalize(cssVariableType); + return { + [`${token.componentCls}-${status}`]: { + color: token[`color${cssVariableType}`], + background: token[`color${capitalizedCssVariableType}Bg`], + borderColor: token[`color${capitalizedCssVariableType}Border`], + [`&${token.componentCls}-borderless`]: { + borderColor: 'transparent', + }, + }, + }; +}; + +// ============================== Export ============================== +export default genSubStyleComponent( + ['Tag', 'status'], + (token) => { + const tagToken = prepareToken(token); + + return [ + genTagStatusStyle(tagToken, 'success', 'Success'), + genTagStatusStyle(tagToken, 'processing', 'Info'), + genTagStatusStyle(tagToken, 'error', 'Error'), + genTagStatusStyle(tagToken, 'warning', 'Warning'), + ]; + }, + prepareCommonToken, +); diff --git a/components/theme/util/genComponentStyleHook.ts b/components/theme/util/genComponentStyleHook.ts index 3784bdf930b3..ddad00bf3792 100644 --- a/components/theme/util/genComponentStyleHook.ts +++ b/components/theme/util/genComponentStyleHook.ts @@ -184,13 +184,19 @@ export interface SubStyleComponentProps { prefixCls: string; } -export function genSubStyleComponent( +export const genSubStyleComponent: ( ...args: Parameters> -): ComponentType { - const useStyle = genComponentStyleHook(...args); +) => ComponentType = (componentName, styleFn, getDefaultToken, options) => { + const useStyle = genComponentStyleHook(componentName, styleFn, getDefaultToken, { + resetStyle: false, + + // Sub Style should default after root one + order: -998, + ...options, + }); return ({ prefixCls }: SubStyleComponentProps) => { useStyle(prefixCls); return null; }; -} +};
+ Only Update when + + password2 + + updated: +
+ password2 +
+ {} +
+ { + "items": [ + {} + ] +} +
{JSON.stringify(form.getFieldsValue(), null, 2)}
+ Only Update when password2 updated: +
password2
Handle Form Data Manually
Customized Validation
Dynamic Rules
Dependencies
Other Form Controls
Disabled Input Debug
Dep Debug
label ellipsis
Test col 24 usage
Ref item
自行处理表单数据
自定义校验
动态校验规则
校验与更新依赖
校验其他组件
测试 label 省略
测试特殊 col 24 用法
引用字段