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 (