chore: fix merge

This commit is contained in:
元凛 2022-12-19 20:55:33 +08:00
commit 5912c0d7b2
313 changed files with 11940 additions and 851 deletions

View File

@ -34,7 +34,7 @@ function getTestRegex(libDir) {
module.exports = {
verbose: true,
testEnvironment: 'jsdom',
setupFiles: ['./tests/setup.js'],
setupFiles: ['./tests/setup.js', 'jest-canvas-mock'],
setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'md'],
modulePathIgnorePatterns: ['/_site/'],

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",
@ -65,6 +67,7 @@ exports[`antd exports modules correctly 1`] = `
"TreeSelect",
"Typography",
"Upload",
"Watermark",
"message",
"notification",
"theme",

View File

@ -1,14 +0,0 @@
import ResponsiveObserve, { responsiveMap } from '../responsiveObserve';
describe('Test ResponsiveObserve', () => {
it('test ResponsiveObserve subscribe and unsubscribe', () => {
const { xs } = responsiveMap;
const subscribeFunc = jest.fn();
const token = ResponsiveObserve.subscribe(subscribeFunc);
expect(ResponsiveObserve.matchHandlers[xs].mql.matches).toBeTruthy();
expect(subscribeFunc).toHaveBeenCalledTimes(1);
ResponsiveObserve.unsubscribe(token);
expect(ResponsiveObserve.matchHandlers[xs].mql.removeListener).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,26 @@
import React from 'react';
import { render } from '../../../tests/utils';
import useResponsiveObserve from '../responsiveObserve';
describe('Test ResponsiveObserve', () => {
it('test ResponsiveObserve subscribe and unsubscribe', () => {
let responsiveObserveRef: any;
const Demo = () => {
const responsiveObserve = useResponsiveObserve();
responsiveObserveRef = responsiveObserve;
return null;
};
render(<Demo />);
const subscribeFunc = jest.fn();
const token = responsiveObserveRef.subscribe(subscribeFunc);
expect(
responsiveObserveRef.matchHandlers[responsiveObserveRef.responsiveMap.xs].mql.matches,
).toBeTruthy();
expect(subscribeFunc).toHaveBeenCalledTimes(1);
responsiveObserveRef.unsubscribe(token);
expect(
responsiveObserveRef.matchHandlers[responsiveObserveRef.responsiveMap.xs].mql.removeListener,
).toHaveBeenCalled();
});
});

View File

@ -1,74 +1,85 @@
import React from 'react';
import type { GlobalToken } from '../theme/interface';
import { useToken } from '../theme/internal';
export type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
export type BreakpointMap = Record<Breakpoint, string>;
export type ScreenMap = Partial<Record<Breakpoint, boolean>>;
export type ScreenSizeMap = Partial<Record<Breakpoint, number>>;
export const responsiveArray: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs'];
export const responsiveMap: BreakpointMap = {
xs: '(max-width: 575px)',
sm: '(min-width: 576px)',
md: '(min-width: 768px)',
lg: '(min-width: 992px)',
xl: '(min-width: 1200px)',
xxl: '(min-width: 1600px)',
};
type SubscribeFunc = (screens: ScreenMap) => void;
const subscribers = new Map<Number, SubscribeFunc>();
let subUid = -1;
let screens = {};
const responsiveObserve = {
matchHandlers: {} as {
[prop: string]: {
mql: MediaQueryList;
listener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null;
};
},
dispatch(pointMap: ScreenMap) {
screens = pointMap;
subscribers.forEach((func) => func(screens));
return subscribers.size >= 1;
},
subscribe(func: SubscribeFunc): number {
if (!subscribers.size) this.register();
subUid += 1;
subscribers.set(subUid, func);
func(screens);
return subUid;
},
unsubscribe(token: number) {
subscribers.delete(token);
if (!subscribers.size) this.unregister();
},
unregister() {
Object.keys(responsiveMap).forEach((screen: Breakpoint) => {
const matchMediaQuery = responsiveMap[screen];
const handler = this.matchHandlers[matchMediaQuery];
handler?.mql.removeListener(handler?.listener);
});
subscribers.clear();
},
register() {
Object.keys(responsiveMap).forEach((screen: Breakpoint) => {
const matchMediaQuery = responsiveMap[screen];
const listener = ({ matches }: { matches: boolean }) => {
this.dispatch({
...screens,
[screen]: matches,
const getResponsiveMap = (token: GlobalToken): BreakpointMap => ({
xs: `(max-width: ${token.screenXSMax}px)`,
sm: `(min-width: ${token.screenSM}px)`,
md: `(min-width: ${token.screenMD}px)`,
lg: `(min-width: ${token.screenLG}px)`,
xl: `(min-width: ${token.screenXL}px)`,
xxl: `(min-width: ${token.screenXXL}px)`,
});
export default function useResponsiveObserver() {
const [, token] = useToken();
const responsiveMap: BreakpointMap = getResponsiveMap(token);
// To avoid repeat create instance, we add `useMemo` here.
return React.useMemo(() => {
const subscribers = new Map<Number, SubscribeFunc>();
let subUid = -1;
let screens = {};
return {
matchHandlers: {} as {
[prop: string]: {
mql: MediaQueryList;
listener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null;
};
},
dispatch(pointMap: ScreenMap) {
screens = pointMap;
subscribers.forEach((func) => func(screens));
return subscribers.size >= 1;
},
subscribe(func: SubscribeFunc): number {
if (!subscribers.size) this.register();
subUid += 1;
subscribers.set(subUid, func);
func(screens);
return subUid;
},
unsubscribe(paramToken: number) {
subscribers.delete(paramToken);
if (!subscribers.size) this.unregister();
},
unregister() {
Object.keys(responsiveMap).forEach((screen: Breakpoint) => {
const matchMediaQuery = responsiveMap[screen];
const handler = this.matchHandlers[matchMediaQuery];
handler?.mql.removeListener(handler?.listener);
});
};
const mql = window.matchMedia(matchMediaQuery);
mql.addListener(listener);
this.matchHandlers[matchMediaQuery] = {
mql,
listener,
};
subscribers.clear();
},
register() {
Object.keys(responsiveMap).forEach((screen: Breakpoint) => {
const matchMediaQuery = responsiveMap[screen];
const listener = ({ matches }: { matches: boolean }) => {
this.dispatch({
...screens,
[screen]: matches,
});
};
const mql = window.matchMedia(matchMediaQuery);
mql.addListener(listener);
this.matchHandlers[matchMediaQuery] = {
mql,
listener,
};
listener(mql);
});
},
};
export default responsiveObserve;
listener(mql);
});
},
responsiveMap,
};
}, [token]);
}

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

@ -156,6 +156,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

@ -156,6 +156,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,8 +1,6 @@
import React from 'react';
import { Anchor, Row, Col } from 'antd';
const { Link } = Anchor;
const App: React.FC = () => (
<Row>
<Col span={16}>
@ -11,11 +9,25 @@ const App: React.FC = () => (
<div id="part-3" style={{ height: '100vh', background: 'rgba(0,0,255,0.02)' }} />
</Col>
<Col span={8}>
<Anchor>
<Link href="#part-1" title="Part 1" />
<Link href="#part-2" title="Part 2" />
<Link href="#part-3" title="Part 3" />
</Anchor>
<Anchor
items={[
{
key: 'part-1',
href: '#part-1',
title: 'Part 1',
},
{
key: 'part-2',
href: '#part-2',
title: 'Part 2',
},
{
key: 'part-3',
href: '#part-3',
title: 'Part 3',
},
]}
/>
</Col>
</Row>
);

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, Row, Col } from 'antd';
const { Link } = Anchor;
const App: React.FC = () => {
const topRef = React.useRef<HTMLDivElement>(null);
const [targetOffset, setTargetOffset] = useState<number | undefined>(undefined);
@ -29,11 +27,26 @@ const App: React.FC = () => {
</div>
</Col>
<Col span={6}>
<Anchor targetOffset={targetOffset}>
<Link href="#part-1" title="Part 1" />
<Link href="#part-2" title="Part 2" />
<Link href="#part-3" title="Part 3" />
</Anchor>
<Anchor
targetOffset={targetOffset}
items={[
{
key: 'part-1',
href: '#part-1',
title: 'Part 1',
},
{
key: 'part-2',
href: '#part-2',
title: 'Part 2',
},
{
key: 'part-3',
href: '#part-3',
title: 'Part 3',
},
]}
/>
</Col>
</Row>

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" iframe="200">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" iframe="200">设置锚点滚动偏移量</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,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/app/demo/basic.tsx extend context correctly 1`] = `
<div
class="ant-app"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open message
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open modal
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open notification
</span>
</button>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/app/demo/basic.tsx correctly 1`] = `
<div
class="ant-app"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open message
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open modal
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Open notification
</span>
</button>
</div>
</div>
</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: {},
notification: {},
modal: {},
} as useAppProps);
export default AppContext;

View File

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

View File

@ -0,0 +1,47 @@
import React from 'react';
import { App, Button, Space } from 'antd';
// Sub page
const MyPage = () => {
const { message, modal, notification } = App.useApp();
const showMessage = () => {
message.success('Success!');
};
const showModal = () => {
modal.warning({
title: 'This is a warning message',
content: 'some messages...some messages...',
});
};
const showNotification = () => {
notification.info({
message: `Notification topLeft`,
description: 'Hello, Ant Design!!',
placement: 'topLeft',
});
};
return (
<Space>
<Button type="primary" onClick={showMessage}>
Open message
</Button>
<Button type="primary" onClick={showModal}>
Open modal
</Button>
<Button type="primary" onClick={showNotification}>
Open notification
</Button>
</Space>
);
};
// Entry component
export default () => (
<App>
<MyPage />
</App>
);

View File

@ -0,0 +1,119 @@
---
category: Components
group: Other
title: App
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*HJz8SZos2wgAAAAAAAAAAAAADrJ8AQ/original
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/basic.tsx">basic</code>
## How to use
### Basic usage
App provides upstream and downstream method calls through `Context`, because useApp needs to be used as a subcomponent, we recommend encapsulating App at the top level in the application.
```tsx
import React from 'react';
import { App } from 'antd';
const MyPage: React.FC = () => {
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: React.FC = () => (
<App>
<MyPage />
</App>
);
export default MyApp;
```
Note: App.useApp must be available under App.
### Sequence with ConfigProvider
The App component can only use the token in the `ConfigProvider`, if you need to use the Token, the ConfigProvider and the App component must appear in pairs.
```tsx
<ConfigProvider theme={{ ... }}>
<App>
...
</App>
</ConfigProvider>
```
### Embedded usage scenarios (if not necessary, try not to do nesting)
```tsx
<App>
<Space>
...
<App>...</App>
</Space>
</App>
```
### Global scene (redux scene)
```tsx
// Entry component
import React, { useEffect } from 'react';
import { App } from 'antd';
import type { MessageInstance } from 'antd/es/message/interface';
import type { NotificationInstance } from 'antd/es/notification/interface';
import type { ModalStaticFunctions } from 'antd/es/modal/confirm';
let message: MessageInstance;
let notification: NotificationInstance;
let modal: Omit<ModalStaticFunctions, 'warn'>;
export default () => {
const staticFunction = App.useApp();
message = staticFunction.message;
modal = staticFunction.modal;
notification = staticFunction.notification;
return null;
};
export { message, notification, modal };
```
```tsx
// sub page
import React from 'react';
import { Button, Space } from 'antd';
import { message, modal, notification } from './store';
export default () => {
const showMessage = () => {
message.success('Success!');
};
return (
<Space>
<Button type="primary" onClick={showMessage}>
Open message
</Button>
</Space>
);
};
```

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

@ -0,0 +1,59 @@
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 = () => React.useContext<useAppProps>(AppContext);
const App: React.FC<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<useAppProps>(
() => ({
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,121 @@
---
category: Components
subtitle: 包裹组件
group: 其他
title: App
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*HJz8SZos2wgAAAAAAAAAAAAADrJ8AQ/original
demo:
cols: 2
---
新的包裹组件,提供重置样式和提供消费上下文的默认环境。
## 何时使用
- 提供可消费 React context 的 `message.xxx`、`Modal.xxx`、`notification.xxx` 的静态方法,可以简化 useMessage 等方法需要手动植入 `contextHolder` 的问题。
- 提供基于 `.ant-app` 的默认重置样式,解决原生元素没有 antd 规范样式的问题。
## 代码演示
<!-- prettier-ignore -->
<code src="./demo/basic.tsx">basic</code>
## 如何使用
### 基础用法
App 组件通过 `Context` 提供上下文方法调用,因而 useApp 需要作为子组件才能使用,我们推荐在应用中顶层包裹 App。
```tsx
import React from 'react';
import { App } from 'antd';
const MyPage: React.FC = () => {
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: React.FC = () => (
<App>
<MyPage />
</App>
);
export default MyApp;
```
注意App.useApp 必须在 App 之下方可使用。
### 与 ConfigProvider 先后顺序
App 组件只能在 `ConfigProvider` 之下才能使用 Design Token 如果需要使用其样式重置能力,则 ConfigProvider 与 App 组件必须成对出现。
```tsx
<ConfigProvider theme={{ ... }}>
<App>
...
</App>
</ConfigProvider>
```
### 内嵌使用场景(如无必要,尽量不做嵌套)
```tsx
<App>
<Space>
...
<App>...</App>
</Space>
</App>
```
### 全局场景redux 场景)
```tsx
// Entry component
import React, { useEffect } from 'react';
import { App } from 'antd';
import type { MessageInstance } from 'antd/es/message/interface';
import type { NotificationInstance } from 'antd/es/notification/interface';
import type { ModalStaticFunctions } from 'antd/es/modal/confirm';
let message: MessageInstance;
let notification: NotificationInstance;
let modal: Omit<ModalStaticFunctions, 'warn'>;
export default () => {
const staticFunction = App.useApp();
message = staticFunction.message;
modal = staticFunction.modal;
notification = staticFunction.notification;
return null;
};
export { message, notification, modal };
```
```tsx
// sub page
import React from 'react';
import { Button, Space } from 'antd';
import { message, modal, notification } from './store';
export default () => {
const showMessage = () => {
message.success('Success!');
};
return (
<Space>
<Button type="primary" onClick={showMessage}>
Open message
</Button>
</Space>
);
};
```

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

@ -10,7 +10,7 @@ import type {
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import * as React from 'react';
import { ConfigContext } from '../config-provider';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import LocaleReceiver from '../locale/LocaleReceiver';
import CalendarHeader from './Header';
import enUS from './locale/en_US';

View File

@ -0,0 +1,3 @@
import euES from '../../date-picker/locale/eu_ES';
export default euES;

View File

@ -7,6 +7,7 @@ import { act, fireEvent, render } from '../../../tests/utils';
import Button from '../../button';
import Input from '../../input';
import Table from '../../table';
import Select from '../../select';
describe('ConfigProvider', () => {
mountTest(() => (
@ -113,6 +114,15 @@ describe('ConfigProvider', () => {
expect(container.querySelector('input')?.autocomplete).toEqual('off');
});
it('select showSearch', () => {
const { container } = render(
<ConfigProvider select={{ showSearch: true }}>
<Select />
</ConfigProvider>,
);
expect(container.querySelectorAll('.ant-select-show-search').length).toBe(1);
});
it('render empty', () => {
let rendered = false;
let cacheRenderEmpty;

View File

@ -2,8 +2,8 @@ import React from 'react';
import { closePicker, openPicker, selectCell } from '../../date-picker/__tests__/utils';
import ConfigProvider from '..';
import DatePicker from '../../date-picker';
import type { Locale } from '../../locale-provider';
import LocaleProvider from '../../locale-provider';
import type { Locale } from '../../locale';
import LocaleProvider from '../../locale';
import enUS from '../../locale/en_US';
import zhCN from '../../locale/zh_CN';
import Modal from '../../modal';

View File

@ -1,7 +1,7 @@
import * as React from 'react';
import type { DerivativeFunc } from '@ant-design/cssinjs';
import type { RequiredMark } from '../form/Form';
import type { Locale } from '../locale-provider';
import type { Locale } from '../locale';
import type { AliasToken, MapToken, OverrideToken, SeedToken } from '../theme/interface';
import type { RenderEmptyHandler } from './defaultRenderEmpty';
import type { SizeType } from './SizeContext';
@ -63,6 +63,9 @@ export interface ConfigConsumerProps {
colon?: boolean;
};
theme?: ThemeConfig;
select?: {
showSearch?: boolean;
};
}
const defaultGetPrefixCls = (suffixCls?: string, customizePrefixCls?: string) => {

View File

@ -57,6 +57,7 @@ Some components use dynamic style to support wave effect. You can config `csp` p
| getTargetContainer | Config Affix, Anchor scroll target container | () => HTMLElement | () => window | 4.2.0 |
| iconPrefixCls | Set icon prefix className | string | `anticon` | 4.11.0 |
| input | Set Input common props | { autoComplete?: string } | - | 4.2.0 |
| select | Set Select common props | { showSearch?: boolean } | - | |
| locale | Language package setting, you can find the packages in [antd/locale](http://unpkg.com/antd/locale/) | object | - | |
| prefixCls | Set prefix className | string | `ant` | |
| renderEmpty | Set empty content of components. Ref [Empty](/components/empty/) | function(componentName: string): ReactNode | - | |

View File

@ -6,9 +6,9 @@ import useMemo from 'rc-util/lib/hooks/useMemo';
import * as React from 'react';
import type { ReactElement } from 'react';
import type { RequiredMark } from '../form/Form';
import type { Locale } from '../locale-provider';
import LocaleProvider, { ANT_MARK } from '../locale-provider';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import type { Locale } from '../locale';
import LocaleProvider, { ANT_MARK } from '../locale';
import LocaleReceiver from '../locale/LocaleReceiver';
import defaultLocale from '../locale/en_US';
import { DesignTokenContext } from '../theme/internal';
import defaultSeedToken from '../theme/themes/seed';
@ -53,6 +53,7 @@ const PASSED_PROPS: Exclude<keyof ConfigConsumerProps, 'rootPrefixCls' | 'getPre
'input',
'pagination',
'form',
'select',
];
export interface ConfigProviderProps {
@ -72,6 +73,9 @@ export interface ConfigProviderProps {
input?: {
autoComplete?: string;
};
select?: {
showSearch?: boolean;
};
pagination?: {
showSizeChanger?: boolean;
};

View File

@ -58,6 +58,7 @@ export default () => (
| getTargetContainer | 配置 Affix、Anchor 滚动监听容器。 | () => HTMLElement | () => window | 4.2.0 |
| iconPrefixCls | 设置图标统一样式前缀 | string | `anticon` | 4.11.0 |
| input | 设置 Input 组件的通用属性 | { autoComplete?: string } | - | 4.2.0 |
| select | 设置 Select 组件的通用属性 | { showSearch?: boolean } | - | |
| locale | 语言包配置,语言包可到 [antd/locale](http://unpkg.com/antd/locale/) 目录下寻找 | object | - | |
| prefixCls | 设置统一样式前缀 | string | `ant` | |
| renderEmpty | 自定义组件空状态。参考 [空状态](/components/empty-cn) | function(componentName: string): ReactNode | - | |

View File

@ -4,9 +4,9 @@ import customParseFormat from 'dayjs/plugin/customParseFormat';
import React from 'react';
import DatePicker from '..';
import ConfigProvider from '../../config-provider';
import type { Locale } from '../../locale-provider';
import LocaleProvider from '../../locale-provider';
import locale from '../../locale-provider/zh_CN';
import type { Locale } from '../../locale';
import LocaleProvider from '../../locale';
import locale from '../../locale/zh_CN';
import jaJP from '../../locale/ja_JP';
import zhTW from '../locale/zh_TW';
import { render } from '../../../tests/utils';

View File

@ -14,7 +14,7 @@ import DisabledContext from '../../config-provider/DisabledContext';
import SizeContext from '../../config-provider/SizeContext';
import { FormItemInputContext } from '../../form/context';
import { useCompactItemContext } from '../../space/Compact';
import LocaleReceiver from '../../locale-provider/LocaleReceiver';
import LocaleReceiver from '../../locale/LocaleReceiver';
import { getMergedStatus, getStatusClassNames } from '../../_util/statusUtils';
import enUS from '../locale/en_US';
import { getRangePlaceholder, transPlacement2DropdownAlign } from '../util';

View File

@ -14,7 +14,7 @@ import { ConfigContext } from '../../config-provider';
import DisabledContext from '../../config-provider/DisabledContext';
import SizeContext from '../../config-provider/SizeContext';
import { FormItemInputContext } from '../../form/context';
import LocaleReceiver from '../../locale-provider/LocaleReceiver';
import LocaleReceiver from '../../locale/LocaleReceiver';
import type { InputStatus } from '../../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../../_util/statusUtils';
import warning from '../../_util/warning';

View File

@ -0,0 +1,20 @@
import CalendarLocale from 'rc-picker/lib/locale/eu_ES';
import TimePickerLocale from '../../time-picker/locale/eu_ES';
import type { PickerLocale } from '../generatePicker';
// Merge into a locale object
const locale: PickerLocale = {
lang: {
placeholder: 'Hautatu data',
rangePlaceholder: ['Hasierako data', 'Amaiera data'],
...CalendarLocale,
},
timePickerLocale: {
...TimePickerLocale,
},
};
// All settings at:
// https://github.com/ant-design/ant-design/blob/master/components/date-picker/locale/example.json
export default locale;

View File

@ -6,7 +6,14 @@ import type { PickerLocale } from '../generatePicker';
const locale: PickerLocale = {
lang: {
placeholder: 'Sélectionner une date',
yearPlaceholder: 'Sélectionner une année',
quarterPlaceholder: 'Sélectionner un trimestre',
monthPlaceholder: 'Sélectionner un mois',
weekPlaceholder: 'Sélectionner une semaine',
rangePlaceholder: ['Date de début', 'Date de fin'],
rangeYearPlaceholder: ['Année de début', 'Année de fin'],
rangeMonthPlaceholder: ['Mois de début', 'Mois de fin'],
rangeWeekPlaceholder: ['Semaine de début', 'Semaine de fin'],
...CalendarLocale,
},
timePickerLocale: {

View File

@ -5,7 +5,7 @@ import * as React from 'react';
import { ConfigContext } from '../config-provider';
import { cloneElement } from '../_util/reactNode';
import type { Breakpoint, ScreenMap } from '../_util/responsiveObserve';
import ResponsiveObserve, { responsiveArray } from '../_util/responsiveObserve';
import useResponsiveObserve, { responsiveArray } from '../_util/responsiveObserve';
import warning from '../_util/warning';
import DescriptionsItem from './Item';
import Row from './Row';
@ -135,10 +135,11 @@ function Descriptions({
const mergedColumn = getColumn(column, screens);
const [wrapSSR, hashId] = useStyle(prefixCls);
const responsiveObserve = useResponsiveObserve();
// Responsive
React.useEffect(() => {
const token = ResponsiveObserve.subscribe((newScreens) => {
const token = responsiveObserve.subscribe((newScreens) => {
if (typeof column !== 'object') {
return;
}
@ -146,7 +147,7 @@ function Descriptions({
});
return () => {
ResponsiveObserve.unsubscribe(token);
responsiveObserve.unsubscribe(token);
};
}, []);

View File

@ -1,7 +1,7 @@
import classNames from 'classnames';
import * as React from 'react';
import { ConfigContext } from '../config-provider';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import LocaleReceiver from '../locale/LocaleReceiver';
import DefaultEmptyImg from './empty';
import SimpleEmptyImg from './simple';

View File

@ -1,4 +1,4 @@
import React, { useRef, memo, useContext } from 'react';
import React, { useRef, memo, useContext, useEffect, useCallback, useMemo } from 'react';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import FileTextOutlined from '@ant-design/icons/FileTextOutlined';
import classNames from 'classnames';
@ -41,23 +41,11 @@ const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
const [open, setOpen] = useMergedState(false, { value: props.open });
const clickAction = useRef<React.HTMLAttributes<HTMLAnchorElement | HTMLButtonElement>>({});
const floatButtonGroupRef = useRef<HTMLDivElement>(null);
const floatButtonRef = useRef<HTMLButtonElement | HTMLAnchorElement>(null);
const hoverAction = useRef<React.HTMLAttributes<HTMLDivElement>>({});
if (trigger === 'click') {
clickAction.current = {
onClick() {
setOpen((prevState) => {
onOpenChange?.(!prevState);
return !prevState;
});
},
};
}
if (trigger === 'hover') {
hoverAction.current = {
const hoverAction = useMemo(() => {
const hoverTypeAction = {
onMouseEnter() {
setOpen(true);
onOpenChange?.(true);
@ -67,11 +55,42 @@ const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
onOpenChange?.(false);
},
};
}
return trigger === 'hover' ? hoverTypeAction : {};
}, [trigger]);
const handleOpenChange = () => {
setOpen((prevState) => {
onOpenChange?.(!prevState);
return !prevState;
});
};
const onClick = useCallback(
(e: MouseEvent) => {
if (floatButtonGroupRef.current!.contains(e.target as Node)) {
if (floatButtonRef.current!.contains(e.target as Node)) {
handleOpenChange();
}
return;
}
setOpen(false);
onOpenChange?.(false);
},
[trigger],
);
useEffect(() => {
if (trigger === 'click') {
document.addEventListener('click', onClick);
return () => {
document.removeEventListener('click', onClick);
};
}
}, [trigger]);
return wrapSSR(
<FloatButtonGroupProvider value={shape}>
<div className={groupCls} style={style} {...hoverAction.current}>
<div ref={floatButtonGroupRef} className={groupCls} style={style} {...hoverAction}>
{trigger && ['click', 'hover'].includes(trigger) ? (
<>
<CSSMotion visible={open} motionName={`${groupPrefixCls}-wrap`}>
@ -80,11 +99,11 @@ const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
)}
</CSSMotion>
<FloatButton
ref={floatButtonRef}
type={type}
shape={shape}
icon={open ? closeIcon : icon}
description={description}
{...clickAction.current}
/>
</>
) : (

View File

@ -58,4 +58,30 @@ describe('FloatButtonGroup', () => {
fireEvent.mouseLeave(container.querySelector('.ant-float-btn-group')!);
expect(onOpenChange).toHaveBeenCalled();
});
it('support click floatButtonGroup not close', () => {
const onOpenChange = jest.fn();
const { container } = render(
<FloatButton.Group trigger="click" onOpenChange={onOpenChange}>
<FloatButton />
<FloatButton />
<FloatButton />
</FloatButton.Group>,
);
fireEvent.click(container.querySelector('.ant-float-btn')!);
fireEvent.click(container.querySelector('.ant-float-btn-group')!);
expect(onOpenChange).toHaveBeenCalledTimes(1);
});
it('support click out auto close', () => {
const onOpenChange = jest.fn();
const { container } = render(
<FloatButton.Group trigger="click" onOpenChange={onOpenChange}>
<FloatButton />
<FloatButton />
<FloatButton />
</FloatButton.Group>,
);
fireEvent.click(container.querySelector('.ant-float-btn')!);
fireEvent.click(container);
expect(onOpenChange).toHaveBeenCalledTimes(2);
});
});

View File

@ -3,7 +3,7 @@ import classNames from 'classnames';
import * as React from 'react';
import type { ColProps } from '../grid/col';
import Col from '../grid/col';
import { useLocaleReceiver } from '../locale-provider/LocaleReceiver';
import { useLocaleReceiver } from '../locale/LocaleReceiver';
import defaultLocale from '../locale/en_US';
import type { TooltipProps } from '../tooltip';
import Tooltip from '../tooltip';

View File

@ -2,9 +2,36 @@ import React from 'react';
import { Col, Row } from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import ResponsiveObserve from '../../_util/responsiveObserve';
import useBreakpoint from '../hooks/useBreakpoint';
import { render, act } from '../../../tests/utils';
import { render } from '../../../tests/utils';
// Mock for `responsiveObserve` to test `unsubscribe` call
jest.mock('../../_util/responsiveObserve', () => {
const modules = jest.requireActual('../../_util/responsiveObserve');
const originHook = modules.default;
const useMockResponsiveObserver = (...args: any[]) => {
const entity = originHook(...args);
if (!entity.unsubscribe.mocked) {
const originUnsubscribe = entity.unsubscribe;
entity.unsubscribe = (...uArgs: any[]) => {
const inst = global as any;
inst.unsubscribeCnt = (inst.unsubscribeCnt || 0) + 1;
originUnsubscribe.call(entity, ...uArgs);
};
entity.unsubscribe.mocked = true;
}
return entity;
};
return {
...modules,
__esModule: true,
default: useMockResponsiveObserver,
};
});
describe('Grid', () => {
mountTest(Row);
@ -13,8 +40,8 @@ describe('Grid', () => {
rtlTest(Row);
rtlTest(Col);
afterEach(() => {
ResponsiveObserve.unregister();
beforeEach(() => {
(global as any).unsubscribeCnt = 0;
});
it('should render Col', () => {
@ -88,13 +115,12 @@ describe('Grid', () => {
expect(asFragment().firstChild).toMatchSnapshot();
});
it('ResponsiveObserve.unsubscribe should be called when unmounted', () => {
const Unmount = jest.spyOn(ResponsiveObserve, 'unsubscribe');
it('useResponsiveObserve.unsubscribe should be called when unmounted', () => {
const { unmount } = render(<Row gutter={{ xs: 20 }} />);
act(() => {
unmount();
});
expect(Unmount).toHaveBeenCalled();
const called: number = (global as any).unsubscribeCnt;
unmount();
expect((global as any).unsubscribeCnt).toEqual(called + 1);
});
it('should work correct when gutter is object', () => {

View File

@ -1,6 +1,6 @@
## zh-CN
参照 Bootstrap 的 [响应式设计](http://getbootstrap.com/css/#grid-media-queries),预设六个响应尺寸:`xs` `sm` `md` `lg` `xl` `xxl`
参照 Bootstrap 的 [响应式设计](http://getbootstrap.com/css/#grid-media-queries),预设六个响应尺寸:`xs` `sm` `md` `lg` `xl` `xxl`
## en-US

View File

@ -1,21 +1,22 @@
import { useEffect, useRef } from 'react';
import useForceUpdate from '../../_util/hooks/useForceUpdate';
import type { ScreenMap } from '../../_util/responsiveObserve';
import ResponsiveObserve from '../../_util/responsiveObserve';
import useResponsiveObserve from '../../_util/responsiveObserve';
function useBreakpoint(refreshOnChange: boolean = true): ScreenMap {
const screensRef = useRef<ScreenMap>({});
const forceUpdate = useForceUpdate();
const responsiveObserve = useResponsiveObserve();
useEffect(() => {
const token = ResponsiveObserve.subscribe((supportScreens) => {
const token = responsiveObserve.subscribe((supportScreens) => {
screensRef.current = supportScreens;
if (refreshOnChange) {
forceUpdate();
}
});
return () => ResponsiveObserve.unsubscribe(token);
return () => responsiveObserve.unsubscribe(token);
}, []);
return screensRef.current;

View File

@ -3,7 +3,7 @@ import * as React from 'react';
import { ConfigContext } from '../config-provider';
import useFlexGapSupport from '../_util/hooks/useFlexGapSupport';
import type { Breakpoint, ScreenMap } from '../_util/responsiveObserve';
import ResponsiveObserve, { responsiveArray } from '../_util/responsiveObserve';
import useResponsiveObserve, { responsiveArray } from '../_util/responsiveObserve';
import RowContext from './RowContext';
import { useRowStyle } from './style';
@ -103,9 +103,11 @@ const Row = React.forwardRef<HTMLDivElement, RowProps>((props, ref) => {
const gutterRef = React.useRef<Gutter | [Gutter, Gutter]>(gutter);
const responsiveObserve = useResponsiveObserve();
// ================================== Effect ==================================
React.useEffect(() => {
const token = ResponsiveObserve.subscribe((screen) => {
const token = responsiveObserve.subscribe((screen) => {
setCurScreens(screen);
const currentGutter = gutterRef.current || 0;
if (
@ -116,7 +118,7 @@ const Row = React.forwardRef<HTMLDivElement, RowProps>((props, ref) => {
setScreens(screen);
}
});
return () => ResponsiveObserve.unsubscribe(token);
return () => responsiveObserve.unsubscribe(token);
}, []);
// ================================== Render ==================================

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,8 @@ 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 Watermark } from './watermark';
export type { WatermarkProps } from './watermark';
export { default as QRCode } from './qrcode';
export type { QRCodeProps, QRPropsCanvas } from './qrcode/interface';
export { default as version } from './version';

View File

@ -1,64 +1,6 @@
import * as React from 'react';
import type { Locale } from '.';
import type { LocaleContextProps } from './context';
import LocaleContext from './context';
import defaultLocaleData from '../locale/en_US';
// locale-provider 文件夹的移除需要修改 @ant-design/tools 和 antd-img-crop
import LocaleReceiver from '../locale/LocaleReceiver';
export type LocaleComponentName = Exclude<keyof Locale, 'locale'>;
export interface LocaleReceiverProps<C extends LocaleComponentName = LocaleComponentName> {
componentName?: C;
defaultLocale?: Locale[C] | (() => Locale[C]);
children: (
locale: NonNullable<Locale[C]>,
localeCode: string,
fullLocale: Locale,
) => React.ReactElement;
}
const LocaleReceiver = <C extends LocaleComponentName = LocaleComponentName>(
props: LocaleReceiverProps<C>,
) => {
const { componentName = 'global' as C, defaultLocale, children } = props;
const antLocale = React.useContext<LocaleContextProps | undefined>(LocaleContext);
const getLocale = React.useMemo<NonNullable<Locale[C]>>(() => {
const locale = defaultLocale || defaultLocaleData[componentName];
const localeFromContext = antLocale?.[componentName] ?? {};
return {
...(locale instanceof Function ? locale() : locale),
...(localeFromContext || {}),
};
}, [componentName, defaultLocale, antLocale]);
const getLocaleCode = React.useMemo<string>(() => {
const localeCode = antLocale && antLocale.locale;
// Had use LocaleProvide but didn't set locale
if (antLocale && antLocale.exist && !localeCode) {
return defaultLocaleData.locale;
}
return localeCode!;
}, [antLocale]);
return children(getLocale, getLocaleCode, antLocale!);
};
export * from '../locale/LocaleReceiver';
export default LocaleReceiver;
export const useLocaleReceiver = <C extends LocaleComponentName = LocaleComponentName>(
componentName: C,
defaultLocale?: Locale[C] | (() => Locale[C]),
): [Locale[C]] => {
const antLocale = React.useContext<LocaleContextProps | undefined>(LocaleContext);
const getLocale = React.useMemo<NonNullable<Locale[C]>>(() => {
const locale = defaultLocale || defaultLocaleData[componentName];
const localeFromContext = antLocale?.[componentName] ?? {};
return {
...(typeof locale === 'function' ? locale() : locale),
...(localeFromContext || {}),
};
}, [componentName, defaultLocale, antLocale]);
return [getLocale];
};

View File

@ -1,7 +1,7 @@
import React, { memo, useContext } from 'react';
import { fireEvent, pureRender } from '../../../tests/utils';
import LocaleProvider from '..';
import LocaleContext from '../context';
import LocaleProvider from '../../locale';
import LocaleContext from '../../locale/context';
let innerCount = 0;
let outerCount = 0;

View File

@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { Modal } from '../..';
import { waitFakeTimer, render, fireEvent } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import zhCN from '../zh_CN';
import zhCN from '../../locale/zh_CN';
const Demo: React.FC<{ type: string }> = ({ type }) => {
useEffect(() => {

View File

@ -68,8 +68,8 @@ import preParsePostFormat from 'dayjs/plugin/preParsePostFormat';
import MockDate from 'mockdate';
import React from 'react';
import { render } from '../../../tests/utils';
import type { Locale } from '..';
import LocaleProvider from '..';
import type { Locale } from '../../locale';
import LocaleProvider from '../../locale';
import {
Calendar,
DatePicker,
@ -82,73 +82,74 @@ import {
Transfer,
} from '../..';
import mountTest from '../../../tests/shared/mountTest';
import arEG from '../ar_EG';
import azAZ from '../az_AZ';
import bgBG from '../bg_BG';
import bnBD from '../bn_BD';
import byBY from '../by_BY';
import caES from '../ca_ES';
import csCZ from '../cs_CZ';
import daDK from '../da_DK';
import deDE from '../de_DE';
import elGR from '../el_GR';
import enGB from '../en_GB';
import enUS from '../en_US';
import esES from '../es_ES';
import etEE from '../et_EE';
import faIR from '../fa_IR';
import fiFI from '../fi_FI';
import frBE from '../fr_BE';
import frCA from '../fr_CA';
import frFR from '../fr_FR';
import gaIE from '../ga_IE';
import glES from '../gl_ES';
import heIL from '../he_IL';
import hiIN from '../hi_IN';
import hrHR from '../hr_HR';
import huHU from '../hu_HU';
import hyAM from '../hy_AM';
import idID from '../id_ID';
import isIS from '../is_IS';
import itIT from '../it_IT';
import jaJP from '../ja_JP';
import kaGE from '../ka_GE';
import kkKZ from '../kk_KZ';
import kmrIQ from '../kmr_IQ';
import kmKH from '../km_KH';
import knIN from '../kn_IN';
import koKR from '../ko_KR';
import kuIQ from '../ku_IQ';
import ltLT from '../lt_LT';
import lvLV from '../lv_LV';
import mkMK from '../mk_MK';
import mlIN from '../ml_IN';
import mnMN from '../mn_MN';
import msMY from '../ms_MY';
import nbNO from '../nb_NO';
import neNP from '../ne_NP';
import nlBE from '../nl_BE';
import nlNL from '../nl_NL';
import plPL from '../pl_PL';
import ptBR from '../pt_BR';
import ptPT from '../pt_PT';
import roRO from '../ro_RO';
import ruRU from '../ru_RU';
import siLK from '../si_LK';
import skSK from '../sk_SK';
import slSI from '../sl_SI';
import srRS from '../sr_RS';
import svSE from '../sv_SE';
import taIN from '../ta_IN';
import thTH from '../th_TH';
import tkTK from '../tk_TK';
import trTR from '../tr_TR';
import ukUA from '../uk_UA';
import urPK from '../ur_PK';
import viVN from '../vi_VN';
import zhCN from '../zh_CN';
import zhHK from '../zh_HK';
import zhTW from '../zh_TW';
import arEG from '../../locale/ar_EG';
import azAZ from '../../locale/az_AZ';
import bgBG from '../../locale/bg_BG';
import bnBD from '../../locale/bn_BD';
import byBY from '../../locale/by_BY';
import caES from '../../locale/ca_ES';
import csCZ from '../../locale/cs_CZ';
import daDK from '../../locale/da_DK';
import deDE from '../../locale/de_DE';
import elGR from '../../locale/el_GR';
import enGB from '../../locale/en_GB';
import enUS from '../../locale/en_US';
import esES from '../../locale/es_ES';
import etEE from '../../locale/et_EE';
import euES from '../../locale/eu_ES';
import faIR from '../../locale/fa_IR';
import fiFI from '../../locale/fi_FI';
import frBE from '../../locale/fr_BE';
import frCA from '../../locale/fr_CA';
import frFR from '../../locale/fr_FR';
import gaIE from '../../locale/ga_IE';
import glES from '../../locale/gl_ES';
import heIL from '../../locale/he_IL';
import hiIN from '../../locale/hi_IN';
import hrHR from '../../locale/hr_HR';
import huHU from '../../locale/hu_HU';
import hyAM from '../../locale/hy_AM';
import idID from '../../locale/id_ID';
import isIS from '../../locale/is_IS';
import itIT from '../../locale/it_IT';
import jaJP from '../../locale/ja_JP';
import kaGE from '../../locale/ka_GE';
import kkKZ from '../../locale/kk_KZ';
import kmrIQ from '../../locale/kmr_IQ';
import kmKH from '../../locale/km_KH';
import knIN from '../../locale/kn_IN';
import koKR from '../../locale/ko_KR';
import kuIQ from '../../locale/ku_IQ';
import ltLT from '../../locale/lt_LT';
import lvLV from '../../locale/lv_LV';
import mkMK from '../../locale/mk_MK';
import mlIN from '../../locale/ml_IN';
import mnMN from '../../locale/mn_MN';
import msMY from '../../locale/ms_MY';
import nbNO from '../../locale/nb_NO';
import neNP from '../../locale/ne_NP';
import nlBE from '../../locale/nl_BE';
import nlNL from '../../locale/nl_NL';
import plPL from '../../locale/pl_PL';
import ptBR from '../../locale/pt_BR';
import ptPT from '../../locale/pt_PT';
import roRO from '../../locale/ro_RO';
import ruRU from '../../locale/ru_RU';
import siLK from '../../locale/si_LK';
import skSK from '../../locale/sk_SK';
import slSI from '../../locale/sl_SI';
import srRS from '../../locale/sr_RS';
import svSE from '../../locale/sv_SE';
import taIN from '../../locale/ta_IN';
import thTH from '../../locale/th_TH';
import tkTK from '../../locale/tk_TK';
import trTR from '../../locale/tr_TR';
import ukUA from '../../locale/uk_UA';
import urPK from '../../locale/ur_PK';
import viVN from '../../locale/vi_VN';
import zhCN from '../../locale/zh_CN';
import zhHK from '../../locale/zh_HK';
import zhTW from '../../locale/zh_TW';
dayjs.extend(preParsePostFormat);
@ -167,6 +168,7 @@ const locales = [
enUS,
esES,
etEE,
euES,
faIR,
fiFI,
frBE,

View File

@ -1,3 +0,0 @@
import locale from '../locale/ar_EG';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/az_AZ';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/bg_BG';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/bn_BD';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/by_BY';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/ca_ES';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/cs_CZ';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/da_DK';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/de_DE';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/el_GR';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/en_GB';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/en_US';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/es_ES';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/et_EE';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/fa_IR';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/fi_FI';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/fr_BE';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/fr_CA';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/fr_FR';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/ga_IE';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/gl_ES';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/he_IL';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/hi_IN';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/hr_HR';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/hu_HU';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/hy_AM';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/id_ID';
export default locale;

View File

@ -0,0 +1,6 @@
// locale-provider 文件夹的移除需要修改 @ant-design/tools 和 antd-img-crop
import locale from '../locale';
export * from '../locale';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/is_IS';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/it_IT';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/ja_JP';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/ka_GE';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/kk_KZ';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/km_KH';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/kmr_IQ';
export default locale;

View File

@ -1,3 +0,0 @@
import locale from '../locale/kn_IN';
export default locale;

Some files were not shown because too many files have changed in this diff Show More