Merge branch 'master' into feature-merge-master

This commit is contained in:
MadCcc 2022-10-17 17:55:50 +08:00
commit 442176627f
51 changed files with 487 additions and 335 deletions

View File

@ -25,7 +25,7 @@ jobs:
uses: actions/checkout@v3
- name: Download commit artifact
uses: dawidd6/action-download-artifact@v2
uses: dawidd6/action-download-artifact@v2.23.0
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
name: commit
@ -35,7 +35,7 @@ jobs:
run: echo "::set-output name=id::$(<commit.txt)"
- name: Download branch artifact
uses: dawidd6/action-download-artifact@v2
uses: dawidd6/action-download-artifact@v2.23.0
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
name: branch
@ -45,7 +45,7 @@ jobs:
run: echo "::set-output name=id::$(<branch.txt)"
- name: Download snapshots artifact
uses: dawidd6/action-download-artifact@v2
uses: dawidd6/action-download-artifact@v2.23.0
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
workflow_conclusion: success

View File

@ -15,6 +15,25 @@ timeline: true
---
## 4.23.6
`2022-10-17`
- Table
- 🐞 Fix Table with sticky header shadow style issue. [#38023](https://github.com/ant-design/ant-design/pull/38023) [@liuycy](https://github.com/liuycy)
- 🐞 Fix Table with `ellipsis` missing `title` attribute. [416c61f](https://github.com/ant-design/ant-design/commit/416c61f)
- 🐞 Fix Breadcrumb not support number `0`. [#38006](https://github.com/ant-design/ant-design/pull/38006) [@li-jia-nan](https://github.com/li-jia-nan)
- Input
- 🐞 Fix Input.TextArea render extra input node when enable `autoSize`. [#38050](https://github.com/ant-design/ant-design/pull/38050)
- 🐞 Fix Input.Password that should not have value prop on input after click toggle icon. [#37900](https://github.com/ant-design/ant-design/pull/37900) [@linxianxi](https://github.com/linxianxi)
- 💄 Fix border style issues for Input.Search in RTL. [#37980](https://github.com/ant-design/ant-design/pull/37980) [@foryuki](https://github.com/foryuki)
- 🐞 Fix AutoComplete warning for unused `dropdownClassName`. [#37974](https://github.com/ant-design/ant-design/pull/37974) [@heiyu4585](https://github.com/heiyu4585)
- 🐞 Fix Typography with ellipsis that the computed `fontSize` style be calculated as empty string in some case. [#37928](https://github.com/ant-design/ant-design/pull/37928) [@zheeeng](https://github.com/zheeeng)
- 🐞 Fix editable Tabs add button missing in edge case. [#37937](https://github.com/ant-design/ant-design/pull/37937)
- 🐞 Fix RangePicker panel blink in some case. [#439](https://github.com/react-component/picker/pull/439)
- 🛠 Refactor Spin with Function Component. [#37969](https://github.com/ant-design/ant-design/pull/37969) [@li-jia-nan](https://github.com/li-jia-nan)
- 🛠 Refactor Statistic.Countdown with Function Component. [#37938](https://github.com/ant-design/ant-design/pull/37938) [@li-jia-nan](https://github.com/li-jia-nan)
## 4.23.5
`2022-10-10`

View File

@ -15,6 +15,25 @@ timeline: true
---
## 4.23.6
`2022-10-17`
- Table
- 🐞 修复 Table 配置固定表头时的阴影样式问题。[#38023](https://github.com/ant-design/ant-design/pull/38023) [@liuycy](https://github.com/liuycy)
- 🐞 修复 Table 配置省略时丢失 `title` 属性问题。[416c61f](https://github.com/ant-design/ant-design/commit/416c61f)
- 🐞 修复 Breadcrumb 不支持数字 `0` 的问题。[#38006](https://github.com/ant-design/ant-design/pull/38006) [@li-jia-nan](https://github.com/li-jia-nan)
- Input
- 🐞 修复 Input.TextArea 配置 `autoSize` 时会额外渲染 input 节点的问题。[#38050](https://github.com/ant-design/ant-design/pull/38050)
- 🐞 修复 Input.Password 在点击隐藏按钮后 input 上会有 value 属性的问题。[#37900](https://github.com/ant-design/ant-design/pull/37900) [@linxianxi](https://github.com/linxianxi)
- 💄 修复 RTL 下 Input.Search 边框样式问题。[#37980](https://github.com/ant-design/ant-design/pull/37980) [@foryuki](https://github.com/foryuki)
- 🐞 修复 AutoComplete 会报未使用的废弃属性 `dropdownClassName` 的问题。[#37974](https://github.com/ant-design/ant-design/pull/37974) [@heiyu4585](https://github.com/heiyu4585)
- 🐞 修复 Typography 省略算法在计算一些元素 fontSize 时为空字符串的情况[#37928](https://github.com/ant-design/ant-design/pull/37928) [@zheeeng](https://github.com/zheeeng)
- 🐞 Fix Tabs 添加按钮在某些边界情况下无法展示的问题。[#37937](https://github.com/ant-design/ant-design/pull/37937)
- 🐞 修复 RangePicker 在某些情况下面板会闪烁的问题。[#439](https://github.com/react-component/picker/pull/439)
- 🛠 重构 Spin 为 Function Component。[#37969](https://github.com/ant-design/ant-design/pull/37969) [@li-jia-nan](https://github.com/li-jia-nan)
- 🛠 重构 Statistic.Countdown 为 Function Component.[#37938](https://github.com/ant-design/ant-design/pull/37938) [@li-jia-nan](https://github.com/li-jia-nan)
## 4.23.5
`2022-10-10`

View File

@ -0,0 +1,23 @@
import React from 'react';
import { isValidElement, cloneElement, isFragment, replaceElement } from '../reactNode';
describe('reactNode test', () => {
it('isValidElement', () => {
expect(isValidElement(null)).toBe(false);
expect(isValidElement(<p>test</p>)).toBe(true);
});
it('isFragment', () => {
expect(isFragment(<p>test</p>)).toBe(false);
expect(isFragment(<>test</>)).toBe(true);
});
it('replaceElement', () => {
const node = <p>test</p>;
expect(replaceElement(null, node)).toBe(node);
expect(replaceElement(node, node)).toStrictEqual(node);
});
it('cloneElement', () => {
const node = <p>test</p>;
expect(cloneElement(null)).toBe(null);
expect(cloneElement(node)).toStrictEqual(node);
});
});

View File

@ -1,9 +1,13 @@
import { sleep } from '../../../tests/utils';
import { waitFakeTimer } from '../../../tests/utils';
import scrollTo from '../scrollTo';
describe('Test ScrollTo function', () => {
let dateNowMock: jest.SpyInstance;
beforeAll(() => {
jest.useFakeTimers();
});
beforeEach(() => {
dateNowMock = jest
.spyOn(Date, 'now')
@ -11,7 +15,12 @@ describe('Test ScrollTo function', () => {
.mockImplementationOnce(() => 1000);
});
afterAll(() => {
jest.useRealTimers();
});
afterEach(() => {
jest.clearAllTimers();
dateNowMock.mockRestore();
});
@ -22,7 +31,7 @@ describe('Test ScrollTo function', () => {
});
scrollTo(1000);
await sleep(20);
await waitFakeTimer();
expect(window.pageYOffset).toBe(1000);
@ -34,7 +43,7 @@ describe('Test ScrollTo function', () => {
scrollTo(1000, {
callback: cbMock,
});
await sleep(20);
await waitFakeTimer();
expect(cbMock).toHaveBeenCalledTimes(1);
});
@ -43,7 +52,7 @@ describe('Test ScrollTo function', () => {
scrollTo(1000, {
getContainer: () => div,
});
await sleep(20);
await waitFakeTimer();
expect(div.scrollTop).toBe(1000);
});
@ -51,7 +60,7 @@ describe('Test ScrollTo function', () => {
scrollTo(1000, {
getContainer: () => document,
});
await sleep(20);
await waitFakeTimer();
expect(document.documentElement.scrollTop).toBe(1000);
});
@ -60,7 +69,7 @@ describe('Test ScrollTo function', () => {
duration: 1100,
getContainer: () => document,
});
await sleep(20);
await waitFakeTimer();
expect(document.documentElement.scrollTop).toBe(1000);
});
});

View File

@ -2,7 +2,7 @@
import KeyCode from 'rc-util/lib/KeyCode';
import raf from 'rc-util/lib/raf';
import React from 'react';
import { sleep, render, fireEvent } from '../../../tests/utils';
import { waitFakeTimer, render, fireEvent } from '../../../tests/utils';
import getDataOrAriaProps from '../getDataOrAriaProps';
import delayRaf from '../raf';
import { isStyleSupport } from '../styleChecker';
@ -14,6 +14,18 @@ import TransButton from '../transButton';
describe('Test utils function', () => {
describe('throttle', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
afterEach(() => {
jest.clearAllTimers();
});
it('throttle function should work', async () => {
const callback = jest.fn();
const throttled = throttleByAnimationFrame(callback);
@ -21,7 +33,7 @@ describe('Test utils function', () => {
throttled();
throttled();
await sleep(20);
await waitFakeTimer();
expect(callback).toHaveBeenCalled();
expect(callback.mock.calls.length).toBe(1);
@ -33,7 +45,7 @@ describe('Test utils function', () => {
throttled();
throttled.cancel();
await sleep(20);
await waitFakeTimer();
expect(callback).not.toHaveBeenCalled();
});
@ -50,7 +62,7 @@ describe('Test utils function', () => {
test.callback();
test.callback();
test.callback();
await sleep(30);
await waitFakeTimer();
expect(callbackFn).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,13 +1,22 @@
import React from 'react';
import mountTest from '../../../tests/shared/mountTest';
import { render, sleep, fireEvent, act } from '../../../tests/utils';
import { render, waitFakeTimer, fireEvent, act } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import Wave from '../wave';
describe('Wave component', () => {
mountTest(Wave);
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
afterEach(() => {
jest.clearAllTimers();
const styles = document.getElementsByTagName('style');
for (let i = 0; i < styles.length; i += 1) {
styles[i].remove();
@ -56,7 +65,7 @@ describe('Wave component', () => {
</Wave>,
);
container.querySelector('button')?.click();
await sleep(0);
await waitFakeTimer();
const styles = (
container.querySelector('button')?.getRootNode() as HTMLButtonElement
).getElementsByTagName('style');
@ -73,7 +82,7 @@ describe('Wave component', () => {
</Wave>,
);
container.querySelector('button')?.click();
await sleep(200);
await waitFakeTimer();
const styles = (
container.querySelector('button')?.getRootNode() as HTMLButtonElement
).getElementsByTagName('style');
@ -89,7 +98,7 @@ describe('Wave component', () => {
</Wave>,
);
container.querySelector('div')?.click();
await sleep(0);
await waitFakeTimer();
const styles = (
container.querySelector('div')?.getRootNode() as HTMLDivElement
).getElementsByTagName('style');
@ -105,7 +114,7 @@ describe('Wave component', () => {
</Wave>,
);
container.querySelector('div')?.click();
await sleep(0);
await waitFakeTimer();
const styles = (
container.querySelector('div')?.getRootNode() as HTMLDivElement
).getElementsByTagName('style');
@ -121,7 +130,7 @@ describe('Wave component', () => {
</Wave>,
);
container.querySelector('div')?.click();
await sleep(0);
await waitFakeTimer();
const styles = (
container.querySelector('div')?.getRootNode() as HTMLDivElement
).getElementsByTagName('style');
@ -139,7 +148,7 @@ describe('Wave component', () => {
</Wave>,
);
container.querySelector('button')?.click();
await sleep(0);
await waitFakeTimer();
const styles = (
container.querySelector('button')?.getRootNode() as HTMLButtonElement
).getElementsByTagName('style');
@ -156,7 +165,7 @@ describe('Wave component', () => {
</ConfigProvider>,
);
container.querySelector('button')?.click();
await sleep(0);
await waitFakeTimer();
const styles = (
container.querySelector('button')?.getRootNode() as HTMLButtonElement
).getElementsByTagName('style');

View File

@ -3,7 +3,7 @@ import * as React from 'react';
export const { isValidElement } = React;
export function isFragment(child: React.ReactElement): boolean {
return child && child.type === React.Fragment;
return child && isValidElement(child) && child.type === React.Fragment;
}
type AnyObject = Record<PropertyKey, any>;

View File

@ -23,8 +23,8 @@ export default function scrollTo(y: number, options: ScrollToOptions = {}) {
const nextScrollTop = easeInOutCubic(time > duration ? duration : time, scrollTop, y, duration);
if (isWindow(container)) {
(container as Window).scrollTo(window.pageXOffset, nextScrollTop);
} else if (container instanceof HTMLDocument || container.constructor.name === 'HTMLDocument') {
(container as HTMLDocument).documentElement.scrollTop = nextScrollTop;
} else if (container instanceof Document || container.constructor.name === 'HTMLDocument') {
(container as Document).documentElement.scrollTop = nextScrollTop;
} else {
(container as HTMLElement).scrollTop = nextScrollTop;
}

View File

@ -3,9 +3,9 @@ import type { InternalAffixClass } from '..';
import Affix from '..';
import accessibilityTest from '../../../tests/shared/accessibilityTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { render, sleep, triggerResize, waitFakeTimer } from '../../../tests/utils';
import { render, triggerResize, waitFakeTimer } from '../../../tests/utils';
import Button from '../../button';
import { getObserverEntities } from '../utils';
import { addObserveTarget, getObserverEntities } from '../utils';
const events: Partial<Record<keyof HTMLElementEventMap, (ev: Partial<Event>) => void>> = {};
@ -66,6 +66,7 @@ describe('Affix Render', () => {
};
beforeEach(() => {
jest.useFakeTimers();
const entities = getObserverEntities();
entities.splice(0, entities.length);
});
@ -81,6 +82,11 @@ describe('Affix Render', () => {
});
});
afterEach(() => {
jest.useRealTimers();
jest.clearAllTimers();
});
afterAll(() => {
domMock.mockRestore();
});
@ -96,12 +102,12 @@ describe('Affix Render', () => {
events.scroll({
type: 'scroll',
});
await sleep(20);
await waitFakeTimer();
};
it('Anchor render perfectly', async () => {
const { container } = render(<AffixMounter />);
await sleep(20);
await waitFakeTimer();
await movePlaceholder(0);
expect(container.querySelector('.ant-affix')).toBeFalsy();
@ -113,10 +119,15 @@ describe('Affix Render', () => {
expect(container.querySelector('.ant-affix')).toBeFalsy();
});
it('Anchor correct render when target is null', async () => {
render(<Affix target={() => null}>test</Affix>);
await waitFakeTimer();
});
it('support offsetBottom', async () => {
const { container } = render(<AffixMounter offsetBottom={0} />);
await sleep(20);
await waitFakeTimer();
await movePlaceholder(300);
expect(container.querySelector('.ant-affix')).toBeTruthy();
@ -132,14 +143,14 @@ describe('Affix Render', () => {
const onChange = jest.fn();
const { container, rerender } = render(<AffixMounter offsetTop={0} onChange={onChange} />);
await sleep(20);
await waitFakeTimer();
await movePlaceholder(-100);
expect(onChange).toHaveBeenLastCalledWith(true);
expect(container.querySelector('.ant-affix')).toHaveStyle({ top: 0 });
rerender(<AffixMounter offsetTop={10} onChange={onChange} />);
await sleep(20);
await waitFakeTimer();
expect(container.querySelector('.ant-affix')).toHaveStyle({ top: `10px` });
});
@ -172,7 +183,6 @@ describe('Affix Render', () => {
expect(affixInstance!.state.status).toBe(0);
expect(affixInstance!.state.affixStyle).toBe(undefined);
expect(affixInstance!.state.placeholderStyle).toBe(undefined);
await sleep(100);
});
it('instance change', async () => {
@ -182,7 +192,7 @@ describe('Affix Render', () => {
const getTarget = () => target;
const { rerender } = render(<Affix target={getTarget}>{null}</Affix>);
await sleep(100);
await waitFakeTimer();
expect(getObserverEntities()).toHaveLength(1);
expect(getObserverEntities()[0].target).toBe(container);
@ -191,6 +201,21 @@ describe('Affix Render', () => {
expect(getObserverEntities()).toHaveLength(1);
expect(getObserverEntities()[0].target).toBe(window);
});
it('check position change before measure', async () => {
const { container } = render(
<>
<Affix offsetTop={10}>
<Button>top</Button>
</Affix>
<Affix offsetBottom={10}>
<Button>bottom</Button>
</Affix>
</>,
);
await waitFakeTimer();
await movePlaceholder(1000);
expect(container.querySelector('.ant-affix')).toBeTruthy();
});
});
describe('updatePosition when size changed', () => {
@ -210,7 +235,7 @@ describe('Affix Render', () => {
},
);
await sleep(20);
await waitFakeTimer();
await movePlaceholder(300);
expect(affixInstance!.state.affixStyle).toBeTruthy();
});
@ -221,8 +246,6 @@ describe('Affix Render', () => {
'.fixed', // outer
].forEach(selector => {
it(`trigger listener when size change: ${selector}`, async () => {
jest.useFakeTimers();
const updateCalled = jest.fn();
const { container } = render(
<AffixMounter offsetBottom={0} onTestUpdatePosition={updateCalled} />,
@ -237,10 +260,13 @@ describe('Affix Render', () => {
await waitFakeTimer();
expect(updateCalled).toHaveBeenCalled();
jest.clearAllTimers();
jest.useRealTimers();
});
});
it('addObserveTarget should not Throw Error when target is null', () => {
expect(() => {
addObserveTarget(null);
}).not.toThrow();
});
});
});

View File

@ -51,8 +51,10 @@ export function getObserverEntities() {
return observerEntities;
}
export function addObserveTarget<T>(target: HTMLElement | Window | null, affix: T): void {
if (!target) return;
export function addObserveTarget<T>(target: HTMLElement | Window | null, affix?: T): void {
if (!target) {
return;
}
let entity: ObserverEntity | undefined = observerEntities.find(item => item.target === target);

View File

@ -1,6 +1,6 @@
import React from 'react';
import Anchor from '..';
import { fireEvent, render, sleep } from '../../../tests/utils';
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
import type { InternalAnchorClass } from '../Anchor';
const { Link } = Anchor;
@ -32,6 +32,7 @@ describe('Anchor Render', () => {
const getClientRectsMock = jest.spyOn(HTMLHeadingElement.prototype, 'getClientRects');
beforeAll(() => {
jest.useFakeTimers();
getBoundingClientRectMock.mockReturnValue({
width: 100,
height: 100,
@ -40,7 +41,12 @@ describe('Anchor Render', () => {
getClientRectsMock.mockReturnValue({ length: 1 } as DOMRectList);
});
afterEach(() => {
jest.clearAllTimers();
});
afterAll(() => {
jest.useRealTimers();
getBoundingClientRectMock.mockRestore();
getClientRectsMock.mockRestore();
});
@ -96,7 +102,7 @@ describe('Anchor Render', () => {
anchorInstance!.handleScrollTo('/#/faq?locale=en#Q1');
expect(anchorInstance!.state.activeLink).toBe('/#/faq?locale=en#Q1');
expect(scrollToSpy).not.toHaveBeenCalled();
await sleep(1000);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenCalled();
});
@ -137,7 +143,7 @@ describe('Anchor Render', () => {
anchorInstance!.handleScrollTo(`##${hash}`);
expect(anchorInstance!.state.activeLink).toBe(`##${hash}`);
const calls = scrollToSpy.mock.calls.length;
await sleep(1000);
await waitFakeTimer();
expect(scrollToSpy.mock.calls.length).toBeGreaterThan(calls);
});
@ -250,7 +256,7 @@ describe('Anchor Render', () => {
);
const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove');
await sleep(1000);
await waitFakeTimer();
rerender(
<Anchor getContainer={getContainerB}>
<Link href={`#${hash}`} title={hash} />
@ -287,7 +293,7 @@ describe('Anchor Render', () => {
const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove');
expect(removeListenerSpy).not.toHaveBeenCalled();
await sleep(1000);
await waitFakeTimer();
rerender(
<Anchor getContainer={getContainerB}>
<Link href={`#${hash1}`} title={hash1} />
@ -354,7 +360,7 @@ describe('Anchor Render', () => {
);
const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove');
expect(removeListenerSpy).not.toHaveBeenCalled();
await sleep(1000);
await waitFakeTimer();
holdContainer.container = document.getElementById(hash2);
rerender(
<Anchor getContainer={getContainer}>
@ -409,21 +415,21 @@ describe('Anchor Render', () => {
);
anchorInstance!.handleScrollTo(`#${hash}`);
await sleep(30);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000);
dateNowMock = dataNowMockFn();
setProps({ offsetTop: 100 });
anchorInstance!.handleScrollTo(`#${hash}`);
await sleep(30);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900);
dateNowMock = dataNowMockFn();
setProps({ targetOffset: 200 });
anchorInstance!.handleScrollTo(`#${hash}`);
await sleep(30);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
dateNowMock.mockRestore();
@ -474,19 +480,19 @@ describe('Anchor Render', () => {
);
anchorInstance!.handleScrollTo(`#${hash}`);
await sleep(30);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000);
dateNowMock = dataNowMockFn();
setProps({ offsetTop: 100 });
anchorInstance!.handleScrollTo(`#${hash}`);
await sleep(30);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900);
dateNowMock = dataNowMockFn();
setProps({ targetOffset: 200 });
anchorInstance!.handleScrollTo(`#${hash}`);
await sleep(30);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
dateNowMock.mockRestore();
@ -584,19 +590,19 @@ describe('Anchor Render', () => {
</Anchor>,
);
anchorInstance!.handleScrollTo(`#${hash}`);
await sleep(30);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000);
dateNowMock = dataNowMockFn();
setProps({ offsetTop: 100 });
anchorInstance!.handleScrollTo(`#${hash}`);
await sleep(30);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900);
dateNowMock = dataNowMockFn();
setProps({ targetOffset: 200 });
anchorInstance!.handleScrollTo(`#${hash}`);
await sleep(30);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
dateNowMock.mockRestore();
@ -653,18 +659,18 @@ describe('Anchor Render', () => {
</Anchor>,
);
anchorInstance!.handleScrollTo(`#${hash}`);
await sleep(30);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
dateNowMock = dataNowMockFn();
setProps({ offsetTop: 100 });
anchorInstance!.handleScrollTo(`#${hash}`);
await sleep(30);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
dateNowMock = dataNowMockFn();
setProps({ targetOffset: 200 });
anchorInstance!.handleScrollTo(`#${hash}`);
await sleep(30);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
dateNowMock.mockRestore();

View File

@ -147,7 +147,7 @@ const AutoComplete: React.ForwardRefRenderFunction<RefSelectProps, AutoCompleteP
ref={ref}
{...omit(props, ['dataSource'])}
prefixCls={prefixCls}
dropdownClassName={popupClassName || dropdownClassName}
popupClassName={popupClassName || dropdownClassName}
className={classNames(`${prefixCls}-auto-complete`, className)}
mode={Select.SECRET_COMBOBOX_MODE_DO_NOT_USE as any}
{...{

View File

@ -83,7 +83,7 @@ const Breadcrumb: BreadcrumbInterface = ({
}) => {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
let crumbs;
let crumbs: React.ReactNode;
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
if (routes && routes.length > 0) {
// generated by route

View File

@ -43,7 +43,7 @@ const BreadcrumbItem: BreadcrumbItemInterface = ({
return breadcrumbItem;
};
let link;
let link: React.ReactNode;
if ('href' in restProps) {
link = (
<a className={`${prefixCls}-link`} {...restProps}>
@ -60,7 +60,7 @@ const BreadcrumbItem: BreadcrumbItemInterface = ({
// wrap to dropDown
link = renderBreadcrumbNode(link);
if (children) {
if (children !== undefined && children !== null) {
return (
<li>
{link}

View File

@ -157,4 +157,15 @@ describe('Breadcrumb', () => {
);
expect(asFragment().firstChild).toMatchSnapshot();
});
it('should support string `0` and number `0`', () => {
const { container } = render(
<Breadcrumb>
<Breadcrumb.Item>{0}</Breadcrumb.Item>
<Breadcrumb.Item>0</Breadcrumb.Item>
</Breadcrumb>,
);
expect(container.querySelectorAll('.ant-breadcrumb-link')[0].textContent).toBe('0');
expect(container.querySelectorAll('.ant-breadcrumb-link')[1].textContent).toBe('0');
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -323,3 +323,36 @@ exports[`Breadcrumb should support custom attribute 1`] = `
</ol>
</nav>
`;
exports[`Breadcrumb should support string \`0\` and number \`0\` 1`] = `
<nav
class="ant-breadcrumb"
>
<ol>
<li>
<span
class="ant-breadcrumb-link"
>
0
</span>
<span
class="ant-breadcrumb-separator"
>
/
</span>
</li>
<li>
<span
class="ant-breadcrumb-link"
>
0
</span>
<span
class="ant-breadcrumb-separator"
>
/
</span>
</li>
</ol>
</nav>
`;

View File

@ -1,8 +1,8 @@
---
order: 28
title:
zh-CN: 基本
en-US: Basic
zh-CN: 弹出位置
en-US: Placement
---
## zh-CN

View File

@ -46,7 +46,7 @@ You should use [Menu](/components/menu/) as `overlay`. The menu items and divide
| disabled | Whether the dropdown menu is disabled | boolean | - | |
| icon | Icon (appears on the right) | ReactNode | - | |
| overlay | The dropdown menu | [Menu](/components/menu) | - | |
| placement | Placement of popup menu: `bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomLeft` | |
| placement | Placement of popup menu: `bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomRight` | |
| size | Size of the button, the same as [Button](/components/button/#API) | string | `default` | |
| trigger | The trigger mode which executes the dropdown action | Array&lt;`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
| type | Type of the button, the same as [Button](/components/button/#API) | string | `default` | |

View File

@ -50,7 +50,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/eedWN59yJ/Dropdown.svg
| disabled | 菜单是否禁用 | boolean | - | |
| icon | 右侧的 icon | ReactNode | - | |
| overlay | 菜单 | [Menu](/components/menu/) | - | |
| placement | 菜单弹出位置:`bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomLeft` | |
| placement | 菜单弹出位置:`bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomRight` | |
| size | 按钮大小,和 [Button](/components/button/#API) 一致 | string | `default` | |
| trigger | 触发下拉的行为 | Array&lt;`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
| type | 按钮类型,和 [Button](/components/button/#API) 一致 | string | `default` | |

View File

@ -13,6 +13,7 @@ import { FormItemInputContext, NoFormStyle } from '../form/context';
import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import warning from '../_util/warning';
import useRemovePasswordTimeout from './hooks/useRemovePasswordTimeout';
import { hasPrefixSuffix } from './utils';
export interface InputFocusOptions extends FocusOptions {
@ -171,25 +172,7 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
}, [inputHasPrefixSuffix]);
// ===================== Remove Password value =====================
const removePasswordTimeoutRef = useRef<number[]>([]);
const removePasswordTimeout = () => {
removePasswordTimeoutRef.current.push(
window.setTimeout(() => {
if (
inputRef.current?.input &&
inputRef.current?.input.getAttribute('type') === 'password' &&
inputRef.current?.input.hasAttribute('value')
) {
inputRef.current?.input.removeAttribute('value');
}
}),
);
};
useEffect(() => {
removePasswordTimeout();
return () => removePasswordTimeoutRef.current.forEach(item => window.clearTimeout(item));
}, []);
const removePasswordTimeout = useRemovePasswordTimeout(inputRef, true);
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
removePasswordTimeout();

View File

@ -2,10 +2,12 @@ import EyeInvisibleOutlined from '@ant-design/icons/EyeInvisibleOutlined';
import EyeOutlined from '@ant-design/icons/EyeOutlined';
import classNames from 'classnames';
import omit from 'rc-util/lib/omit';
import { composeRef } from 'rc-util/lib/ref';
import * as React from 'react';
import { useState } from 'react';
import { useRef, useState } from 'react';
import type { ConfigConsumerProps } from '../config-provider';
import { ConfigConsumer } from '../config-provider';
import useRemovePasswordTimeout from './hooks/useRemovePasswordTimeout';
import type { InputProps, InputRef } from './Input';
import Input from './Input';
@ -26,12 +28,19 @@ const ActionMap: Record<string, string> = {
const Password = React.forwardRef<InputRef, PasswordProps>((props, ref) => {
const [visible, setVisible] = useState(false);
const inputRef = useRef<InputRef>(null);
// Remove Password value
const removePasswordTimeout = useRemovePasswordTimeout(inputRef);
const onVisibleChange = () => {
const { disabled } = props;
if (disabled) {
return;
}
if (visible) {
removePasswordTimeout();
}
setVisible(prevState => !prevState);
};
@ -87,7 +96,7 @@ const Password = React.forwardRef<InputRef, PasswordProps>((props, ref) => {
omittedProps.size = size;
}
return <Input ref={ref} {...omittedProps} />;
return <Input ref={composeRef(ref, inputRef)} {...omittedProps} />;
};
return <ConfigConsumer>{renderPassword}</ConfigConsumer>;

View File

@ -108,4 +108,17 @@ describe('Input.Password', () => {
await sleep();
expect(container.querySelector('input')?.getAttribute('value')).toBeFalsy();
});
it('should not show value attribute in input element after toggle visibility', async () => {
const { container } = render(<Input.Password />);
fireEvent.change(container.querySelector('input')!, { target: { value: 'value' } });
await sleep();
expect(container.querySelector('input')?.getAttribute('value')).toBeFalsy();
fireEvent.click(container.querySelector('.ant-input-password-icon')!);
await sleep();
expect(container.querySelector('input')?.getAttribute('value')).toBeTruthy();
fireEvent.click(container.querySelector('.ant-input-password-icon')!);
await sleep();
expect(container.querySelector('input')?.getAttribute('value')).toBeFalsy();
});
});

View File

@ -0,0 +1,32 @@
import { useEffect, useRef } from 'react';
import type { InputRef } from '../Input';
export default function useRemovePasswordTimeout(
inputRef: React.RefObject<InputRef>,
triggerOnMount?: boolean,
) {
const removePasswordTimeoutRef = useRef<number[]>([]);
const removePasswordTimeout = () => {
removePasswordTimeoutRef.current.push(
window.setTimeout(() => {
if (
inputRef.current?.input &&
inputRef.current?.input.getAttribute('type') === 'password' &&
inputRef.current?.input.hasAttribute('value')
) {
inputRef.current?.input.removeAttribute('value');
}
}),
);
};
useEffect(() => {
if (triggerOnMount) {
removePasswordTimeout();
}
return () => removePasswordTimeoutRef.current.forEach(item => window.clearTimeout(item));
}, []);
return removePasswordTimeout;
}

View File

@ -182,8 +182,11 @@
&:hover,
&:focus {
+ .@{ant-prefix}-input-group-addon .@{search-prefix}-button:not(.@{ant-prefix}-btn-primary) {
border-right-color: @input-hover-border-color;
border-left-color: @border-color-base;
&:hover {
border-left-color: @input-hover-border-color;
}
}
}
}

View File

@ -1,6 +1,6 @@
import { act } from 'react-dom/test-utils';
import message, { getInstance } from '..';
import { sleep } from '../../../tests/utils';
import { waitFakeTimer } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
describe('message.config', () => {
@ -87,7 +87,6 @@ describe('message.config', () => {
});
it('should be able to config duration', async () => {
jest.useRealTimers();
message.config({
duration: 0.5,
});
@ -97,7 +96,7 @@ describe('message.config', () => {
});
expect(getInstance()?.component.state.notices).toHaveLength(1);
await sleep(1000);
await waitFakeTimer();
expect(getInstance()?.component.state.notices).toHaveLength(0);
message.config({
duration: 3,
@ -180,10 +179,10 @@ describe('message.config', () => {
}
const [container1, removeContainer1] = createContainer();
const [container2, removeContainer2] = createContainer();
expect((container1 as HTMLDivElement).querySelector('.ant-message-notice')).toBeFalsy();
expect((container2 as HTMLDivElement).querySelector('.ant-message-notice')).toBeFalsy();
expect(container1.querySelector('.ant-message-notice')).toBeFalsy();
expect(container2.querySelector('.ant-message-notice')).toBeFalsy();
message.config({
getContainer: () => container1 as HTMLDivElement,
getContainer: () => container1,
});
const messageText1 = 'mounted in container1';
@ -191,20 +190,16 @@ describe('message.config', () => {
message.info(messageText1);
});
expect(
(container1 as HTMLDivElement).querySelector('.ant-message-notice')?.textContent,
).toEqual(messageText1);
expect(container1.querySelector('.ant-message-notice')?.textContent).toEqual(messageText1);
message.config({
getContainer: () => container2 as HTMLDivElement,
getContainer: () => container2,
});
const messageText2 = 'mounted in container2';
act(() => {
message.info(messageText2);
});
expect(
(container2 as HTMLDivElement).querySelector('.ant-message-notice')?.textContent,
).toEqual(messageText2);
expect(container2.querySelector('.ant-message-notice')?.textContent).toEqual(messageText2);
if (typeof removeContainer1 === 'function') {
removeContainer1();
}

View File

@ -1,5 +1,5 @@
import notification, { getInstance } from '..';
import { sleep, act } from '../../../tests/utils';
import { waitFakeTimer, act } from '../../../tests/utils';
describe('notification.config', () => {
beforeEach(() => {
@ -32,24 +32,19 @@ describe('notification.config', () => {
});
});
await act(async () => {
await Promise.resolve();
});
await waitFakeTimer();
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(5);
expect(document.querySelectorAll('.ant-notification-notice')[4]?.textContent).toBe(
'Notification last',
);
act(() => {
jest.runAllTimers();
});
await waitFakeTimer();
await act(async () => {
await sleep(500);
});
expect((await getInstance('ant-notification-topRight'))?.component.state.notices).toHaveLength(
0,
);
jest.useRealTimers();
});
});

View File

@ -1,7 +1,7 @@
import React from 'react';
import { UserOutlined } from '@ant-design/icons';
import notification, { getInstance, type NotificationInstance } from '..';
import { sleep, act } from '../../../tests/utils';
import { waitFakeTimer, act } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
@ -39,7 +39,7 @@ describe('notification', () => {
});
}
await sleep();
await waitFakeTimer();
const count = document.querySelectorAll('.additional-holder').length;
expect(count).toEqual(1);

View File

@ -1,5 +1,4 @@
import React from 'react';
// eslint-disable-next-line import/no-named-as-default
import { render } from '@testing-library/react';
import debounce from 'lodash/debounce';
import Spin from '..';
@ -32,7 +31,7 @@ describe('delay spinning', () => {
jest.useRealTimers();
});
it('should cancel debounce function when unmount', async () => {
it('should cancel debounce function when unmount', () => {
const debouncedFn = jest.fn();
const cancel = jest.fn();
(debouncedFn as any).cancel = cancel;

View File

@ -1,6 +1,6 @@
import React from 'react';
// eslint-disable-next-line import/no-named-as-default
import { render } from '@testing-library/react';
import { waitFakeTimer } from '../../../tests/utils';
import Spin from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
@ -15,10 +15,8 @@ describe('Spin', () => {
<div>content</div>
</Spin>,
);
expect((container.querySelector('.ant-spin-nested-loading')! as HTMLElement).style.length).toBe(
0,
);
expect((container.querySelector('.ant-spin')! as HTMLElement).style.background).toBe('red');
expect(container.querySelector<HTMLElement>('.ant-spin-nested-loading')?.style.length).toBe(0);
expect(container.querySelector<HTMLElement>('.ant-spin')?.style.background).toBe('red');
});
it("should render custom indicator when it's set", () => {
@ -27,11 +25,15 @@ describe('Spin', () => {
expect(asFragment().firstChild).toMatchSnapshot();
});
it('should be controlled by spinning', () => {
it('should be controlled by spinning', async () => {
jest.useFakeTimers();
const { container, rerender } = render(<Spin spinning={false} />);
expect(container.querySelector('.ant-spin-spinning')).toBeFalsy();
rerender(<Spin spinning />);
await waitFakeTimer();
expect(container.querySelector('.ant-spin-spinning')).toBeTruthy();
jest.clearAllTimers();
jest.useRealTimers();
});
it('if indicator set null should not be render default indicator', () => {

View File

@ -32,11 +32,6 @@ export type SpinFCType = React.FC<SpinProps> & {
setDefaultIndicator: (indicator: React.ReactNode) => void;
};
export interface SpinState {
spinning?: boolean;
notCssAnimationSupported?: boolean;
}
// Render indicator
let defaultIndicator: React.ReactNode = null;
@ -56,8 +51,8 @@ function renderIndicator(prefixCls: string, props: SpinClassProps): React.ReactN
}
if (isValidElement(defaultIndicator)) {
return cloneElement(defaultIndicator as SpinIndicator, {
className: classNames((defaultIndicator as SpinIndicator).props.className, dotClassName),
return cloneElement(defaultIndicator, {
className: classNames(defaultIndicator.props.className, dotClassName),
});
}
@ -75,79 +70,37 @@ function shouldDelay(spinning?: boolean, delay?: number): boolean {
return !!spinning && !!delay && !isNaN(Number(delay));
}
class Spin extends React.Component<SpinClassProps, SpinState> {
static defaultProps = {
spinning: true,
size: 'default' as SpinSize,
wrapperClassName: '',
};
const Spin: React.FC<SpinClassProps> = props => {
const {
spinPrefixCls: prefixCls,
spinning: customSpinning = true,
delay,
className,
size = 'default',
tip,
wrapperClassName,
style,
children,
...restProps
} = props;
originalUpdateSpinning: () => void;
const [spinning, setSpinning] = React.useState<boolean>(
() => customSpinning && !shouldDelay(customSpinning, delay),
);
constructor(props: SpinClassProps) {
super(props);
const { spinning, delay } = props;
const shouldBeDelayed = shouldDelay(spinning, delay);
this.state = {
spinning: spinning && !shouldBeDelayed,
React.useEffect(() => {
const updateSpinning = debounce<() => void>(() => {
setSpinning(customSpinning);
}, delay);
updateSpinning();
return () => {
updateSpinning?.cancel?.();
};
this.originalUpdateSpinning = this.updateSpinning;
this.debouncifyUpdateSpinning(props);
}
}, [delay, customSpinning]);
componentDidMount() {
this.updateSpinning();
}
componentDidUpdate() {
this.debouncifyUpdateSpinning();
this.updateSpinning();
}
componentWillUnmount() {
this.cancelExistingSpin();
}
debouncifyUpdateSpinning = (props?: SpinClassProps) => {
const { delay } = props || this.props;
if (delay) {
this.cancelExistingSpin();
this.updateSpinning = debounce(this.originalUpdateSpinning, delay);
}
};
updateSpinning = () => {
const { spinning } = this.props;
const { spinning: currentSpinning } = this.state;
if (currentSpinning !== spinning) {
this.setState({ spinning });
}
};
cancelExistingSpin() {
const { updateSpinning } = this;
if (updateSpinning && (updateSpinning as any).cancel) {
(updateSpinning as any).cancel();
}
}
isNestedPattern() {
return !!(this.props && typeof this.props.children !== 'undefined');
}
renderSpin = ({ direction }: ConfigConsumerProps) => {
const {
spinPrefixCls: prefixCls,
className,
size,
tip,
wrapperClassName,
style,
...restProps
} = this.props;
const { spinning } = this.state;
const isNestedPattern = () => typeof children !== 'undefined';
const renderSpin = ({ direction }: ConfigConsumerProps) => {
const spinClassName = classNames(
prefixCls,
{
@ -161,7 +114,7 @@ class Spin extends React.Component<SpinClassProps, SpinState> {
);
// fix https://fb.me/react-unknown-prop
const divProps = omit(restProps, ['spinning', 'delay', 'indicator', 'prefixCls']);
const divProps = omit(restProps, ['indicator', 'prefixCls']);
const spinElement = (
<div
@ -171,11 +124,12 @@ class Spin extends React.Component<SpinClassProps, SpinState> {
aria-live="polite"
aria-busy={spinning}
>
{renderIndicator(prefixCls, this.props)}
{renderIndicator(prefixCls, props)}
{tip ? <div className={`${prefixCls}-text`}>{tip}</div> : null}
</div>
);
if (this.isNestedPattern()) {
if (isNestedPattern()) {
const containerClassName = classNames(`${prefixCls}-container`, {
[`${prefixCls}-blur`]: spinning,
});
@ -183,20 +137,17 @@ class Spin extends React.Component<SpinClassProps, SpinState> {
<div {...divProps} className={classNames(`${prefixCls}-nested-loading`, wrapperClassName)}>
{spinning && <div key="loading">{spinElement}</div>}
<div className={containerClassName} key="container">
{this.props.children}
{children}
</div>
</div>
);
}
return spinElement;
};
return <ConfigConsumer>{renderSpin}</ConfigConsumer>;
};
render() {
return <ConfigConsumer>{this.renderSpin}</ConfigConsumer>;
}
}
const SpinFC: SpinFCType = (props: SpinProps) => {
const SpinFC: SpinFCType = props => {
const { prefixCls: customizePrefixCls } = props;
const { getPrefixCls } = React.useContext(ConfigContext);

View File

@ -1,8 +1,9 @@
import * as React from 'react';
import useForceUpdate from '../_util/hooks/useForceUpdate';
import { cloneElement } from '../_util/reactNode';
import type { StatisticProps } from './Statistic';
import Statistic from './Statistic';
import type { countdownValueType, FormatConfig } from './utils';
import type { countdownValueType, FormatConfig, valueType } from './utils';
import { formatCountdown } from './utils';
const REFRESH_INTERVAL = 1000 / 30;
@ -15,84 +16,54 @@ interface CountdownProps extends StatisticProps {
}
function getTime(value?: countdownValueType) {
return new Date(value as any).getTime();
return new Date(value as valueType).getTime();
}
class Countdown extends React.Component<CountdownProps, {}> {
static defaultProps: Partial<CountdownProps> = {
format: 'HH:mm:ss',
const Countdown: React.FC<CountdownProps> = props => {
const { value, format = 'HH:mm:ss', onChange, onFinish } = props;
const forceUpdate = useForceUpdate();
const countdown = React.useRef<NodeJS.Timer | null>(null);
const stopTimer = () => {
onFinish?.();
if (countdown.current) {
clearInterval(countdown.current);
countdown.current = null;
}
};
countdownId?: number;
componentDidMount() {
this.syncTimer();
}
componentDidUpdate() {
this.syncTimer();
}
componentWillUnmount() {
this.stopTimer();
}
syncTimer = () => {
const { value } = this.props;
const syncTimer = () => {
const timestamp = getTime(value);
if (timestamp >= Date.now()) {
this.startTimer();
} else {
this.stopTimer();
countdown.current = setInterval(() => {
forceUpdate();
onChange?.(timestamp - Date.now());
if (timestamp < Date.now()) {
stopTimer();
}
}, REFRESH_INTERVAL);
}
};
startTimer = () => {
if (this.countdownId) return;
const { onChange, value } = this.props;
const timestamp = getTime(value);
this.countdownId = window.setInterval(() => {
this.forceUpdate();
if (onChange && timestamp > Date.now()) {
onChange(timestamp - Date.now());
React.useEffect(() => {
syncTimer();
return () => {
if (countdown.current) {
clearInterval(countdown.current);
countdown.current = null;
}
}, REFRESH_INTERVAL);
};
};
}, [value]);
stopTimer = () => {
const { onFinish, value } = this.props;
if (this.countdownId) {
clearInterval(this.countdownId);
this.countdownId = undefined;
const formatter = (formatValue: countdownValueType, config: FormatConfig) =>
formatCountdown(formatValue, { ...config, format });
const timestamp = getTime(value);
if (onFinish && timestamp < Date.now()) {
onFinish();
}
}
};
const valueRender = (node: React.ReactElement<HTMLDivElement>) =>
cloneElement(node, { title: undefined });
formatCountdown = (value: countdownValueType, config: FormatConfig) => {
const { format } = this.props;
return formatCountdown(value, { ...config, format });
};
return <Statistic {...props} valueRender={valueRender} formatter={formatter} />;
};
// Countdown do not need display the timestamp
// eslint-disable-next-line class-methods-use-this
valueRender = (node: React.ReactElement<HTMLDivElement>) =>
cloneElement(node, {
title: undefined,
});
render() {
return (
<Statistic valueRender={this.valueRender} {...this.props} formatter={this.formatCountdown} />
);
}
}
export default Countdown;
export default React.memo(Countdown);

View File

@ -5,7 +5,6 @@ import Statistic from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render, sleep } from '../../../tests/utils';
import type Countdown from '../Countdown';
import { formatTimeStr } from '../utils';
describe('Statistic', () => {
@ -100,13 +99,8 @@ describe('Statistic', () => {
it('time going', async () => {
const now = Date.now() + 1000;
const onFinish = jest.fn();
const instance = React.createRef<Countdown>();
const { unmount } = render(
<Statistic.Countdown ref={instance} value={now} onFinish={onFinish} />,
);
// setInterval should work
expect(instance.current!.countdownId).not.toBe(undefined);
const { unmount } = render(<Statistic.Countdown value={now} onFinish={onFinish} />);
await sleep(10);
@ -156,11 +150,9 @@ describe('Statistic', () => {
describe('time finished', () => {
it('not call if time already passed', () => {
const now = Date.now() - 1000;
const instance = React.createRef<Countdown>();
const onFinish = jest.fn();
render(<Statistic.Countdown ref={instance} value={now} onFinish={onFinish} />);
render(<Statistic.Countdown value={now} onFinish={onFinish} />);
expect(instance.current!.countdownId).toBe(undefined);
expect(onFinish).not.toHaveBeenCalled();
});

View File

@ -557,7 +557,6 @@ const ForwardTable = React.forwardRef(InternalTable) as <RecordType extends obje
type InternalTableType = typeof ForwardTable;
interface TableInterface extends InternalTableType {
defaultProps?: Partial<TableProps<any>>;
SELECTION_COLUMN: typeof SELECTION_COLUMN;
EXPAND_COLUMN: typeof RcTable.EXPAND_COLUMN;
SELECTION_ALL: 'SELECT_ALL';

View File

@ -227,6 +227,22 @@ describe('Table', () => {
});
});
// https://github.com/ant-design/ant-design/issues/37977
it('should render title when enable ellipsis, sorter and filters', () => {
const data = [] as any;
const columns = [
{ title: 'id', dataKey: 'id', ellipsis: true, sorter: true, filters: [] },
{ title: 'age', dataKey: 'age', ellipsis: true, sorter: true },
{ title: 'age', dataKey: 'age', ellipsis: true, filters: [] },
];
const { container } = render(<Table columns={columns} dataSource={data} />);
container
.querySelectorAll<HTMLTableCellElement>('.ant-table-thead th.ant-table-cell')
.forEach(td => {
expect((td.attributes as any).title).toBeTruthy();
});
});
it('warn about rowKey when using index parameter', () => {
warnSpy.mockReset();
const columns = [

View File

@ -20375,6 +20375,7 @@ Array [
aria-label="Name sortable"
class="ant-table-cell ant-table-cell-ellipsis ant-table-column-has-sorters"
tabindex="0"
title="Name"
>
<div
class="ant-table-filter-column"
@ -20681,6 +20682,7 @@ Array [
aria-label="Age sortable"
class="ant-table-cell ant-table-cell-ellipsis ant-table-column-has-sorters"
tabindex="0"
title="Age"
>
<div
class="ant-table-column-sorters"
@ -20766,6 +20768,7 @@ Array [
aria-label="Address sortable"
class="ant-table-cell ant-table-cell-ellipsis ant-table-column-has-sorters"
tabindex="0"
title="Address"
>
<div
class="ant-table-filter-column"

View File

@ -15393,6 +15393,7 @@ Array [
aria-label="Name sortable"
class="ant-table-cell ant-table-cell-ellipsis ant-table-column-has-sorters"
tabindex="0"
title="Name"
>
<div
class="ant-table-filter-column"
@ -15487,6 +15488,7 @@ Array [
aria-label="Age sortable"
class="ant-table-cell ant-table-cell-ellipsis ant-table-column-has-sorters"
tabindex="0"
title="Age"
>
<div
class="ant-table-column-sorters"
@ -15548,6 +15550,7 @@ Array [
aria-label="Address sortable"
class="ant-table-cell ant-table-cell-ellipsis ant-table-column-has-sorters"
tabindex="0"
title="Address"
>
<div
class="ant-table-filter-column"

View File

@ -64,7 +64,7 @@ const App: React.FC = () => {
};
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', selectedRowKeys);
console.log('selectedRowKeys changed: ', newSelectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys);
};

View File

@ -55,7 +55,7 @@ const App: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', selectedRowKeys);
console.log('selectedRowKeys changed: ', newSelectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys);
};

View File

@ -207,18 +207,15 @@ function injectSorter<RecordType>(
// Inform the screen-reader so it can tell the visually impaired user which column is sorted
if (sorterOrder) {
if (sorterOrder === 'ascend') {
cell['aria-sort'] = 'ascending';
} else {
cell['aria-sort'] = 'descending';
}
cell['aria-sort'] = sorterOrder === 'ascend' ? 'ascending' : 'descending';
} else {
cell['aria-label'] = `${renderColumnTitle(column.title, {})} sortable`;
}
cell.className = classNames(cell.className, `${prefixCls}-column-has-sorters`);
cell.tabIndex = 0;
if (column.ellipsis) {
cell.title = (renderColumnTitle(column.title, {}) ?? '').toString();
}
return cell;
},
};

View File

@ -655,7 +655,7 @@
position: absolute;
top: 0;
bottom: 0;
z-index: @zindex-table-fixed;
z-index: calc(@table-sticky-zindex + 1);
width: 30px;
transition: box-shadow 0.3s;
content: '';

View File

@ -23,7 +23,6 @@ export interface DirectoryTreeProps<T extends BasicDataNode = DataNode> extends
type DirectoryTreeCompoundedComponent = (<T extends BasicDataNode | DataNode = DataNode>(
props: React.PropsWithChildren<DirectoryTreeProps<T>> & { ref?: React.Ref<RcTree> },
) => React.ReactElement) & {
defaultProps: Partial<React.PropsWithChildren<DirectoryTreeProps<any>>>;
displayName?: string;
};

View File

@ -157,7 +157,6 @@ export interface TreeProps<T extends BasicDataNode = DataNode>
type CompoundedComponent = (<T extends BasicDataNode | DataNode = DataNode>(
props: React.PropsWithChildren<TreeProps<T>> & { ref?: React.Ref<RcTree> },
) => React.ReactElement) & {
defaultProps: Partial<React.PropsWithChildren<TreeProps<any>>>;
TreeNode: typeof TreeNode;
DirectoryTree: typeof DirectoryTree;
};

View File

@ -289,7 +289,7 @@ const Base = React.forwardRef((props: InternalBlockProps, ref: any) => {
const [ellipsisFontSize, setEllipsisFontSize] = React.useState(0);
const onResize = ({ offsetWidth }: { offsetWidth: number }, element: HTMLElement) => {
setEllipsisWidth(offsetWidth);
setEllipsisFontSize(parseInt(window.getComputedStyle?.(element).fontSize, 10));
setEllipsisFontSize(parseInt(window.getComputedStyle?.(element).fontSize, 10) || 0);
};
// >>>>> JS Ellipsis

View File

@ -14,4 +14,4 @@ services:
- './jest-puppeteer.config.js:/app/jest-puppeteer.config.js'
- './imageSnapshots:/app/imageSnapshots'
- './imageDiffSnapshots:/app/imageDiffSnapshots'
entrypoint: "jest --config .jest.image.js --no-cache -i"
entrypoint: "npm run test-image:docker"

View File

@ -1,6 +1,6 @@
{
"name": "antd",
"version": "4.23.5",
"version": "4.23.6",
"description": "An enterprise-class UI design language and React components implementation",
"title": "Ant Design",
"keywords": [
@ -99,6 +99,7 @@
"tsc": "tsc --noEmit",
"site:test": "jest --config .jest.site.js --cache=false --force-exit",
"test-image": "npm run dist && docker-compose run tests",
"test-image:docker": "node node_modules/puppeteer/install.js && jest --config .jest.image.js --no-cache -i",
"argos": "node ./scripts/argos-upload.js",
"version": "node ./scripts/generate-version",
"install-react-16": "npm i --no-save --legacy-peer-deps react@16 react-dom@16",
@ -139,7 +140,7 @@
"rc-motion": "^2.6.1",
"rc-notification": "~4.6.0",
"rc-pagination": "~3.1.17",
"rc-picker": "~2.6.10",
"rc-picker": "~2.6.11",
"rc-progress": "~3.3.2",
"rc-rate": "~2.9.0",
"rc-resize-observer": "^1.2.0",
@ -150,7 +151,7 @@
"rc-switch": "~3.2.0",
"rc-table": "~7.26.0",
"rc-tabs": "~12.2.0",
"rc-textarea": "~0.4.3",
"rc-textarea": "~0.4.5",
"rc-tooltip": "~5.2.0",
"rc-tree": "~5.7.0",
"rc-tree-select": "~5.5.0",
@ -188,9 +189,9 @@
"@types/react-sticky": "^6.0.4",
"@types/react-window": "^1.8.2",
"@types/warning": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"antd-img-crop": "^4.0.0",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"antd-img-crop": "4.2.5",
"array-move": "^4.0.0",
"babel-plugin-add-react-displayname": "^0.0.5",
"bisheng": "^3.7.0-alpha.4",
@ -291,7 +292,7 @@
"stylelint-declaration-block-no-ignored-properties": "^2.1.0",
"stylelint-order": "^5.0.0",
"theme-switcher": "^1.0.2",
"typescript": "~4.8.0",
"typescript": "~4.8.4",
"webpack-bundle-analyzer": "^4.1.0",
"xhr-mock": "^2.4.1",
"yaml-front-matter": "^4.0.0"

View File

@ -36,6 +36,7 @@ const MAINTAINERS = [
'kerm1it',
'madccc',
'MadCcc',
'li-jia-nan',
].map(author => author.toLowerCase());
const cwd = process.cwd();

View File

@ -13,12 +13,6 @@ interface ColorPickerProps {
}
export default class ColorPicker extends Component<ColorPickerProps> {
static defaultProps = {
onChange: noop,
onChangeComplete: noop,
position: 'bottom',
};
static getDerivedStateFromProps(props: ColorPickerProps) {
if ('color' in props) {
return {
@ -43,28 +37,28 @@ export default class ColorPicker extends Component<ColorPickerProps> {
};
handleChange = (color: { hex: string }) => {
const { onChange } = this.props;
const { onChange = noop } = this.props;
this.setState({ color: color.hex });
onChange(color.hex, color);
};
handleChangeComplete = (color: { hex: string }) => {
const { onChangeComplete } = this.props;
const { onChangeComplete = noop } = this.props;
this.setState({ color: color.hex });
onChangeComplete(color.hex);
};
render() {
const { small, position, presetColors } = this.props;
const { small, position = 'bottom', presetColors } = this.props;
const { color, displayColorPicker } = this.state;
const width = small ? 80 : 120;
const styles = {
const styles: Record<PropertyKey, React.CSSProperties> = {
color: {
width: `${width}px`,
height: small ? '16px' : '24px',
borderRadius: '2px',
background: color,
} as React.CSSProperties,
},
swatch: {
padding: '4px',
background: '#fff',
@ -72,22 +66,22 @@ export default class ColorPicker extends Component<ColorPickerProps> {
boxShadow: '0 0 0 1px rgba(0,0,0,.1)',
display: 'inline-block',
cursor: 'pointer',
} as React.CSSProperties,
},
popover: {
position: 'absolute',
zIndex: 10,
} as React.CSSProperties,
},
cover: {
position: 'fixed',
top: '0px',
right: '0px',
bottom: '0px',
left: '0px',
} as React.CSSProperties,
},
wrapper: {
position: 'inherit',
zIndex: 100,
} as React.CSSProperties,
},
};
if (position === 'top') {

View File

@ -48,12 +48,13 @@ class Demo extends React.Component {
}
shouldComponentUpdate(nextProps, nextState) {
const { codeExpand, copied, copyTooltipOpen } = this.state;
const { codeExpand, copied, copyTooltipOpen, codeType } = this.state;
const { expand, theme, showRiddleButton } = this.props;
return (
(codeExpand || expand) !== (nextState.codeExpand || nextProps.expand) ||
copied !== nextState.copied ||
copyTooltipOpen !== nextState.copyTooltipOpen ||
codeType !== nextState.copyTooltipOpen ||
nextProps.theme !== theme ||
nextProps.showRiddleButton !== showRiddleButton
);
@ -64,10 +65,12 @@ class Demo extends React.Component {
const { codeType } = this.state;
if (typeof document !== 'undefined') {
const div = document.createElement('div');
const divJSX = document.createElement('div');
div.innerHTML = highlightedCodes[codeType] || highlightedCodes.jsx;
return div.textContent;
divJSX.innerHTML = highlightedCodes.jsx;
return [divJSX.textContent, div.textContent];
}
return '';
return ['', ''];
}
handleCodeExpand = demo => {
@ -139,7 +142,7 @@ class Demo extends React.Component {
theme,
showRiddleButton,
} = props;
const { copied, copyTooltipOpen } = state;
const { copied, copyTooltipOpen, codeType } = state;
if (!this.liveDemo) {
this.liveDemo = meta.iframe ? (
<BrowserFrame>
@ -186,7 +189,18 @@ class Demo extends React.Component {
</body>
</html>`;
const sourceCode = this.getSourceCode();
const tsconfig = `{
"compilerOptions": {
"jsx": "react-jsx",
"target": "esnext",
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
}
}`;
const [sourceCode, sourceCodeTyped] = this.getSourceCode();
const suffix = codeType === 'tsx' ? 'tsx' : 'js';
const dependencies = sourceCode.split('\n').reduce(
(acc, line) => {
@ -206,6 +220,10 @@ class Demo extends React.Component {
);
dependencies['@ant-design/icons'] = 'latest';
if (suffix === 'tsx') {
dependencies['@types/react'] = '^18.0.0';
dependencies['@types/react-dom'] = '^18.0.0';
}
dependencies.react = '^18.0.0';
dependencies['react-dom'] = '^18.0.0';
@ -267,7 +285,7 @@ class Demo extends React.Component {
};
// Reorder source code
let parsedSourceCode = sourceCode;
let parsedSourceCode = suffix === 'tsx' ? sourceCodeTyped : sourceCode;
let importReactContent = "import React from 'react';";
const importReactReg = /import React(\D*)from 'react';/;
@ -321,8 +339,8 @@ createRoot(document.getElementById('container')).render(<Demo />);
files: {
'package.json': { content: codesandboxPackage },
'index.css': { content: indexCssContent },
'index.js': { content: indexJsContent },
'demo.js': { content: demoJsContent },
[`index.${suffix}`]: { content: indexJsContent },
[`demo.${suffix}`]: { content: demoJsContent },
'index.html': {
content: html,
},
@ -334,11 +352,14 @@ createRoot(document.getElementById('container')).render(<Demo />);
dependencies,
files: {
'index.css': indexCssContent,
'index.js': indexJsContent,
'demo.js': demoJsContent,
[`index.${suffix}`]: indexJsContent,
[`demo.${suffix}`]: demoJsContent,
'index.html': html,
},
};
if (suffix === 'tsx') {
stackblitzPrefillConfig.files['tsconfig.json'] = tsconfig;
}
let codeBox = (
<section className={codeBoxClass} id={meta.id}>
@ -424,13 +445,15 @@ createRoot(document.getElementById('container')).render(<Demo />);
className="code-box-code-action"
onClick={() => {
this.track({ type: 'stackblitz', demo: meta.id });
stackblitzSdk.openProject(stackblitzPrefillConfig);
stackblitzSdk.openProject(stackblitzPrefillConfig, {
openFile: [`demo.${suffix}`],
});
}}
>
<ThunderboltOutlined className="code-box-stackblitz" />
</span>
</Tooltip>
<CopyToClipboard text={sourceCode} onCopy={() => this.handleCodeCopied(meta.id)}>
<CopyToClipboard text={sourceCodeTyped} onCopy={() => this.handleCodeCopied(meta.id)}>
<Tooltip
open={copyTooltipOpen}
onOpenChange={this.onCopyTooltipOpenChange}

View File

@ -15,6 +15,8 @@ if (typeof window !== 'undefined') {
// ref: https://github.com/ant-design/ant-design/issues/18774
if (!window.matchMedia) {
Object.defineProperty(global.window, 'matchMedia', {
writable: true,
configurable: true,
value: jest.fn(query => ({
matches: query.includes('max-width'),
addListener: jest.fn(),

View File

@ -3,7 +3,8 @@
"baseUrl": "./",
"paths": {
"antd": ["components/index.tsx"],
"antd/es/*": ["components/*"]
"antd/es/*": ["components/*"],
"antd/lib/*": ["components/*"]
},
"strictNullChecks": true,
"module": "esnext",