From c499ae1192ec178e4d20f8ed4ae03ff3e077bf65 Mon Sep 17 00:00:00 2001 From: Matthew Horan Date: Mon, 27 Jan 2025 06:57:02 -0800 Subject: [PATCH] Fixes for Keyboard Observer, KeyboardAvoidingView on iOS (#48131) Summary: **Convert keyboard position to window coordinate space on iOS** The keyboard frame passed to keyboardWillChangeFrame is in the screen's coordinate space [1]. It needs to be converted to the window's coordinate space to support Slide Over and Stage Manager. [1] https://developer.apple.com/documentation/uikit/uikeyboardframeenduserinfokey?language=objc |Before|After| |---|---| |![Simulator Screenshot - iPad mini (A17 Pro) - 2024-12-05 at 18 58 31](https://github.com/user-attachments/assets/6af21fde-32f1-4b15-83be-09a6dbae8784)|![Simulator Screenshot - iPad mini (A17 Pro) - 2024-12-05 at 19 01 27](https://github.com/user-attachments/assets/c63c2bef-c1a6-4ad4-91cf-74629c26dbb0)| **Improve detached keyboard detection on iOS** The iOS keyboard may be in one of three states: 1) floating (previously supported with a width check) 2) split 3) or undocked. In addition, when using Stage Manager, the keyboard may be wider than the window itself. This would cause the floating keyboard check to incorrectly set the bottom position to zero. Instead, rely on the fact that the UIKeyboardWillHideNotification notification is sent when the keyboard is in any detached state. This requires listening for UIKeyboardWillShowNotification instead of UIKeyboardWillChangeFrameNotification. This is fine, since the show notification is also sent when the keyboard resizes while open. Combined with the coordinate system adjustments in the previous commit, this also fixes an issue with Stage Manager, since the keyboard may be attached yet wider than the application window. |Before|After| |---|---| |![Simulator Screenshot - iPad mini (A17 Pro) - 2024-12-05 at 18 58 40](https://github.com/user-attachments/assets/4e747ece-b084-49ad-a8d6-5dfe623ae597)|![Simulator Screenshot - iPad mini (A17 Pro) - 2024-12-05 at 19 01 41](https://github.com/user-attachments/assets/50391302-4c83-4aa3-a96b-6056bba891ec)| |![Simulator Screenshot - iPad mini (A17 Pro) - 2024-12-05 at 19 48 19](https://github.com/user-attachments/assets/576691b3-6b84-41cf-87f4-0cf52fc405d4)|![Simulator Screenshot - iPad mini (A17 Pro) - 2024-12-05 at 19 05 33](https://github.com/user-attachments/assets/4bb60ca6-beb3-471e-bc71-38a18fc9c0f1)| |![Simulator Screenshot - iPad mini (A17 Pro) - 2024-12-05 at 19 48 55](https://github.com/user-attachments/assets/baf456fb-aa61-455b-9c20-a01a6a979cb5)|![Simulator Screenshot - iPad mini (A17 Pro) - 2024-12-05 at 19 05 56](https://github.com/user-attachments/assets/0b7fe954-a72f-407a-bb41-0f47253c9448)| ## Changelog: [iOS] [FIXED] - Keyboard events are converted to window coordinate space [iOS] [FIXED] - Improve detached keyboard detection, support Stage Manager on iOS Pull Request resolved: https://github.com/facebook/react-native/pull/48131 Test Plan: See screenshots above. Reviewed By: yungsters Differential Revision: D67337314 Pulled By: cipolleschi fbshipit-source-id: abe872ac8c83336f316917074f72cdb23a39caab --- .../Keyboard/KeyboardAvoidingView.js | 32 ++++++++----------- .../__snapshots__/public-api-test.js.snap | 3 +- .../React/CoreModules/RCTKeyboardObserver.mm | 5 +++ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js index 3159bfabd7e..08fdf276faa 100644 --- a/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js +++ b/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js @@ -9,7 +9,6 @@ */ import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; -import type {DimensionsPayload} from '../../Utilities/NativeDeviceInfo'; import type { ViewLayout, ViewLayoutEvent, @@ -19,7 +18,6 @@ import type {KeyboardEvent, KeyboardMetrics} from './Keyboard'; import LayoutAnimation from '../../LayoutAnimation/LayoutAnimation'; import StyleSheet from '../../StyleSheet/StyleSheet'; -import Dimensions from '../../Utilities/Dimensions'; import Platform from '../../Utilities/Platform'; import {type EventSubscription} from '../../vendor/emitter/EventEmitter'; import AccessibilityInfo from '../AccessibilityInfo/AccessibilityInfo'; @@ -68,7 +66,6 @@ class KeyboardAvoidingView extends React.Component { viewRef: {current: React.ElementRef | null, ...}; _initialFrameHeight: number = 0; _bottom: number = 0; - _windowWidth: number = Dimensions.get('window').width; constructor(props: Props) { super(props); @@ -115,6 +112,12 @@ class KeyboardAvoidingView extends React.Component { this._updateBottomIfNecessary(); }; + _onKeyboardHide = (event: ?KeyboardEvent) => { + this._keyboardEvent = null; + // $FlowFixMe[unused-promise] + this._updateBottomIfNecessary(); + }; + _onLayout = async (event: ViewLayoutEvent) => { event.persist(); @@ -135,10 +138,6 @@ class KeyboardAvoidingView extends React.Component { } }; - _onDimensionsChange = ({window}: DimensionsPayload) => { - this._windowWidth = window?.width ?? 0; - }; - // Avoid unnecessary renders if the KeyboardAvoidingView is disabled. _setBottom = (value: number) => { const enabled = this.props.enabled ?? true; @@ -154,15 +153,6 @@ class KeyboardAvoidingView extends React.Component { return; } - if ( - Platform.OS === 'ios' && - this._windowWidth !== this._keyboardEvent.endCoordinates.width - ) { - // The keyboard is not the standard bottom-of-the-screen keyboard. For example, floating keyboard on iPadOS. - this._setBottom(0); - return; - } - const {duration, easing, endCoordinates} = this._keyboardEvent; const height = await this._relativeKeyboardHeight(endCoordinates); @@ -200,8 +190,14 @@ class KeyboardAvoidingView extends React.Component { if (Platform.OS === 'ios') { this._subscriptions = [ - Keyboard.addListener('keyboardWillChangeFrame', this._onKeyboardChange), - Dimensions.addEventListener('change', this._onDimensionsChange), + // When undocked, split or floating, iOS will emit + // UIKeyboardWillHideNotification notification. + // UIKeyboardWillChangeFrameNotification will be emitted before + // UIKeyboardWillHideNotification, so we need to listen to + // keyboardWillHide and keyboardWillShow instead of + // keyboardWillChangeFrame. + Keyboard.addListener('keyboardWillHide', this._onKeyboardHide), + Keyboard.addListener('keyboardWillShow', this._onKeyboardChange), ]; } else { this._subscriptions = [ diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 1a6ce01d197..d3581254467 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -1792,12 +1792,11 @@ declare class KeyboardAvoidingView extends React.Component { viewRef: { current: React.ElementRef | null, ... }; _initialFrameHeight: number; _bottom: number; - _windowWidth: number; constructor(props: Props): void; _relativeKeyboardHeight(keyboardFrame: KeyboardMetrics): Promise; _onKeyboardChange: $FlowFixMe; + _onKeyboardHide: $FlowFixMe; _onLayout: $FlowFixMe; - _onDimensionsChange: $FlowFixMe; _setBottom: $FlowFixMe; _updateBottomIfNecessary: $FlowFixMe; componentDidUpdate(_: Props, prevState: State): void; diff --git a/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm b/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm index ce483ddceb9..79f4f8f4170 100644 --- a/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm +++ b/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm @@ -9,6 +9,7 @@ #import #import +#import #import "CoreModulesPlugins.h" @@ -117,6 +118,10 @@ static NSDictionary *RCTParseKeyboardNotification(NSNotification *notification) static_cast([userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]); NSInteger isLocalUserInfoKey = [userInfo[UIKeyboardIsLocalUserInfoKey] integerValue]; + UIWindow *window = RCTKeyWindow(); + beginFrame = [window convertRect:beginFrame fromCoordinateSpace:window.screen.coordinateSpace]; + endFrame = [window convertRect:endFrame fromCoordinateSpace:window.screen.coordinateSpace]; + return @{ @"startCoordinates" : RCTRectDictionaryValue(beginFrame), @"endCoordinates" : RCTRectDictionaryValue(endFrame),