fix: input work with Upload (#51874)
Some checks are pending
Publish Any Commit / build (push) Waiting to run
🔀 Sync mirror to Gitee / mirror (push) Waiting to run
✅ test / lint (push) Waiting to run
✅ test / test-react-legacy (16, 1/2) (push) Waiting to run
✅ test / test-react-legacy (16, 2/2) (push) Waiting to run
✅ test / test-react-legacy (17, 1/2) (push) Waiting to run
✅ test / test-react-legacy (17, 2/2) (push) Waiting to run
✅ test / test-node (push) Waiting to run
✅ test / test-react-latest (dom, 1/2) (push) Waiting to run
✅ test / test-react-latest (dom, 2/2) (push) Waiting to run
✅ test / test-react-latest-dist (dist, 1/2) (push) Blocked by required conditions
✅ test / test-react-latest-dist (dist, 2/2) (push) Blocked by required conditions
✅ test / test-react-latest-dist (dist-min, 1/2) (push) Blocked by required conditions
✅ test / test-react-latest-dist (dist-min, 2/2) (push) Blocked by required conditions
✅ test / test-coverage (push) Blocked by required conditions
✅ test / build (push) Waiting to run
✅ test / test lib/es module (es, 1/2) (push) Waiting to run
✅ test / test lib/es module (es, 2/2) (push) Waiting to run
✅ test / test lib/es module (lib, 1/2) (push) Waiting to run
✅ test / test lib/es module (lib, 2/2) (push) Waiting to run
👁️ Visual Regression Persist Start / test image (push) Waiting to run

* fix: input work with Upload

* test: update snapshot
This commit is contained in:
二货爱吃白萝卜 2024-12-03 19:07:28 +08:00 committed by GitHub
parent f94261d1c9
commit 0e9ac35e3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 347 additions and 3 deletions

View File

@ -13,6 +13,7 @@ import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
import { FormItemInputContext } from '../form/context';
import GroupContext from './GroupContext';
import useStyle from './style';
import useBubbleLock from './useBubbleLock';
export interface AbstractCheckboxProps<T> {
prefixCls?: string;
@ -153,6 +154,11 @@ const InternalCheckbox: React.ForwardRefRenderFunction<CheckboxRef, CheckboxProp
TARGET_CLS,
hashId,
);
// ============================ Event Lock ============================
const [onLabelClick, onInputClick] = useBubbleLock(checkboxProps.onClick);
// ============================== Render ==============================
return wrapCSSVar(
<Wave component="Checkbox" disabled={mergedDisabled}>
<label
@ -160,10 +166,12 @@ const InternalCheckbox: React.ForwardRefRenderFunction<CheckboxRef, CheckboxProp
style={{ ...checkbox?.style, ...style }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={onLabelClick}
>
{/* @ts-ignore */}
<RcCheckbox
{...checkboxProps}
onClick={onInputClick}
prefixCls={prefixCls}
className={checkboxClass}
disabled={mergedDisabled}

View File

@ -5,13 +5,21 @@ import { resetWarned } from '../../_util/warning';
import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render } from '../../../tests/utils';
import { act, fireEvent, render } from '../../../tests/utils';
describe('Checkbox', () => {
focusTest(Checkbox, { refFocus: true });
mountTest(Checkbox);
rtlTest(Checkbox);
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('responses hover events', () => {
const onMouseEnter = jest.fn();
const onMouseLeave = jest.fn();
@ -63,4 +71,36 @@ describe('Checkbox', () => {
expect(checkboxInput.indeterminate).toBe(false);
});
it('event bubble should not trigger twice', () => {
const onClick = jest.fn();
const onRootClick = jest.fn();
const { container } = render(
<div onClick={onRootClick}>
<Checkbox onClick={onClick} />
</div>,
);
// Click on label
fireEvent.click(container.querySelector('label')!);
expect(onClick).toHaveBeenCalledTimes(1);
expect(onRootClick).toHaveBeenCalledTimes(1);
act(() => {
jest.runAllTimers();
});
// Click on input
fireEvent.click(container.querySelector('input')!);
expect(onClick).toHaveBeenCalledTimes(2);
expect(onRootClick).toHaveBeenCalledTimes(2);
act(() => {
jest.runAllTimers();
});
// Click on input again
fireEvent.click(container.querySelector('input')!);
expect(onClick).toHaveBeenCalledTimes(3);
expect(onRootClick).toHaveBeenCalledTimes(3);
});
});

View File

@ -0,0 +1,37 @@
import React from 'react';
import raf from 'rc-util/lib/raf';
/**
* When click on the label,
* the event will be stopped to prevent the label from being clicked twice.
* label click -> input click -> label click again
*/
export default function useBubbleLock(
onOriginInputClick?: React.MouseEventHandler<HTMLInputElement>,
) {
const labelClickLockRef = React.useRef<number | null>(null);
const clearLock = () => {
raf.cancel(labelClickLockRef.current!);
labelClickLockRef.current = null;
};
const onLabelClick: React.MouseEventHandler<HTMLLabelElement> = () => {
clearLock();
labelClickLockRef.current = raf(() => {
labelClickLockRef.current = null;
});
};
const onInputClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
if (labelClickLockRef.current) {
e.stopPropagation();
clearLock();
}
onOriginInputClick?.(e);
};
return [onLabelClick, onInputClick] as const;
}

View File

@ -429,6 +429,101 @@ exports[`renders components/radio/demo/component-token.tsx extend context correc
exports[`renders components/radio/demo/component-token.tsx extend context correctly 2`] = `[]`;
exports[`renders components/radio/demo/debug-upload.tsx extend context correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<span
class="ant-upload-wrapper"
>
<div
class="ant-upload ant-upload-select"
>
<span
class="ant-upload"
>
<input
accept=""
name="file"
style="display: none;"
type="file"
/>
<label
class="ant-radio-wrapper"
>
<span
class="ant-radio ant-wave-target"
>
<input
class="ant-radio-input"
type="radio"
/>
<span
class="ant-radio-inner"
/>
</span>
<span>
Radio
</span>
</label>
</span>
</div>
<div
class="ant-upload-list ant-upload-list-text"
/>
</span>
</div>
<div
class="ant-space-item"
>
<span
class="ant-upload-wrapper"
>
<div
class="ant-upload ant-upload-select"
>
<span
class="ant-upload"
>
<input
accept=""
name="file"
style="display: none;"
type="file"
/>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox ant-wave-target"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
<span>
Checkbox
</span>
</label>
</span>
</div>
<div
class="ant-upload-list ant-upload-list-text"
/>
</span>
</div>
</div>
`;
exports[`renders components/radio/demo/debug-upload.tsx extend context correctly 2`] = `[]`;
exports[`renders components/radio/demo/disabled.tsx extend context correctly 1`] = `
Array [
<label

View File

@ -423,6 +423,99 @@ exports[`renders components/radio/demo/component-token.tsx correctly 1`] = `
</div>
`;
exports[`renders components/radio/demo/debug-upload.tsx correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
>
<div
class="ant-space-item"
>
<span
class="ant-upload-wrapper"
>
<div
class="ant-upload ant-upload-select"
>
<span
class="ant-upload"
>
<input
accept=""
name="file"
style="display:none"
type="file"
/>
<label
class="ant-radio-wrapper"
>
<span
class="ant-radio ant-wave-target"
>
<input
class="ant-radio-input"
type="radio"
/>
<span
class="ant-radio-inner"
/>
</span>
<span>
Radio
</span>
</label>
</span>
</div>
<div
class="ant-upload-list ant-upload-list-text"
/>
</span>
</div>
<div
class="ant-space-item"
>
<span
class="ant-upload-wrapper"
>
<div
class="ant-upload ant-upload-select"
>
<span
class="ant-upload"
>
<input
accept=""
name="file"
style="display:none"
type="file"
/>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox ant-wave-target"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
<span>
Checkbox
</span>
</label>
</span>
</div>
<div
class="ant-upload-list ant-upload-list-text"
/>
</span>
</div>
</div>
`;
exports[`renders components/radio/demo/disabled.tsx correctly 1`] = `
Array [
<label

View File

@ -4,7 +4,7 @@ import Radio, { Button, Group } from '..';
import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render } from '../../../tests/utils';
import { act, fireEvent, render } from '../../../tests/utils';
import Form from '../../form';
describe('Radio', () => {
@ -17,6 +17,14 @@ describe('Radio', () => {
rtlTest(Group);
rtlTest(Button);
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should render correctly', () => {
const { container } = render(<Radio className="customized">Test</Radio>);
expect(container.firstChild).toMatchSnapshot();
@ -58,4 +66,36 @@ describe('Radio', () => {
it('have static property for type detecting', () => {
expect(Radio.__ANT_RADIO).toBeTruthy();
});
it('event bubble should not trigger twice', () => {
const onClick = jest.fn();
const onRootClick = jest.fn();
const { container } = render(
<div onClick={onRootClick}>
<Radio onClick={onClick} />
</div>,
);
// Click on label
fireEvent.click(container.querySelector('label')!);
expect(onClick).toHaveBeenCalledTimes(1);
expect(onRootClick).toHaveBeenCalledTimes(1);
act(() => {
jest.runAllTimers();
});
// Click on input
fireEvent.click(container.querySelector('input')!);
expect(onClick).toHaveBeenCalledTimes(2);
expect(onRootClick).toHaveBeenCalledTimes(2);
act(() => {
jest.runAllTimers();
});
// Click on input again
fireEvent.click(container.querySelector('input')!);
expect(onClick).toHaveBeenCalledTimes(3);
expect(onRootClick).toHaveBeenCalledTimes(3);
});
});

View File

@ -0,0 +1,7 @@
## zh-CN
最简单的用法。
## en-US
The simplest use.

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Checkbox, Radio, Space, Upload } from 'antd';
const App: React.FC = () => (
<Space>
<Upload>
<Radio>Radio</Radio>
</Upload>
<Upload>
<Checkbox>Checkbox</Checkbox>
</Upload>
</Space>
);
export default App;

View File

@ -30,6 +30,7 @@ demo:
<code src="./demo/badge.tsx" debug>Badge style</code>
<code src="./demo/wireframe.tsx" debug>Wireframe</code>
<code src="./demo/component-token.tsx" debug>Component Token</code>
<code src="./demo/debug-upload.tsx" debug>Upload Debug</code>
<!-- prettier-ignore-end -->
## API

View File

@ -31,6 +31,7 @@ demo:
<code src="./demo/badge.tsx" debug>测试 Badge 的样式</code>
<code src="./demo/wireframe.tsx" debug>线框风格</code>
<code src="./demo/component-token.tsx" debug>组件 Token</code>
<code src="./demo/debug-upload.tsx" debug>Upload Debug</code>
<!-- prettier-ignore-end -->
## API

View File

@ -6,6 +6,7 @@ import { composeRef } from 'rc-util/lib/ref';
import { devUseWarning } from '../_util/warning';
import Wave from '../_util/wave';
import { TARGET_CLS } from '../_util/wave/interface';
import useBubbleLock from '../checkbox/useBubbleLock';
import { ConfigContext } from '../config-provider';
import DisabledContext from '../config-provider/DisabledContext';
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
@ -82,6 +83,10 @@ const InternalRadio: React.ForwardRefRenderFunction<RadioRef, RadioProps> = (pro
rootCls,
);
// ============================ Event Lock ============================
const [onLabelClick, onInputClick] = useBubbleLock(radioProps.onClick);
// ============================== Render ==============================
return wrapCSSVar(
<Wave component="Radio" disabled={radioProps.disabled}>
<label
@ -90,6 +95,7 @@ const InternalRadio: React.ForwardRefRenderFunction<RadioRef, RadioProps> = (pro
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
title={title}
onClick={onLabelClick}
>
{/* @ts-ignore */}
<RcCheckbox
@ -98,6 +104,7 @@ const InternalRadio: React.ForwardRefRenderFunction<RadioRef, RadioProps> = (pro
type="radio"
prefixCls={prefixCls}
ref={mergedRef}
onClick={onInputClick}
/>
{children !== undefined ? <span>{children}</span> : null}
</label>

View File

@ -231,7 +231,7 @@
"cross-fetch": "^4.0.0",
"dekko": "^0.2.1",
"dotenv": "^16.4.5",
"dumi": "~2.4.13",
"dumi": "~2.4.14",
"dumi-plugin-color-chunk": "^1.1.2",
"eslint": "^9.13.0",
"eslint-plugin-compat": "^6.0.1",