From 76a07a0d8d8aca841fcf1ba9f7f2379bf437462c 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: Sun, 2 Jul 2023 20:01:46 +0800 Subject: [PATCH 1/2] fix: solve the issue that input-number's value auto change after fast click step handle in safari --- src/StepHandler.tsx | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/StepHandler.tsx b/src/StepHandler.tsx index 10a66ea4..69cbf66a 100644 --- a/src/StepHandler.tsx +++ b/src/StepHandler.tsx @@ -32,13 +32,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 +60,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(cancelAnimationFrame); + }, []); // ======================= Render ======================= const isMobile = useMobile(); @@ -74,11 +80,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(requestAnimationFrame(onStopStep)); + const sharedHandlerProps = { unselectable: 'on' as const, role: 'button', - onMouseUp: onStopStep, - onMouseLeave: onStopStep, + onMouseUp: safeOnStopStep, + onMouseLeave: safeOnStopStep, }; return ( From 957cc8984ca3cd6dbcfc3d20b824079157d39a71 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 10:45:56 +0800 Subject: [PATCH 2/2] feat: add test case --- src/StepHandler.tsx | 5 +++-- tests/longPress.test.tsx | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/StepHandler.tsx b/src/StepHandler.tsx index 69cbf66a..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. @@ -62,7 +63,7 @@ export default function StepHandler({ React.useEffect(() => () => { onStopStep(); - frameIds.current.forEach(cancelAnimationFrame); + frameIds.current.forEach(id => raf.cancel(id)); }, []); // ======================= Render ======================= @@ -85,7 +86,7 @@ export default function StepHandler({ // 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(requestAnimationFrame(onStopStep)); + const safeOnStopStep = () => frameIds.current.push(raf(onStopStep)); const sharedHandlerProps = { unselectable: 'on' as const, 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)); + }); });