From 204f28650efba2929279c366d0f2944ea5802c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kiner-tang=28=E6=96=87=E8=BE=89=29?= <1127031143@qq.com> Date: Mon, 3 Jul 2023 13:57:30 +0800 Subject: [PATCH] fix: solve the issue that input-number's value auto change after fast click step handle in safari (#585) * fix: solve the issue that input-number's value auto change after fast click step handle in safari * feat: add test case --- src/StepHandler.tsx | 28 +++++++++++++++++++++------- tests/longPress.test.tsx | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/StepHandler.tsx b/src/StepHandler.tsx index 10a66ea4..aaafb41e 100644 --- a/src/StepHandler.tsx +++ b/src/StepHandler.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import classNames from 'classnames'; import useMobile from 'rc-util/lib/hooks/useMobile'; +import raf from 'rc-util/lib/raf'; /** * When click and hold on a button - the speed of auto changing the value. @@ -32,13 +33,20 @@ export default function StepHandler({ }: StepHandlerProps) { // ======================== Step ======================== const stepTimeoutRef = React.useRef(); + const frameIds = React.useRef([]); const onStepRef = React.useRef(); onStepRef.current = onStep; + const onStopStep = () => { + clearTimeout(stepTimeoutRef.current); + }; + + // We will interval update step when hold mouse down const onStepMouseDown = (e: React.MouseEvent, up: boolean) => { e.preventDefault(); + onStopStep(); onStepRef.current(up); @@ -53,11 +61,10 @@ export default function StepHandler({ stepTimeoutRef.current = setTimeout(loopStep, STEP_DELAY); }; - const onStopStep = () => { - clearTimeout(stepTimeoutRef.current); - }; - - React.useEffect(() => onStopStep, []); + React.useEffect(() => () => { + onStopStep(); + frameIds.current.forEach(id => raf.cancel(id)); + }, []); // ======================= Render ======================= const isMobile = useMobile(); @@ -74,11 +81,18 @@ export default function StepHandler({ [`${handlerClassName}-down-disabled`]: downDisabled, }); + // fix: https://github.com/ant-design/ant-design/issues/43088 + // In Safari, When we fire onmousedown and onmouseup events in quick succession, + // there may be a problem that the onmouseup events are executed first, + // resulting in a disordered program execution. + // So, we need to use requestAnimationFrame to ensure that the onmouseup event is executed after the onmousedown event. + const safeOnStopStep = () => frameIds.current.push(raf(onStopStep)); + const sharedHandlerProps = { unselectable: 'on' as const, role: 'button', - onMouseUp: onStopStep, - onMouseLeave: onStopStep, + onMouseUp: safeOnStopStep, + onMouseLeave: safeOnStopStep, }; return ( diff --git a/tests/longPress.test.tsx b/tests/longPress.test.tsx index da9b8873..df8fe995 100644 --- a/tests/longPress.test.tsx +++ b/tests/longPress.test.tsx @@ -35,4 +35,36 @@ describe('InputNumber.LongPress', () => { }); await waitFor(() => expect(onChange).toHaveBeenCalledWith(14)); }); + + it('Simulates event calls out of order in Safari', async () => { + const onChange = jest.fn(); + const { container } = render(); + fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + act(() => { + jest.advanceTimersByTime(10); + }); + fireEvent.mouseUp(container.querySelector('.rc-input-number-handler-up')); + act(() => { + jest.advanceTimersByTime(10); + }); + fireEvent.mouseUp(container.querySelector('.rc-input-number-handler-up')); + act(() => { + jest.advanceTimersByTime(10); + }); + fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + act(() => { + jest.advanceTimersByTime(10); + }); + fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + act(() => { + jest.advanceTimersByTime(10); + }); + fireEvent.mouseUp(container.querySelector('.rc-input-number-handler-up')); + + act(() => { + jest.advanceTimersByTime(600 + 200 * 5 + 100); + }); + + await waitFor(() => expect(onChange).toBeCalledTimes(3)); + }); });