chore: resolve conflict

This commit is contained in:
afc163 2022-12-07 12:15:17 +08:00
commit a87e68c797
95 changed files with 2685 additions and 174 deletions

View File

@ -10,20 +10,11 @@
An enterprise-class UI design language and React UI library.
[![CI status][github-action-image]][github-action-url]
[![codecov][codecov-image]][codecov-url]
[![NPM version][npm-image]][npm-url]
[![NPM downloads][download-image]][download-url]
[![CI status][github-action-image]][github-action-url] [![codecov][codecov-image]][codecov-url] [![NPM version][npm-image]][npm-url] [![NPM downloads][download-image]][download-url]
[![Total alerts][lgtm-image]][lgtm-url]
[![][bundlephobia-image]][bundlephobia-url]
[![][bundlesize-js-image]][unpkg-js-url]
[![FOSSA Status][fossa-image]][fossa-url]
[![Total alerts][lgtm-image]][lgtm-url] [![][bundlephobia-image]][bundlephobia-url] [![][bundlesize-js-image]][unpkg-js-url] [![FOSSA Status][fossa-image]][fossa-url]
[![Follow Twitter][twitter-image]][twitter-url]
[![Renovate status][renovate-image]][renovate-dashboard-url]
[![][issues-helper-image]][issues-helper-url]
[![Issues need help][help-wanted-image]][help-wanted-url]
[![Follow Twitter][twitter-image]][twitter-url] [![Renovate status][renovate-image]][renovate-dashboard-url] [![][issues-helper-image]][issues-helper-url] [![Issues need help][help-wanted-image]][help-wanted-url]
[npm-image]: http://img.shields.io/npm/v/antd.svg?style=flat-square
[npm-url]: http://npmjs.org/package/antd

View File

@ -5,6 +5,7 @@ exports[`antd exports modules correctly 1`] = `
"Affix",
"Alert",
"Anchor",
"App",
"AutoComplete",
"Avatar",
"BackTop",
@ -40,6 +41,7 @@ exports[`antd exports modules correctly 1`] = `
"Popconfirm",
"Popover",
"Progress",
"QRCode",
"Radio",
"Rate",
"Result",

View File

@ -6,10 +6,18 @@ import type { ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import getScroll from '../_util/getScroll';
import scrollTo from '../_util/scrollTo';
import warning from '../_util/warning';
import AnchorContext from './context';
import type { AnchorLinkBaseProps } from './AnchorLink';
import AnchorLink from './AnchorLink';
import useStyle from './style';
export interface AnchorLinkItemProps extends AnchorLinkBaseProps {
key: React.Key;
children?: AnchorLinkItemProps[];
}
export type AnchorContainer = HTMLElement | Window;
function getDefaultContainer() {
@ -45,6 +53,9 @@ export interface AnchorProps {
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
/**
* @deprecated Please use `items` instead.
*/
children?: React.ReactNode;
offsetTop?: number;
bounds?: number;
@ -61,6 +72,7 @@ export interface AnchorProps {
targetOffset?: number;
/** Listening event when scrolling change active link */
onChange?: (currentActiveLink: string) => void;
items?: AnchorLinkItemProps[];
}
interface InternalAnchorProps extends AnchorProps {
@ -100,6 +112,7 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
affix = true,
showInkInFixed = false,
children,
items,
bounds,
targetOffset,
onClick,
@ -108,6 +121,11 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
getCurrentAnchor,
} = props;
// =================== Warning =====================
if (process.env.NODE_ENV !== 'production') {
warning(!children, 'Anchor', '`Anchor children` is deprecated. Please use `items` instead.');
}
const [links, setLinks] = React.useState<string[]>([]);
const [activeLink, setActiveLink] = React.useState<string | null>(null);
const activeLinkRef = React.useRef<string | null>(activeLink);
@ -257,13 +275,22 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
...style,
};
const createNestedLink = (options?: AnchorLinkItemProps[]) =>
Array.isArray(options)
? options.map((item) => (
<AnchorLink {...item} key={item.key}>
{createNestedLink(item.children)}
</AnchorLink>
))
: null;
const anchorContent = (
<div ref={wrapperRef} className={wrapperClass} style={wrapperStyle}>
<div className={anchorClass}>
<div className={`${prefixCls}-ink`}>
<span className={inkClass} ref={spanLinkNode} />
</div>
{children}
{'items' in props ? createNestedLink(items) : children}
</div>
</div>
);

View File

@ -5,15 +5,18 @@ import { ConfigConsumer } from '../config-provider';
import type { AntAnchor } from './Anchor';
import AnchorContext from './context';
export interface AnchorLinkProps {
export interface AnchorLinkBaseProps {
prefixCls?: string;
href: string;
target?: string;
title: React.ReactNode;
children?: React.ReactNode;
className?: string;
}
export interface AnchorLinkProps extends AnchorLinkBaseProps {
children?: React.ReactNode;
}
const AnchorLink: React.FC<AnchorLinkProps> = (props) => {
const { href = '#', title, prefixCls: customizePrefixCls, children, className, target } = props;

View File

@ -432,4 +432,64 @@ describe('Anchor Render', () => {
}).not.toThrow();
});
});
it('renders items correctly', () => {
const { container, asFragment } = render(
<Anchor
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Item Basic Demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
children: [
{
key: '4',
href: '#anchor-props',
title: 'Anchor Props',
children: [
{
key: '5',
href: '#link-props',
title: 'Link Props',
},
],
},
],
},
]}
/>,
);
expect(container.querySelectorAll('.ant-anchor .ant-anchor-link').length).toBe(5);
const linkTitles = Array.from(container.querySelector('.ant-anchor')?.childNodes!)
.slice(1)
.map((n) => (n as HTMLElement).querySelector('.ant-anchor-link-title'));
expect((linkTitles[0] as HTMLAnchorElement).href).toContain('#components-anchor-demo-basic');
expect((linkTitles[1] as HTMLAnchorElement).href).toContain('#components-anchor-demo-static');
expect((linkTitles[2] as HTMLAnchorElement).href).toContain('#api');
expect(asFragment().firstChild).toMatchSnapshot();
expect(
(
container.querySelector(
'.ant-anchor .ant-anchor-link .ant-anchor-link .ant-anchor-link-title',
) as HTMLAnchorElement
)?.href,
).toContain('#anchor-props');
expect(
(
container.querySelector(
'.ant-anchor .ant-anchor-link .ant-anchor-link .ant-anchor-link .ant-anchor-link-title',
) as HTMLAnchorElement
)?.href,
).toContain('#link-props');
});
});

View File

@ -0,0 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Anchor Render renders items correctly 1`] = `
<div>
<div
class=""
>
<div
class="ant-anchor-wrapper"
style="max-height: 100vh;"
>
<div
class="ant-anchor"
>
<div
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-basic"
title="Item Basic Demo"
>
Item Basic Demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-static"
title="Static demo"
>
Static demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#api"
title="API"
>
API
</a>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#anchor-props"
title="Anchor Props"
>
Anchor Props
</a>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#link-props"
title="Link Props"
>
Link Props
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -154,6 +154,86 @@ exports[`renders ./components/anchor/demo/customizeHighlight.tsx extend context
</div>
`;
exports[`renders ./components/anchor/demo/legacy-anchor.tsx extend context correctly 1`] = `
<div>
<div
class=""
>
<div
class="ant-anchor-wrapper"
style="max-height:100vh"
>
<div
class="ant-anchor"
>
<div
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-basic"
title="Basic demo"
>
Basic demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-static"
title="Static demo"
>
Static demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#api"
title="API"
>
API
</a>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#anchor-props"
title="Anchor Props"
>
Anchor Props
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#link-props"
title="Link Props"
>
Link Props
</a>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`renders ./components/anchor/demo/onChange.tsx extend context correctly 1`] = `
<div
class="ant-anchor-wrapper"

View File

@ -154,6 +154,86 @@ exports[`renders ./components/anchor/demo/customizeHighlight.tsx correctly 1`] =
</div>
`;
exports[`renders ./components/anchor/demo/legacy-anchor.tsx correctly 1`] = `
<div>
<div
class=""
>
<div
class="ant-anchor-wrapper"
style="max-height:100vh"
>
<div
class="ant-anchor"
>
<div
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-basic"
title="Basic demo"
>
Basic demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-static"
title="Static demo"
>
Static demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#api"
title="API"
>
API
</a>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#anchor-props"
title="Anchor Props"
>
Anchor Props
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#link-props"
title="Link Props"
>
Link Props
</a>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`renders ./components/anchor/demo/onChange.tsx correctly 1`] = `
<div
class="ant-anchor-wrapper"

View File

@ -1,17 +1,38 @@
import React from 'react';
import { Anchor } from 'antd';
const { Link } = Anchor;
const App: React.FC = () => (
<Anchor>
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-static" title="Static demo" />
<Link href="#api" title="API">
<Link href="#anchor-props" title="Anchor Props" />
<Link href="#link-props" title="Link Props" />
</Link>
</Anchor>
<Anchor
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Basic demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
children: [
{
key: '4',
href: '#anchor-props',
title: 'Anchor Props',
},
{
key: '5',
href: '#link-props',
title: 'Link Props',
},
],
},
]}
/>
);
export default App;

View File

@ -1,19 +1,42 @@
import React from 'react';
import { Anchor } from 'antd';
const { Link } = Anchor;
const getCurrentAnchor = () => '#components-anchor-demo-static';
const App: React.FC = () => (
<Anchor affix={false} getCurrentAnchor={getCurrentAnchor}>
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-static" title="Static demo" />
<Link href="#api" title="API">
<Link href="#anchor-props" title="Anchor Props" />
<Link href="#link-props" title="Link Props" />
</Link>
</Anchor>
<Anchor
affix={false}
getCurrentAnchor={getCurrentAnchor}
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Basic demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
children: [
{
key: '4',
href: '#anchor-props',
title: 'Anchor Props',
},
{
key: '5',
href: '#link-props',
title: 'Link Props',
},
],
},
]}
/>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
Debug usage
## en-US
Debug usage

View File

@ -0,0 +1,17 @@
import React from 'react';
import { Anchor } from 'antd';
const { Link } = Anchor;
const App: React.FC = () => (
<Anchor>
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-static" title="Static demo" />
<Link href="#api" title="API">
<Link href="#anchor-props" title="Anchor Props" />
<Link href="#link-props" title="Link Props" />
</Link>
</Anchor>
);
export default App;

View File

@ -1,21 +1,44 @@
import React from 'react';
import { Anchor } from 'antd';
const { Link } = Anchor;
const onChange = (link: string) => {
console.log('Anchor:OnChange', link);
};
const App: React.FC = () => (
<Anchor affix={false} onChange={onChange}>
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-static" title="Static demo" />
<Link href="#api" title="API">
<Link href="#anchor-props" title="Anchor Props" />
<Link href="#link-props" title="Link Props" />
</Link>
</Anchor>
<Anchor
affix={false}
onChange={onChange}
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Basic demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
children: [
{
key: '4',
href: '#anchor-props',
title: 'Anchor Props',
},
{
key: '5',
href: '#link-props',
title: 'Link Props',
},
],
},
]}
/>
);
export default App;

View File

@ -1,8 +1,6 @@
import React from 'react';
import { Anchor } from 'antd';
const { Link } = Anchor;
const handleClick = (
e: React.MouseEvent<HTMLElement>,
link: {
@ -15,14 +13,39 @@ const handleClick = (
};
const App: React.FC = () => (
<Anchor affix={false} onClick={handleClick}>
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-static" title="Static demo" />
<Link href="#api" title="API">
<Link href="#anchor-props" title="Anchor Props" />
<Link href="#link-props" title="Link Props" />
</Link>
</Anchor>
<Anchor
affix={false}
onClick={handleClick}
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Basic demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
children: [
{
key: '4',
href: '#anchor-props',
title: 'Anchor Props',
},
{
key: '5',
href: '#link-props',
title: 'Link Props',
},
],
},
]}
/>
);
export default App;

View File

@ -1,17 +1,39 @@
import React from 'react';
import { Anchor } from 'antd';
const { Link } = Anchor;
const App: React.FC = () => (
<Anchor affix={false}>
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-static" title="Static demo" />
<Link href="#api" title="API">
<Link href="#anchor-props" title="Anchor Props" />
<Link href="#link-props" title="Link Props" />
</Link>
</Anchor>
<Anchor
affix={false}
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Basic demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
children: [
{
key: '4',
href: '#anchor-props',
title: 'Anchor Props',
},
{
key: '5',
href: '#link-props',
title: 'Link Props',
},
],
},
]}
/>
);
export default App;

View File

@ -1,8 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Anchor } from 'antd';
const { Link } = Anchor;
const App: React.FC = () => {
const [targetOffset, setTargetOffset] = useState<number | undefined>(undefined);
@ -11,14 +9,38 @@ const App: React.FC = () => {
}, []);
return (
<Anchor targetOffset={targetOffset}>
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-static" title="Static demo" />
<Link href="#api" title="API">
<Link href="#anchor-props" title="Anchor Props" />
<Link href="#link-props" title="Link Props" />
</Link>
</Anchor>
<Anchor
targetOffset={targetOffset}
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Basic demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
children: [
{
key: '4',
href: '#anchor-props',
title: 'Anchor Props',
},
{
key: '5',
href: '#link-props',
title: 'Link Props',
},
],
},
]}
/>
);
};

View File

@ -28,6 +28,7 @@ For displaying anchor hyperlinks on page and jumping between them.
<code src="./demo/customizeHighlight.tsx">Customize the anchor highlight</code>
<code src="./demo/targetOffset.tsx">Set Anchor scroll offset</code>
<code src="./demo/onChange.tsx">Listening for anchor link change</code>
<code src="./demo/legacy-anchor.tsx" debug>Deprecated JSX demo</code>
## API
@ -44,6 +45,7 @@ For displaying anchor hyperlinks on page and jumping between them.
| targetOffset | Anchor scroll offset, default as `offsetTop`, [example](#components-anchor-demo-targetOffset) | number | - | |
| onChange | Listening for anchor link change | (currentActiveLink: string) => void | | |
| onClick | Set the handler to handle `click` event | (e: MouseEvent, link: object) => void | - | |
| items | Data configuration option content, support nesting through children | { href, title, target, children }\[] | - | |
### Link Props

View File

@ -29,6 +29,7 @@ group:
<code src="./demo/customizeHighlight.tsx">自定义锚点高亮</code>
<code src="./demo/targetOffset.tsx">设置锚点滚动偏移量</code>
<code src="./demo/onChange.tsx">监听锚点链接改变</code>
<code src="./demo/legacy-anchor.tsx" debug>废弃的 JSX 示例</code>
## API
@ -45,6 +46,7 @@ group:
| targetOffset | 锚点滚动偏移量,默认与 offsetTop 相同,[例子](#components-anchor-demo-targetOffset) | number | - | |
| onChange | 监听锚点链接改变 | (currentActiveLink: string) => void | - | |
| onClick | `click` 事件的 handler | (e: MouseEvent, link: object) => void | - | |
| items | 数据化配置选项内容,支持通过 children 嵌套 | { href, title, target, children }\[] | - | |
### Link Props

View File

@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/app/demo/message.tsx extend context correctly 1`] = `
<div
class="ant-app"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open message
</span>
</button>
</div>
`;
exports[`renders ./components/app/demo/modal.tsx extend context correctly 1`] = `
<div
class="ant-app"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open modal
</span>
</button>
</div>
`;
exports[`renders ./components/app/demo/notification.tsx extend context correctly 1`] = `
<div
class="ant-app"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open notification
</span>
</button>
</div>
`;

View File

@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/app/demo/message.tsx correctly 1`] = `
<div
class="ant-app"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open message
</span>
</button>
</div>
`;
exports[`renders ./components/app/demo/modal.tsx correctly 1`] = `
<div
class="ant-app"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open modal
</span>
</button>
</div>
`;
exports[`renders ./components/app/demo/notification.tsx correctly 1`] = `
<div
class="ant-app"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open notification
</span>
</button>
</div>
`;

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App rtl render component should be rendered correctly in RTL direction 1`] = `
<div
class="ant-app"
/>
`;
exports[`App single 1`] = `
<div
class="ant-app"
>
<div>
Hello World
</div>
</div>
`;

View File

@ -0,0 +1,3 @@
import { extendTest } from '../../../tests/shared/demoTest';
extendTest('app');

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('app');

View File

@ -0,0 +1,5 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('app', () => {
imageDemoTest('app');
});

View File

@ -0,0 +1,33 @@
import React from 'react';
import App from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { render } from '../../../tests/utils';
describe('App', () => {
mountTest(App);
rtlTest(App);
it('single', () => {
// Sub page
const MyPage = () => {
const { message } = App.useApp();
React.useEffect(() => {
message.success('Good!');
}, [message]);
return <div>Hello World</div>;
};
// Entry component
const MyApp = () => (
<App>
<MyPage />
</App>
);
const { getByText, container } = render(<MyApp />);
expect(getByText('Hello World')).toBeTruthy();
expect(container.firstChild).toMatchSnapshot();
});
});

19
components/app/context.ts Normal file
View File

@ -0,0 +1,19 @@
import React from 'react';
import type { MessageInstance } from '../message/interface';
import type { NotificationInstance } from '../notification/interface';
import type { ModalStaticFunctions } from '../modal/confirm';
type ModalType = Omit<ModalStaticFunctions, 'warn'>;
export interface useAppProps {
message: MessageInstance;
notification: NotificationInstance;
modal: ModalType;
}
const AppContext = React.createContext<useAppProps>({
message: {} as MessageInstance,
notification: {} as NotificationInstance,
modal: {} as ModalType,
});
export default AppContext;

View File

@ -0,0 +1,7 @@
## zh-CN
获取 `message` 静态方法.
## en-US
Static method for `message`.

View File

@ -0,0 +1,24 @@
import React from 'react';
import { App, Button } from 'antd';
// Sub page
const MyPage = () => {
const { message } = App.useApp();
const showMessage = () => {
message.success('Success!');
};
return (
<Button type="primary" onClick={showMessage}>
Open message
</Button>
);
};
// Entry component
export default () => (
<App>
<MyPage />
</App>
);

View File

@ -0,0 +1,7 @@
## zh-CN
获取 `modal` 静态方法.
## en-US
Static method for `modal`.

View File

@ -0,0 +1,27 @@
import React from 'react';
import { App, Button } from 'antd';
// Sub page
const MyPage = () => {
const { modal } = App.useApp();
const showModal = () => {
modal.warning({
title: 'This is a warning message',
content: 'some messages...some messages...',
});
};
return (
<Button type="primary" onClick={showModal}>
Open modal
</Button>
);
};
// Entry component
export default () => (
<App>
<MyPage />
</App>
);

View File

@ -0,0 +1,7 @@
## zh-CN
获取 `notification` 静态方法.
## en-US
Static method for `notification`.

View File

@ -0,0 +1,28 @@
import React from 'react';
import { App, Button } from 'antd';
// Sub page
const MyPage = () => {
const { notification } = App.useApp();
const showNotification = () => {
notification.info({
message: `Notification topLeft`,
description: 'Hello, Ant Design!!',
placement: 'topLeft',
});
};
return (
<Button type="primary" onClick={showNotification}>
Open notification
</Button>
);
};
// Entry component
export default () => (
<App>
<MyPage />
</App>
);

View File

@ -0,0 +1,43 @@
---
category: Components
group: Other
title: App
cover: https://gw.alipayobjects.com/zos/bmw-prod/cc3fcbfa-bf5b-4c8c-8a3d-c3f8388c75e8.svg
demo:
cols: 2
---
New App Component which provide global style & static function replacement.
## When To Use
Static function in React 18 concurrent mode will not well support. In v5, we recommend to use hooks for the static replacement. But it will make user manual work on define this.
## Examples
<!-- prettier-ignore -->
<code src="./demo/message.tsx">message</code>
<code src="./demo/notification.tsx">notification</code>
<code src="./demo/modal.tsx">modal</code>
## How to use
```javascript
import React from 'react';
import { App } from 'antd';
const MyPage = () => {
const { message, notification, modal } = App.useApp();
message.success('Good!');
notification.info({ message: 'Good' });
modal.warning({ title: 'Good' });
// ....
// other message,notification,modal static function
return <div>Hello word</div>;
};
const MyApp = () => (
<App>
<MyPage />
</App>
);
```

60
components/app/index.tsx Normal file
View File

@ -0,0 +1,60 @@
import React, { useContext } from 'react';
import type { ReactNode } from 'react';
import classNames from 'classnames';
import type { ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import useStyle from './style';
import useMessage from '../message/useMessage';
import useNotification from '../notification/useNotification';
import useModal from '../modal/useModal';
import AppContext from './context';
import type { useAppProps } from './context';
export type AppProps = {
className?: string;
prefixCls?: string;
children?: ReactNode;
};
const useApp: () => useAppProps = () => React.useContext(AppContext);
const App: React.ForwardRefRenderFunction<HTMLDivElement, AppProps> & {
useApp: () => useAppProps;
} = (props) => {
const { prefixCls: customizePrefixCls, children, className } = props;
const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
const prefixCls = getPrefixCls('app', customizePrefixCls);
const [wrapSSR, hashId] = useStyle(prefixCls);
const customClassName = classNames(hashId, prefixCls, className);
const [messageApi, messageContextHolder] = useMessage();
const [notificationApi, notificationContextHolder] = useNotification();
const [ModalApi, ModalContextHolder] = useModal();
const memoizedContextValue = React.useMemo(
() => ({
message: messageApi,
notification: notificationApi,
modal: ModalApi,
}),
[messageApi, notificationApi, ModalApi],
);
return wrapSSR(
<AppContext.Provider value={memoizedContextValue}>
<div className={customClassName}>
{ModalContextHolder}
{messageContextHolder}
{notificationContextHolder}
{children}
</div>
</AppContext.Provider>,
);
};
if (process.env.NODE_ENV !== 'production') {
App.displayName = 'App';
}
App.useApp = useApp;
export default App;

View File

@ -0,0 +1,45 @@
---
category: Components
subtitle: 包裹组件
group: 其他
title: App
cover: https://gw.alipayobjects.com/zos/bmw-prod/cc3fcbfa-bf5b-4c8c-8a3d-c3f8388c75e8.svg
demo:
cols: 2
---
新的包裹组件,提供重置样式和提供消费上下文的默认环境。
## 何时使用
- 提供可消费 React context 的 `message.xxx`、`Modal.xxx`、`notification.xxx` 的静态方法,可以简化 useMessage 等方法需要手动植入 `contextHolder` 的问题。
- 提供基于 `.ant-app` 的默认重置样式,解决原生元素没有 antd 规范样式的问题。
## 代码演示
<!-- prettier-ignore -->
<code src="./demo/message.tsx">message</code>
<code src="./demo/notification.tsx">notification</code>
<code src="./demo/modal.tsx">modal</code>
## How to use
```javascript
import React from 'react';
import { App } from 'antd';
const MyPage = () => {
const { message, notification, modal } = App.useApp();
message.success('Good!');
notification.info({ message: 'Good' });
modal.warning({ title: 'Good' });
// ....
// other message,notification,modal static function
return <div>Hello word</div>;
};
const MyApp = () => (
<App>
<MyPage />
</App>
);
```

View File

@ -0,0 +1,22 @@
import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook } from '../../theme/internal';
export type ComponentToken = {};
interface AppToken extends FullToken<'App'> {}
// =============================== Base ===============================
const genBaseStyle: GenerateStyle<AppToken> = (token) => {
const { componentCls, colorText, fontSize, lineHeight, fontFamily } = token;
return {
[componentCls]: {
color: colorText,
fontSize,
lineHeight,
fontFamily,
},
};
};
// ============================== Export ==============================
export default genComponentStyleHook('App', (token) => [genBaseStyle(token)]);

View File

@ -135,6 +135,7 @@ export { default as Tooltip } from './tooltip';
export type { TooltipProps } from './tooltip';
export { default as Tour } from './tour';
export type { TourProps, TourStepProps } from './tour/interface';
export { default as App } from './app';
export { default as Transfer } from './transfer';
export type { TransferProps } from './transfer';
export { default as Tree } from './tree';
@ -149,4 +150,6 @@ export { default as Typography } from './typography';
export type { TypographyProps } from './typography';
export { default as Upload } from './upload';
export type { UploadFile, UploadProps } from './upload';
export { default as QRCode } from './qrcode';
export type { QRCodeProps, QRPropsCanvas } from './qrcode/interface';
export { default as version } from './version';

View File

@ -46,6 +46,10 @@ export interface Locale {
Image?: {
preview: string;
};
QRCode?: {
expired: string;
refresh: string;
};
}
export interface LocaleProviderProps {

View File

@ -136,6 +136,10 @@ const localeValues: Locale = {
Image: {
preview: 'Preview',
},
QRCode: {
expired: 'QRCode is expired',
refresh: 'click refresh',
},
};
export default localeValues;

View File

@ -136,6 +136,10 @@ const localeValues: Locale = {
Image: {
preview: '预览',
},
QRCode: {
expired: '二维码过期',
refresh: '点击刷新',
},
};
export default localeValues;

View File

@ -1,5 +1,5 @@
import React from 'react';
import Mentions from '..';
import Mentions, { Option } from '..';
import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
@ -85,6 +85,20 @@ describe('Mentions', () => {
expect(wrapper.container.querySelectorAll('.bamboo-light').length).toBeTruthy();
});
it('warning if use Mentions.Option', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(
<Mentions style={{ width: '100%' }} defaultValue="@afc163">
<Option value="afc163">afc163</Option>
<Option value="zombieJ">zombieJ</Option>
<Option value="yesmeck">yesmeck</Option>
</Mentions>,
);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Mentions] `Mentions.Option` is deprecated. Please use `options` instead.',
);
});
it('do not lose label when use children Option', () => {
const wrapper = render(
<Mentions style={{ width: '100%' }}>

View File

@ -26,6 +26,24 @@ When you need to mention someone or something.
<code src="./demo/status.tsx">Status</code>
<code src="./demo/render-panel.tsx" debug>_InternalPanelDoNotUseOrYouWillBeFired</code>
### Usage upgrade after 5.1.0
```__react
import Alert from '../alert';
ReactDOM.render(<Alert message="After version 5.1.0, we provide a simpler usage <Mentions options={[...]} /> with better performance and potential of writing simpler code style in your applications. Meanwhile, we deprecated the old usage in browser console, we will remove it in antd 6.0." />, mountNode);
```
```jsx
// works when >=5.1.0, recommended ✅
const options = [{ value: 'sample', label: 'sample' }];
return <Mentions options={options} />;
// works when <5.1.0, deprecated when >=5.1.0 🙅🏻‍♀️
<Mentions onChange={onChange}>
<Mentions.Option value="sample">Sample</Mentions.Option>
</Mentions>;
```
## API
### Mention
@ -61,10 +79,11 @@ When you need to mention someone or something.
### Option
| Property | Description | Type | Default |
| --------- | --------------------------- | ------------------- | ------- |
| label | Title of the option | React.ReactNode | - |
| key | The key value of the option | string | - |
| disabled | Optional | boolean | - |
| className | className | string | - |
| style | The style of the option | React.CSSProperties | - |
<!-- prettier-ignore -->
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| label | Title of the option | React.ReactNode | - |
| key | The key value of the option | string | - |
| disabled | Optional | boolean | - |
| className | className | string | - |
| style | The style of the option | React.CSSProperties | - |

View File

@ -15,6 +15,7 @@ import genPurePanel from '../_util/PurePanel';
import Spin from '../spin';
import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import warning from '../_util/warning';
import useStyle from './style';
@ -86,6 +87,16 @@ const InternalMentions: React.ForwardRefRenderFunction<MentionsRef, MentionProps
const [focused, setFocused] = React.useState(false);
const innerRef = React.useRef<MentionsRef>();
const mergedRef = composeRef(ref, innerRef);
// =================== Warning =====================
if (process.env.NODE_ENV !== 'production') {
warning(
!children,
'Mentions',
'`Mentions.Option` is deprecated. Please use `options` instead.',
);
}
const { getPrefixCls, renderEmpty, direction } = React.useContext(ConfigContext);
const {
status: contextStatus,

View File

@ -27,6 +27,24 @@ demo:
<code src="./demo/status.tsx">自定义状态</code>
<code src="./demo/render-panel.tsx" debug>_InternalPanelDoNotUseOrYouWillBeFired</code>
### 5.1.0 用法升级
```__react
import Alert from '../alert';
ReactDOM.render(<Alert message="在 5.1.0 版本后,我们提供了 <Mentions options={[...]} /> 的简写方式,有更好的性能和更方便的数据组织方式,开发者不再需要自行拼接 JSX。同时我们废弃了原先的写法你还是可以在 5.x 继续使用,但会在控制台看到警告,并会在 6.0 后移除。" />, mountNode);
```
```jsx
// >=5.1.0 可用,推荐的写法 ✅
const options = [{ value: 'sample', label: 'sample' }];
return <Mentions options={options} />;
// <5.1.0 可用>=5.1.0 时不推荐 🙅🏻‍♀️
<Mentions onChange={onChange}>
<Mentions.Option value="sample">Sample</Mentions.Option>
</Mentions>;
```
## API
### Mentions

View File

@ -10,12 +10,8 @@ export interface MenuDividerProps extends React.HTMLAttributes<HTMLLIElement> {
dashed?: boolean;
}
const MenuDivider: React.FC<MenuDividerProps> = ({
prefixCls: customizePrefixCls,
className,
dashed,
...restProps
}) => {
const MenuDivider: React.FC<MenuDividerProps> = (props) => {
const { prefixCls: customizePrefixCls, className, dashed, ...restProps } = props;
const { getPrefixCls } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('menu', customizePrefixCls);

View File

@ -2,6 +2,7 @@ import classNames from 'classnames';
import type { MenuItemProps as RcMenuItemProps } from 'rc-menu';
import { Item } from 'rc-menu';
import toArray from 'rc-util/lib/Children/toArray';
import omit from 'rc-util/lib/omit';
import * as React from 'react';
import type { SiderContextProps } from '../layout/Sider';
import { SiderContext } from '../layout/Sider';
@ -17,15 +18,16 @@ export interface MenuItemProps extends Omit<RcMenuItemProps, 'title'> {
title?: React.ReactNode;
}
export default class MenuItem extends React.Component<MenuItemProps> {
static contextType = MenuContext;
context: MenuContextProps;
renderItemChildren(inlineCollapsed: boolean) {
const { prefixCls, firstLevel } = this.context;
const { icon, children } = this.props;
const MenuItem: React.FC<MenuItemProps> = (props) => {
const { className, children, icon, title, danger } = props;
const {
prefixCls,
firstLevel,
direction,
disableMenuItemTitleTooltip,
inlineCollapsed: isInlineCollapsed,
} = React.useContext<MenuContextProps>(MenuContext);
const renderItemChildren = (inlineCollapsed: boolean) => {
const wrapNode = <span className={`${prefixCls}-title-content`}>{children}</span>;
// inline-collapsed.md demo 依赖 span 来隐藏文字,有 icon 属性,则内部包裹一个 span
// ref: https://github.com/ant-design/ant-design/pull/23456
@ -35,25 +37,16 @@ export default class MenuItem extends React.Component<MenuItemProps> {
}
}
return wrapNode;
}
renderItem = ({ siderCollapsed }: SiderContextProps) => {
const { prefixCls, firstLevel, inlineCollapsed, direction, disableMenuItemTitleTooltip } =
this.context;
const { className, children } = this.props;
const { title, icon, danger, ...rest } = this.props;
};
const renderItem = ({ siderCollapsed }: SiderContextProps) => {
let tooltipTitle = title;
if (typeof title === 'undefined') {
tooltipTitle = firstLevel ? children : '';
} else if (title === false) {
tooltipTitle = '';
}
const tooltipProps: TooltipProps = {
title: tooltipTitle,
};
if (!siderCollapsed && !inlineCollapsed) {
const tooltipProps: TooltipProps = { title: tooltipTitle };
if (!siderCollapsed && !isInlineCollapsed) {
tooltipProps.title = null;
// Reset `open` to fix control mode tooltip display not correct
// ref: https://github.com/ant-design/ant-design/issues/16742
@ -63,7 +56,7 @@ export default class MenuItem extends React.Component<MenuItemProps> {
let returnNode = (
<Item
{...rest}
{...omit(props, ['title', 'icon', 'danger'])}
className={classNames(
{
[`${prefixCls}-item-danger`]: danger,
@ -79,7 +72,7 @@ export default class MenuItem extends React.Component<MenuItemProps> {
`${prefixCls}-item-icon`,
),
})}
{this.renderItemChildren(inlineCollapsed)}
{renderItemChildren(isInlineCollapsed)}
</Item>
);
@ -97,8 +90,7 @@ export default class MenuItem extends React.Component<MenuItemProps> {
return returnNode;
};
return <SiderContext.Consumer>{renderItem}</SiderContext.Consumer>;
};
render() {
return <SiderContext.Consumer>{this.renderItem}</SiderContext.Consumer>;
}
}
export default MenuItem;

View File

@ -15,17 +15,14 @@ export interface OverrideContextProps {
const OverrideContext = React.createContext<OverrideContextProps | null>(null);
/** @internal Only used for Dropdown component. Do not use this in your production. */
export const OverrideProvider = ({
children,
...restProps
}: OverrideContextProps & { children: React.ReactNode }) => {
export const OverrideProvider: React.FC<OverrideContextProps & { children: React.ReactNode }> = (
props,
) => {
const { children, ...restProps } = props;
const override = React.useContext(OverrideContext);
const context = React.useMemo(
() => ({
...override,
...restProps,
}),
const context = React.useMemo<OverrideContextProps>(
() => ({ ...override, ...restProps }),
[
override,
restProps.prefixCls,

View File

@ -3,7 +3,7 @@ import { SubMenu as RcSubMenu, useFullPath } from 'rc-menu';
import omit from 'rc-util/lib/omit';
import * as React from 'react';
import { cloneElement, isValidElement } from '../_util/reactNode';
import type { MenuTheme } from './MenuContext';
import type { MenuContextProps, MenuTheme } from './MenuContext';
import MenuContext from './MenuContext';
interface TitleEventEntity {
@ -27,7 +27,7 @@ export interface SubMenuProps {
theme?: MenuTheme;
}
function SubMenu(props: SubMenuProps) {
const SubMenu: React.FC<SubMenuProps> = (props) => {
const { popupClassName, icon, title, theme: customTheme } = props;
const context = React.useContext(MenuContext);
const { prefixCls, inlineCollapsed, theme: contextTheme, mode } = context;
@ -60,11 +60,8 @@ function SubMenu(props: SubMenuProps) {
);
}
const contextValue = React.useMemo(
() => ({
...context,
firstLevel: false,
}),
const contextValue = React.useMemo<MenuContextProps>(
() => ({ ...context, firstLevel: false }),
[context],
);
@ -84,6 +81,6 @@ function SubMenu(props: SubMenuProps) {
/>
</MenuContext.Provider>
);
}
};
export default SubMenu;

View File

@ -22,9 +22,9 @@ export type MenuRef = {
type CompoundedComponent = React.ForwardRefExoticComponent<
MenuProps & React.RefAttributes<MenuRef>
> & {
Divider: typeof MenuDivider;
Item: typeof Item;
SubMenu: typeof SubMenu;
Divider: typeof MenuDivider;
ItemGroup: typeof ItemGroup;
};
@ -33,18 +33,17 @@ const Menu = forwardRef<MenuRef, MenuProps>((props, ref) => {
const context = React.useContext(SiderContext);
useImperativeHandle(ref, () => ({
menu: menuRef.current,
focus: (options) => {
menuRef.current?.focus(options);
},
menu: menuRef.current,
}));
return <InternalMenu ref={menuRef} {...props} {...context} />;
}) as CompoundedComponent;
Menu.Divider = MenuDivider;
Menu.Item = Item;
Menu.SubMenu = SubMenu;
Menu.Divider = MenuDivider;
Menu.ItemGroup = ItemGroup;
export default Menu;

View File

@ -16,7 +16,7 @@ import OverrideContext from './OverrideContext';
import useItems from './hooks/useItems';
import type { ItemType } from './hooks/useItems';
import MenuContext from './MenuContext';
import type { MenuTheme } from './MenuContext';
import type { MenuTheme, MenuContextProps } from './MenuContext';
export interface MenuProps extends Omit<RcMenuProps, 'items'> {
theme?: MenuTheme;
@ -131,7 +131,7 @@ const InternalMenu = forwardRef<RcMenuRef, InternalMenuProps>((props, ref) => {
}
// ======================== Context ==========================
const contextValue = React.useMemo(
const contextValue = React.useMemo<MenuContextProps>(
() => ({
prefixCls,
inlineCollapsed: mergedInlineCollapsed || false,

View File

@ -0,0 +1,403 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/qrcode/demo/Popover.tsx extend context correctly 1`] = `
Array [
<img
alt="icon"
height="100"
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
width="100"
/>,
<div>
<div
class="ant-popover"
style="opacity:0"
>
<div
class="ant-popover-content"
>
<div
class="ant-popover-arrow"
>
<span
class="ant-popover-arrow-content"
/>
</div>
<div
class="ant-popover-inner"
role="tooltip"
style="padding:0"
>
<div
class="ant-popover-inner-content"
>
<div
class="ant-qrcode ant-qrcode-borderless"
style="width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
</div>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders ./components/qrcode/demo/base.tsx extend context correctly 1`] = `
<div
class="ant-qrcode"
style="width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
`;
exports[`renders ./components/qrcode/demo/customColor.tsx extend context correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<div
class="ant-qrcode"
style="margin-bottom:16px;background-color:#f5f5f5;width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
</div>
<div
class="ant-space-item"
>
<div
class="ant-qrcode"
style="margin-bottom:16px;background-color:#f5f5f5;width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
</div>
</div>
`;
exports[`renders ./components/qrcode/demo/customSize.tsx extend context correctly 1`] = `
Array [
<div
class="ant-btn-group"
style="margin-bottom:16px"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span
aria-label="minus"
class="anticon anticon-minus"
role="img"
>
<svg
aria-hidden="true"
data-icon="minus"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M872 474H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h720c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
<span>
Smaller
</span>
</button>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span
aria-label="plus"
class="anticon anticon-plus"
role="img"
>
<svg
aria-hidden="true"
data-icon="plus"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<defs>
<style />
</defs>
<path
d="M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z"
/>
<path
d="M176 474h672q8 0 8 8v60q0 8-8 8H176q-8 0-8-8v-60q0-8 8-8z"
/>
</svg>
</span>
<span>
Larger
</span>
</button>
</div>,
<div
class="ant-qrcode"
style="width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
<img
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
style="display:none"
/>
</div>,
]
`;
exports[`renders ./components/qrcode/demo/download.tsx extend context correctly 1`] = `
<div
id="myqrcode"
>
<div
class="ant-qrcode"
style="margin-bottom:16px;width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Download
</span>
</button>
</div>
`;
exports[`renders ./components/qrcode/demo/errorlevel.tsx extend context correctly 1`] = `
Array [
<div
class="ant-qrcode"
style="margin-bottom:16px;width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>,
<div
class="ant-segmented"
>
<div
class="ant-segmented-group"
>
<label
class="ant-segmented-item ant-segmented-item-selected"
>
<input
checked=""
class="ant-segmented-item-input"
type="radio"
/>
<div
class="ant-segmented-item-label"
title="L"
>
L
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
type="radio"
/>
<div
class="ant-segmented-item-label"
title="M"
>
M
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
type="radio"
/>
<div
class="ant-segmented-item-label"
title="Q"
>
Q
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
type="radio"
/>
<div
class="ant-segmented-item-label"
title="H"
>
H
</div>
</label>
</div>
</div>,
]
`;
exports[`renders ./components/qrcode/demo/icon.tsx extend context correctly 1`] = `
<div
class="ant-qrcode"
style="width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
<img
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
style="display:none"
/>
</div>
`;
exports[`renders ./components/qrcode/demo/status.tsx extend context correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center"
style="flex-wrap:wrap;margin-bottom:-8px"
>
<div
class="ant-space-item"
style="margin-right:8px;padding-bottom:8px"
>
<div
class="ant-qrcode"
style="width:160px;height:160px"
>
<div
class="ant-qrcode-mask"
>
<div
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</div>
</div>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
</div>
<div
class="ant-space-item"
style="padding-bottom:8px"
>
<div
class="ant-qrcode"
style="width:160px;height:160px"
>
<div
class="ant-qrcode-mask"
>
<p>
QRCode is expired
</p>
<button
class="ant-btn ant-btn-link"
type="button"
>
<span
aria-label="reload"
class="anticon anticon-reload"
role="img"
>
<svg
aria-hidden="true"
data-icon="reload"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.1 209.3l-56.4 44.1C775.8 155.1 656.2 92 521.9 92 290 92 102.3 279.5 102 511.5 101.7 743.7 289.8 932 521.9 932c181.3 0 335.8-115 394.6-276.1 1.5-4.2-.7-8.9-4.9-10.3l-56.7-19.5a8 8 0 00-10.1 4.8c-1.8 5-3.8 10-5.9 14.9-17.3 41-42.1 77.8-73.7 109.4A344.77 344.77 0 01655.9 829c-42.3 17.9-87.4 27-133.8 27-46.5 0-91.5-9.1-133.8-27A341.5 341.5 0 01279 755.2a342.16 342.16 0 01-73.7-109.4c-17.9-42.4-27-87.4-27-133.9s9.1-91.5 27-133.9c17.3-41 42.1-77.8 73.7-109.4 31.6-31.6 68.4-56.4 109.3-73.8 42.3-17.9 87.4-27 133.8-27 46.5 0 91.5 9.1 133.8 27a341.5 341.5 0 01109.3 73.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 003 14.1l175.6 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c-.1-6.6-7.8-10.3-13-6.2z"
/>
</svg>
</span>
<span>
click refresh
</span>
</button>
</div>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,363 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/qrcode/demo/Popover.tsx correctly 1`] = `
<img
alt="icon"
height="100"
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
width="100"
/>
`;
exports[`renders ./components/qrcode/demo/base.tsx correctly 1`] = `
<div
class="ant-qrcode"
style="width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
`;
exports[`renders ./components/qrcode/demo/customColor.tsx correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<div
class="ant-qrcode"
style="margin-bottom:16px;background-color:#f5f5f5;width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
</div>
<div
class="ant-space-item"
>
<div
class="ant-qrcode"
style="margin-bottom:16px;background-color:#f5f5f5;width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
</div>
</div>
`;
exports[`renders ./components/qrcode/demo/customSize.tsx correctly 1`] = `
Array [
<div
class="ant-btn-group"
style="margin-bottom:16px"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span
aria-label="minus"
class="anticon anticon-minus"
role="img"
>
<svg
aria-hidden="true"
data-icon="minus"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M872 474H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h720c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
<span>
Smaller
</span>
</button>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span
aria-label="plus"
class="anticon anticon-plus"
role="img"
>
<svg
aria-hidden="true"
data-icon="plus"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<defs>
<style />
</defs>
<path
d="M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z"
/>
<path
d="M176 474h672q8 0 8 8v60q0 8-8 8H176q-8 0-8-8v-60q0-8 8-8z"
/>
</svg>
</span>
<span>
Larger
</span>
</button>
</div>,
<div
class="ant-qrcode"
style="width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
<img
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
style="display:none"
/>
</div>,
]
`;
exports[`renders ./components/qrcode/demo/download.tsx correctly 1`] = `
<div
id="myqrcode"
>
<div
class="ant-qrcode"
style="margin-bottom:16px;width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Download
</span>
</button>
</div>
`;
exports[`renders ./components/qrcode/demo/errorlevel.tsx correctly 1`] = `
Array [
<div
class="ant-qrcode"
style="margin-bottom:16px;width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>,
<div
class="ant-segmented"
>
<div
class="ant-segmented-group"
>
<label
class="ant-segmented-item ant-segmented-item-selected"
>
<input
checked=""
class="ant-segmented-item-input"
type="radio"
/>
<div
class="ant-segmented-item-label"
title="L"
>
L
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
type="radio"
/>
<div
class="ant-segmented-item-label"
title="M"
>
M
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
type="radio"
/>
<div
class="ant-segmented-item-label"
title="Q"
>
Q
</div>
</label>
<label
class="ant-segmented-item"
>
<input
class="ant-segmented-item-input"
type="radio"
/>
<div
class="ant-segmented-item-label"
title="H"
>
H
</div>
</label>
</div>
</div>,
]
`;
exports[`renders ./components/qrcode/demo/icon.tsx correctly 1`] = `
<div
class="ant-qrcode"
style="width:160px;height:160px"
>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
<img
src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
style="display:none"
/>
</div>
`;
exports[`renders ./components/qrcode/demo/status.tsx correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center"
style="flex-wrap:wrap;margin-bottom:-8px"
>
<div
class="ant-space-item"
style="margin-right:8px;padding-bottom:8px"
>
<div
class="ant-qrcode"
style="width:160px;height:160px"
>
<div
class="ant-qrcode-mask"
>
<div
aria-busy="true"
aria-live="polite"
class="ant-spin ant-spin-spinning"
>
<span
class="ant-spin-dot ant-spin-dot-spin"
>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
<i
class="ant-spin-dot-item"
/>
</span>
</div>
</div>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
</div>
<div
class="ant-space-item"
style="padding-bottom:8px"
>
<div
class="ant-qrcode"
style="width:160px;height:160px"
>
<div
class="ant-qrcode-mask"
>
<p>
QRCode is expired
</p>
<button
class="ant-btn ant-btn-link"
type="button"
>
<span
aria-label="reload"
class="anticon anticon-reload"
role="img"
>
<svg
aria-hidden="true"
data-icon="reload"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.1 209.3l-56.4 44.1C775.8 155.1 656.2 92 521.9 92 290 92 102.3 279.5 102 511.5 101.7 743.7 289.8 932 521.9 932c181.3 0 335.8-115 394.6-276.1 1.5-4.2-.7-8.9-4.9-10.3l-56.7-19.5a8 8 0 00-10.1 4.8c-1.8 5-3.8 10-5.9 14.9-17.3 41-42.1 77.8-73.7 109.4A344.77 344.77 0 01655.9 829c-42.3 17.9-87.4 27-133.8 27-46.5 0-91.5-9.1-133.8-27A341.5 341.5 0 01279 755.2a342.16 342.16 0 01-73.7-109.4c-17.9-42.4-27-87.4-27-133.9s9.1-91.5 27-133.9c17.3-41 42.1-77.8 73.7-109.4 31.6-31.6 68.4-56.4 109.3-73.8 42.3-17.9 87.4-27 133.8-27 46.5 0 91.5 9.1 133.8 27a341.5 341.5 0 01109.3 73.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 003 14.1l175.6 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c-.1-6.6-7.8-10.3-13-6.2z"
/>
</svg>
</span>
<span>
click refresh
</span>
</button>
</div>
<canvas
height="134"
style="height:134px;width:134px"
width="134"
/>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QRCode test rtl render component should be rendered correctly in RTL direction 1`] = `null`;
exports[`QRCode test should correct render 1`] = `
<div>
<div
class="ant-qrcode"
style="width: 160px; height: 160px;"
>
<canvas
height="134"
style="height: 134px; width: 134px;"
width="134"
/>
</div>
</div>
`;
exports[`QRCode test should render \`null\` and console Error when value not exist 1`] = `null`;

View File

@ -0,0 +1,3 @@
import { extendTest } from '../../../tests/shared/demoTest';
extendTest('qrcode');

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('qrcode');

View File

@ -0,0 +1,5 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('QRCode image', () => {
imageDemoTest('qrcode');
});

View File

@ -0,0 +1,82 @@
import React, { useState } from 'react';
import QRCode from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render } from '../../../tests/utils';
import type { QRCodeProps } from '../interface';
describe('QRCode test', () => {
mountTest(QRCode);
rtlTest(QRCode);
it('should correct render', () => {
const { container } = render(<QRCode value="test" />);
expect(
container
?.querySelector<HTMLDivElement>('.ant-qrcode')
?.querySelector<HTMLCanvasElement>('canvas'),
).toBeTruthy();
expect(container).toMatchSnapshot();
});
it('should render `null` and console Error when value not exist', () => {
const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const { container } = render(<QRCode value={undefined as unknown as string} />);
expect(container.firstChild).toBe(null);
expect(container.firstChild).toMatchSnapshot();
expect(errSpy).toHaveBeenCalledWith('Warning: [antd: QRCode] need to receive `value` props');
errSpy.mockRestore();
});
it('support custom icon', () => {
const { container } = render(<QRCode value="test" icon="test" />);
expect(
container
?.querySelector<HTMLDivElement>('.ant-qrcode')
?.querySelector<HTMLImageElement>('img'),
).toBeTruthy();
});
it('support custom size', () => {
const { container } = render(<QRCode value="test" size={100} />);
const wapper = container.querySelector<HTMLDivElement>('.ant-qrcode');
expect(wapper?.style?.width).toBe('100px');
expect(wapper?.style?.height).toBe('100px');
});
it('support refresh', () => {
const refresh = jest.fn();
const { container } = render(<QRCode value="test" status="expired" onRefresh={refresh} />);
fireEvent.click(
container
?.querySelector<HTMLDivElement>('.ant-qrcode')
?.querySelector<HTMLButtonElement>('button.ant-btn-link')!,
);
expect(refresh).toHaveBeenCalled();
});
it('support loading', () => {
const Demo: React.FC = () => {
const [status, setStatus] = useState<QRCodeProps['status']>('active');
return (
<>
<QRCode value="test" status={status} />
<button type="button" onClick={() => setStatus('loading')}>
set loading
</button>
</>
);
};
const { container } = render(<Demo />);
expect(container.querySelector<HTMLDivElement>('.ant-spin-spinning')).toBeFalsy();
fireEvent.click(container?.querySelector<HTMLButtonElement>('button')!);
expect(container.querySelector<HTMLDivElement>('.ant-spin-spinning')).toBeTruthy();
});
it('support bordered', () => {
const { container } = render(<QRCode value="test" bordered={false} />);
expect(container?.querySelector<HTMLDivElement>('.ant-qrcode')).toHaveClass(
'ant-qrcode-borderless',
);
});
});

View File

@ -0,0 +1,7 @@
## zh-CN
带气泡卡片的例子。
## en-US
With Popover.

View File

@ -0,0 +1,12 @@
import React from 'react';
import { QRCode, Popover } from 'antd';
const src = 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg';
const App: React.FC = () => (
<Popover overlayInnerStyle={{ padding: 0 }} content={<QRCode value={src} bordered={false} />}>
<img width={100} height={100} src={src} alt="icon" />
</Popover>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
基本用法。
## en-US
Basic Usage.

View File

@ -0,0 +1,6 @@
import React from 'react';
import { QRCode } from 'antd';
const App: React.FC = () => <QRCode value="https://ant.design/" />;
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
通过设置 `color` 自定义二维码颜色,通过设置 `style` 自定义背景颜色。
## en-US
Custom Color.

View File

@ -0,0 +1,24 @@
import React from 'react';
import { QRCode, Space, theme } from 'antd';
const { useToken } = theme;
const App: React.FC = () => {
const { token } = useToken();
return (
<Space>
<QRCode
value="https://ant.design/"
color={token.colorSuccessText}
style={{ marginBottom: 16, backgroundColor: token.colorBgLayout }}
/>
<QRCode
value="https://ant.design/"
color={token.colorInfoText}
style={{ marginBottom: 16, backgroundColor: token.colorBgLayout }}
/>
</Space>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
自定义尺寸
## en-US
Custom Size.

View File

@ -0,0 +1,48 @@
import React, { useState } from 'react';
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
import { QRCode, Button } from 'antd';
const App: React.FC = () => {
const [size, setSize] = useState<number>(160);
const increase = () => {
setSize((prevSize) => {
const newSize = prevSize + 10;
if (newSize > 300) {
return 300;
}
return newSize;
});
};
const decline = () => {
setSize((prevSize) => {
const newSize = prevSize - 10;
if (newSize < 48) {
return 48;
}
return newSize;
});
};
return (
<>
<Button.Group style={{ marginBottom: 16 }}>
<Button onClick={decline} disabled={size <= 48} icon={<MinusOutlined />}>
Smaller
</Button>
<Button onClick={increase} disabled={size >= 300} icon={<PlusOutlined />}>
Larger
</Button>
</Button.Group>
<QRCode
size={size}
iconSize={size / 4}
value="https://ant.design/"
icon="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
/>
</>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
下载二维码的简单实现。
## en-US
A way to download QRCode.

View File

@ -0,0 +1,26 @@
import React from 'react';
import { QRCode, Button } from 'antd';
const downloadQRCode = () => {
const canvas = document.getElementById('myqrcode')?.querySelector<HTMLCanvasElement>('canvas');
if (canvas) {
const url = canvas.toDataURL();
const a = document.createElement('a');
a.download = 'QRCode.png';
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
};
const App: React.FC = () => (
<div id="myqrcode">
<QRCode value="https://ant.design/" style={{ marginBottom: 16 }} />
<Button type="primary" onClick={downloadQRCode}>
Download
</Button>
</div>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
通过设置 errorLevel 调整不同的容错等级。
## en-US
set Error Level.

View File

@ -0,0 +1,19 @@
import React, { useState } from 'react';
import type { QRCodeProps } from 'antd';
import { Segmented, QRCode } from 'antd';
const App: React.FC = () => {
const [level, setLevel] = useState<string | number>('L');
return (
<>
<QRCode
style={{ marginBottom: 16 }}
errorLevel={level as QRCodeProps['errorLevel']}
value="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
/>
<Segmented options={['L', 'M', 'Q', 'H']} value={level} onChange={setLevel} />
</>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
带 Icon 的二维码。
## en-US
QRCode with Icon.

View File

@ -0,0 +1,11 @@
import React from 'react';
import { QRCode } from 'antd';
const App: React.FC = () => (
<QRCode
value="https://ant.design/"
icon="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
/>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
可以通过 `status` 的值控制二维码的状态。
## en-US
The status can be controlled by the value `status`.

View File

@ -0,0 +1,11 @@
import React from 'react';
import { QRCode, Space } from 'antd';
const App: React.FC = () => (
<Space wrap>
<QRCode value="https://ant.design/" status="loading" />
<QRCode value="https://ant.design/" status="expired" onRefresh={() => console.log('refresh')} />
</Space>
);
export default App;

View File

@ -0,0 +1,56 @@
---
category: Components
title: QRCode
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*cJopQrf0ncwAAAAAAAAAAAAADrJ8AQ/original
demo:
cols: 2
group:
title: Data Display
order: 5
---
Components that can convert links into QR codes, and support custom color and logo. Available since `antd@5.1.0`.
<Alert message="If the QR code cannot be scanned for identification, it may be because the link address is too long, which leads to too dense pixels. You can configure the QR code to be larger through `size`, or shorten the link through short link services."></Alert>
## When To Use
Used when the link needs to be converted into a QR Code.
## Examples
<!-- prettier-ignore -->
<code src="./demo/base.tsx">base</code>
<code src="./demo/icon.tsx">With Icon</code>
<code src="./demo/status.tsx">other statu</code>
<code src="./demo/customSize.tsx">Custom Size</code>
<code src="./demo/customColor.tsx">Custom Color</code>
<code src="./demo/download.tsx">Download QRCode</code>
<code src="./demo/errorlevel.tsx">Error Level</code>
<code src="./demo/Popover.tsx">Advanced Usage</code>
## API
> This component is available since `antd@5.1.0`
| Property | Description | Type | Default |
| :-- | :-- | :-- | :-- |
| value | scanned link | string | - |
| icon | include image url (only image link are supported) | string | - |
| size | QRCode size | number | 128 |
| iconSize | include image size | number | 32 |
| color | QRCode Color | string | `#000` |
| bordered | Whether has border style | boolean | `true` |
| errorLevel | Error Code Level | `'L' \| 'M' \| 'Q' \| 'H' ` | `M` |
| status | QRCode statu | `active \| expired \| loading ` | `active` |
| onRefresh | callback | `() => void` | - |
## FAQ
### About QRCode ErrorLevel
The ErrorLevel means that the QR code can be scanned normally after being blocked, and the maximum area that can be blocked is the error correction rate.
Generally, the QR code is divided into 4 error correction levels: Level `L` can correct about `7%` errors, Level `M` can correct about `15%` errors, Level `Q` can correct about `25%` errors, and Level `H` can correct about `30%` errors. When the content encoding of the QR code carries less information, in other words, when the value link is short, set different error correction levels, and the generated image will not change.
> For more information, see the: [https://www.qrcode.com/en/about/error_correction](https://www.qrcode.com/en/about/error_correction.html)

View File

@ -0,0 +1,92 @@
import React, { useMemo, useContext } from 'react';
import { QRCodeCanvas } from 'qrcode.react';
import classNames from 'classnames';
import { ReloadOutlined } from '@ant-design/icons';
import { ConfigContext } from '../config-provider';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import type { ConfigConsumerProps } from '../config-provider';
import type { QRCodeProps, QRPropsCanvas } from './interface';
import warning from '../_util/warning';
import useStyle from './style/index';
import Spin from '../spin';
import Button from '../button';
import theme from '../theme';
const { useToken } = theme;
const QRCode: React.FC<QRCodeProps> = (props) => {
const {
value,
icon = '',
size = 160,
iconSize = 40,
color = '#000',
errorLevel = 'M',
status = 'active',
bordered = true,
onRefresh,
style,
className,
prefixCls: customizePrefixCls,
} = props;
const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
const prefixCls = getPrefixCls('qrcode', customizePrefixCls);
const [wrapSSR, hashId] = useStyle(prefixCls);
const { token } = useToken();
const qrCodeProps = useMemo<QRPropsCanvas>(() => {
const imageSettings: QRCodeProps['imageSettings'] = {
src: icon,
x: undefined,
y: undefined,
height: iconSize,
width: iconSize,
excavate: true,
};
return {
value,
size: size - (token.paddingSM + token.lineWidth) * 2,
level: errorLevel,
bgColor: 'transparent',
fgColor: color,
imageSettings: icon ? imageSettings : undefined,
};
}, [errorLevel, color, icon, iconSize, size, value]);
if (!value) {
if (process.env.NODE_ENV !== 'production') {
warning(false, 'QRCode', 'need to receive `value` props');
}
return null;
}
const cls = classNames(prefixCls, className, hashId, {
[`${prefixCls}-borderless`]: !bordered,
});
return wrapSSR(
<LocaleReceiver componentName="QRCode">
{(locale) => (
<div style={{ ...style, width: size, height: size }} className={cls}>
{status !== 'active' && (
<div className={`${prefixCls}-mask`}>
{status === 'loading' && <Spin />}
{status === 'expired' && (
<>
<p>{locale.expired}</p>
{typeof onRefresh === 'function' && (
<Button type="link" icon={<ReloadOutlined />} onClick={onRefresh}>
{locale.refresh}
</Button>
)}
</>
)}
</div>
)}
<QRCodeCanvas {...qrCodeProps} />
</div>
)}
</LocaleReceiver>,
);
};
export default QRCode;

View File

@ -0,0 +1,57 @@
---
category: Components
subtitle: 二维码
title: QRCode
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*cJopQrf0ncwAAAAAAAAAAAAADrJ8AQ/original
demo:
cols: 2
group:
title: 数据展示
order: 5
---
能够将链接转换生成二维码的组件,支持自定义配色和 Logo 配置,自 `antd@5.1.0` 版本开始提供该组件。
<Alert message="若二维码无法扫码识别,可能是因为链接地址过长导致像素过于密集,可以通过 `size` 配置二维码更大,或者通过短链接服务等方式将链接变短。"></Alert>
## 何时使用
当需要将链接转换成为二维码时使用。
## 代码演示
<!-- prettier-ignore -->
<code src="./demo/base.tsx">基本使用</code>
<code src="./demo/icon.tsx">带 Icon 的例子</code>
<code src="./demo/status.tsx">不同的状态</code>
<code src="./demo/customSize.tsx">自定义尺寸</code>
<code src="./demo/customColor.tsx">自定义颜色</code>
<code src="./demo/download.tsx">下载二维码</code>
<code src="./demo/errorlevel.tsx">纠错比例</code>
<code src="./demo/Popover.tsx">高级用法</code>
## API
> 自 `antd@5.1.0` 版本开始提供该组件。
| 参数 | 说明 | 类型 | 默认值 |
| :-- | :-- | :-- | :-- |
| value | 扫描后的地址 | string | - |
| icon | 二维码中图片的地址(目前只支持图片地址) | string | - |
| size | 二维码大小 | number | 160 |
| iconSize | 二维码中图片的大小 | number | 40 |
| color | 二维码颜色 | string | `#000` |
| bordered | 是否有边框 | boolean | `true` |
| errorLevel | 二维码纠错等级 | `'L' \| 'M' \| 'Q' \| 'H' ` | `M` |
| status | 二维码状态 | `active \| expired \| loading ` | `active` |
| onRefresh | 点击"点击刷新"的回调 | `() => void` | - |
## FAQ
### 关于二维码纠错等级
纠错等级也叫纠错率,就是指二维码可以被遮挡后还能正常扫描,而这个能被遮挡的最大面积就是纠错率。
通常情况下二维码分为 4 个纠错级别:`L级` 可纠正约 `7%` 错误、`M级` 可纠正约 `15%` 错误、`Q级` 可纠正约 `25%` 错误、`H级` 可纠正约`30%` 错误。并不是所有位置都可以缺损,像最明显的三个角上的方框,直接影响初始定位。中间零散的部分是内容编码,可以容忍缺损。当二维码的内容编码携带信息比较少的时候,也就是链接比较短的时候,设置不同的纠错等级,生成的图片不会发生变化。
> 有关更多信息,可参阅相关资料:[https://www.qrcode.com/zh/about/error_correction](https://www.qrcode.com/zh/about/error_correction.html)

View File

@ -0,0 +1,33 @@
import type { CSSProperties } from 'react';
interface ImageSettings {
src: string;
height: number;
width: number;
excavate: boolean;
x?: number;
y?: number;
}
interface QRProps {
value: string;
size?: number;
level?: string;
color?: string;
style?: CSSProperties;
includeMargin?: boolean;
imageSettings?: ImageSettings;
}
export type QRPropsCanvas = QRProps & React.CanvasHTMLAttributes<HTMLCanvasElement>;
export interface QRCodeProps extends QRProps {
className?: string;
prefixCls?: string;
icon?: string;
iconSize?: number;
bordered?: boolean;
errorLevel?: 'L' | 'M' | 'Q' | 'H';
status?: 'active' | 'expired' | 'loading';
onRefresh?: () => void;
}

View File

@ -0,0 +1,59 @@
import type { FullToken, GenerateStyle } from '../../theme/internal';
import { mergeToken, genComponentStyleHook } from '../../theme/internal';
import { resetComponent } from '../../style';
export interface ComponentToken {}
interface QRCodeToken extends FullToken<'QRCode'> {
QRCodeMaskBackgroundColor: string;
}
const genQRCodeStyle: GenerateStyle<QRCodeToken> = (token) => {
const { componentCls } = token;
return {
[componentCls]: {
...resetComponent(token),
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: token.paddingSM,
borderRadius: token.borderRadiusLG,
border: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`,
position: 'relative',
width: '100%',
height: '100%',
overflow: 'hidden',
[`& > ${componentCls}-mask`]: {
position: 'absolute',
insetBlockStart: 0,
insetInlineStart: 0,
zIndex: 10,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%',
color: token.colorText,
lineHeight: token.lineHeight,
background: token.QRCodeMaskBackgroundColor,
textAlign: 'center',
},
'&-icon': {
marginBlockEnd: token.marginXS,
fontSize: token.controlHeight,
},
},
[`${componentCls}-borderless`]: {
borderColor: 'transparent',
},
};
};
export default genComponentStyleHook<'QRCode'>('QRCode', (token) =>
genQRCodeStyle(
mergeToken<QRCodeToken>(token, {
QRCodeMaskBackgroundColor: 'rgba(255, 255, 255, 0.96)',
}),
),
);

View File

@ -103,15 +103,11 @@ export const genLinkStyle = (token: DerivativeToken): CSSObject => ({
},
});
export const genCommonStyle = (token: DerivativeToken, componentPrefixCls: string): CSSObject => {
const { fontFamily, fontSize } = token;
export const genCommonStyle = (componentPrefixCls: string): CSSObject => {
const rootPrefixSelector = `[class^="${componentPrefixCls}"], [class*=" ${componentPrefixCls}"]`;
return {
[rootPrefixSelector]: {
fontFamily,
fontSize,
boxSizing: 'border-box',
'&::before, &::after': {

View File

@ -164,7 +164,7 @@ const columns = [
<!-- prettier-ignore -->
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| --- | --- | --- | --- | --- | --- |
| align | 设置列的对齐方式 | `left` \| `right` \| `center` | `left` | |
| className | 列样式类名 | string | - | |
| colSpan | 表头列合并,设置为 0 时,不渲染 | number | - | |

View File

@ -41,7 +41,7 @@ Ant Design has 3 types of Tabs for different situations.
<!-- prettier-ignore -->
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| --- | --- | --- | --- | --- | --- |
| activeKey | Current TabPane's key | string | - | |
| addIcon | Customize add icon | ReactNode | - | 4.4.0 |
| animated | Whether to change tabs with animation. Only works while `tabPosition="top"` | boolean \| { inkBar: boolean, tabPane: boolean } | { inkBar: true, tabPane: false } | |

View File

@ -46,6 +46,8 @@ import type { ComponentToken as TransferComponentToken } from '../../transfer/st
import type { ComponentToken as TypographyComponentToken } from '../../typography/style';
import type { ComponentToken as UploadComponentToken } from '../../upload/style';
import type { ComponentToken as TourComponentToken } from '../../tour/style';
import type { ComponentToken as QRCodeComponentToken } from '../../qrcode/style';
import type { ComponentToken as AppComponentToken } from '../../app/style';
export interface ComponentTokenMap {
Affix?: {};
@ -108,4 +110,6 @@ export interface ComponentTokenMap {
Space?: SpaceComponentToken;
Progress?: ProgressComponentToken;
Tour?: TourComponentToken;
QRCode?: QRCodeComponentToken;
App?: AppComponentToken;
}

View File

@ -43,14 +43,14 @@ const seedToken: SeedToken = {
// Motion
motionUnit: 0.1,
motionBase: 0,
motionEaseOutCirc: `cubic-bezier(0.08, 0.82, 0.17, 1)`,
motionEaseInOutCirc: `cubic-bezier(0.78, 0.14, 0.15, 0.86)`,
motionEaseOutCirc: 'cubic-bezier(0.08, 0.82, 0.17, 1)',
motionEaseInOutCirc: 'cubic-bezier(0.78, 0.14, 0.15, 0.86)',
motionEaseOut: 'cubic-bezier(0.215, 0.61, 0.355, 1)',
motionEaseInOut: `cubic-bezier(0.645, 0.045, 0.355, 1)`,
motionEaseOutBack: `cubic-bezier(0.12, 0.4, 0.29, 1.46)`,
motionEaseInBack: `cubic-bezier(0.71, -0.46, 0.88, 0.6)`,
motionEaseInQuint: `cubic-bezier(0.645, 0.045, 0.355, 1)`,
motionEaseOutQuint: `cubic-bezier(0.23, 1, 0.32, 1)`,
motionEaseInOut: 'cubic-bezier(0.645, 0.045, 0.355, 1)',
motionEaseOutBack: 'cubic-bezier(0.12, 0.4, 0.29, 1.46)',
motionEaseInBack: 'cubic-bezier(0.71, -0.46, 0.88, 0.6)',
motionEaseInQuint: 'cubic-bezier(0.645, 0.045, 0.355, 1)',
motionEaseOutQuint: 'cubic-bezier(0.23, 1, 0.32, 1)',
// Radius
borderRadius: 6,

View File

@ -175,7 +175,7 @@ export default function formatToken(derivativeToken: RawMergedToken): AliasToken
screenXXLMin: screenXXL,
// FIXME: component box-shadow, should be removed
boxShadowPopoverArrow: `3px 3px 7px rgba(0, 0, 0, 0.1)`,
boxShadowPopoverArrow: '3px 3px 7px rgba(0, 0, 0, 0.1)',
boxShadowCard: `
0 1px 2px -2px ${new TinyColor('rgba(0, 0, 0, 0.16)').toRgbString()},
0 3px 6px 0 ${new TinyColor('rgba(0, 0, 0, 0.12)').toRgbString()},
@ -201,10 +201,10 @@ export default function formatToken(derivativeToken: RawMergedToken): AliasToken
0 -3px 6px -4px rgba(0, 0, 0, 0.12),
0 -9px 28px 8px rgba(0, 0, 0, 0.05)
`,
boxShadowTabsOverflowLeft: `inset 10px 0 8px -8px rgba(0, 0, 0, 0.08)`,
boxShadowTabsOverflowRight: `inset -10px 0 8px -8px rgba(0, 0, 0, 0.08)`,
boxShadowTabsOverflowTop: `inset 0 10px 8px -8px rgba(0, 0, 0, 0.08)`,
boxShadowTabsOverflowBottom: `inset 0 -10px 8px -8px rgba(0, 0, 0, 0.08)`,
boxShadowTabsOverflowLeft: 'inset 10px 0 8px -8px rgba(0, 0, 0, 0.08)',
boxShadowTabsOverflowRight: 'inset -10px 0 8px -8px rgba(0, 0, 0, 0.08)',
boxShadowTabsOverflowTop: 'inset 0 10px 8px -8px rgba(0, 0, 0, 0.08)',
boxShadowTabsOverflowBottom: 'inset 0 -10px 8px -8px rgba(0, 0, 0, 0.08)',
// Override AliasToken
...overrideTokens,

View File

@ -87,7 +87,7 @@ export default function genComponentStyleHook<ComponentName extends OverrideComp
overrideComponentToken: token[component],
});
flush(component, mergedComponentToken);
return [genCommonStyle(token, prefixCls), styleInterpolation];
return [genCommonStyle(prefixCls), styleInterpolation];
},
),
hashId,

View File

@ -16,8 +16,9 @@ function getAlphaColor(frontColor: string, backgroundColor: string): string {
const r = Math.round((fR - bR * (1 - fA)) / fA);
const g = Math.round((fG - bG * (1 - fA)) / fA);
const b = Math.round((fB - bB * (1 - fA)) / fA);
if (isStableColor(r) && isStableColor(g) && isStableColor(b))
if (isStableColor(r) && isStableColor(g) && isStableColor(b)) {
return new TinyColor({ r, g, b, a: Math.round(fA * 100) / 100 }).toRgbString();
}
}
// fallback

View File

@ -31,6 +31,85 @@ exports[`Tour basic 1`] = `
</div>
`;
exports[`Tour custom step pre btn & next btn className & style 1`] = `
<div>
<div
class="ant-tour"
style="z-index: 1090; opacity: 0;"
>
<div
class="ant-tour-content"
>
<div
class="ant-tour-inner"
>
<span
aria-label="close"
class="anticon anticon-close ant-tour-close"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
/>
</svg>
</span>
<div
class="ant-tour-header"
>
<div
class="ant-tour-title"
>
Show in Center
</div>
</div>
<div
class="ant-tour-description"
>
Here is the content of Tour.
</div>
<div
class="ant-tour-footer"
>
<div
class="ant-tour-sliders"
>
<span
class="ant-tour-slider-active ant-tour-slider"
/>
<span
class="ant-tour-slider"
/>
</div>
<div
class="ant-tour-buttons"
>
<button
class="ant-btn ant-btn-primary ant-btn-sm ant-tour-next-btn customClassName"
style="background-color: rgb(69, 69, 255);"
type="button"
>
<span>
Next
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Tour rtl render component should be rendered correctly in RTL direction 1`] = `null`;
exports[`Tour single 1`] = `

View File

@ -259,4 +259,44 @@ describe('Tour', () => {
panelRender({ total: undefined, title: <div>test</div> }, 0, 'default');
}).not.toThrow();
});
it('custom step pre btn & next btn className & style', () => {
const App: React.FC = () => (
<Tour
steps={[
{
title: 'Show in Center',
description: 'Here is the content of Tour.',
nextButtonProps: {
className: 'customClassName',
style: {
backgroundColor: 'rgb(69,69,255)',
},
},
},
{
title: 'With Cover',
description: 'Here is the content of Tour.',
cover: (
<img
alt="tour.png"
src="https://user-images.githubusercontent.com/5378891/197385811-55df8480-7ff4-44bd-9d43-a7dade598d70.png"
/>
),
},
]}
/>
);
const { container } = render(<App />);
// className
expect(
screen.getByRole('button', { name: 'Next' }).className.includes('customClassName'),
).toEqual(true);
// style
expect(screen.getByRole('button', { name: 'Next' }).style.backgroundColor).toEqual(
'rgb(69, 69, 255)',
);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -15,8 +15,18 @@ export interface TourProps extends Omit<RCTourProps, 'renderPanel'> {
export interface TourStepProps extends RCTourStepProps {
cover?: ReactNode; // 展示的图片或者视频
nextButtonProps?: { children?: ReactNode; onClick?: () => void };
prevButtonProps?: { children?: ReactNode; onClick?: () => void };
nextButtonProps?: {
children?: ReactNode;
onClick?: () => void;
className?: string;
style?: React.CSSProperties;
};
prevButtonProps?: {
children?: ReactNode;
onClick?: () => void;
className?: string;
style?: React.CSSProperties;
};
stepRender?: (current: number, total: number) => ReactNode;
type?: 'default' | 'primary'; // default 类型,影响底色与文字颜色
}

View File

@ -103,7 +103,7 @@ const panelRender = (
{...prevButtonProps}
onClick={prevBtnClick}
size="small"
className={`${prefixCls}-prev-btn`}
className={classNames(`${prefixCls}-prev-btn`, prevButtonProps?.className)}
>
{prevButtonProps?.children ?? contextLocale.Previous}
</Button>
@ -113,7 +113,7 @@ const panelRender = (
{...nextButtonProps}
onClick={nextBtnClick}
size="small"
className={`${prefixCls}-next-btn`}
className={classNames(`${prefixCls}-next-btn`, nextButtonProps?.className)}
>
{nextButtonProps?.children ??
(isLastStep ? contextLocale.Finish : contextLocale.Next)}

View File

@ -209,12 +209,12 @@ const genBaseStyle: GenerateStyle<TourToken> = (token) => {
// =========== Limit left and right placement radius ==============
[[
`&-placement-left`,
`&-placement-leftTop`,
`&-placement-leftBottom`,
`&-placement-right`,
`&-placement-rightTop`,
`&-placement-rightBottom`,
'&-placement-left',
'&-placement-leftTop',
'&-placement-leftBottom',
'&-placement-right',
'&-placement-rightTop',
'&-placement-rightBottom',
].join(',')]: {
[`${componentCls}-inner`]: {
borderRadius:

View File

@ -37,7 +37,7 @@ demo:
<!-- prettier-ignore -->
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| --- | --- | --- | --- | --- | --- |
| allowDrop | 是否允许拖拽时放置在该节点 | ({ dropNode, dropPosition }) => boolean | - | |
| autoExpandParent | 是否自动展开父节点 | boolean | false | |
| blockNode | 是否节点占据一行 | boolean | false | |

View File

@ -120,6 +120,7 @@
"copy-to-clipboard": "^3.2.0",
"dayjs": "^1.11.1",
"lodash": "^4.17.21",
"qrcode.react": "^3.1.0",
"rc-cascader": "~3.7.0",
"rc-checkbox": "~2.3.0",
"rc-collapse": "~3.4.2",
@ -321,7 +322,7 @@
"bundlesize": [
{
"path": "./dist/antd.min.js",
"maxSize": "377 kB"
"maxSize": "381 kB"
}
],
"tnpm": {

View File

@ -5,6 +5,7 @@ exports[`antd dist files exports modules correctly 1`] = `
"Affix",
"Alert",
"Anchor",
"App",
"AutoComplete",
"Avatar",
"BackTop",
@ -40,6 +41,7 @@ exports[`antd dist files exports modules correctly 1`] = `
"Popconfirm",
"Popover",
"Progress",
"QRCode",
"Radio",
"Rate",
"Result",

View File

@ -7,6 +7,7 @@ import glob from 'glob';
import { configureToMatchImageSnapshot } from 'jest-image-snapshot';
import MockDate from 'mockdate';
import ReactDOMServer from 'react-dom/server';
import { App } from '../../components';
const toMatchImageSnapshot = configureToMatchImageSnapshot({
customSnapshotsDir: `${process.cwd()}/imageSnapshots`,
@ -35,7 +36,9 @@ export default function imageTest(component: React.ReactElement) {
const cache = createCache();
const html = ReactDOMServer.renderToString(
<StyleProvider cache={cache}>{component}</StyleProvider>,
<App>
<StyleProvider cache={cache}>{component}</StyleProvider>,
</App>,
);
const styleStr = extractStyle(cache);