diff --git a/e2e/AndroidUtils.js b/e2e/AndroidUtils.js index e594e7bd55..942233b82d 100644 --- a/e2e/AndroidUtils.js +++ b/e2e/AndroidUtils.js @@ -13,6 +13,7 @@ const utils = { 'pm revoke com.reactnativenavigation.playground android.permission.READ_PHONE_STATE' ), executeShellCommand: (command) => { + // TODO Change to use Detox's ADB (see keyboard driver) exec.execSync(`adb -s ${device.id} shell ${command}`); }, setDemoMode: () => { diff --git a/e2e/Keyboard.test.js b/e2e/Keyboard.test.js index 94b2ea08e3..f8ccd96a59 100644 --- a/e2e/Keyboard.test.js +++ b/e2e/Keyboard.test.js @@ -1,18 +1,39 @@ import { default as TestIDs, default as testIDs } from '../playground/src/testIDs'; -import Android from './AndroidUtils'; +import { device } from 'detox'; import Utils from './Utils'; +import kbdDriver from './drivers/androidKeyboard'; -const { elementByLabel, elementById } = Utils; +const { elementByLabel, elementById, sleep } = Utils; + +const KBD_OBSCURED_TEXT = 'Keyboard Demo'; describe.e2e('Keyboard', () => { + beforeAll(async () => { + await kbdDriver.init(); + await kbdDriver.enableOnScreenKeyboard(); + + if (device.getPlatform() === 'android') { + // 1st-time Android keyboard appearance is laggy (Android's lazy init?) + await device.launchApp({ newInstance: true }); + await elementById(TestIDs.KEYBOARD_SCREEN_BTN).tap(); + await elementById(TestIDs.TEXT_INPUT1).tap(); + await sleep(2000); + } + }); + + afterAll(async () => { + await kbdDriver.restoreOnScreenKeyboard(); + }); + beforeEach(async () => { await device.launchApp({ newInstance: true }); await elementById(TestIDs.KEYBOARD_SCREEN_BTN).tap(); }); it('Push - should close keyboard when Back clicked', async () => { + await expect(elementByLabel(KBD_OBSCURED_TEXT)).toBeVisible(); await elementById(TestIDs.TEXT_INPUT1).tap(); - await expect(elementByLabel('Keyboard Demo')).not.toBeVisible(); + await expect(elementByLabel(KBD_OBSCURED_TEXT)).not.toBeVisible(); await elementById(TestIDs.BACK_BUTTON).tap(); await expect(elementById(testIDs.MAIN_BOTTOM_TABS)).toBeVisible(); }); @@ -20,7 +41,7 @@ describe.e2e('Keyboard', () => { it('Modal - should close keyboard when close clicked', async () => { await elementById(TestIDs.MODAL_BTN).tap(); await elementById(TestIDs.TEXT_INPUT1).tap(); - await expect(elementByLabel('Keyboard Demo')).not.toBeVisible(); + await expect(elementByLabel(KBD_OBSCURED_TEXT)).not.toBeVisible(); await elementById(TestIDs.DISMISS_MODAL_TOPBAR_BTN).tap(); await expect(elementById(testIDs.MAIN_BOTTOM_TABS)).toBeVisible(); }); @@ -46,4 +67,10 @@ describe.e2e('Keyboard', () => { await elementById(TestIDs.MODAL_BTN).tap(); await expect(elementById(TestIDs.TEXT_INPUT1)).not.toBeFocused(); }); + + it(':android: should respect UI with keyboard awareness', async () => { + await elementById(TestIDs.PUSH_KEYBOARD_SCREEN_STICKY_FOOTER).tap(); + await elementById(TestIDs.TEXT_INPUT2).tap(); + await expect(elementByLabel(KBD_OBSCURED_TEXT)).toBeVisible(); + }); }); diff --git a/e2e/Utils.js b/e2e/Utils.js index bcdf357db7..c8c8ea1260 100644 --- a/e2e/Utils.js +++ b/e2e/Utils.js @@ -9,8 +9,7 @@ function convertToSSIMFormat(image) { data: new Uint8ClampedArray(image.data), width: image.width, height: image.height -} - ; + }; } function loadImage(path) { @@ -34,6 +33,29 @@ function bitmapDiff(imagePath, expectedImagePath, ssimThreshold = SSIM_SCORE_THR } } +const sleep = (ms) => + new Promise((res) => setTimeout(res, ms)); + +/** + * @param tries Total tries to attempt (retries + 1) + * @param delay Delay between retries, in milliseconds + * @param {Function>} func + * @returns {Promise} + * @throws {Error} if the function fails after all retries + */ +async function retry({ tries = 3, delay = 1000 }, func) { + for (let i = 0; i < tries; i++) { + const result = await func(); + if (result) { + return; + } + + await sleep(delay); + } + + throw new Error(`Failed even after ${tries} attempts`); +} + const utils = { elementByLabel: (label) => { // uncomment for running tests with rn's new arch @@ -58,7 +80,8 @@ const utils = { return element(by.type('_UIModernBarButton').and(by.label('Back'))).tap(); } }, - sleep: (ms) => new Promise((res) => setTimeout(res, ms)), + sleep, + retry, expectImagesToBeEqual: (imagePath, expectedImagePath) => { bitmapDiff(imagePath, expectedImagePath); diff --git a/e2e/drivers/androidKeyboard.js b/e2e/drivers/androidKeyboard.js new file mode 100644 index 0000000000..222bbb0158 --- /dev/null +++ b/e2e/drivers/androidKeyboard.js @@ -0,0 +1,66 @@ +import { device } from 'detox'; +import Utils from '../Utils'; + +const { retry, sleep } = Utils; + +const androidKbdDriver = { + async init() { + if (device.getPlatform() !== 'android') { + return; + } + + await this._initADB(); + await this._initKbdState(); + }, + + async enableOnScreenKeyboard() { + if (!this.adb) { + // Not initialized (iOS?) + return; + } + await this._setOnscreenKeyboard(true); + }, + + async restoreOnScreenKeyboard() { + if (!this.adb) { + // Not initialized (iOS?) + return; + } + await this._setOnscreenKeyboard(this.kbdEnabled); + }, + + _initADB() { + const { id: adbName } = device; + const { adb } = device.deviceDriver; + + if (!adb || !adbName) { + throw new Error(`Keyboard driver init failed (id=${adbName}, hasADB=${!!adb})`); + } + + this.adb = adb; + this.adbName = adbName; + }, + + async _initKbdState() { + this.kbdEnabled = await this.adb.shell(this.adbName, 'settings get Secure show_ime_with_hard_keyboard'); + + if (!(this.kbdEnabled === '0' || this.kbdEnabled === '1')) { + console.warn('[KbdDriver] Unable to get on-screen KBD setting, defaulting to false'); + this.kbdEnabled = '0'; + } + }, + + async _setOnscreenKeyboard(_value) { + const value = (!!Number(_value) ? '1' : '0'); + + await retry( { tries: 10 }, async () => { + await this.adb.shell(this.adbName, `settings put Secure show_ime_with_hard_keyboard ${value}`); + await sleep(1000); + + const result = await this.adb.shell(this.adbName, 'settings get Secure show_ime_with_hard_keyboard'); + return result === value; + }); + } +} + +module.exports = androidKbdDriver; diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java index 78be5f656c..b3deb2cd87 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java @@ -316,10 +316,12 @@ public Animator getPopAnimation(Options appearingOptions, Options disappearingOp @Override protected WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets) { Insets sysInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - view.setPaddingRelative(0, 0, 0, sysInsets.bottom); - return WindowInsetsCompat.CONSUMED; - } + Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()); + int bottomInset = (imeInsets.bottom > 0) ? 0 : sysInsets.bottom; + view.setPaddingRelative(0, 0, 0, bottomInset); + return insets; + } @RestrictTo(RestrictTo.Scope.TESTS) public BottomTabs getBottomTabs() { diff --git a/playground/src/screens/KeyboardScreen.tsx b/playground/src/screens/KeyboardScreen.tsx index d439e72ef4..a199efaacf 100644 --- a/playground/src/screens/KeyboardScreen.tsx +++ b/playground/src/screens/KeyboardScreen.tsx @@ -1,5 +1,15 @@ import React from 'react'; -import { View, ScrollView, Dimensions, StyleSheet, Image, TextInput, Text } from 'react-native'; +import { + SafeAreaView, + View, + ScrollView, + Dimensions, + StyleSheet, + Image, + TextInput, + Text, + KeyboardAvoidingView, +} from 'react-native'; import { NavigationProps, NavigationComponent, @@ -15,6 +25,7 @@ const KEYBOARD_LABEL = 'Keyboard Demo'; interface Props extends NavigationProps { title?: string; autoFocus?: boolean; + stickyFooter?: boolean; } export default class KeyboardScreen extends NavigationComponent { @@ -34,6 +45,7 @@ export default class KeyboardScreen extends NavigationComponent { }, }; } + constructor(props: Props) { super(props); Navigation.events().bindComponent(this); @@ -50,30 +62,41 @@ export default class KeyboardScreen extends NavigationComponent { } render() { + const FooterRoot = this.props.stickyFooter === true ? KeyboardAvoidingView : View; return ( - +