Skip to content

Commit 7133f32

Browse files
authored
Fix keyboard-awareness bug (#8087) (#8090)
1 parent 20d0883 commit 7133f32

7 files changed

Lines changed: 175 additions & 27 deletions

File tree

e2e/AndroidUtils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const utils = {
1313
'pm revoke com.reactnativenavigation.playground android.permission.READ_PHONE_STATE'
1414
),
1515
executeShellCommand: (command) => {
16+
// TODO Change to use Detox's ADB (see keyboard driver)
1617
exec.execSync(`adb -s ${device.id} shell ${command}`);
1718
},
1819
setDemoMode: () => {

e2e/Keyboard.test.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,47 @@
11
import { default as TestIDs, default as testIDs } from '../playground/src/testIDs';
2-
import Android from './AndroidUtils';
2+
import { device } from 'detox';
33
import Utils from './Utils';
4+
import kbdDriver from './drivers/androidKeyboard';
45

5-
const { elementByLabel, elementById } = Utils;
6+
const { elementByLabel, elementById, sleep } = Utils;
7+
8+
const KBD_OBSCURED_TEXT = 'Keyboard Demo';
69

710
describe.e2e('Keyboard', () => {
11+
beforeAll(async () => {
12+
await kbdDriver.init();
13+
await kbdDriver.enableOnScreenKeyboard();
14+
15+
if (device.getPlatform() === 'android') {
16+
// 1st-time Android keyboard appearance is laggy (Android's lazy init?)
17+
await device.launchApp({ newInstance: true });
18+
await elementById(TestIDs.KEYBOARD_SCREEN_BTN).tap();
19+
await elementById(TestIDs.TEXT_INPUT1).tap();
20+
await sleep(2000);
21+
}
22+
});
23+
24+
afterAll(async () => {
25+
await kbdDriver.restoreOnScreenKeyboard();
26+
});
27+
828
beforeEach(async () => {
929
await device.launchApp({ newInstance: true });
1030
await elementById(TestIDs.KEYBOARD_SCREEN_BTN).tap();
1131
});
1232

1333
it('Push - should close keyboard when Back clicked', async () => {
34+
await expect(elementByLabel(KBD_OBSCURED_TEXT)).toBeVisible();
1435
await elementById(TestIDs.TEXT_INPUT1).tap();
15-
await expect(elementByLabel('Keyboard Demo')).not.toBeVisible();
36+
await expect(elementByLabel(KBD_OBSCURED_TEXT)).not.toBeVisible();
1637
await elementById(TestIDs.BACK_BUTTON).tap();
1738
await expect(elementById(testIDs.MAIN_BOTTOM_TABS)).toBeVisible();
1839
});
1940

2041
it('Modal - should close keyboard when close clicked', async () => {
2142
await elementById(TestIDs.MODAL_BTN).tap();
2243
await elementById(TestIDs.TEXT_INPUT1).tap();
23-
await expect(elementByLabel('Keyboard Demo')).not.toBeVisible();
44+
await expect(elementByLabel(KBD_OBSCURED_TEXT)).not.toBeVisible();
2445
await elementById(TestIDs.DISMISS_MODAL_TOPBAR_BTN).tap();
2546
await expect(elementById(testIDs.MAIN_BOTTOM_TABS)).toBeVisible();
2647
});
@@ -46,4 +67,10 @@ describe.e2e('Keyboard', () => {
4667
await elementById(TestIDs.MODAL_BTN).tap();
4768
await expect(elementById(TestIDs.TEXT_INPUT1)).not.toBeFocused();
4869
});
70+
71+
it(':android: should respect UI with keyboard awareness', async () => {
72+
await elementById(TestIDs.PUSH_KEYBOARD_SCREEN_STICKY_FOOTER).tap();
73+
await elementById(TestIDs.TEXT_INPUT2).tap();
74+
await expect(elementByLabel(KBD_OBSCURED_TEXT)).toBeVisible();
75+
});
4976
});

e2e/Utils.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ function convertToSSIMFormat(image) {
99
data: new Uint8ClampedArray(image.data),
1010
width: image.width,
1111
height: image.height
12-
}
13-
;
12+
};
1413
}
1514

1615
function loadImage(path) {
@@ -34,6 +33,29 @@ function bitmapDiff(imagePath, expectedImagePath, ssimThreshold = SSIM_SCORE_THR
3433
}
3534
}
3635

36+
const sleep = (ms) =>
37+
new Promise((res) => setTimeout(res, ms));
38+
39+
/**
40+
* @param tries Total tries to attempt (retries + 1)
41+
* @param delay Delay between retries, in milliseconds
42+
* @param {Function<Promise<Boolean>>} func
43+
* @returns {Promise<void>}
44+
* @throws {Error} if the function fails after all retries
45+
*/
46+
async function retry({ tries = 3, delay = 1000 }, func) {
47+
for (let i = 0; i < tries; i++) {
48+
const result = await func();
49+
if (result) {
50+
return;
51+
}
52+
53+
await sleep(delay);
54+
}
55+
56+
throw new Error(`Failed even after ${tries} attempts`);
57+
}
58+
3759
const utils = {
3860
elementByLabel: (label) => {
3961
// uncomment for running tests with rn's new arch
@@ -58,7 +80,8 @@ const utils = {
5880
return element(by.type('_UIModernBarButton').and(by.label('Back'))).tap();
5981
}
6082
},
61-
sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
83+
sleep,
84+
retry,
6285
expectImagesToBeEqual: (imagePath, expectedImagePath) => {
6386
bitmapDiff(imagePath, expectedImagePath);
6487

e2e/drivers/androidKeyboard.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { device } from 'detox';
2+
import Utils from '../Utils';
3+
4+
const { retry, sleep } = Utils;
5+
6+
const androidKbdDriver = {
7+
async init() {
8+
if (device.getPlatform() !== 'android') {
9+
return;
10+
}
11+
12+
await this._initADB();
13+
await this._initKbdState();
14+
},
15+
16+
async enableOnScreenKeyboard() {
17+
if (!this.adb) {
18+
// Not initialized (iOS?)
19+
return;
20+
}
21+
await this._setOnscreenKeyboard(true);
22+
},
23+
24+
async restoreOnScreenKeyboard() {
25+
if (!this.adb) {
26+
// Not initialized (iOS?)
27+
return;
28+
}
29+
await this._setOnscreenKeyboard(this.kbdEnabled);
30+
},
31+
32+
_initADB() {
33+
const { id: adbName } = device;
34+
const { adb } = device.deviceDriver;
35+
36+
if (!adb || !adbName) {
37+
throw new Error(`Keyboard driver init failed (id=${adbName}, hasADB=${!!adb})`);
38+
}
39+
40+
this.adb = adb;
41+
this.adbName = adbName;
42+
},
43+
44+
async _initKbdState() {
45+
this.kbdEnabled = await this.adb.shell(this.adbName, 'settings get Secure show_ime_with_hard_keyboard');
46+
47+
if (!(this.kbdEnabled === '0' || this.kbdEnabled === '1')) {
48+
console.warn('[KbdDriver] Unable to get on-screen KBD setting, defaulting to false');
49+
this.kbdEnabled = '0';
50+
}
51+
},
52+
53+
async _setOnscreenKeyboard(_value) {
54+
const value = (!!Number(_value) ? '1' : '0');
55+
56+
await retry( { tries: 10 }, async () => {
57+
await this.adb.shell(this.adbName, `settings put Secure show_ime_with_hard_keyboard ${value}`);
58+
await sleep(1000);
59+
60+
const result = await this.adb.shell(this.adbName, 'settings get Secure show_ime_with_hard_keyboard');
61+
return result === value;
62+
});
63+
}
64+
}
65+
66+
module.exports = androidKbdDriver;

lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,12 @@ public Animator getPopAnimation(Options appearingOptions, Options disappearingOp
316316
@Override
317317
protected WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets) {
318318
Insets sysInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars());
319-
view.setPaddingRelative(0, 0, 0, sysInsets.bottom);
320-
return WindowInsetsCompat.CONSUMED;
321-
}
319+
Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
322320

321+
int bottomInset = (imeInsets.bottom > 0) ? 0 : sysInsets.bottom;
322+
view.setPaddingRelative(0, 0, 0, bottomInset);
323+
return insets;
324+
}
323325

324326
@RestrictTo(RestrictTo.Scope.TESTS)
325327
public BottomTabs getBottomTabs() {

playground/src/screens/KeyboardScreen.tsx

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import React from 'react';
2-
import { View, ScrollView, Dimensions, StyleSheet, Image, TextInput, Text } from 'react-native';
2+
import {
3+
SafeAreaView,
4+
View,
5+
ScrollView,
6+
Dimensions,
7+
StyleSheet,
8+
Image,
9+
TextInput,
10+
Text,
11+
KeyboardAvoidingView,
12+
} from 'react-native';
313
import {
414
NavigationProps,
515
NavigationComponent,
@@ -15,6 +25,7 @@ const KEYBOARD_LABEL = 'Keyboard Demo';
1525
interface Props extends NavigationProps {
1626
title?: string;
1727
autoFocus?: boolean;
28+
stickyFooter?: boolean;
1829
}
1930

2031
export default class KeyboardScreen extends NavigationComponent<Props> {
@@ -34,6 +45,7 @@ export default class KeyboardScreen extends NavigationComponent<Props> {
3445
},
3546
};
3647
}
48+
3749
constructor(props: Props) {
3850
super(props);
3951
Navigation.events().bindComponent(this);
@@ -50,30 +62,41 @@ export default class KeyboardScreen extends NavigationComponent<Props> {
5062
}
5163

5264
render() {
65+
const FooterRoot = this.props.stickyFooter === true ? KeyboardAvoidingView : View;
5366
return (
54-
<View style={styles.root}>
67+
<SafeAreaView style={styles.root}>
5568
<ScrollView>
5669
<Image style={styles.image} source={require('../../img/2048.jpeg')} />
5770
<View style={{ alignItems: 'center' }}>
5871
<Button
5972
style={styles.button}
60-
label={'Modal Keyboard Screen'}
73+
label={'Modal screen'}
6174
testID={testIDs.MODAL_BTN}
6275
onPress={async () => {
6376
await this.openModalKeyboard(undefined);
6477
}}
6578
/>
79+
<View style={styles.row}>
80+
<Button
81+
style={styles.button}
82+
label={'Push screen w/ focus'}
83+
testID={testIDs.PUSH_FOCUSED_KEYBOARD_SCREEN}
84+
onPress={async () => {
85+
await this.openPushedKeyboard(undefined, true);
86+
}}
87+
/>
88+
<Button
89+
style={styles.button}
90+
label={'w/ sticky-footer'}
91+
testID={testIDs.PUSH_KEYBOARD_SCREEN_STICKY_FOOTER}
92+
onPress={async () => {
93+
await this.openPushedKeyboard(undefined, undefined, true);
94+
}}
95+
/>
96+
</View>
6697
<Button
6798
style={styles.button}
68-
label={'Push Focused Keyboard Screen'}
69-
testID={testIDs.PUSH_FOCUSED_KEYBOARD_SCREEN}
70-
onPress={async () => {
71-
await this.openPushedKeyboard(undefined, true);
72-
}}
73-
/>
74-
<Button
75-
style={styles.button}
76-
label={'Show Focused Keyboard Screen Modal'}
99+
label={'Modal screen w/ focus'}
77100
testID={testIDs.MODAL_FOCUSED_KEYBOARD_SCREEN}
78101
onPress={async () => {
79102
await this.openModalKeyboard(undefined, true);
@@ -104,20 +127,21 @@ export default class KeyboardScreen extends NavigationComponent<Props> {
104127
/>
105128
</View>
106129
</ScrollView>
107-
<View style={styles.footer}>
108-
<Text style={styles.input}> {KEYBOARD_LABEL}</Text>
109-
</View>
110-
</View>
130+
<FooterRoot behavior="height" style={styles.footer}>
131+
<Text style={styles.input}>{KEYBOARD_LABEL}</Text>
132+
</FooterRoot>
133+
</SafeAreaView>
111134
);
112135
}
113136

114-
openPushedKeyboard = async (text?: string, autoFocus?: boolean) => {
137+
openPushedKeyboard = async (text?: string, autoFocus?: boolean, stickyFooter?: boolean) => {
115138
await Navigation.push(this.props.componentId, {
116139
component: {
117140
name: Screens.KeyboardScreen,
118141
passProps: {
119142
title: text,
120143
autoFocus,
144+
stickyFooter,
121145
},
122146
},
123147
});
@@ -169,4 +193,8 @@ const styles = StyleSheet.create({
169193
width: screenWidth,
170194
resizeMode: 'cover',
171195
},
196+
row: {
197+
flexDirection: 'row',
198+
justifyContent: 'space-between',
199+
},
172200
});

playground/src/testIDs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const testIDs = {
2828
MODAL_DISABLED_BACK_BTN: 'SHOW_MODAL_DISABLED_BACK_BTN',
2929
PUSH_FOCUSED_KEYBOARD_SCREEN: 'PUSH_FOCUSED_KEYBOARD_SCREEN',
3030
MODAL_FOCUSED_KEYBOARD_SCREEN: 'MODAL_FOCUSED_KEYBOARD_SCREEN',
31+
PUSH_KEYBOARD_SCREEN_STICKY_FOOTER: 'PUSH_KEYBOARD_SCREEN_STICKY_FOOTER',
3132
PAGE_SHEET_MODAL_BTN: 'SHOW_PAGE_SHEET_MODAL_BUTTON',
3233
DISMISS_MODAL_BTN: 'DISMISS_MODAL_BUTTON',
3334
DISMISS_REACT_MODAL_BTN: 'DISMISS_REACT_MODAL_BUTTON',

0 commit comments

Comments
 (0)