From a038583155d284b3b7e97318a71f2665d64a0337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BA=A2?= Date: Mon, 1 Apr 2024 21:42:17 +0800 Subject: [PATCH 01/22] feat(upload): support ref.nativeElenent (#48210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(upload): support ref.nativeElenent * test: add unit test * Update components/upload/__tests__/upload.test.tsx Signed-off-by: 红 --------- Signed-off-by: 红 --- components/upload/Upload.tsx | 17 ++++++++++++++--- components/upload/__tests__/upload.test.tsx | 7 +++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/components/upload/Upload.tsx b/components/upload/Upload.tsx index b13296acc6..e30a1dc004 100644 --- a/components/upload/Upload.tsx +++ b/components/upload/Upload.tsx @@ -32,6 +32,11 @@ export interface UploadRef { onError: (error: Error, response: any, file: RcFile) => void; fileList: UploadFile[]; upload: RcUpload | null; + /** + * Get native element for wrapping upload + * @since 5.17.0 + */ + nativeElement: HTMLSpanElement | null; } const InternalUpload: React.ForwardRefRenderFunction = (props, ref) => { @@ -79,6 +84,7 @@ const InternalUpload: React.ForwardRefRenderFunction = ( const [dragState, setDragState] = React.useState('drop'); const upload = React.useRef(null); + const wrapRef = React.useRef(null); if (process.env.NODE_ENV !== 'production') { const warning = devUseWarning('Upload'); @@ -331,6 +337,7 @@ const InternalUpload: React.ForwardRefRenderFunction = ( onError, fileList: mergedFileList, upload: upload.current, + nativeElement: wrapRef.current, })); const { getPrefixCls, direction, upload: ctxUpload } = React.useContext(ConfigContext); @@ -431,6 +438,8 @@ const InternalUpload: React.ForwardRefRenderFunction = ( const mergedStyle: React.CSSProperties = { ...ctxUpload?.style, ...style }; + // ======================== Render ======================== + if (type === 'drag') { const dragCls = classNames(hashId, prefixCls, `${prefixCls}-drag`, { [`${prefixCls}-drag-uploading`]: mergedFileList.some((file) => file.status === 'uploading'), @@ -440,7 +449,7 @@ const InternalUpload: React.ForwardRefRenderFunction = ( }); return wrapCSSVar( - +
= ( if (listType === 'picture-card' || listType === 'picture-circle') { return wrapCSSVar( - {renderUploadList(uploadButton, !!children)}, + + {renderUploadList(uploadButton, !!children)} + , ); } return wrapCSSVar( - + {uploadButton} {renderUploadList()} , diff --git a/components/upload/__tests__/upload.test.tsx b/components/upload/__tests__/upload.test.tsx index 9841b171ae..a9ec579fb4 100644 --- a/components/upload/__tests__/upload.test.tsx +++ b/components/upload/__tests__/upload.test.tsx @@ -1082,4 +1082,11 @@ describe('Upload', () => { expect(file.status).toBe('done'); }); }); + + it('container ref', () => { + const ref = React.createRef(); + render(); + expect(ref.current?.nativeElement).toBeTruthy(); + expect(ref.current?.nativeElement instanceof HTMLElement).toBeTruthy(); + }); }); From 9680481546574db70fb9696f17db64b10326c54c 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: Wed, 3 Apr 2024 17:12:02 +0800 Subject: [PATCH 02/22] feat: Mix style framework support (#48229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add layer mark * docs: enable @layer for test * docs: add doc of third part lib * docs: update order * Update docs/react/compatible-style.zh-CN.md Co-authored-by: lijianan <574980606@qq.com> Signed-off-by: 二货爱吃白萝卜 * Update docs/react/compatible-style.en-US.md Co-authored-by: lijianan <574980606@qq.com> Signed-off-by: 二货爱吃白萝卜 * docs: ssr or reset case --------- Signed-off-by: 二货爱吃白萝卜 Co-authored-by: lijianan <574980606@qq.com> --- .dumi/theme/common/styles/Common.tsx | 129 ++++++++++-------- .dumi/theme/layouts/GlobalLayout.tsx | 1 + .dumi/theme/plugin.ts | 3 +- components/date-picker/locale/ru_RU.ts | 15 +- components/form/demo/basic.tsx | 4 +- .../table/hooks/useFilter/FilterDropdown.tsx | 4 +- .../theme/util/genComponentStyleHook.tsx | 3 + components/theme/util/useResetIconStyle.ts | 6 +- docs/react/compatible-style.en-US.md | 114 +++++++++++++++- docs/react/compatible-style.zh-CN.md | 114 +++++++++++++++- package.json | 2 +- 11 files changed, 326 insertions(+), 69 deletions(-) diff --git a/.dumi/theme/common/styles/Common.tsx b/.dumi/theme/common/styles/Common.tsx index c2f9cf7f2b..2744394b20 100644 --- a/.dumi/theme/common/styles/Common.tsx +++ b/.dumi/theme/common/styles/Common.tsx @@ -1,77 +1,86 @@ -import { css, Global } from '@emotion/react'; import React from 'react'; +import { css, Global } from '@emotion/react'; import { useTheme } from 'antd-style'; +import { updateCSS } from 'rc-util/lib/Dom/dynamicCSS'; export default () => { const { anchorTop } = useTheme(); + React.useInsertionEffect(() => { + updateCSS(`@layer global, antd;`, 'site-global', { + prepend: true, + }); + }, []); + return ( ); }; diff --git a/.dumi/theme/layouts/GlobalLayout.tsx b/.dumi/theme/layouts/GlobalLayout.tsx index ab1ab33f28..c45a6c6881 100644 --- a/.dumi/theme/layouts/GlobalLayout.tsx +++ b/.dumi/theme/layouts/GlobalLayout.tsx @@ -204,6 +204,7 @@ const GlobalLayout: React.FC = () => { diff --git a/.dumi/theme/plugin.ts b/.dumi/theme/plugin.ts index bb544dc16e..f30a91caa0 100644 --- a/.dumi/theme/plugin.ts +++ b/.dumi/theme/plugin.ts @@ -169,7 +169,8 @@ const RoutesPlugin = (api: IApi) => { const matchRegex = / + + + + + + +``` + +#### ✅ Correct + +```html + + + + + + + + + +``` diff --git a/docs/react/compatible-style.zh-CN.md b/docs/react/compatible-style.zh-CN.md index c049085cbe..fd0b28e639 100644 --- a/docs/react/compatible-style.zh-CN.md +++ b/docs/react/compatible-style.zh-CN.md @@ -11,9 +11,33 @@ Ant Design 支持最近 2 个版本的现代浏览器。如果你需要兼容旧 查看 [`@ant-design/cssinjs`](https://github.com/ant-design/cssinjs#styleprovider). +## `layer` 降权 + +Ant Design 从 `5.17.0` 起支持配置 `layer` 进行统一降权。经过降权后,antd 的样式将始终低于默认的 CSS 选择器优先级,以便于用户进行样式覆盖(请务必注意检查 `@layer` 浏览器兼容性): + +```tsx +import { StyleProvider } from '@ant-design/cssinjs'; + +export default () => ( + + + +); +``` + +antd 的样式会被封装在 `@layer` 中,以降低优先级: + +```diff +++ @layer antd { + :where(.css-bAMboO).ant-btn { + color: #fff; + } +++ } +``` + ## `:where` 选择器 -Ant Design 的 CSS-in-JS 默认通过 `:where` 选择器降低 CSS Selector 优先级,以减少用户升级时额外调整自定义样式的成本,不过 `:where` 语法的[兼容性](https://developer.mozilla.org/en-US/docs/Web/CSS/:where#browser_compatibility)在低版本浏览器比较差。在某些场景下你如果需要支持旧版浏览器(或与 [TailwindCSS 优先级冲突](https://github.com/ant-design/ant-design/issues/38794#issuecomment-1328262525)),你可以使用 `@ant-design/cssinjs` 取消默认的降权操作(请注意版本保持与 antd 一致): +Ant Design 的 CSS-in-JS 默认通过 `:where` 选择器降低 CSS Selector 优先级,以减少用户升级时额外调整自定义样式的成本,不过 `:where` 语法的[兼容性](https://developer.mozilla.org/en-US/docs/Web/CSS/:where#browser_compatibility)在低版本浏览器比较差。在某些场景下你如果需要支持旧版浏览器,你可以使用 `@ant-design/cssinjs` 取消默认的降权操作(请注意版本保持与 antd 一致): ```tsx import { StyleProvider } from '@ant-design/cssinjs'; @@ -147,3 +171,91 @@ root.render( , ); ``` + +## 兼容三方样式库 + +在某些情况下,你可能需要 antd 与其他样式库共存,比如 `Tailwind CSS`、`Emotion`、`styled-components` 等。不同于传统 CSS 方案,这些三方库往往不太容易通过提升 CSS 选择器优先级的方式覆盖 antd 的样式。你可以通过为 antd 配置 `@layer` 降低其 CSS 选择器权重,同时通过合理安排 `@layer` 顺序来解决样式覆盖问题: + +### antd 配置 `@layer` + +```tsx +import { StyleProvider } from '@ant-design/cssinjs'; + +export default () => ( + + + +); +``` + +### TailwindCSS 排布 `@layer` + +在 global.css 中,调整 `@layer` 来控制样式的覆盖顺序。让 `tailwind-base` 置于 `antd` 之前: + +```less +@layer tailwind-base, antd; + +@layer tailwind-base { + @tailwind base; +} +@tailwind components; +@tailwind utilities; +``` + +### reset.css + +如果你使用了 antd 的 `reset.css` 样式,你需要为其也指定 `@layer` 以防止将 antd 降权的样式覆盖: + +```less +@layer reset, antd; + +@import url(reset.css) layer(reset); +``` + +### 其他 CSS-in-JS 库 + +当你为 antd 配置完 `@layer` 后,你不需要为其他的 CSS-in-JS 库做任何额外的配置。你的 CSS-in-JS 已经可以完全覆盖 antd 的样式了。 + +### SSR 场景 + +在 SSR 场景下,样式往往会通过 ` + + + + + + +``` + +#### ✅ 正确的写法 + +```html + + + + + + + + + +``` diff --git a/package.json b/package.json index f58b3d9bb2..0997cae331 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ ], "dependencies": { "@ant-design/colors": "^7.0.2", - "@ant-design/cssinjs": "^1.18.5", + "@ant-design/cssinjs": "^1.19.1", "@ant-design/icons": "^5.3.6", "@ant-design/react-slick": "~1.1.2", "@babel/runtime": "^7.24.1", From 42c5a2fb21e6dc3a999119f50fc10318d5f83afb Mon Sep 17 00:00:00 2001 From: lijianan <574980606@qq.com> Date: Fri, 5 Apr 2024 22:30:39 +0800 Subject: [PATCH 03/22] chore: update size-limit (#48292) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 023f47da76..947f0d98c0 100644 --- a/package.json +++ b/package.json @@ -352,11 +352,11 @@ "size-limit": [ { "path": "./dist/antd.min.js", - "limit": "337 KiB" + "limit": "338 KiB" }, { "path": "./dist/antd-with-locales.min.js", - "limit": "384 KiB" + "limit": "385 KiB" } ], "title": "Ant Design", From 14f8279aea037afd45f63800fbedb4aac1f41ec8 Mon Sep 17 00:00:00 2001 From: George H Date: Sun, 7 Apr 2024 22:42:20 +0800 Subject: [PATCH 04/22] feat: Support iconPosition with button icon (#47791) * feat: button support iconPosition * fix: iconPosition compatible LoadingIcon * docs(Button): update Button with iconPosition demo * fix: delete debug type * fix: iconPosition for InnerLoadingIcon with not existIcon * chore: update button snapshots * fix: fixed loading-icon-end style * chore: refactor buttonContent with genButtonContent * fix: iconPosition compatible with rtl * docs(Button): update button with iconPosition demo * chore: update test * fix: iconPosition compatible with rtl * docs(Buttob): add icon-position demo * restore icon demo * update snapshots * docs: update icon button demo * docs: update iconPosition button demo * Update components/button/index.zh-CN.md Co-authored-by: kiner-tang <1127031143@qq.com> Signed-off-by: George H * Update components/button/index.zh-CN.md Co-authored-by: kiner-tang <1127031143@qq.com> Signed-off-by: George H * Update components/button/demo/icon-position.md Co-authored-by: afc163 Signed-off-by: George H * Update components/button/index.en-US.md Co-authored-by: afc163 Signed-off-by: George H * Update components/button/index.en-US.md Co-authored-by: afc163 Signed-off-by: George H --------- Signed-off-by: George H Co-authored-by: kiner-tang <1127031143@qq.com> Co-authored-by: afc163 --- components/button/LoadingIcon.tsx | 44 +- .../__snapshots__/demo-extend.test.ts.snap | 461 ++++++++++++++++++ .../__tests__/__snapshots__/demo.test.ts.snap | 382 +++++++++++++++ components/button/button.tsx | 33 +- components/button/demo/icon-position.md | 7 + components/button/demo/icon-position.tsx | 60 +++ components/button/demo/icon.md | 8 +- components/button/index.en-US.md | 2 + components/button/index.zh-CN.md | 6 +- components/button/style/index.ts | 8 +- 10 files changed, 980 insertions(+), 31 deletions(-) create mode 100644 components/button/demo/icon-position.md create mode 100644 components/button/demo/icon-position.tsx diff --git a/components/button/LoadingIcon.tsx b/components/button/LoadingIcon.tsx index 74918bb19c..8ba499d3fb 100644 --- a/components/button/LoadingIcon.tsx +++ b/components/button/LoadingIcon.tsx @@ -1,7 +1,9 @@ +import React, { forwardRef } from 'react'; import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; import classNames from 'classnames'; import CSSMotion from 'rc-motion'; -import React, { forwardRef } from 'react'; + +import type { ButtonProps } from './button'; import IconWrapper from './IconWrapper'; type InnerLoadingIconProps = { @@ -9,27 +11,29 @@ type InnerLoadingIconProps = { className?: string; style?: React.CSSProperties; iconClassName?: string; -}; +} & Pick; -const InnerLoadingIcon = forwardRef( - ({ prefixCls, className, style, iconClassName }, ref) => { - const mergedIconCls = classNames(`${prefixCls}-loading-icon`, className); +const InnerLoadingIcon = forwardRef((props, ref) => { + const { prefixCls, className, style, iconClassName, iconPosition = 'start' } = props; + const mergedIconCls = classNames(className, { + [`${prefixCls}-loading-icon-end`]: iconPosition === 'end', + [`${prefixCls}-loading-icon`]: iconPosition === 'start', + }); - return ( - - - - ); - }, -); + return ( + + + + ); +}); -export interface LoadingIconProps { +export type LoadingIconProps = { prefixCls: string; existIcon: boolean; loading?: boolean | object; className?: string; style?: React.CSSProperties; -} +} & Pick; const getCollapsedWidth = (): React.CSSProperties => ({ width: 0, @@ -44,11 +48,18 @@ const getRealWidth = (node: HTMLElement): React.CSSProperties => ({ }); const LoadingIcon: React.FC = (props) => { - const { prefixCls, loading, existIcon, className, style } = props; + const { prefixCls, loading, existIcon, className, style, iconPosition } = props; const visible = !!loading; if (existIcon) { - return ; + return ( + + ); } return ( @@ -72,6 +83,7 @@ const LoadingIcon: React.FC = (props) => { style={{ ...style, ...motionStyle }} ref={ref} iconClassName={motionCls} + iconPosition={iconPosition} /> )} diff --git a/components/button/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/button/__tests__/__snapshots__/demo-extend.test.ts.snap index 0c49239da2..0968f8619f 100644 --- a/components/button/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/button/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -1520,6 +1520,467 @@ exports[`renders components/button/demo/icon.tsx extend context correctly 1`] = exports[`renders components/button/demo/icon.tsx extend context correctly 2`] = `[]`; +exports[`renders components/button/demo/icon-position.tsx extend context correctly 1`] = ` +Array [ +
+
+
+ + +
+
+
, + , +
+
+ +
+
+
+ +
+
+ + + +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ + +
+
+
+ +
+
+ + + + + + + + + +
+
, +] +`; + +exports[`renders components/button/demo/icon-position.tsx extend context correctly 2`] = `[]`; + exports[`renders components/button/demo/legacy-group.tsx extend context correctly 1`] = ` Array [
`; +exports[`renders components/button/demo/icon-position.tsx correctly 1`] = ` +Array [ +
+
+
+ + +
+
+
, + , +
+
+ + + + + +
+
+ + + + + + + + + + + + +
+
, +] +`; + exports[`renders components/button/demo/legacy-group.tsx correctly 1`] = ` Array [
) : ( - + ); const kids = children || children === 0 ? spaceChildren(children, needInserted && autoInsertSpace) : null; + const genButtonContent = (iconComponent: React.ReactNode, kidsComponent: React.ReactNode) => { + const isRTL = direction === 'rtl'; + const iconFirst = (iconPosition === 'start' && !isRTL) || (iconPosition === 'end' && isRTL); + + return ( + <> + {iconFirst ? iconComponent : kidsComponent} + {iconFirst ? kidsComponent : iconComponent} + + ); + }; + if (linkButtonRestProps.href !== undefined) { return wrapCSSVar( } tabIndex={mergedDisabled ? -1 : 0} > - {iconNode} - {kids} + {genButtonContent(iconNode, kids)} , ); } @@ -288,8 +310,7 @@ const InternalButton: React.ForwardRefRenderFunction< disabled={mergedDisabled} ref={buttonRef as React.Ref} > - {iconNode} - {kids} + {genButtonContent(iconNode, kids)} {/* Styles: compact */} {!!compactItemClassnames && } diff --git a/components/button/demo/icon-position.md b/components/button/demo/icon-position.md new file mode 100644 index 0000000000..086f349023 --- /dev/null +++ b/components/button/demo/icon-position.md @@ -0,0 +1,7 @@ +## zh-CN + +通过设置 `iconPosition` 为 `start` 或 `end` 分别设置按钮图标的位置。 + +## en-US + +Set the position of the button icon by setting `iconPosition` to `start` or `end` respectively. diff --git a/components/button/demo/icon-position.tsx b/components/button/demo/icon-position.tsx new file mode 100644 index 0000000000..6847919c5a --- /dev/null +++ b/components/button/demo/icon-position.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; +import { SearchOutlined } from '@ant-design/icons'; +import { Button, Divider, Flex, Radio, Space, Tooltip } from 'antd'; + +const App: React.FC = () => { + const [position, setPosition] = useState<'start' | 'end'>('start'); + + return ( + <> + + setPosition(e.target.value)}> + start + end + + + + Preview + + + + + + + + + + + + + + + + + + + ); +}; + +export default App; diff --git a/components/button/demo/icon.md b/components/button/demo/icon.md index a41ab8ac51..17e596af26 100644 --- a/components/button/demo/icon.md +++ b/components/button/demo/icon.md @@ -1,11 +1,7 @@ ## zh-CN -当需要在 `Button` 内嵌入 `Icon` 时,可以设置 `icon` 属性,或者直接在 `Button` 内使用 `Icon` 组件。 - -如果想控制 `Icon` 具体的位置,只能直接使用 `Icon` 组件,而非 `icon` 属性。 +可以通过 `icon `属性添加图标,并使用 `iconPosition` 调整图标的位置。 ## en-US -`Button` components can contain an `Icon`. This is done by setting the `icon` property or placing an `Icon` component within the `Button`. - -If you want specific control over the positioning and placement of the `Icon`, then that should be done by placing the `Icon` component within the `Button` rather than using the `icon` property. +You can add an icon through the `icon` property and adjust the position of the icon using `iconPosition`. diff --git a/components/button/index.en-US.md b/components/button/index.en-US.md index c45426722b..dbd7332666 100644 --- a/components/button/index.en-US.md +++ b/components/button/index.en-US.md @@ -35,6 +35,7 @@ And 4 other properties additionally. Type Icon +Icon Position Debug Icon Debug Block Size @@ -65,6 +66,7 @@ Different button styles can be generated by setting Button properties. The recom | href | Redirect url of link button | string | - | | | htmlType | Set the original html `type` of `button`, see: [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type) | string | `button` | | | icon | Set the icon component of button | ReactNode | - | | +| iconPosition | Set the icon position of button | `start` \| `end` | `start` | 5.17.0 | | loading | Set the loading status of button | boolean \| { delay: number } | false | | | shape | Can be set button shape | `default` \| `circle` \| `round` | `default` | | | size | Set the size of button | `large` \| `middle` \| `small` | `middle` | | diff --git a/components/button/index.zh-CN.md b/components/button/index.zh-CN.md index cfc91eaee1..b7eec5e64c 100644 --- a/components/button/index.zh-CN.md +++ b/components/button/index.zh-CN.md @@ -37,11 +37,12 @@ group: 按钮类型 -图标按钮 +按钮图标 +按钮图标位置 调试图标按钮 调试按钮block属性 按钮尺寸 -不可用状态 +不可用状态 加载中状态 多个按钮组合 幽灵按钮 @@ -70,6 +71,7 @@ group: | href | 点击跳转的地址,指定此属性 button 的行为和 a 链接一致 | string | - | | | htmlType | 设置 `button` 原生的 `type` 值,可选值请参考 [HTML 标准](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type) | string | `button` | | | icon | 设置按钮的图标组件 | ReactNode | - | | +| iconPosition | 设置按钮图标组件的位置 | `start` \| `end` | `start` | 5.17.0 | | loading | 设置按钮载入状态 | boolean \| { delay: number } | false | | | shape | 设置按钮形状 | `default` \| `circle` \| `round` | `default` | | | size | 设置按钮大小 | `large` \| `middle` \| `small` | `middle` | | diff --git a/components/button/style/index.ts b/components/button/style/index.ts index a538053116..20e9d37210 100644 --- a/components/button/style/index.ts +++ b/components/button/style/index.ts @@ -13,7 +13,6 @@ export type { ComponentToken }; // ============================== Shared ============================== const genSharedButtonStyle: GenerateStyle = (token): CSSObject => { const { componentCls, iconCls, fontWeight } = token; - return { [componentCls]: { outline: 'none', @@ -41,6 +40,10 @@ const genSharedButtonStyle: GenerateStyle = (token): CSS [`${componentCls}-icon`]: { lineHeight: 0, + // iconPosition in end + [`&-end`]: { + marginInlineStart: token.marginXS, + }, }, // Leave a space between icon and text. @@ -52,6 +55,9 @@ const genSharedButtonStyle: GenerateStyle = (token): CSS [`&${componentCls}-loading-icon, &:not(:last-child)`]: { marginInlineEnd: token.marginXS, }, + [`&${componentCls}-loading-icon-end`]: { + marginInlineStart: token.marginXS, + }, }, '> a': { From 4621bd9f5ed1c66e70d6ef64de04a4565c1c4f46 Mon Sep 17 00:00:00 2001 From: lijianan <574980606@qq.com> Date: Tue, 9 Apr 2024 10:24:17 +0800 Subject: [PATCH 05/22] feat: Alert support id and ref (#48336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Alert support id and ref * test: add test case * chore: adjust ts def --------- Co-authored-by: 二货机器人 --- components/alert/Alert.tsx | 21 +++++++++++++++++---- components/alert/ErrorBoundary.tsx | 4 +++- components/alert/__tests__/index.test.tsx | 10 ++++++++++ components/alert/index.ts | 5 +---- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/components/alert/Alert.tsx b/components/alert/Alert.tsx index 21cb5f0dee..34e9a2702d 100644 --- a/components/alert/Alert.tsx +++ b/components/alert/Alert.tsx @@ -15,6 +15,10 @@ import { devUseWarning } from '../_util/warning'; import { ConfigContext } from '../config-provider'; import useStyle from './style'; +export interface AlertRef { + nativeElement: HTMLDivElement; +} + export interface AlertProps { /** Type of Alert styles, options:`success`, `info`, `warning`, `error` */ type?: 'success' | 'info' | 'warning' | 'error'; @@ -48,6 +52,8 @@ export interface AlertProps { onMouseEnter?: React.MouseEventHandler; onMouseLeave?: React.MouseEventHandler; onClick?: React.MouseEventHandler; + + id?: string; } const iconMapFilled = { @@ -102,7 +108,7 @@ const CloseIconNode: React.FC = (props) => { ) : null; }; -const Alert: React.FC = (props) => { +const Alert = React.forwardRef((props, ref) => { const { description, prefixCls: customizePrefixCls, @@ -120,6 +126,7 @@ const Alert: React.FC = (props) => { closeText, closeIcon, action, + id, ...otherProps } = props; @@ -130,7 +137,12 @@ const Alert: React.FC = (props) => { warning.deprecated(!closeText, 'closeText', 'closable.closeIcon'); } - const ref = React.useRef(null); + const internalRef = React.useRef(null); + + React.useImperativeHandle(ref, () => ({ + nativeElement: internalRef.current!, + })); + const { getPrefixCls, direction, alert } = React.useContext(ConfigContext); const prefixCls = getPrefixCls('alert', customizePrefixCls); @@ -224,7 +236,8 @@ const Alert: React.FC = (props) => { > {({ className: motionClassName, style: motionStyle }) => (
= (props) => { )} , ); -}; +}); if (process.env.NODE_ENV !== 'production') { Alert.displayName = 'Alert'; diff --git a/components/alert/ErrorBoundary.tsx b/components/alert/ErrorBoundary.tsx index c9f29da938..c12595e11e 100644 --- a/components/alert/ErrorBoundary.tsx +++ b/components/alert/ErrorBoundary.tsx @@ -6,6 +6,7 @@ interface ErrorBoundaryProps { message?: React.ReactNode; description?: React.ReactNode; children?: React.ReactNode; + id?: string; } interface ErrorBoundaryStates { @@ -28,7 +29,7 @@ class ErrorBoundary extends React.Component { warnSpy.mockRestore(); }); + + it('should support id and ref', () => { + const alertRef = React.createRef(); + const { container } = render(); + const element = container.querySelector('#test-id'); + expect(element).toBeTruthy(); + expect(alertRef.current?.nativeElement).toBeTruthy(); + expect(alertRef.current?.nativeElement).toBe(element); + }); }); diff --git a/components/alert/index.ts b/components/alert/index.ts index 66e8ed2adf..a9f01e8343 100644 --- a/components/alert/index.ts +++ b/components/alert/index.ts @@ -1,12 +1,9 @@ -import type React from 'react'; - -import type { AlertProps } from './Alert'; import InternalAlert from './Alert'; import ErrorBoundary from './ErrorBoundary'; export type { AlertProps } from './Alert'; -type CompoundedComponent = React.FC & { +type CompoundedComponent = typeof InternalAlert & { ErrorBoundary: typeof ErrorBoundary; }; From 53cbceb7dbf1d10323a097dc8b089c7e2c712de0 Mon Sep 17 00:00:00 2001 From: lijianan <574980606@qq.com> Date: Tue, 9 Apr 2024 14:47:58 +0800 Subject: [PATCH 06/22] feat: Input.OTP support mask prop (#48257) * feat: Input.OTP support mask prop * fix: fix * fix: fix * test: add test case * test: add test case * chore: fix * chore: update * chore: remove * chore: rename useOTPSingleValue * fix: fix * fix: fix * chore: rm 3 lib * chore: add 3 lib * fix: fix * fix: fix * test: fix test case * test: fix test case * fix: fix * fix: fix --------- Signed-off-by: lijianan <574980606@qq.com> --- components/input/OTP/OTPInput.tsx | 9 +- components/input/OTP/index.tsx | 22 +- components/input/Password.tsx | 10 +- .../__snapshots__/demo-extend.test.ts.snap | 457 +++++++++--------- .../__snapshots__/demo.test.tsx.snap | 451 ++++++++--------- components/input/__tests__/otp.test.tsx | 36 +- components/input/demo/otp.tsx | 11 +- components/input/index.en-US.md | 5 + components/input/index.zh-CN.md | 5 + 9 files changed, 551 insertions(+), 455 deletions(-) diff --git a/components/input/OTP/OTPInput.tsx b/components/input/OTP/OTPInput.tsx index a89d730dee..a910841f08 100644 --- a/components/input/OTP/OTPInput.tsx +++ b/components/input/OTP/OTPInput.tsx @@ -9,10 +9,14 @@ export interface OTPInputProps extends Omit { onChange: (index: number, value: string) => void; /** Tell parent to do active offset */ onActiveChange: (nextIndex: number) => void; + + mask?: boolean | string; } const OTPInput = React.forwardRef((props, ref) => { - const { value, onChange, onActiveChange, index, ...restProps } = props; + const { value, onChange, onActiveChange, index, mask, ...restProps } = props; + + const internalValue = value && typeof mask === 'string' ? mask : value; const onInternalChange: React.ChangeEventHandler = (e) => { onChange(index, e.target.value); @@ -56,13 +60,14 @@ const OTPInput = React.forwardRef((props, ref) => { ); }); diff --git a/components/input/OTP/index.tsx b/components/input/OTP/index.tsx index d268431380..b42dc9ee0c 100644 --- a/components/input/OTP/index.tsx +++ b/components/input/OTP/index.tsx @@ -5,11 +5,13 @@ import pickAttrs from 'rc-util/lib/pickAttrs'; import { getMergedStatus } from '../../_util/statusUtils'; import type { InputStatus } from '../../_util/statusUtils'; +import { devUseWarning } from '../../_util/warning'; import { ConfigContext } from '../../config-provider'; import useCSSVarCls from '../../config-provider/hooks/useCSSVarCls'; import useSize from '../../config-provider/hooks/useSize'; import type { SizeType } from '../../config-provider/SizeContext'; import { FormItemInputContext } from '../../form/context'; +import type { FormItemStatusContextProps } from '../../form/context'; import type { Variant } from '../../form/hooks/useVariants'; import type { InputRef } from '../Input'; import useStyle from '../style/otp'; @@ -42,6 +44,8 @@ export interface OTPProps extends Omit, 'on // Status disabled?: boolean; status?: InputStatus; + + mask?: boolean | string; } function strToArr(str: string) { @@ -61,9 +65,19 @@ const OTP = React.forwardRef((props, ref) => { disabled, status: customStatus, autoFocus, + mask, ...restProps } = props; + if (process.env.NODE_ENV !== 'production') { + const warning = devUseWarning('Input.OTP'); + warning( + !(typeof mask === 'string' && mask.length > 1), + 'usage', + '`mask` prop should be a single character.', + ); + } + const { getPrefixCls, direction } = React.useContext(ConfigContext); const prefixCls = getPrefixCls('otp', customizePrefixCls); @@ -85,7 +99,7 @@ const OTP = React.forwardRef((props, ref) => { const formContext = React.useContext(FormItemInputContext); const mergedStatus = getMergedStatus(formContext.status, customStatus); - const proxyFormContext = React.useMemo( + const proxyFormContext = React.useMemo( () => ({ ...formContext, status: mergedStatus, @@ -194,10 +208,11 @@ const OTP = React.forwardRef((props, ref) => { }; // ======================== Render ======================== - const inputSharedProps = { + const inputSharedProps: Partial = { variant, disabled, status: mergedStatus as InputStatus, + mask, }; return wrapCSSVar( @@ -216,10 +231,9 @@ const OTP = React.forwardRef((props, ref) => { )} > - {new Array(length).fill(0).map((_, index) => { + {Array.from({ length }).map((_, index) => { const key = `otp-${index}`; const singleValue = valueCells[index] || ''; - return ( { diff --git a/components/input/Password.tsx b/components/input/Password.tsx index 4ac551b29e..787a8ca46a 100644 --- a/components/input/Password.tsx +++ b/components/input/Password.tsx @@ -35,7 +35,13 @@ const actionMap: Record> type IconPropsType = React.HTMLAttributes & React.Attributes; const Password = React.forwardRef((props, ref) => { - const { visibilityToggle = true } = props; + const { + disabled, + action = 'click', + visibilityToggle = true, + iconRender = defaultIconRender, + } = props; + const visibilityControlled = typeof visibilityToggle === 'object' && visibilityToggle.visible !== undefined; const [visible, setVisible] = useState(() => @@ -53,7 +59,6 @@ const Password = React.forwardRef((props, ref) => { const removePasswordTimeout = useRemovePasswordTimeout(inputRef); const onVisibleChange = () => { - const { disabled } = props; if (disabled) { return; } @@ -70,7 +75,6 @@ const Password = React.forwardRef((props, ref) => { }; const getIcon = (prefixCls: string) => { - const { action = 'click', iconRender = defaultIconRender } = props; const iconTrigger = actionMap[action] || ''; const icon = iconRender(visible); const iconProps: IconPropsType = { diff --git a/components/input/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/input/__tests__/__snapshots__/demo-extend.test.ts.snap index e0e725077e..8ce331e0d5 100644 --- a/components/input/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/input/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -10116,242 +10116,259 @@ exports[`renders components/input/demo/group.tsx extend context correctly 2`] = exports[`renders components/input/demo/otp.tsx extend context correctly 1`] = `
-
-
- With formatter (Upcase) -
+ With formatter (Upcase) + +
+ + + + + +
-
-
- - - - - - -
+ With Disabled + +
+ + + + + +
-
-
- With Disabled -
+ With Length (8) + +
+ + + + + + + +
-
-
- - - - - - -
+ With variant + +
+ + + + + +
-
-
- With Length (8) -
-
+ With custom display character +
-
- - - - - - - - -
-
-
-
- With variant -
-
-
-
- - - - - - -
+ + + + + +
`; -exports[`renders components/input/demo/otp.tsx extend context correctly 2`] = `[]`; +exports[`renders components/input/demo/otp.tsx extend context correctly 2`] = ` +[ + "Warning: [antd: Input.OTP] \`mask\` prop should be a single character.", +] +`; exports[`renders components/input/demo/password-input.tsx extend context correctly 1`] = `
-
-
- With formatter (Upcase) -
+ With formatter (Upcase) + +
+ + + + + +
-
-
- - - - - - -
+ With Disabled + +
+ + + + + +
-
-
- With Disabled -
+ With Length (8) + +
+ + + + + + + +
-
-
- - - - - - -
+ With variant + +
+ + + + + +
-
-
- With Length (8) -
-
+ With custom display character +
-
- - - - - - - - -
-
-
-
- With variant -
-
-
-
- - - - - - -
+ + + + + +
`; diff --git a/components/input/__tests__/otp.test.tsx b/components/input/__tests__/otp.test.tsx index 8e2849a093..854b1d9b7e 100644 --- a/components/input/__tests__/otp.test.tsx +++ b/components/input/__tests__/otp.test.tsx @@ -13,13 +13,13 @@ describe('Input.OTP', () => { mountTest(Input.OTP); rtlTest(Input.OTP); - function getText(container: HTMLElement) { - const inputList = container.querySelectorAll('input'); + const getText = (container: HTMLElement) => { + const inputList = container.querySelectorAll('input'); return Array.from(inputList) .map((input) => input.value || ' ') .join('') .replace(/\s*$/, ''); - } + }; beforeEach(() => { jest.useFakeTimers(); @@ -128,4 +128,34 @@ describe('Input.OTP', () => { fireEvent.input(container.querySelector('input')!, { target: { value: 'little' } }); expect(getText(container)).toBe('LITTLE'); }); + + it('support mask prop', () => { + // default + const { container, rerender } = render(); + expect(getText(container)).toBe('bamboo'); + + // support string + rerender(); + expect(getText(container)).toBe('******'); + + // support emoji + rerender(); + expect(getText(container)).toBe('🔒🔒🔒🔒🔒🔒'); + }); + + it('should throw Error when mask.length > 1', () => { + const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + render(); + expect(errSpy).toHaveBeenCalledWith( + 'Warning: [antd: Input.OTP] `mask` prop should be a single character.', + ); + errSpy.mockRestore(); + }); + + it('should not throw Error when mask.length <= 1', () => { + const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + render(); + expect(errSpy).not.toHaveBeenCalled(); + errSpy.mockRestore(); + }); }); diff --git a/components/input/demo/otp.tsx b/components/input/demo/otp.tsx index 9b82e3d24d..9e70d36b12 100644 --- a/components/input/demo/otp.tsx +++ b/components/input/demo/otp.tsx @@ -1,6 +1,7 @@ import React from 'react'; +import { Flex, Input, Typography } from 'antd'; import type { GetProp } from 'antd'; -import { Input, Space, Typography } from 'antd'; +import type { OTPProps } from 'antd/es/input/OTP'; const { Title } = Typography; @@ -9,12 +10,12 @@ const App: React.FC = () => { console.log('onChange:', text); }; - const sharedProps = { + const sharedProps: OTPProps = { onChange, }; return ( - + With formatter (Upcase) str.toUpperCase()} {...sharedProps} /> With Disabled @@ -23,7 +24,9 @@ const App: React.FC = () => { With variant - + With custom display character + + ); }; diff --git a/components/input/index.en-US.md b/components/input/index.en-US.md index 4c9931ecd1..d24fcfc4a5 100644 --- a/components/input/index.en-US.md +++ b/components/input/index.en-US.md @@ -124,11 +124,16 @@ Supports all props of `Input`. Added in `5.16.0`. +> Notes for developers +> +> When the `mask` prop is string, we recommend receiving a single character or a single emoji. If multiple characters or multiple emoji are passed, a warning will be thrown. + | Property | Description | Type | Default | Version | | --- | --- | --- | --- | --- | | defaultValue | Default value | string | - | | | disabled | Whether the input is disabled | boolean | false | | | formatter | Format display, blank fields will be filled with ` ` | (value: string) => string | - | | +| mask | Custom display, the original value will not be modified | boolean \| string | `false` | `5.17.0` | | length | The number of input elements | number | 6 | | | status | Set validation status | 'error' \| 'warning' | - | | | size | The size of the input box | `small` \| `middle` \| `large` | `middle` | | diff --git a/components/input/index.zh-CN.md b/components/input/index.zh-CN.md index 3fd03b2641..7ccd116396 100644 --- a/components/input/index.zh-CN.md +++ b/components/input/index.zh-CN.md @@ -125,11 +125,16 @@ interface CountConfig { `5.16.0` 新增。 +> 开发者注意事项: +> +> 当 `mask` 属性的类型为 string 时,我们强烈推荐接收单个字符或单个 emoji,如果传入多个字符或多个 emoji,则会在控制台抛出警告。 + | 参数 | 说明 | 类型 | 默认值 | 版本 | | --- | --- | --- | --- | --- | | defaultValue | 默认值 | string | - | | | disabled | 是否禁用 | boolean | false | | | formatter | 格式化展示,留空字段会被 ` ` 填充 | (value: string) => string | - | | +| mask | 自定义展示,和 `formatter` 的区别是不会修改原始值 | boolean \| string | `false` | `5.17.0` | | length | 输入元素数量 | number | 6 | | | status | 设置校验状态 | 'error' \| 'warning' | - | | | size | 输入框大小 | `small` \| `middle` \| `large` | `middle` | | From 6a7945d2fd2d35cd9c9aa17f5e6c0d36f7342c71 Mon Sep 17 00:00:00 2001 From: lijianan <574980606@qq.com> Date: Thu, 11 Apr 2024 15:29:10 +0800 Subject: [PATCH 07/22] feat: Button support autoInsertSpace prop (#48348) * feat: Button support autoInsertSpaceInButton prop * demo: update demo * fix: fix * test: add test case * docs: update docs * chore: deprecated autoInsertSpaceInButton prop * Update components/button/button.tsx Co-authored-by: MadCcc Signed-off-by: lijianan <574980606@qq.com> * fix: fix * fix: fix * Update components/button/button.tsx Co-authored-by: MadCcc Signed-off-by: lijianan <574980606@qq.com> * fix: fix * Update components/button/button.tsx Signed-off-by: lijianan <574980606@qq.com> * fix: fix * fix: fix --------- Signed-off-by: lijianan <574980606@qq.com> Co-authored-by: MadCcc --- .../__snapshots__/demo-extend.test.ts.snap | 25 +++++++++++++++++++ .../__tests__/__snapshots__/demo.test.ts.snap | 23 +++++++++++++++++ components/button/__tests__/index.test.tsx | 6 +++++ components/button/button.tsx | 15 ++++++----- components/button/demo/noSpace.md | 7 ++++++ components/button/demo/noSpace.tsx | 15 +++++++++++ components/button/index.en-US.md | 15 ++--------- components/button/index.zh-CN.md | 16 +++--------- .../config-provider/__tests__/index.test.tsx | 12 ++++++++- .../__tests__/useConfig.test.tsx | 15 +++++++++++ components/config-provider/context.ts | 4 ++- components/config-provider/index.en-US.md | 3 +-- components/config-provider/index.tsx | 20 ++++++++++++++- components/config-provider/index.zh-CN.md | 3 +-- 14 files changed, 140 insertions(+), 39 deletions(-) create mode 100644 components/button/demo/noSpace.md create mode 100644 components/button/demo/noSpace.tsx diff --git a/components/button/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/button/__tests__/__snapshots__/demo-extend.test.ts.snap index d94a8373dc..49edf12208 100644 --- a/components/button/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/button/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -2776,6 +2776,31 @@ exports[`renders components/button/demo/multiple.tsx extend context correctly 1` exports[`renders components/button/demo/multiple.tsx extend context correctly 2`] = `[]`; +exports[`renders components/button/demo/noSpace.tsx extend context correctly 1`] = ` +
+ + +
+`; + +exports[`renders components/button/demo/noSpace.tsx extend context correctly 2`] = `[]`; + exports[`renders components/button/demo/size.tsx extend context correctly 1`] = ` Array [
`; +exports[`renders components/button/demo/noSpace.tsx correctly 1`] = ` +
+ + +
+`; + exports[`renders components/button/demo/size.tsx correctly 1`] = ` Array [
{ const { container } = render(); + expect(container.querySelector('button')?.textContent).toBe(text); + }); }); diff --git a/components/button/button.tsx b/components/button/button.tsx index 4a62cc6ef8..cdd7090500 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -51,6 +51,7 @@ type MergedHTMLAttributes = Omit< export interface ButtonProps extends BaseButtonProps, MergedHTMLAttributes { href?: string; htmlType?: ButtonHTMLType; + autoInsertSpace?: boolean; } type LoadingConfigType = { @@ -98,6 +99,7 @@ const InternalCompoundedButton = React.forwardRef< htmlType = 'button', classNames: customClassNames, style: customStyle = {}, + autoInsertSpace, ...rest } = props; @@ -105,7 +107,10 @@ const InternalCompoundedButton = React.forwardRef< // Compatible with original `type` behavior const mergedType = type || 'default'; - const { getPrefixCls, autoInsertSpaceInButton, direction, button } = useContext(ConfigContext); + const { getPrefixCls, direction, button } = useContext(ConfigContext); + + const mergedInsertSpace = autoInsertSpace ?? button?.autoInsertSpace ?? true; + const prefixCls = getPrefixCls('btn', customizePrefixCls); const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); @@ -151,7 +156,7 @@ const InternalCompoundedButton = React.forwardRef< useEffect(() => { // FIXME: for HOC usage like - if (!buttonRef || !(buttonRef as any).current || autoInsertSpaceInButton === false) { + if (!buttonRef || !(buttonRef as any).current || !mergedInsertSpace) { return; } const buttonText = (buttonRef as any).current.textContent; @@ -190,7 +195,6 @@ const InternalCompoundedButton = React.forwardRef< ); } - const autoInsertSpace = autoInsertSpaceInButton !== false; const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction); const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined }; @@ -214,7 +218,7 @@ const InternalCompoundedButton = React.forwardRef< [`${prefixCls}-icon-only`]: !children && children !== 0 && !!iconType, [`${prefixCls}-background-ghost`]: ghost && !isUnBorderedButtonType(mergedType), [`${prefixCls}-loading`]: innerLoading, - [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace && !innerLoading, + [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && mergedInsertSpace && !innerLoading, [`${prefixCls}-block`]: block, [`${prefixCls}-dangerous`]: !!danger, [`${prefixCls}-rtl`]: direction === 'rtl', @@ -252,12 +256,11 @@ const InternalCompoundedButton = React.forwardRef< ); const kids = - children || children === 0 ? spaceChildren(children, needInserted && autoInsertSpace) : null; + children || children === 0 ? spaceChildren(children, needInserted && mergedInsertSpace) : null; const genButtonContent = (iconComponent: React.ReactNode, kidsComponent: React.ReactNode) => { const isRTL = direction === 'rtl'; const iconFirst = (iconPosition === 'start' && !isRTL) || (iconPosition === 'end' && isRTL); - return ( <> {iconFirst ? iconComponent : kidsComponent} diff --git a/components/button/demo/noSpace.md b/components/button/demo/noSpace.md new file mode 100644 index 0000000000..8a5d3a8570 --- /dev/null +++ b/components/button/demo/noSpace.md @@ -0,0 +1,7 @@ +## zh-CN + +我们默认在两个汉字之间添加空格,可以通过设置 `autoInsertSpace` 为 `false` 关闭。 + +## en-US + +We add a space between two Chinese characters by default, which can be removed by setting `autoInsertSpace` to `false`. diff --git a/components/button/demo/noSpace.tsx b/components/button/demo/noSpace.tsx new file mode 100644 index 0000000000..9eeab746b2 --- /dev/null +++ b/components/button/demo/noSpace.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Button, Flex } from 'antd'; + +const App: React.FC = () => ( + + + + +); + +export default App; diff --git a/components/button/index.en-US.md b/components/button/index.en-US.md index dcf9f4a60c..f250d3b14c 100644 --- a/components/button/index.en-US.md +++ b/components/button/index.en-US.md @@ -58,6 +58,7 @@ Different button styles can be generated by setting Button properties. The recom | Property | Description | Type | Default | Version | | --- | --- | --- | --- | --- | +| autoInsertSpace | We add a space between two Chinese characters by default, which can be removed by setting `autoInsertSpace` to `false`. | boolean | `true` | 5.17.0 | | block | Option to fit button width to its parent width | boolean | false | | | classNames | Semantic DOM class | [Record](#semantic-dom) | - | 5.4.0 | | danger | Set the danger status of button | boolean | false | | @@ -73,7 +74,7 @@ Different button styles can be generated by setting Button properties. The recom | styles | Semantic DOM style | [Record](#semantic-dom) | - | 5.4.0 | | target | Same as target attribute of a, works when href is specified | string | - | | | type | Set button type | `primary` \| `dashed` \| `link` \| `text` \| `default` | `default` | | -| onClick | Set the handler to handle `click` event | (event: MouseEvent) => void | - | | +| onClick | Set the handler to handle `click` event | (event: React.MouseEvent) => void | - | | It accepts all props which native buttons support. @@ -97,18 +98,6 @@ If you don't need this feature, you can set `disabled` of `wave` in [ConfigProvi ``` -### How to remove space between 2 chinese characters? - -Following the Ant Design specification, we will add one space between if Button (exclude Text button and Link button) contains two Chinese characters only. If you don't need that, you can use [ConfigProvider](/components/config-provider/#api) to set `autoInsertSpaceInButton` as `false`. - -```jsx - - - -``` - -Button with two Chinese characters -