Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import type {
} from '../View/ViewPropTypes';
import type {KeyboardEvent, KeyboardMetrics} from './Keyboard';

import KeyboardAvoidingViewNativeComponent from '../../../src/private/components/keyboardavoidingview/specs/KeyboardAvoidingViewNativeComponent';
import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';
import LayoutAnimation from '../../LayoutAnimation/LayoutAnimation';
import StyleSheet from '../../StyleSheet/StyleSheet';
import NativeDeviceInfo from '../../Utilities/NativeDeviceInfo';
import Platform from '../../Utilities/Platform';
import warnOnce from '../../Utilities/warnOnce';
import {type EventSubscription} from '../../vendor/emitter/EventEmitter';
import AccessibilityInfo from '../AccessibilityInfo/AccessibilityInfo';
import View from '../View/View';
Expand All @@ -44,6 +48,15 @@ type KeyboardAvoidingViewPropsCore = Readonly<{
*/
enabled?: ?boolean,

/**
* Follow the keyboard animation natively, frame by frame.
* Requires the `useSharedAnimatedBackend` feature flag and an edge-to-edge
* app; otherwise, this prop has no effect.
*
* @default `false`
*/
experimental_native?: ?boolean,

/**
* Distance between the top of the user screen and the React Native view,
* may be non-zero in some use cases.
Expand All @@ -63,6 +76,46 @@ type KeyboardAvoidingViewState = {
bottom: number,
};

function getIsNativeSupported(): boolean {
// eslint-disable-next-line react-hooks/rules-of-hooks
if (!ReactNativeFeatureFlags.useSharedAnimatedBackend()) {
if (__DEV__) {
warnOnce(
'KeyboardAvoidingView-experimental_native-flag',
'KeyboardAvoidingView: `experimental_native` has no effect because ' +
'the `useSharedAnimatedBackend` feature flag is disabled.',
);
}
return false;
}
if (
Platform.OS === 'android' &&
NativeDeviceInfo.getConstants().isEdgeToEdge !== true
) {
if (__DEV__) {
warnOnce(
'KeyboardAvoidingView-experimental_native-edge-to-edge',
'KeyboardAvoidingView: `experimental_native` has no effect because ' +
'the app is not edge-to-edge.',
);
}
return false;
}
return true;
}

let isNativeSupported: ?boolean = null;

function shouldUseNative(props: KeyboardAvoidingViewProps): boolean {
if (props.experimental_native !== true || props.behavior == null) {
return false;
}
if (isNativeSupported == null) {
isNativeSupported = getIsNativeSupported();
}
return isNativeSupported;
}

/**
* Automatically adjusts its height, position, or bottom padding based on the
* keyboard height to remain visible while the virtual keyboard is displayed.
Expand Down Expand Up @@ -161,6 +214,10 @@ class KeyboardAvoidingView extends React.Component<
};

_updateBottomIfNecessary = async () => {
if (shouldUseNative(this.props)) {
return;
}

if (this._keyboardEvent == null) {
this._setBottom(0);
return;
Expand Down Expand Up @@ -196,13 +253,37 @@ class KeyboardAvoidingView extends React.Component<
if (enabled && this._bottom !== prevState.bottom) {
this.setState({bottom: this._bottom});
}
this._updateSubscriptions();
}

componentDidMount(): void {
if (!Keyboard.isVisible()) {
this._keyboardEvent = null;
this._setBottom(0);
}
this._updateSubscriptions();
}

// Only the JavaScript implementation needs keyboard events.
_updateSubscriptions(): void {
const shouldSubscribe = !shouldUseNative(this.props);

if (shouldSubscribe === this._subscriptions.length > 0) {
return;
}

if (!shouldSubscribe) {
this._subscriptions.forEach(subscription => {
subscription.remove();
});
this._subscriptions = [];
// The last event goes stale while unsubscribed.
this._keyboardEvent = null;
if (this._bottom !== 0) {
this._setBottom(0);
}
return;
}

if (Platform.OS === 'ios') {
this._subscriptions = [
Expand Down Expand Up @@ -235,12 +316,47 @@ class KeyboardAvoidingView extends React.Component<
children,
contentContainerStyle,
enabled = true,
// eslint-disable-next-line no-unused-vars
experimental_native,
keyboardVerticalOffset = 0,
style,
onLayout,
...props
} = this.props;

if (shouldUseNative(this.props)) {
if (behavior === 'position') {
return (
<View
ref={this.viewRef}
style={style}
onLayout={onLayout}
collapsable={false}
{...props}>
<KeyboardAvoidingViewNativeComponent
behavior="position"
enabled={enabled === true}
keyboardVerticalOffset={keyboardVerticalOffset}
style={contentContainerStyle}>
{children}
</KeyboardAvoidingViewNativeComponent>
</View>
);
}

return (
<KeyboardAvoidingViewNativeComponent
ref={this.viewRef}
behavior={behavior}
enabled={enabled === true}
keyboardVerticalOffset={keyboardVerticalOffset}
style={style}
onLayout={onLayout}
{...props}>
{children}
</KeyboardAvoidingViewNativeComponent>
);
}

const bottomHeight = enabled === true ? this.state.bottom : 0;
switch (behavior) {
case 'height':
Expand Down
11 changes: 10 additions & 1 deletion packages/react-native/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ let reactFabric = RNTarget(
"consistency",
"uimanager/consistency/tests",
"components/inputaccessory",
"components/keyboardavoidingview",
"components/modal",
"components/rncore",
"components/safeareaview",
Expand All @@ -513,6 +514,12 @@ let reactFabricInputAccessory = RNTarget(
dependencies: [.reactNativeDependencies, .reactCore, .reactJsiExecutor, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .yoga, .reactRendererDebug, .reactGraphics, .reactFabric, .reactTurboModuleBridging]
)

let reactFabricKeyboardAvoidingView = RNTarget(
name: .reactFabricKeyboardAvoidingView,
path: "ReactCommon/react/renderer/components/keyboardavoidingview",
dependencies: [.reactNativeDependencies, .reactCore, .reactJsiExecutor, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .yoga, .reactRendererDebug, .reactGraphics, .reactFabric, .reactTurboModuleBridging]
)

let reactFabricModal = RNTarget(
name: .reactFabricModal,
path: "ReactCommon/react/renderer/components/modal",
Expand Down Expand Up @@ -588,7 +595,7 @@ let reactRCTFabric = RNTarget(
name: .reactRCTFabric,
path: "React/Fabric",
searchPaths: [virtualViewPath],
dependencies: [.reactNativeDependencies, .reactCore, .reactRCTImage, .yoga, .reactRCTText, .jsi, .reactFabricInputAccessory, .reactFabricModal, .reactFabricSafeAreaView, .reactFabricSwitch, .reactFabricText, .reactFabricTextInput, .reactFabricUnimplementedView, .reactFabricTextLayoutManager, .reactGraphics, .reactImageManager, .reactDebug, .reactUtils, .reactPerformanceTimeline, .reactRendererDebug, .reactRendererConsistency, .reactRuntimeScheduler, .reactRCTAnimation, .reactJsInspector, .reactJsInspectorNetwork, .reactJsInspectorTracing, .reactFabric, .reactFabricImage, .rctSwiftUIWrapper]
dependencies: [.reactNativeDependencies, .reactCore, .reactRCTImage, .yoga, .reactRCTText, .jsi, .reactFabricInputAccessory, .reactFabricKeyboardAvoidingView, .reactFabricModal, .reactFabricSafeAreaView, .reactFabricSwitch, .reactFabricText, .reactFabricTextInput, .reactFabricUnimplementedView, .reactFabricTextLayoutManager, .reactGraphics, .reactImageManager, .reactDebug, .reactUtils, .reactPerformanceTimeline, .reactRendererDebug, .reactRendererConsistency, .reactRuntimeScheduler, .reactRCTAnimation, .reactJsInspector, .reactJsInspectorNetwork, .reactJsInspectorTracing, .reactFabric, .reactFabricImage, .rctSwiftUIWrapper]
)

/// React-ImageManagerApple.podspec
Expand Down Expand Up @@ -710,6 +717,7 @@ let targets = [
reactRCTFabric,
reactFabricImage,
reactFabricInputAccessory,
reactFabricKeyboardAvoidingView,
reactFabricModal,
reactFabricSafeAreaView,
reactFabricSwitch,
Expand Down Expand Up @@ -897,6 +905,7 @@ extension String {

static let reactFabricImage = "React-FabricImage"
static let reactFabricInputAccessory = "React-FabricInputAccessory"
static let reactFabricKeyboardAvoidingView = "React-FabricKeyboardAvoidingView"
static let reactFabricModal = "React-FabricModal"
static let reactFabricSafeAreaView = "React-FabricSafeAreaView"
static let reactFabricSwitch = "React-FabricSwitch"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import <UIKit/UIKit.h>

#import <React/RCTViewComponentView.h>

NS_ASSUME_NONNULL_BEGIN

/**
* UIView class for <KeyboardAvoidingView> component.
*
* Follows the keyboard with `UIKeyboardLayoutGuide` and sends the keyboard
* inset to the shared animation backend on each frame.
*/
@interface RCTKeyboardAvoidingViewComponentView : RCTViewComponentView

@end

NS_ASSUME_NONNULL_END
Loading
Loading