diff --git a/.maestro/android-only/issue_1154_page_change_focus_repro.yaml b/.maestro/android-only/issue_1154_page_change_focus_repro.yaml
new file mode 100644
index 00000000..81cd4423
--- /dev/null
+++ b/.maestro/android-only/issue_1154_page_change_focus_repro.yaml
@@ -0,0 +1,80 @@
+appId: com.pagerviewexample
+tags:
+ - android
+ - regression
+---
+- runFlow: ../setup/issue_1154_page_change_focus_repro_setup.yaml
+
+# Leaving a page via setPage must blur its focused input and hide the
+# keyboard. Before the fix the off-screen input kept focus and the keyboard.
+- tapOn:
+ id: 'issue-1154-go-1'
+
+- tapOn:
+ id: 'issue-1154-input-1'
+
+- extendedWaitUntil:
+ visible:
+ id: 'issue-1154-status'
+ text: 'Selected: 1 \| Focused: input 1 \| Keyboard: visible'
+ timeout: 10000
+
+- tapOn:
+ id: 'issue-1154-go-0'
+
+- extendedWaitUntil:
+ visible:
+ id: 'issue-1154-status'
+ text: 'Selected: 0 \| Focused: none \| Keyboard: hidden'
+ timeout: 10000
+
+# Same when the page is left with a swipe.
+- tapOn:
+ id: 'issue-1154-input-0'
+
+- extendedWaitUntil:
+ visible:
+ id: 'issue-1154-status'
+ text: 'Selected: 0 \| Focused: input 0 \| Keyboard: visible'
+ timeout: 10000
+
+- swipe:
+ from:
+ id: 'issue-1154-label-0'
+ start: 90%, 50%
+ end: 10%, 50%
+ duration: 100
+
+- extendedWaitUntil:
+ visible:
+ id: 'issue-1154-status'
+ text: 'Selected: 1 \| Focused: none \| Keyboard: hidden'
+ timeout: 10000
+
+# An input focused right after setPage, while the pager is still animating,
+# must keep focus.
+- tapOn:
+ id: 'issue-1154-mode-onPress'
+
+- tapOn:
+ id: 'issue-1154-go-0'
+
+- extendedWaitUntil:
+ visible:
+ id: 'issue-1154-status'
+ text: 'Selected: 0 \| Focused: input 0 \| Keyboard: visible'
+ timeout: 10000
+
+# An input focused from onPageSelected must end up focused with the keyboard
+# open.
+- tapOn:
+ id: 'issue-1154-mode-onSelected'
+
+- tapOn:
+ id: 'issue-1154-go-1'
+
+- extendedWaitUntil:
+ visible:
+ id: 'issue-1154-status'
+ text: 'Selected: 1 \| Focused: input 1 \| Keyboard: visible'
+ timeout: 10000
diff --git a/.maestro/setup/issue_1154_page_change_focus_repro_setup.yaml b/.maestro/setup/issue_1154_page_change_focus_repro_setup.yaml
new file mode 100644
index 00000000..9d0ca7c2
--- /dev/null
+++ b/.maestro/setup/issue_1154_page_change_focus_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 #1154 Page Change Focus Repro'
+ direction: DOWN
+
+- tapOn:
+ id: 'Issue #1154 Page Change Focus Repro'
+
+- extendedWaitUntil:
+ visible:
+ id: 'issue-1154-status'
+ timeout: 10000
diff --git a/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt b/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt
index 5eaa6069..17e2766b 100644
--- a/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt
+++ b/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt
@@ -5,6 +5,7 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.LocalOverscrollConfiguration
@@ -433,11 +434,33 @@ class ComposePagerView(context: Context) : FrameLayout(context) {
return
}
lastEmittedPageSelected = position
+ clearFocusOutsidePage(position)
UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)?.dispatchEvent(
PageSelectedEvent(id, position)
)
}
+ private fun clearFocusOutsidePage(position: Int) {
+ val focused = findFocus() ?: return
+ val selectedPage = pages.getOrNull(position)
+ if (selectedPage?.hasFocus() == true) {
+ return
+ }
+ val inputMethodManager = context.getSystemService(InputMethodManager::class.java)
+ val wasServingFocused = inputMethodManager?.isActive(focused) == true
+ clearFocus()
+ if (hasFocus() && selectedPage?.hasFocus() != true) {
+ if (selectedPage?.requestFocus(View.FOCUS_FORWARD) != true) {
+ composeView?.requestFocus(View.FOCUS_FORWARD)
+ }
+ }
+ // The keyboard has to be hidden manually, since unlike ViewPager2's
+ // RecyclerView in v8, the pager no longer detaches the previous page.
+ if (wasServingFocused && !focused.hasFocus()) {
+ inputMethodManager?.hideSoftInputFromWindow(focused.windowToken, 0)
+ }
+ }
+
private fun dispatchPageScroll(position: Int, offset: Float) {
UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)?.dispatchEvent(
PageScrollEvent(id, position, offset)
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 8275475a..4efff2a1 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -46,6 +46,7 @@ import {
Issue1142PagerListScreen,
Issue1142SearchBarInsetRepro,
} from './gh-issues/Issue1142SearchBarInsetRepro';
+import { Issue1154PageChangeFocusRepro } from './gh-issues/Issue1154PageChangeFocusRepro';
function BasicPagerViewExampleScreen() {
return ;
@@ -138,6 +139,10 @@ const ghIssues: Example[] = [
component: Issue1142SearchBarInsetRepro,
name: 'Issue #1142 Search Bar Inset Repro',
},
+ {
+ component: Issue1154PageChangeFocusRepro,
+ name: 'Issue #1154 Page Change Focus Repro',
+ },
];
const allExamples = [
diff --git a/example/src/gh-issues/Issue1154PageChangeFocusRepro.tsx b/example/src/gh-issues/Issue1154PageChangeFocusRepro.tsx
new file mode 100644
index 00000000..b5e577a3
--- /dev/null
+++ b/example/src/gh-issues/Issue1154PageChangeFocusRepro.tsx
@@ -0,0 +1,162 @@
+/**
+ * Repro for #1154: on Android, a focused TextInput must lose focus and hide
+ * the keyboard when its page is left, via setPage or a swipe. The mode buttons
+ * focus the new page's input from onPageSelected or right after setPage, which
+ * must keep working.
+ */
+import React, { useEffect, useRef, useState } from 'react';
+import {
+ Button,
+ Keyboard,
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+} from 'react-native';
+import PagerView from 'react-native-pager-view';
+
+const PAGES = ['Page 0', 'Page 1', 'Page 2'];
+const PAGE_COLORS = ['#fde2e2', '#e2fde6', '#e2e8fd'];
+
+export function Issue1154PageChangeFocusRepro() {
+ const pagerRef = useRef(null);
+ const [selectedPage, setSelectedPage] = useState(0);
+ const [focusedInput, setFocusedInput] = useState(null);
+ const [keyboardVisible, setKeyboardVisible] = useState(false);
+ const [events, setEvents] = useState([]);
+ const [autoFocusMode, setAutoFocusMode] = useState<
+ 'off' | 'onSelected' | 'onPress'
+ >('off');
+ const inputRefs = useRef>([]);
+ const log = (event: string) =>
+ setEvents((current) => [...current.slice(-11), event]);
+
+ useEffect(() => {
+ const show = Keyboard.addListener('keyboardDidShow', () => {
+ setKeyboardVisible(true);
+ log('keyboardShow');
+ });
+ const hide = Keyboard.addListener('keyboardDidHide', () => {
+ setKeyboardVisible(false);
+ log('keyboardHide');
+ });
+ return () => {
+ show.remove();
+ hide.remove();
+ };
+ }, []);
+
+ return (
+
+
+ Selected: {selectedPage} | Focused:{' '}
+ {focusedInput === null ? 'none' : `input ${focusedInput}`} | Keyboard:{' '}
+ {keyboardVisible ? 'visible' : 'hidden'}
+
+
+ Events: {events.join(', ')}
+
+
+ {PAGES.map((label, i) => (
+
+
+ {(['off', 'onSelected', 'onPress'] as const).map((mode) => (
+
+ {
+ setSelectedPage(e.nativeEvent.position);
+ log(`selected ${e.nativeEvent.position}`);
+ if (autoFocusMode === 'onSelected') {
+ inputRefs.current[e.nativeEvent.position]?.focus();
+ }
+ }}
+ >
+ {PAGES.map((label, i) => (
+
+
+ {label}
+
+ {
+ inputRefs.current[i] = input;
+ }}
+ testID={`issue-1154-input-${i}`}
+ placeholder={`Input on ${label}`}
+ style={styles.input}
+ onFocus={() => {
+ setFocusedInput(i);
+ log(`focus ${i}`);
+ }}
+ onBlur={() => {
+ setFocusedInput((current) => (current === i ? null : current));
+ log(`blur ${i}`);
+ }}
+ />
+
+ ))}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ screen: {
+ flex: 1,
+ },
+ status: {
+ padding: 12,
+ fontSize: 16,
+ },
+ events: {
+ paddingHorizontal: 12,
+ paddingBottom: 12,
+ fontSize: 12,
+ },
+ buttons: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ paddingBottom: 12,
+ },
+ pager: {
+ flex: 1,
+ },
+ page: {
+ flex: 1,
+ padding: 24,
+ },
+ pageLabel: {
+ fontSize: 24,
+ fontWeight: '600',
+ marginBottom: 16,
+ },
+ input: {
+ borderWidth: 1,
+ borderColor: 'black',
+ padding: 8,
+ backgroundColor: 'white',
+ },
+});