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
80 changes: 80 additions & 0 deletions .maestro/android-only/issue_1154_page_change_focus_repro.yaml
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions .maestro/setup/issue_1154_page_change_focus_repro_setup.yaml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions android/src/main/java/com/reactnativepagerview/ComposePagerView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
Issue1142PagerListScreen,
Issue1142SearchBarInsetRepro,
} from './gh-issues/Issue1142SearchBarInsetRepro';
import { Issue1154PageChangeFocusRepro } from './gh-issues/Issue1154PageChangeFocusRepro';

function BasicPagerViewExampleScreen() {
return <BasicPagerViewExample isHorizontal={true} />;
Expand Down Expand Up @@ -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 = [
Expand Down
162 changes: 162 additions & 0 deletions example/src/gh-issues/Issue1154PageChangeFocusRepro.tsx
Original file line number Diff line number Diff line change
@@ -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<PagerView>(null);
const [selectedPage, setSelectedPage] = useState(0);
const [focusedInput, setFocusedInput] = useState<number | null>(null);
const [keyboardVisible, setKeyboardVisible] = useState(false);
const [events, setEvents] = useState<string[]>([]);
const [autoFocusMode, setAutoFocusMode] = useState<
'off' | 'onSelected' | 'onPress'
>('off');
const inputRefs = useRef<Array<TextInput | null>>([]);
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 (
<View style={styles.screen}>
<Text testID="issue-1154-status" style={styles.status}>
Selected: {selectedPage} | Focused:{' '}
{focusedInput === null ? 'none' : `input ${focusedInput}`} | Keyboard:{' '}
{keyboardVisible ? 'visible' : 'hidden'}
</Text>
<Text testID="issue-1154-events" style={styles.events}>
Events: {events.join(', ')}
</Text>
<View style={styles.buttons}>
{PAGES.map((label, i) => (
<Button
key={label}
testID={`issue-1154-go-${i}`}
title={`Go ${i}`}
onPress={() => {
pagerRef.current?.setPage(i);
if (autoFocusMode === 'onPress') {
inputRefs.current[i]?.focus();
}
}}
/>
))}
</View>
<View style={styles.buttons}>
{(['off', 'onSelected', 'onPress'] as const).map((mode) => (
<Button
key={mode}
testID={`issue-1154-mode-${mode}`}
title={`${autoFocusMode === mode ? '* ' : ''}${mode}`}
onPress={() => setAutoFocusMode(mode)}
/>
))}
</View>
<PagerView
ref={pagerRef}
style={styles.pager}
onPageSelected={(e) => {
setSelectedPage(e.nativeEvent.position);
log(`selected ${e.nativeEvent.position}`);
if (autoFocusMode === 'onSelected') {
inputRefs.current[e.nativeEvent.position]?.focus();
}
}}
>
{PAGES.map((label, i) => (
<View
key={label}
style={[styles.page, { backgroundColor: PAGE_COLORS[i] }]}
>
<Text testID={`issue-1154-label-${i}`} style={styles.pageLabel}>
{label}
</Text>
<TextInput
ref={(input) => {
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}`);
}}
/>
</View>
))}
</PagerView>
</View>
);
}

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',
},
});