Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions e2e/tests/issues/issue-1050-back-gesture.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { freshAppBeforeEach, expect, test } from '../support/test.ts';
import type { App, Screen } from '../support/test.ts';
import { openExample, restoreLtrAfterEach } from '../support/app.ts';
import type { LayoutDirection } from '../support/app.ts';

freshAppBeforeEach();

// iOS-only: the fix arbitrates against the navigation controller's back
// gesture. A mid-screen back swipe is the full-screen content pop of iOS 26,
// which the native stack gets without fullScreenGestureEnabled.
const options = { tags: ['regression', 'issue-1050'], platforms: ['ios'] };

const EXAMPLE = 'Issue #1050 Back Gesture Repro';
const NESTED_EXAMPLE = 'Issue #1050 Nested Stack Back Gesture Repro';

async function openRepro(
app: App,
screen: Screen,
example: string,
direction: LayoutDirection = 'ltr'
): Promise<void> {
await openExample(app, screen, example, direction);
await expectPage(screen, 0);
}

/**
* Drags across the middle of the pager, away from the screen edge so the edge
* pop recognizer can't take it. `back` drags the way a back swipe goes in the
* layout direction, otherwise towards the next page.
*/
async function dragAcrossPager(
screen: Screen,
direction: LayoutDirection,
back: boolean
): Promise<void> {
const box = await screen.getByTestId('issue-1050-pager').boundingBox();
if (!box) throw new Error('issue-1050-pager has no bounds');
const y = box.y + box.height / 2;
const left = box.x + box.width * 0.3;
const right = box.x + box.width * 0.9;
const towardsRight = back === (direction === 'ltr');
await screen.swipe({
from: { x: towardsRight ? left : right, y },
to: { x: towardsRight ? right : left, y },
duration: 300,
});
}

async function expectPage(screen: Screen, page: number): Promise<void> {
await expect(screen.getByTestId('issue-1050-status')).toHaveText(
`Selected: ${page}`
);
}

/** The home list stays scrolled to the example's entry after the pop. */
async function expectPopped(screen: Screen, example: string): Promise<void> {
await expect(screen.getByTestId(example)).toBeVisible();
await expect(screen.getByTestId('issue-1050-pager')).toBeHidden();
}

function describeBackGesture(direction: LayoutDirection): void {
test(
'a back swipe on the first page pops the screen',
options,
async ({ app, screen }) => {
await openRepro(app, screen, EXAMPLE, direction);

// Before the fix the pager claimed the drag and the screen stayed.
await dragAcrossPager(screen, direction, true);
await expectPopped(screen, EXAMPLE);
}
);

test(
'a back swipe on a later page goes to the previous page',
options,
async ({ app, screen }) => {
await openRepro(app, screen, EXAMPLE, direction);
await dragAcrossPager(screen, direction, false);
await expectPage(screen, 1);

await dragAcrossPager(screen, direction, true);
await expectPage(screen, 0);
}
);

test(
'a forward swipe on the first page still goes to the next page',
options,
async ({ app, screen }) => {
await openRepro(app, screen, EXAMPLE, direction);

await dragAcrossPager(screen, direction, false);
await expectPage(screen, 1);
}
);
}

test.describe('back gesture over the pager', () => {
test.describe('in LTR', () => {
describeBackGesture('ltr');
});

test.describe('in RTL', () => {
restoreLtrAfterEach();
describeBackGesture('rtl');
});
});

test.describe('back gesture over a pager in a nested stack', () => {
test(
'a back swipe on the first page pops the outer stack',
options,
async ({ app, screen }) => {
await openRepro(app, screen, NESTED_EXAMPLE);

// The inner stack has only its root screen. Before skipping stacks that
// can't pop, the pager stopped there and kept the drag.
await dragAcrossPager(screen, 'ltr', true);
await expectPopped(screen, NESTED_EXAMPLE);
}
);

test(
'a forward swipe on the first page still goes to the next page',
options,
async ({ app, screen }) => {
await openRepro(app, screen, NESTED_EXAMPLE);

await dragAcrossPager(screen, 'ltr', false);
await expectPage(screen, 1);
}
);
});
20 changes: 19 additions & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,17 @@ import CoverflowExample from './tabView/CoverflowExample';
import { TabViewInsideScrollViewExample } from './tabView/TabViewInsideScrollViewExample';
import ReanimatedOnPageScrollExample from './ReanimatedOnPageScrollExample';
import { MaterialTopBarExample } from './MaterialTopTabExample';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import {
createNativeStackNavigator,
type NativeStackNavigationOptions,
} from '@react-navigation/native-stack';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { PagerHookExample } from './PagerHookExample';
import { NestedHorizontalScrollViewExample } from './NestedHorizontalScrollViewExample';
import {
Issue1050BackGestureRepro,
Issue1050NestedStackBackGestureRepro,
} from './gh-issues/Issue1050BackGestureRepro';
import { Issue1096KeyboardShrinkRepro } from './gh-issues/Issue1096KeyboardShrinkRepro';
import { Issue1098NestedPagerRepro } from './gh-issues/Issue1098NestedPagerRepro';
import { Issue1099SafeAreaRepro } from './gh-issues/Issue1099SafeAreaRepro';
Expand Down Expand Up @@ -69,6 +76,7 @@ type Example = {
component: React.ComponentType;
name: string;
testID?: string;
options?: NativeStackNavigationOptions;
};

const chevron =
Expand All @@ -82,6 +90,7 @@ const sections: { title: string; data: Example[] }[] = [
component: BasicPagerViewExampleScreen,
name: 'Basic Example',
testID: 'example-basic-horizontal',
options: { fullScreenGestureEnabled: false },
},
{
component: VerticalBasicPagerViewExampleScreen,
Expand Down Expand Up @@ -147,6 +156,14 @@ const sections: { title: string; data: Example[] }[] = [
component: Issue1083ModalSetPageExample,
name: 'Issue #1083 Modal SetPage Repro',
},
{
component: Issue1050BackGestureRepro,
name: 'Issue #1050 Back Gesture Repro',
},
{
component: Issue1050NestedStackBackGestureRepro,
name: 'Issue #1050 Nested Stack Back Gesture Repro',
},
{
component: Issue1096KeyboardShrinkRepro,
name: 'Issue #1096 Keyboard Shrink Repro',
Expand Down Expand Up @@ -290,6 +307,7 @@ export function Navigation() {
key={index}
name={example.name}
component={example.component}
options={example.options}
/>
))}
<NavigationStack.Screen
Expand Down
58 changes: 58 additions & 0 deletions example/src/gh-issues/Issue1050BackGestureRepro.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Repro for #1050: a horizontal pager on a stack screen. On iOS 26 a back
* swipe started mid-screen on page 0 should close the screen and return to the
* example list; on later pages it should change the page.
*
* The nested variant puts the same screen on the root of a native stack that
* is itself a screen of the app's stack. The inner stack can't go back, the
* outer one can, and the back swipe should still close the screen.
*/
import React, { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import PagerView from 'react-native-pager-view';

const InnerStack = createNativeStackNavigator();

export function Issue1050BackGestureRepro() {
const [selectedPage, setSelectedPage] = useState(0);

return (
<View style={styles.container}>
<Text testID="issue-1050-status" style={styles.status}>
{`Selected: ${selectedPage}`}
</Text>
<PagerView
testID="issue-1050-pager"
style={styles.pager}
initialPage={0}
onPageSelected={(e) => setSelectedPage(e.nativeEvent.position)}
>
{[0, 1, 2].map((page) => (
<View key={page} style={styles.page}>
<Text style={styles.label}>{`Page ${page}`}</Text>
</View>
))}
</PagerView>
</View>
);
}

export function Issue1050NestedStackBackGestureRepro() {
return (
<InnerStack.Navigator screenOptions={{ headerShown: false }}>
<InnerStack.Screen
name="Issue1050InnerRoot"
component={Issue1050BackGestureRepro}
/>
</InnerStack.Navigator>
);
}

const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: 'white' },
status: { padding: 16, fontSize: 16 },
pager: { flex: 1 },
page: { flex: 1, alignItems: 'center', justifyContent: 'center' },
label: { fontSize: 24 },
});
1 change: 1 addition & 0 deletions ios/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ let package = Package(
path: ".",
sources: [
"Extensions.swift",
"PagerGestureObserver.swift",
"PagerScrollDelegate.swift",
"PagerView.swift",
"PagerViewIOS15Compat.swift",
Expand Down
Loading