diff --git a/.maestro/issues/issue_1096_keyboard_shrink_repro.yaml b/.maestro/issues/issue_1096_keyboard_shrink_repro.yaml new file mode 100644 index 00000000..6ac972f9 --- /dev/null +++ b/.maestro/issues/issue_1096_keyboard_shrink_repro.yaml @@ -0,0 +1,38 @@ +appId: com.pagerviewexample +tags: + - ios + - regression +--- +- runFlow: ../setup/issue_1096_keyboard_shrink_repro_setup.yaml + +# With the keyboard open, SwiftUI keyboard avoidance must not shrink the page. +# A shrunk page clips Row 5 and moves its native frame, so the tap misses and +# the counter stays at 0. +- tapOn: + id: 'issue-1096-input' + +- waitForAnimationToEnd: + timeout: 3000 + +- assertVisible: + id: 'issue-1096-last-row' + +- tapOn: + id: 'issue-1096-last-row' + +- assertVisible: 'last-row taps: 1' + +# Dismiss the keyboard deterministically before checking the sheet drops back +# down. `hideKeyboard` alone is unreliable here: on iOS it only performs two +# blind swipes at the screen centre, and this screen has no scroll view with a +# non-default `keyboardDismissMode` under that point, so the keyboard survives. +# A single-line TextInput blurs on submit, so Enter is the reliable dismiss. +- pressKey: Enter + +# No-op once the keyboard is already down; still dismisses natively on Android. +- hideKeyboard + +- tapOn: + id: 'issue-1096-last-row' + +- assertVisible: 'last-row taps: 2' diff --git a/.maestro/setup/issue_1096_keyboard_shrink_repro_setup.yaml b/.maestro/setup/issue_1096_keyboard_shrink_repro_setup.yaml new file mode 100644 index 00000000..017759ce --- /dev/null +++ b/.maestro/setup/issue_1096_keyboard_shrink_repro_setup.yaml @@ -0,0 +1,17 @@ +appId: ${APP_ID} +--- +- launchApp + +# The issue examples sit below the fundamental examples on the home screen. +- scrollUntilVisible: + element: + id: 'Issue #1096 Keyboard Shrink Repro' + direction: DOWN + +- tapOn: + id: 'Issue #1096 Keyboard Shrink Repro' + +- extendedWaitUntil: + visible: + id: 'issue-1096-pager' + timeout: 10000 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 285d3356..0f78b0c9 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1402,7 +1402,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-pager-view (9.0.3): + - react-native-pager-view (9.0.5): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2496,7 +2496,7 @@ SPEC CHECKSUMS: React-Mapbuffer: 1aa9126122d4247ffc24bf9d28d50ce923499a71 React-microtasksnativemodule: d86581169e9bb5bb6f5fc3c5052f890016c1bf21 React-mutationobservernativemodule: 9a0c4e866f1ef2a57acebc902ebacbdf469ae741 - react-native-pager-view: 27f9cb535d59648492018892b7e3bffe0b511807 + react-native-pager-view: a18f87e19eedc3053afd4150fb52ac5f92f02d64 react-native-safe-area-context: c1eb308f4b36372a4de4b3bdaa8ed695ec3dd461 React-NativeModulesApple: cc6ec4767844d610e92cc358bd3ea34937438d56 React-networking: a8ce15641ed7775d5b54a9d0d32defc367c2216e diff --git a/example/src/App.tsx b/example/src/App.tsx index 1b8fd45a..8275475a 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -34,6 +34,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { PagerHookExample } from './PagerHookExample'; import { NestedHorizontalScrollViewExample } from './NestedHorizontalScrollViewExample'; +import { Issue1096KeyboardShrinkRepro } from './gh-issues/Issue1096KeyboardShrinkRepro'; import { Issue1098NestedPagerRepro } from './gh-issues/Issue1098NestedPagerRepro'; import { Issue1099SafeAreaRepro } from './gh-issues/Issue1099SafeAreaRepro'; import { @@ -121,6 +122,10 @@ const ghIssues: Example[] = [ component: Issue1083ModalSetPageExample, name: 'Issue #1083 Modal SetPage Repro', }, + { + component: Issue1096KeyboardShrinkRepro, + name: 'Issue #1096 Keyboard Shrink Repro', + }, { component: Issue1098NestedPagerRepro, name: 'Issue #1098 Nested Pager Repro', diff --git a/example/src/gh-issues/Issue1096KeyboardShrinkRepro.tsx b/example/src/gh-issues/Issue1096KeyboardShrinkRepro.tsx new file mode 100644 index 00000000..b4ec75bd --- /dev/null +++ b/example/src/gh-issues/Issue1096KeyboardShrinkRepro.tsx @@ -0,0 +1,148 @@ +/** + * Repro for #1096: SwiftUI keyboard avoidance must not shrink PagerView pages. + * + * The pager sits in a bottom sheet that follows the keyboard with a transform, + * like react-native-keyboard-controller's KeyboardStickyView. React Native + * keeps the page at full height, but without the fix the page is framed to a + * keyboard-shrunk GeometryReader: the lower rows are clipped, and their native + * frames no longer match where React Native laid them out, so taps miss them. + */ +import React, { useEffect, useRef, useState } from 'react'; +import { + Animated, + Keyboard, + Pressable, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; +import PagerView from 'react-native-pager-view'; + +const ROWS = ['Row 1', 'Row 2', 'Row 3', 'Row 4', 'Row 5']; + +function useKeyboardLift() { + const lift = useRef(new Animated.Value(0)).current; + + useEffect(() => { + const show = Keyboard.addListener('keyboardWillShow', (event) => { + Animated.timing(lift, { + toValue: -event.endCoordinates.height, + duration: event.duration, + useNativeDriver: true, + }).start(); + }); + const hide = Keyboard.addListener('keyboardWillHide', (event) => { + Animated.timing(lift, { + toValue: 0, + duration: event.duration, + useNativeDriver: true, + }).start(); + }); + return () => { + show.remove(); + hide.remove(); + }; + }, [lift]); + + return lift; +} + +export function Issue1096KeyboardShrinkRepro() { + const lift = useKeyboardLift(); + const [lastRowTaps, setLastRowTaps] = useState(0); + + return ( + + + Focus the input. All five rows must stay visible above the input, and + tapping Row 5 must increment the counter. + + + + + {ROWS.map((row, index) => + index === ROWS.length - 1 ? ( + setLastRowTaps((count) => count + 1)} + > + {row} + + ) : ( + + {row} + + ) + )} + + + Second page + + + + {`last-row taps: ${lastRowTaps}`} + + + + + ); +} + +const styles = StyleSheet.create({ + screen: { + flex: 1, + justifyContent: 'flex-end', + }, + description: { + position: 'absolute', + top: 24, + left: 24, + right: 24, + fontSize: 16, + lineHeight: 22, + textAlign: 'center', + }, + sheet: { + backgroundColor: 'magenta', + paddingBottom: 34, + }, + pager: { + height: 5 * 56, + }, + page: { + flex: 1, + backgroundColor: '#b9f6ca', + }, + row: { + height: 56, + justifyContent: 'center', + paddingHorizontal: 16, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#00000033', + }, + rowText: { + color: '#111111', + fontSize: 18, + }, + counter: { + padding: 8, + color: '#ffffff', + fontWeight: '600', + }, + input: { + height: 44, + marginHorizontal: 16, + paddingHorizontal: 12, + borderRadius: 8, + backgroundColor: '#ffffff', + }, +}); diff --git a/ios/PagerView.swift b/ios/PagerView.swift index 5d90f7f6..aa737f1e 100644 --- a/ios/PagerView.swift +++ b/ios/PagerView.swift @@ -54,6 +54,9 @@ struct PagerView: View { } } } + // GeometryReader is the root since the vertical layout; without this it + // honours the keyboard region and pages get framed to the shrunk proxy. + .ignoresSafeArea() .onAppear { // Apply initial prop values that .onChange won't catch // (.onChange only fires on changes, not on initial values)