Merge branch 'master' into next-merge-master

This commit is contained in:
MadCcc 2022-10-14 11:35:18 +08:00
commit 4490d9dbdc
29 changed files with 252 additions and 197 deletions

View File

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

View File

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

View File

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

View File

@ -1,17 +1,26 @@
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();
});
beforeEach(() => {
document.body.innerHTML = '';
});
afterEach(() => {
jest.clearAllTimers();
const styles = document.getElementsByTagName('style');
for (let i = 0; i < styles.length; i += 1) {
styles[i].remove();
@ -69,7 +78,7 @@ describe('Wave component', () => {
</Wave>,
);
container.querySelector('button')?.click();
await sleep(0);
await waitFakeTimer();
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
container.querySelector('button')?.getRootNode() as HTMLButtonElement
).getElementsByTagName('style');
@ -87,7 +96,7 @@ describe('Wave component', () => {
</Wave>,
);
container.querySelector('button')?.click();
await sleep(200);
await waitFakeTimer();
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
container.querySelector('button')?.getRootNode() as HTMLButtonElement
).getElementsByTagName('style');
@ -104,7 +113,7 @@ describe('Wave component', () => {
</Wave>,
);
container.querySelector('div')?.click();
await sleep(0);
await waitFakeTimer();
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
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();
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
container.querySelector('div')?.getRootNode() as HTMLDivElement
).getElementsByTagName('style');
@ -138,7 +147,7 @@ describe('Wave component', () => {
</Wave>,
);
container.querySelector('div')?.click();
await sleep(0);
await waitFakeTimer();
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
container.querySelector('div')?.getRootNode() as HTMLDivElement
).getElementsByTagName('style');
@ -157,7 +166,7 @@ describe('Wave component', () => {
</Wave>,
);
container.querySelector('button')?.click();
await sleep(0);
await waitFakeTimer();
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
container.querySelector('button')?.getRootNode() as HTMLButtonElement
).getElementsByTagName('style');
@ -175,7 +184,7 @@ describe('Wave component', () => {
</ConfigProvider>,
);
container.querySelector('button')?.click();
await sleep(0);
await waitFakeTimer();
let styles: HTMLCollectionOf<HTMLStyleElement> | HTMLStyleElement[] = (
container.querySelector('button')?.getRootNode() as HTMLButtonElement
).getElementsByTagName('style');
@ -242,7 +251,7 @@ describe('Wave component', () => {
</Wave>,
);
fireEvent.click(container.querySelector('button')!);
await sleep(200);
await waitFakeTimer();
let styles = (container.querySelector('button')!.getRootNode() as any).getElementsByTagName(
'style',
);
@ -261,7 +270,7 @@ describe('Wave component', () => {
</Wave>,
);
fireEvent.click(container.querySelector('button')!);
await sleep(200);
await waitFakeTimer();
let styles = (container.querySelector('button')!.getRootNode() as any).getElementsByTagName(
'style',
);

View File

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

View File

@ -1,9 +1,9 @@
import React from 'react';
import type { InternalAffixClass } from '..';
import type { AffixProps, 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';
@ -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,16 @@ describe('Affix Render', () => {
expect(container.querySelector('.ant-affix')).toBeFalsy();
});
it('Anchor correct render when target is null', async () => {
expect(() => {
render(<Affix target={null as unknown as AffixProps['target']}>test</Affix>);
}).not.toThrow();
});
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 +144,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 +184,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 +193,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);
@ -210,7 +221,7 @@ describe('Affix Render', () => {
},
);
await sleep(20);
await waitFakeTimer();
await movePlaceholder(300);
expect(affixInstance!.state.affixStyle).toBeTruthy();
});
@ -221,8 +232,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,9 +246,6 @@ describe('Affix Render', () => {
await waitFakeTimer();
expect(updateCalled).toHaveBeenCalled();
jest.clearAllTimers();
jest.useRealTimers();
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -204,7 +204,7 @@ class Spin extends React.Component<SpinClassProps, SpinState> {
}
}
const SpinFC: SpinFCType = (props: SpinProps) => {
const SpinFC: SpinFCType = props => {
const { prefixCls: customizePrefixCls } = props;
const { getPrefixCls } = React.useContext(ConfigContext);

View File

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

View File

@ -5,7 +5,6 @@ import Statistic from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render, sleep } from '../../../tests/utils';
import type Countdown from '../Countdown';
import { formatTimeStr } from '../utils';
describe('Statistic', () => {
@ -105,13 +104,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);
@ -161,11 +155,9 @@ describe('Statistic', () => {
describe('time finished', () => {
it('not call if time already passed', () => {
const now = Date.now() - 1000;
const instance = React.createRef<Countdown>();
const onFinish = jest.fn();
render(<Statistic.Countdown ref={instance} value={now} onFinish={onFinish} />);
render(<Statistic.Countdown value={now} onFinish={onFinish} />);
expect(instance.current!.countdownId).toBe(undefined);
expect(onFinish).not.toHaveBeenCalled();
});

View File

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

View File

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

View File

@ -20374,6 +20374,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"
@ -20680,6 +20681,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"
@ -20765,6 +20767,7 @@ Array [
aria-label="Address sortable"
class="ant-table-cell ant-table-cell-ellipsis ant-table-column-has-sorters"
tabindex="0"
title="Address"
>
<div
class="ant-table-filter-column"

View File

@ -15392,6 +15392,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"
@ -15486,6 +15487,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"
@ -15547,6 +15549,7 @@ Array [
aria-label="Address sortable"
class="ant-table-cell ant-table-cell-ellipsis ant-table-column-has-sorters"
tabindex="0"
title="Address"
>
<div
class="ant-table-filter-column"

View File

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

View File

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

View File

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

View File

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

View File

@ -185,9 +185,9 @@
"@types/react-window": "^1.8.2",
"@types/shallowequal": "^1.1.1",
"@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 +291,7 @@
"stylelint-order": "^5.0.0",
"theme-switcher": "^1.0.2",
"ts-node": "^10.8.2",
"typescript": "~4.8.0",
"typescript": "~4.8.4",
"webpack-bundle-analyzer": "^4.1.0",
"xhr-mock": "^2.4.1",
"yaml-front-matter": "^4.0.0"

View File

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

View File

@ -64,10 +64,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 => {
@ -181,7 +183,7 @@ class Demo extends React.Component {
</body>
</html>`;
const sourceCode = this.getSourceCode();
const [sourceCode, sourceCodeTyped] = this.getSourceCode();
const dependencies = sourceCode.split('\n').reduce(
(acc, line) => {
@ -422,7 +424,7 @@ createRoot(document.getElementById('container')).render(<Demo />);
<ThunderboltOutlined className="code-box-stackblitz" />
</span>
</Tooltip>
<CopyToClipboard text={sourceCode} onCopy={() => this.handleCodeCopied(meta.id)}>
<CopyToClipboard text={sourceCodeTyped} onCopy={() => this.handleCodeCopied(meta.id)}>
<Tooltip
open={copyTooltipOpen}
onOpenChange={this.onCopyTooltipOpenChange}

View File

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