mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-29 05:29:37 +08:00
commit
84475b4ea5
6
.github/workflows/ui-upload.yml
vendored
6
.github/workflows/ui-upload.yml
vendored
@ -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
|
||||
|
@ -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`
|
||||
|
@ -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`
|
||||
|
23
components/_util/__tests__/reactNode.test.tsx
Normal file
23
components/_util/__tests__/reactNode.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
|
@ -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>;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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}
|
||||
{...{
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -1,8 +1,8 @@
|
||||
---
|
||||
order: 28
|
||||
title:
|
||||
zh-CN: 基本
|
||||
en-US: Basic
|
||||
zh-CN: 弹出位置
|
||||
en-US: Placement
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
@ -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<`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
|
||||
| type | Type of the button, the same as [Button](/components/button/#API) | string | `default` | |
|
||||
|
@ -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<`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
|
||||
| type | 按钮类型,和 [Button](/components/button/#API) 一致 | string | `default` | |
|
||||
|
@ -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();
|
||||
|
@ -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>;
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
32
components/input/hooks/useRemovePasswordTimeout.ts
Normal file
32
components/input/hooks/useRemovePasswordTimeout.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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', () => {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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 = [
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
@ -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: '';
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -1145,12 +1145,13 @@ Array [
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
>
|
||||
This is a loooooooooooooooooooooooooooooooong editable text
|
||||
<!-- -->
|
||||
with suffix.
|
||||
<div
|
||||
aria-label="Edit"
|
||||
class="ant-typography-edit"
|
||||
role="button"
|
||||
style="border: 0px; background: transparent; padding: 0px; line-height: inherit; display: inline-block;"
|
||||
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
@ -1175,8 +1176,8 @@ Array [
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast"
|
||||
style="opacity: 0;"
|
||||
class="ant-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
@ -1199,13 +1200,13 @@ Array [
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position: fixed; display: block; left: 0px; top: 0px; z-index: -9999; visibility: hidden; pointer-events: none; font-size: 14px; word-break: keep-all; white-space: nowrap;"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;font-size:0;word-break:keep-all;white-space:nowrap"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position: fixed; display: block; left: 0px; top: 0px; z-index: -9999; visibility: hidden; pointer-events: none; font-size: 14px; width: 0px; white-space: normal; margin: 0px; padding: 0px;"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;font-size:0;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@ -1217,7 +1218,7 @@ Array [
|
||||
aria-label="Edit"
|
||||
class="ant-typography-edit"
|
||||
role="button"
|
||||
style="border: 0px; background: transparent; padding: 0px; line-height: inherit; display: inline-block;"
|
||||
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
@ -1242,8 +1243,8 @@ Array [
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast"
|
||||
style="opacity: 0;"
|
||||
class="ant-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
@ -1322,9 +1323,7 @@ Array [
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
Trigger edit with:,
|
||||
<!-- -->,
|
||||
,
|
||||
Trigger edit with: ,
|
||||
<div
|
||||
class="ant-radio-group ant-radio-group-outline"
|
||||
>
|
||||
|
@ -857,12 +857,13 @@ Array [
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
|
||||
>
|
||||
This is a loooooooooooooooooooooooooooooooong editable text
|
||||
<!-- -->
|
||||
with suffix.
|
||||
<div
|
||||
aria-label="Edit"
|
||||
class="ant-typography-edit"
|
||||
role="button"
|
||||
style="border: 0px; background: transparent; padding: 0px; line-height: inherit; display: inline-block;"
|
||||
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
@ -887,13 +888,13 @@ Array [
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position: fixed; display: block; left: 0px; top: 0px; z-index: -9999; visibility: hidden; pointer-events: none; font-size: 14px; word-break: keep-all; white-space: nowrap;"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;font-size:0;word-break:keep-all;white-space:nowrap"
|
||||
>
|
||||
lg
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style="position: fixed; display: block; left: 0px; top: 0px; z-index: -9999; visibility: hidden; pointer-events: none; font-size: 14px; width: 0px; white-space: normal; margin: 0px; padding: 0px;"
|
||||
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;font-size:0;width:0;white-space:normal;margin:0;padding:0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@ -905,7 +906,7 @@ Array [
|
||||
aria-label="Edit"
|
||||
class="ant-typography-edit"
|
||||
role="button"
|
||||
style="border: 0px; background: transparent; padding: 0px; line-height: inherit; display: inline-block;"
|
||||
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
@ -962,9 +963,7 @@ Array [
|
||||
</span>
|
||||
</div>
|
||||
</div>,
|
||||
Trigger edit with:,
|
||||
<!-- -->,
|
||||
,
|
||||
Trigger edit with: ,
|
||||
<div
|
||||
class="ant-radio-group ant-radio-group-outline"
|
||||
>
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { fireEvent, render, sleep, triggerResize, waitFor } from '../../../tests/utils';
|
||||
import type { EllipsisConfig } from '../Base';
|
||||
import { fireEvent, render } from '../../../tests/utils';
|
||||
import Base from '../Base';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
@ -12,11 +10,10 @@ jest.mock('../../_util/styleChecker', () => ({
|
||||
isStyleSupport: () => true,
|
||||
}));
|
||||
|
||||
describe('Typography.Ellipsis', () => {
|
||||
describe('Typography.Editable', () => {
|
||||
const LINE_STR_COUNT = 20;
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
let mockRectSpy: ReturnType<typeof spyElementPrototypes>;
|
||||
let getWidthTimes = 0;
|
||||
|
||||
beforeAll(() => {
|
||||
mockRectSpy = spyElementPrototypes(HTMLElement, {
|
||||
@ -28,12 +25,6 @@ describe('Typography.Ellipsis', () => {
|
||||
return lines * 16;
|
||||
},
|
||||
},
|
||||
offsetWidth: {
|
||||
get: () => {
|
||||
getWidthTimes += 1;
|
||||
return 100;
|
||||
},
|
||||
},
|
||||
getBoundingClientRect() {
|
||||
let html = this.innerHTML;
|
||||
html = html.replace(/<[^>]*>/g, '');
|
||||
@ -43,11 +34,6 @@ describe('Typography.Ellipsis', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
errorSpy.mockReset();
|
||||
getWidthTimes = 0;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
errorSpy.mockRestore();
|
||||
mockRectSpy.mockRestore();
|
||||
@ -56,385 +42,40 @@ describe('Typography.Ellipsis', () => {
|
||||
const fullStr =
|
||||
'Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light';
|
||||
|
||||
it('should trigger update', async () => {
|
||||
const ref = React.createRef<any>();
|
||||
const onEllipsis = jest.fn();
|
||||
const { container, rerender, unmount } = render(
|
||||
<Base ellipsis={{ onEllipsis }} component="p" editable ref={ref}>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
triggerResize(ref.current);
|
||||
await sleep(20);
|
||||
|
||||
expect(container.firstChild?.textContent).toEqual('Bamboo is Little ...');
|
||||
expect(onEllipsis).toHaveBeenCalledWith(true);
|
||||
onEllipsis.mockReset();
|
||||
|
||||
// Second resize
|
||||
rerender(
|
||||
<Base ellipsis={{ rows: 2, onEllipsis }} component="p" editable>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
expect(container.textContent).toEqual('Bamboo is Little Light Bamboo is Litt...');
|
||||
expect(onEllipsis).not.toHaveBeenCalled();
|
||||
|
||||
// Third resize
|
||||
rerender(
|
||||
<Base ellipsis={{ rows: 99, onEllipsis }} component="p" editable>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
expect(container.querySelector('p')?.textContent).toEqual(fullStr);
|
||||
expect(onEllipsis).toHaveBeenCalledWith(false);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('support css multiple lines', async () => {
|
||||
const { container: wrapper } = render(
|
||||
<Base ellipsis={{ rows: 2 }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.querySelectorAll('.ant-typography-ellipsis-multiple-line').length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
(
|
||||
wrapper.querySelector<HTMLDivElement>('.ant-typography-ellipsis-multiple-line')
|
||||
?.style as any
|
||||
)?.WebkitLineClamp,
|
||||
).toEqual('2');
|
||||
});
|
||||
|
||||
it('string with parentheses', async () => {
|
||||
const parenthesesStr = `Ant Design, a design language (for background applications, is refined by
|
||||
Ant UED Team. Ant Design, a design language for background applications,
|
||||
is refined by Ant UED Team. Ant Design, a design language for background
|
||||
applications, is refined by Ant UED Team. Ant Design, a design language
|
||||
for background applications, is refined by Ant UED Team. Ant Design, a
|
||||
design language for background applications, is refined by Ant UED Team.
|
||||
Ant Design, a design language for background applications, is refined by
|
||||
Ant UED Team.`;
|
||||
const ref = React.createRef<any>();
|
||||
const onEllipsis = jest.fn();
|
||||
const { container: wrapper, unmount } = render(
|
||||
<Base ellipsis={{ onEllipsis }} component="p" editable ref={ref}>
|
||||
{parenthesesStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
triggerResize(ref.current);
|
||||
await sleep(20);
|
||||
|
||||
expect(wrapper.firstChild?.textContent).toEqual('Ant Design, a des...');
|
||||
const ellipsisSpans = wrapper.querySelectorAll('span[aria-hidden]');
|
||||
expect(ellipsisSpans[ellipsisSpans.length - 1].textContent).toEqual('...');
|
||||
onEllipsis.mockReset();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should middle ellipsis', async () => {
|
||||
const suffix = '--suffix';
|
||||
const ref = React.createRef<any>();
|
||||
const { container: wrapper, unmount } = render(
|
||||
<Base ellipsis={{ rows: 1, suffix }} component="p" ref={ref}>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
triggerResize(ref.current);
|
||||
await sleep(20);
|
||||
|
||||
expect(wrapper.querySelector('p')?.textContent).toEqual('Bamboo is...--suffix');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should front or middle ellipsis', async () => {
|
||||
it('should use editConfig.text over children in editing mode ', async () => {
|
||||
const suffix = '--The information is very important';
|
||||
const ref = React.createRef<any>();
|
||||
const {
|
||||
container: wrapper,
|
||||
rerender,
|
||||
unmount,
|
||||
} = render(
|
||||
<Base ellipsis={{ rows: 1, suffix }} component="p" ref={ref}>
|
||||
const { container: wrapper, unmount } = render(
|
||||
<Base
|
||||
ellipsis={{ rows: 1, suffix }}
|
||||
component="p"
|
||||
editable={{ text: fullStr + suffix }}
|
||||
ref={ref}
|
||||
>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
triggerResize(ref.current);
|
||||
await sleep(20);
|
||||
fireEvent.click(wrapper.querySelector('.ant-typography-edit')!);
|
||||
|
||||
expect(wrapper.querySelector('p')?.textContent).toEqual(
|
||||
'...--The information is very important',
|
||||
);
|
||||
|
||||
rerender(
|
||||
<Base ellipsis={{ rows: 2, suffix }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
expect(wrapper.querySelector('p')?.textContent).toEqual(
|
||||
'Ba...--The information is very important',
|
||||
);
|
||||
|
||||
rerender(
|
||||
<Base ellipsis={{ rows: 99, suffix }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
expect(wrapper.querySelector('p')?.textContent).toEqual(fullStr + suffix);
|
||||
expect(wrapper.querySelector('textarea')?.textContent).toEqual(fullStr + suffix);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('connect children', async () => {
|
||||
const bamboo = 'Bamboo';
|
||||
const is = ' is ';
|
||||
|
||||
it('should use children as the fallback of editConfig.text in editing mode', async () => {
|
||||
const suffix = '--The information is very important';
|
||||
const ref = React.createRef<any>();
|
||||
const { container: wrapper } = render(
|
||||
<Base ellipsis component="p" editable ref={ref}>
|
||||
{bamboo}
|
||||
{is}
|
||||
<code>Little</code>
|
||||
<code>Light</code>
|
||||
</Base>,
|
||||
);
|
||||
|
||||
triggerResize(ref.current);
|
||||
await sleep(20);
|
||||
|
||||
expect(wrapper.textContent).toEqual('Bamboo is Little...');
|
||||
});
|
||||
|
||||
it('should expandable work', async () => {
|
||||
const onExpand = jest.fn();
|
||||
const { container: wrapper } = render(
|
||||
<Base ellipsis={{ expandable: true, onExpand }} component="p" copyable editable>
|
||||
const { container: wrapper, unmount } = render(
|
||||
<Base ellipsis={{ rows: 1, suffix }} component="p" ref={ref} editable>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
fireEvent.click(wrapper.querySelector('.ant-typography-expand')!);
|
||||
expect(onExpand).toHaveBeenCalled();
|
||||
expect(wrapper.querySelector('p')?.textContent).toEqual(fullStr);
|
||||
});
|
||||
fireEvent.click(wrapper.querySelector('.ant-typography-edit')!);
|
||||
|
||||
it('should have custom expand style', async () => {
|
||||
const symbol = 'more';
|
||||
const { container } = render(
|
||||
<Base ellipsis={{ expandable: true, symbol }} component="p">
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
expect(container.querySelector('.ant-typography-expand')?.textContent).toEqual('more');
|
||||
});
|
||||
expect(wrapper.querySelector('textarea')?.textContent).toEqual(fullStr);
|
||||
|
||||
describe('native css ellipsis', () => {
|
||||
it('can use css ellipsis', () => {
|
||||
const { container } = render(<Base ellipsis component="p" />);
|
||||
expect(container.querySelector('.ant-typography-ellipsis-single-line')).toBeTruthy();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/36786
|
||||
it('Tooltip should recheck on parent visible change', () => {
|
||||
const originIntersectionObserver = global.IntersectionObserver;
|
||||
|
||||
let elementChangeCallback: () => void;
|
||||
const observeFn = jest.fn();
|
||||
const disconnectFn = jest.fn();
|
||||
|
||||
(global as any).IntersectionObserver = class MockIntersectionObserver {
|
||||
constructor(callback: () => IntersectionObserverCallback) {
|
||||
elementChangeCallback = callback;
|
||||
}
|
||||
|
||||
observe = observeFn;
|
||||
|
||||
disconnect = disconnectFn;
|
||||
};
|
||||
|
||||
const { container, unmount } = render(<Base ellipsis component="p" />);
|
||||
|
||||
expect(observeFn).toHaveBeenCalled();
|
||||
|
||||
// Hide first
|
||||
act(() => {
|
||||
elementChangeCallback?.();
|
||||
});
|
||||
|
||||
// Trigger visible should trigger recheck
|
||||
getWidthTimes = 0;
|
||||
Object.defineProperty(container.querySelector('.ant-typography'), 'offsetParent', {
|
||||
get: () => document.body,
|
||||
});
|
||||
act(() => {
|
||||
elementChangeCallback?.();
|
||||
});
|
||||
|
||||
expect(getWidthTimes).toBeGreaterThan(0);
|
||||
|
||||
unmount();
|
||||
expect(disconnectFn).toHaveBeenCalled();
|
||||
|
||||
global.IntersectionObserver = originIntersectionObserver;
|
||||
});
|
||||
|
||||
it('should calculate padding', () => {
|
||||
const { container } = render(
|
||||
<Base ellipsis component="p" style={{ paddingTop: '12px', paddingBottom: '12px' }} />,
|
||||
);
|
||||
expect(container.querySelector('.ant-typography-ellipsis-single-line')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should tooltip support', () => {
|
||||
let domSpy: ReturnType<typeof spyElementPrototypes>;
|
||||
|
||||
beforeAll(() => {
|
||||
domSpy = spyElementPrototypes(HTMLElement, {
|
||||
offsetWidth: {
|
||||
get: () => 100,
|
||||
},
|
||||
scrollWidth: {
|
||||
get: () => 200,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
domSpy.mockRestore();
|
||||
});
|
||||
|
||||
async function getWrapper(tooltip?: EllipsisConfig['tooltip']) {
|
||||
const ref = React.createRef<any>();
|
||||
const wrapper = render(
|
||||
<Base ellipsis={{ tooltip }} component="p" ref={ref}>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
triggerResize(ref.current);
|
||||
await sleep(20);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
it('boolean', async () => {
|
||||
const { container, baseElement } = await getWrapper(true);
|
||||
fireEvent.mouseEnter(container.firstChild!);
|
||||
await waitFor(() => {
|
||||
expect(baseElement.querySelector('.ant-tooltip-open')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('customize', async () => {
|
||||
const { container, baseElement } = await getWrapper('Bamboo is Light');
|
||||
fireEvent.mouseEnter(container.firstChild!);
|
||||
await waitFor(() => {
|
||||
expect(baseElement.querySelector('.ant-tooltip-open')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
it('tooltip props', async () => {
|
||||
const { container, baseElement } = await getWrapper({
|
||||
title: 'This is tooltip',
|
||||
className: 'tooltip-class-name',
|
||||
});
|
||||
fireEvent.mouseEnter(container.firstChild!);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.tooltip-class-name')).toBeTruthy();
|
||||
expect(baseElement.querySelector('.ant-tooltip-open')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
it('tooltip title true', async () => {
|
||||
const { container, baseElement } = await getWrapper({
|
||||
title: true,
|
||||
className: 'tooltip-class-name',
|
||||
});
|
||||
fireEvent.mouseEnter(container.firstChild!);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.tooltip-class-name')).toBeTruthy();
|
||||
expect(baseElement.querySelector('.ant-tooltip-open')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
it('tooltip element', async () => {
|
||||
const { container, baseElement } = await getWrapper(
|
||||
<div className="tooltip-class-name">title</div>,
|
||||
);
|
||||
fireEvent.mouseEnter(container.firstChild!);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.tooltip-class-name')).toBeTruthy();
|
||||
expect(baseElement.querySelector('.ant-tooltip-open')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('js ellipsis should show aria-label', () => {
|
||||
const { container: titleWrapper } = render(
|
||||
<Base
|
||||
component={undefined as unknown as string}
|
||||
title="bamboo"
|
||||
ellipsis={{ expandable: true }}
|
||||
/>,
|
||||
);
|
||||
expect(titleWrapper.querySelector('.ant-typography')?.getAttribute('aria-label')).toEqual(
|
||||
'bamboo',
|
||||
);
|
||||
|
||||
const { container: tooltipWrapper } = render(
|
||||
<Base
|
||||
component={undefined as unknown as string}
|
||||
ellipsis={{ expandable: true, tooltip: 'little' }}
|
||||
/>,
|
||||
);
|
||||
expect(tooltipWrapper.querySelector('.ant-typography')?.getAttribute('aria-label')).toEqual(
|
||||
'little',
|
||||
);
|
||||
});
|
||||
|
||||
it('should display tooltip if line clamp', async () => {
|
||||
mockRectSpy = spyElementPrototypes(HTMLElement, {
|
||||
scrollHeight: {
|
||||
get() {
|
||||
let html = this.innerHTML;
|
||||
html = html.replace(/<[^>]*>/g, '');
|
||||
const lines = Math.ceil(html.length / LINE_STR_COUNT);
|
||||
return lines * 16;
|
||||
},
|
||||
},
|
||||
offsetHeight: {
|
||||
get: () => 32,
|
||||
},
|
||||
offsetWidth: {
|
||||
get: () => 100,
|
||||
},
|
||||
scrollWidth: {
|
||||
get: () => 100,
|
||||
},
|
||||
});
|
||||
|
||||
const ref = React.createRef<any>();
|
||||
const { container, baseElement } = render(
|
||||
<Base
|
||||
component={undefined as unknown as string}
|
||||
ellipsis={{ tooltip: 'This is tooltip', rows: 2 }}
|
||||
ref={ref}
|
||||
>
|
||||
Ant Design, a design language for background applications, is refined by Ant UED Team.
|
||||
</Base>,
|
||||
);
|
||||
triggerResize(ref.current);
|
||||
await sleep(20);
|
||||
|
||||
fireEvent.mouseEnter(container.firstChild!);
|
||||
await waitFor(() => {
|
||||
expect(baseElement.querySelector('.ant-tooltip-open')).not.toBeNull();
|
||||
});
|
||||
mockRectSpy.mockRestore();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
@ -200,43 +200,6 @@ describe('Typography.Ellipsis', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should use editConfig.text over children in editing mode ', async () => {
|
||||
const suffix = '--The information is very important';
|
||||
const ref = React.createRef<any>();
|
||||
const { container: wrapper, unmount } = render(
|
||||
<Base
|
||||
ellipsis={{ rows: 1, suffix }}
|
||||
component="p"
|
||||
editable={{ text: fullStr + suffix }}
|
||||
ref={ref}
|
||||
>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
fireEvent.click(wrapper.querySelector('.ant-typography-edit')!);
|
||||
|
||||
expect(wrapper.querySelector('textarea')?.textContent).toEqual(fullStr + suffix);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should use children as the fallback of editConfig.text in editing mode', async () => {
|
||||
const suffix = '--The information is very important';
|
||||
const ref = React.createRef<any>();
|
||||
const { container: wrapper, unmount } = render(
|
||||
<Base ellipsis={{ rows: 1, suffix }} component="p" ref={ref} editable>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
fireEvent.click(wrapper.querySelector('.ant-typography-edit')!);
|
||||
|
||||
expect(wrapper.querySelector('textarea')?.textContent).toEqual(fullStr);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('connect children', async () => {
|
||||
const bamboo = 'Bamboo';
|
||||
const is = ' is ';
|
||||
|
@ -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"
|
||||
|
15
package.json
15
package.json
@ -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"
|
||||
|
@ -36,6 +36,7 @@ const MAINTAINERS = [
|
||||
'kerm1it',
|
||||
'madccc',
|
||||
'MadCcc',
|
||||
'li-jia-nan',
|
||||
].map(author => author.toLowerCase());
|
||||
|
||||
const cwd = process.cwd();
|
||||
|
@ -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') {
|
||||
|
@ -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}
|
||||
|
@ -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(),
|
||||
|
@ -3,7 +3,8 @@
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"antd": ["components/index.tsx"],
|
||||
"antd/es/*": ["components/*"]
|
||||
"antd/es/*": ["components/*"],
|
||||
"antd/lib/*": ["components/*"]
|
||||
},
|
||||
"strictNullChecks": true,
|
||||
"module": "esnext",
|
||||
|
Loading…
Reference in New Issue
Block a user