diff --git a/src/PickerInput/Selector/Input.tsx b/src/PickerInput/Selector/Input.tsx index 22051545c..7e2fae5da 100644 --- a/src/PickerInput/Selector/Input.tsx +++ b/src/PickerInput/Selector/Input.tsx @@ -97,6 +97,8 @@ const Input = React.forwardRef((props, ref) => { // ========================= Refs ========================= const holderRef = React.useRef(null); const inputRef = React.useRef(null); + // When mousedown get focus, defer selection to mouseUp so click position is used + const mouseDownRef = React.useRef(false); React.useImperativeHandle(ref, () => ({ nativeElement: holderRef.current, @@ -153,6 +155,12 @@ const Input = React.forwardRef((props, ref) => { }; const onFormatPaste: React.ClipboardEventHandler = (event) => { + // Block paste until selection is set (after mouseUp when focus was by mousedown) + if (mouseDownRef.current) { + event.preventDefault(); + return; + } + // Get paste text const pasteText = event.clipboardData.getData('text'); @@ -164,8 +172,6 @@ const Input = React.forwardRef((props, ref) => { // ======================== Mouse ========================= // When `mouseDown` get focus, it's better to not to change the selection // Since the up position maybe not is the first cell - const mouseDownRef = React.useRef(false); - const onFormatMouseDown: React.MouseEventHandler = () => { mouseDownRef.current = true; }; @@ -221,6 +227,12 @@ const Input = React.forwardRef((props, ref) => { }; const onFormatKeyDown: React.KeyboardEventHandler = (event) => { + // Block key input until selection is set (after mouseUp when focus was by mousedown) + if (mouseDownRef.current) { + event.preventDefault(); + return; + } + onSharedKeyDown(event); const { key } = event; diff --git a/tests/new-range.spec.tsx b/tests/new-range.spec.tsx index f9faa18ee..4d80c6a4b 100644 --- a/tests/new-range.spec.tsx +++ b/tests/new-range.spec.tsx @@ -1,5 +1,5 @@ // In theory, all RangePicker test cases should be paired with SinglePicker -import { act, fireEvent, render } from '@testing-library/react'; +import { act, createEvent, fireEvent, render } from '@testing-library/react'; import dayjs, { type Dayjs } from 'dayjs'; import 'dayjs/locale/ar'; import { spyElementPrototype } from '@rc-component/util/lib/test/domHook'; @@ -995,6 +995,27 @@ describe('NewPicker.Range', () => { expect(onChange).toHaveBeenCalledWith(expect.anything(), ['20200903', '20200905']); }); + it('blocks paste while mouse is down', () => { + const { container } = render(); + + const startInput = container.querySelectorAll('input')[0]; + + // Simulate focus gained by mousedown, then paste before mouse up. + fireEvent.mouseDown(startInput); + fireEvent.focus(startInput); + + const pasteEvent = createEvent.paste(startInput, { + clipboardData: { + getData: () => '20200903', + }, + }); + pasteEvent.preventDefault = jest.fn(); + fireEvent(startInput, pasteEvent); + + // Guard in Input should prevent default while mouse is down + expect(pasteEvent.preventDefault).toHaveBeenCalled(); + }); + it('click to change selection cell', () => { const { container } = render(); @@ -1010,6 +1031,38 @@ describe('NewPicker.Range', () => { expect(startInput.selectionEnd).toEqual(6); }); + it('blocks key input while mouse is down', () => { + const { container } = render(); + + const startInput = container.querySelectorAll('input')[0]; + + // Simulate focus gained by mousedown, then key input before mouse up. + fireEvent.mouseDown(startInput); + fireEvent.focus(startInput); + + const keyDownEvent = createEvent.keyDown(startInput, { + key: '1', + }); + keyDownEvent.preventDefault = jest.fn(); + fireEvent(startInput, keyDownEvent); + + // Guard in Input should prevent default while mouse is down + expect(keyDownEvent.preventDefault).toHaveBeenCalled(); + }); + + it('focus by mousedown defers selection sync to mouseUp', () => { + const { container } = render(); + + const startInput = container.querySelectorAll('input')[0]; + + fireEvent.mouseDown(startInput); + fireEvent.focus(startInput); + + fireEvent.mouseUp(startInput); + expect(startInput.selectionStart).toBeDefined(); + expect(startInput.selectionEnd).toBeDefined(); + }); + it('blur to reset back text', async () => { const { container } = render();