From 9ffe5a04e0c4fda157fd4ace2f17ef9a9ab60150 Mon Sep 17 00:00:00 2001 From: Caspian Zhao Date: Mon, 2 Mar 2026 16:53:54 +0800 Subject: [PATCH] fix: recover gesture after display toggle regression (#885) --- e2e/11-display-toggle-regression.yaml | 40 +++++++++++++++++---------- src/components/Carousel.test.tsx | 39 ++++++++++++++++++++++++++ src/components/ScrollViewGesture.tsx | 29 +++++++++++++++++-- 3 files changed, 92 insertions(+), 16 deletions(-) diff --git a/e2e/11-display-toggle-regression.yaml b/e2e/11-display-toggle-regression.yaml index f4a65370..aaebb1fa 100644 --- a/e2e/11-display-toggle-regression.yaml +++ b/e2e/11-display-toggle-regression.yaml @@ -5,13 +5,19 @@ name: Display Toggle Gesture Regression # Navigate to E2E test page and reset defaults - runFlow: helpers/navigate-to-e2e.yaml # Baseline swipe: ensure gestures work before toggling display -- swipe: - start: "85%, 22%" - end: "15%, 22%" - duration: 500 - -- waitForAnimationToEnd: - timeout: 3000 +- repeat: + while: + notVisible: "Current Index: 1" + times: 3 + commands: + - tapOn: + id: "e2e-carousel" + - swipe: + start: "85%, 22%" + end: "15%, 22%" + duration: 500 + - waitForAnimationToEnd: + timeout: 3000 - assertVisible: "Current Index: 1" @@ -33,12 +39,18 @@ name: Display Toggle Gesture Regression - assertVisible: "Display: flex" # If issue exists, this swipe won't change index anymore -- swipe: - start: "85%, 22%" - end: "15%, 22%" - duration: 500 - -- waitForAnimationToEnd: - timeout: 3000 +- repeat: + while: + notVisible: "Current Index: 2" + times: 3 + commands: + - tapOn: + id: "e2e-carousel" + - swipe: + start: "85%, 22%" + end: "15%, 22%" + duration: 500 + - waitForAnimationToEnd: + timeout: 3000 - assertVisible: "Current Index: 2" diff --git a/src/components/Carousel.test.tsx b/src/components/Carousel.test.tsx index a2f9d867..af6a2639 100644 --- a/src/components/Carousel.test.tsx +++ b/src/components/Carousel.test.tsx @@ -373,6 +373,45 @@ describe("Test the real swipe behavior of Carousel to ensure it's working as exp } }); + it("display toggle regression #885: should keep swipe working after hide/show layout cycle", async () => { + const progress = { current: 0 }; + const Wrapper = createCarousel(progress); + const { getByTestId } = render(); + await verifyInitialRender(getByTestId); + + const initialPanCount = mockPan.mock.calls.length; + expect(initialPanCount).toBeGreaterThan(0); + + swipeToLeftOnce(); + await waitFor(() => expect(progress.current).toBe(1)); + + const contentContainer = getByTestId("carousel-content-container"); + expect(typeof contentContainer.props.onLayout).toBe("function"); + + act(() => { + contentContainer.props.onLayout?.({ + nativeEvent: { layout: { width: 0, height: 0 } }, + } as any); + }); + + act(() => { + contentContainer.props.onLayout?.({ + nativeEvent: { layout: { width: slideWidth, height: slideHeight } }, + } as any); + }); + + act(() => { + jest.runOnlyPendingTimers(); + }); + + await waitFor(() => { + expect(mockPan.mock.calls.length).toBeGreaterThan(initialPanCount); + }); + + swipeToLeftOnce(); + await waitFor(() => expect(progress.current).toBe(2)); + }); + it("`loop` prop: should swipe back to the first item when loop is true", async () => { const progress = { current: 0 }; const Wrapper = createCarousel(progress); diff --git a/src/components/ScrollViewGesture.tsx b/src/components/ScrollViewGesture.tsx index 81881d86..e3d41042 100644 --- a/src/components/ScrollViewGesture.tsx +++ b/src/components/ScrollViewGesture.tsx @@ -3,6 +3,7 @@ import React, { useCallback } from "react"; import type { LayoutChangeEvent, StyleProp, ViewStyle } from "react-native"; import type { GestureStateChangeEvent, + PanGesture, PanGestureHandlerEventPayload, } from "react-native-gesture-handler"; import { GestureDetector } from "react-native-gesture-handler"; @@ -70,6 +71,7 @@ const IScrollViewGesture: React.FC> = (props) => { } = props; const maxPage = dataLength; + const [gestureEpoch, bumpGestureEpoch] = React.useReducer((value: number) => value + 1, 0); const isHorizontal = useDerivedValue(() => !vertical, [vertical]); const max = useSharedValue(0); const panOffset = useSharedValue(undefined); // set to undefined when not actively in a pan gesture @@ -77,6 +79,7 @@ const IScrollViewGesture: React.FC> = (props) => { const validStart = useSharedValue(false); const scrollEndTranslation = useSharedValue(0); const scrollEndVelocity = useSharedValue(0); + const layoutWasHidden = useSharedValue(false); const containerRef = useAnimatedRef(); const maxScrollDistancePerSwipeIsSet = typeof maxScrollDistancePerSwipe === "number"; const minScrollDistancePerSwipeIsSet = typeof minScrollDistancePerSwipe === "number"; @@ -479,8 +482,15 @@ const IScrollViewGesture: React.FC> = (props) => { ] ); + const onConfigurePanGestureProxy = React.useCallback( + (gesture: PanGesture) => { + onConfigurePanGesture?.(gesture); + }, + [onConfigurePanGesture, gestureEpoch] + ); + const gesture = usePanGestureProxy({ - onConfigurePanGesture, + onConfigurePanGesture: onConfigurePanGestureProxy, onGestureStart, onGestureUpdate, onGestureEnd, @@ -495,6 +505,13 @@ const IScrollViewGesture: React.FC> = (props) => { const measuredHeight = e.nativeEvent.layout.height; const measuredSize = Math.round((vertical ? measuredHeight : measuredWidth) || 0); + if (measuredSize <= 0) { + layoutWasHidden.value = true; + } else if (layoutWasHidden.value) { + layoutWasHidden.value = false; + scheduleOnRN(bumpGestureEpoch); + } + if (!sizeExplicit && measuredSize > 0) { const current = resolvedSize.value ?? 0; if (Math.abs(current - measuredSize) > 0) { @@ -509,7 +526,15 @@ const IScrollViewGesture: React.FC> = (props) => { height: measuredHeight, }); }, - [updateContainerSize, resolvedSize, sizePhase, vertical, sizeExplicit] + [ + updateContainerSize, + resolvedSize, + sizePhase, + vertical, + sizeExplicit, + layoutWasHidden, + bumpGestureEpoch, + ] ); return (