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:

<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

[ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests
-->
[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
This commit is contained in:
Matthew Horan
2025-01-27 06:57:02 -08:00
committed by Facebook GitHub Bot
parent 4141560afc
commit c499ae1192
3 changed files with 20 additions and 20 deletions
@@ -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<Props, State> {
viewRef: {current: React.ElementRef<typeof View> | 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<Props, State> {
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<Props, State> {
}
};
_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<Props, State> {
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<Props, State> {
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 = [
@@ -1792,12 +1792,11 @@ declare class KeyboardAvoidingView extends React.Component<Props, State> {
viewRef: { current: React.ElementRef<typeof View> | null, ... };
_initialFrameHeight: number;
_bottom: number;
_windowWidth: number;
constructor(props: Props): void;
_relativeKeyboardHeight(keyboardFrame: KeyboardMetrics): Promise<number>;
_onKeyboardChange: $FlowFixMe;
_onKeyboardHide: $FlowFixMe;
_onLayout: $FlowFixMe;
_onDimensionsChange: $FlowFixMe;
_setBottom: $FlowFixMe;
_updateBottomIfNecessary: $FlowFixMe;
componentDidUpdate(_: Props, prevState: State): void;
@@ -9,6 +9,7 @@
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTEventDispatcherProtocol.h>
#import <React/RCTUtils.h>
#import "CoreModulesPlugins.h"
@@ -117,6 +118,10 @@ static NSDictionary *RCTParseKeyboardNotification(NSNotification *notification)
static_cast<UIViewAnimationCurve>([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),