diff --git a/Libraries/Components/Keyboard/Keyboard.js b/Libraries/Components/Keyboard/Keyboard.js index 4cd19ca398139d..6930a0578fbc12 100644 --- a/Libraries/Components/Keyboard/Keyboard.js +++ b/Libraries/Components/Keyboard/Keyboard.js @@ -37,6 +37,7 @@ export type KeyboardEvent = $ReadOnly<{| easing?: string, endCoordinates: ScreenRect, startCoordinates?: ScreenRect, + isEventFromThisApp?: boolean, |}>; type KeyboardEventListener = (e: KeyboardEvent) => void; diff --git a/Libraries/Components/Keyboard/__tests__/Keyboard-test.js b/Libraries/Components/Keyboard/__tests__/Keyboard-test.js new file mode 100644 index 00000000000000..98f8b363521196 --- /dev/null +++ b/Libraries/Components/Keyboard/__tests__/Keyboard-test.js @@ -0,0 +1,93 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + * @emails oncall+react_native + */ + +'use strict'; + +const Keyboard = require('Keyboard'); +const dismissKeyboard = require('dismissKeyboard'); +const LayoutAnimation = require('LayoutAnimation'); + +const NativeEventEmitter = require('NativeEventEmitter'); +const NativeModules = require('NativeModules'); + +jest.mock('LayoutAnimation'); + +describe('Keyboard', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('exposes KeyboardEventEmitter methods', () => { + const KeyboardObserver = NativeModules.KeyboardObserver; + const KeyboardEventEmitter = new NativeEventEmitter(KeyboardObserver); + + // $FlowFixMe + expect(Keyboard._subscriber).toBe(KeyboardEventEmitter._subscriber); + expect(Keyboard._nativeModule).toBe(KeyboardEventEmitter._nativeModule); + }); + + it('uses dismissKeyboard utility', () => { + expect(Keyboard.dismiss).toBe(dismissKeyboard); + }); + + describe('scheduling layout animation', () => { + const scheduleLayoutAnimation = ( + duration: number | null, + easing: string | null, + ): void => Keyboard.scheduleLayoutAnimation({duration, easing}); + + it('triggers layout animation', () => { + scheduleLayoutAnimation(12, 'spring'); + expect(LayoutAnimation.configureNext).toHaveBeenCalledWith({ + duration: 12, + update: { + duration: 12, + type: 'spring', + }, + }); + }); + + it('does not trigger animation when duration is null', () => { + scheduleLayoutAnimation(null, 'spring'); + expect(LayoutAnimation.configureNext).not.toHaveBeenCalled(); + }); + + it('does not trigger animation when duration is 0', () => { + scheduleLayoutAnimation(0, 'spring'); + expect(LayoutAnimation.configureNext).not.toHaveBeenCalled(); + }); + + describe('animation update type', () => { + const assertAnimationUpdateType = type => + expect(LayoutAnimation.configureNext).toHaveBeenCalledWith( + expect.objectContaining({ + duration: expect.anything(), + update: {duration: expect.anything(), type}, + }), + ); + + it('retrieves type from LayoutAnimation', () => { + scheduleLayoutAnimation(12, 'linear'); + assertAnimationUpdateType('linear'); + }); + + it("defaults to 'keyboard' when key in LayoutAnimation is not found", () => { + scheduleLayoutAnimation(12, 'some-unknown-animation-type'); + assertAnimationUpdateType('keyboard'); + }); + + it("defaults to 'keyboard' when easing is null", () => { + scheduleLayoutAnimation(12, null); + assertAnimationUpdateType('keyboard'); + }); + }); + }); +}); diff --git a/React/Modules/RCTKeyboardObserver.m b/React/Modules/RCTKeyboardObserver.m index ea04868cb1064f..999083c160a927 100644 --- a/React/Modules/RCTKeyboardObserver.m +++ b/React/Modules/RCTKeyboardObserver.m @@ -110,12 +110,14 @@ - (void)EVENT:(NSNotification *)notification \ CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; + NSInteger isLocalUserInfoKey = [userInfo[UIKeyboardIsLocalUserInfoKey] integerValue]; return @{ @"startCoordinates": RCTRectDictionaryValue(beginFrame), @"endCoordinates": RCTRectDictionaryValue(endFrame), @"duration": @(duration * 1000.0), // ms @"easing": RCTAnimationNameForCurve(curve), + @"isEventFromThisApp": isLocalUserInfoKey == 1 ? @YES : @NO, }; #endif }