Skip to content

Commit 03937a7

Browse files
authored
feat: OverKeyboardView with custom ShadowNode (#863)
## 📜 Description Stretch `OverKeyboardView` to full screen height on fabric/android. ## 💡 Motivation and Context To make `OverKeyboardView` to stretch to full screen we have to override layout on ShadowNodes side. For paper architecture we already do that in corresponding `ShadowNode` kotlin class. Since new architecture is C++ based - we have to make corresponding changes in C++ code. Unfortunately there is no easy way to add custom properties/code in existing/auto-generated shadow-nodes. To make it working we have to genereate them ourself and change the code accordingly. In this PR I did that. I copied autogenerated code, formatted it according to `cpplint` rules and did a proper linking. After that I wrote additional code that uses values passed from kotlin in c++ and properly resized the inner view. Last, but not least - on iOS we manually specify the size of inner child, on Android - we are laid out by ShadowNodes. To keep the same behavior on Android we need to stretch to full screen width `top: 0, right: 0, bottom: 0, left: 0`, while on iOS we just have to specify exact dimensions. Also on Android/Fabric I discovered, that if view keeps mounted, then the view intercepts touches (even if it's not laid out properly). To fix that and match RN behavior I added `visible && children` condition - in this case we don't mount children and thus they are not clickable 🙃 Closes #862 ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### Example - added black color for "KYC" button (toolbar); ### JS - removed `architecture.ts` file; - generate `interfaceOnly` from `codegen`; - don't mount children until `visible=true` (to match Modal behavior); - added `react-native.config.js`; ### C++ - created `common` folder that contains all `ShadowNode`s; ### iOS - added `common` dependency in `Podfile` for new architecture; ### Android - added jni folder; - added `CMakeList.txt`; - added custom `detekt` config; - pass `StateWrapper` to `OverKeyboardView`; - added `stretchTo` function to `OverKeyboardView`; ## 🤔 How Has This Been Tested? Tested locally on Medium Phone API 35 (paper and fabric) and on CI via e2e tests. ## 📸 Screenshots (if appropriate): |Before|After| |-------|-----| |<img width="368" alt="image" src="https://github.com/user-attachments/assets/cc06c212-3524-4fc1-b7c3-15b69c29ad49" />|<img width="370" alt="image" src="https://github.com/user-attachments/assets/87dbffa7-a028-4b8e-bfd1-ba717d401ac9" />| ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
1 parent da87e79 commit 03937a7

35 files changed

+492
-21
lines changed

.github/workflows/verify-android.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ jobs:
7979
curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.1/detekt-cli-1.23.1.zip
8080
unzip detekt-cli-1.23.1.zip
8181
- name: Run Detekt
82-
run: ./detekt-cli-1.23.1/bin/detekt-cli
82+
run: ./detekt-cli-1.23.1/bin/detekt-cli --config detekt.yml --build-upon-default-config
8383
unit-tests:
8484
name: 📖 Unit tests
8585
runs-on: ubuntu-latest

.github/workflows/verify-cpp.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ on:
77
paths:
88
- ".github/workflows/verify-cpp.yml"
99
- "android/src/main/**"
10+
- "common/cpp/**"
1011
pull_request:
1112
paths:
1213
- ".github/workflows/verify-cpp.yml"
1314
- "android/src/main/**"
15+
- "common/cpp/**"
1416

1517
jobs:
1618
lint:
@@ -33,4 +35,4 @@ jobs:
3335
python -m pip install --upgrade pip
3436
pip install cpplint
3537
- name: Run cpplint
36-
run: cpplint --linelength=230 --filter=-legal/copyright,-readability/todo,-build/namespaces,-whitespace/comments,-build/c++11,-runtime/int,-runtime/references --quiet --recursive android/src/main/
38+
run: cpplint --linelength=230 --filter=-legal/copyright,-readability/todo,-build/namespaces,-whitespace/comments,-build/c++11,-runtime/int,-runtime/references,-whitespace/indent_namespace --quiet --recursive android/src/main/ common/cpp/

FabricExample/ios/Podfile.lock

+23-1
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,28 @@ PODS:
12831283
- ReactCommon/turbomodule/core
12841284
- Yoga
12851285
- react-native-keyboard-controller (1.16.8):
1286+
- DoubleConversion
1287+
- glog
1288+
- hermes-engine
1289+
- RCT-Folly (= 2024.11.18.00)
1290+
- RCTRequired
1291+
- RCTTypeSafety
1292+
- React-Core
1293+
- React-debug
1294+
- React-Fabric
1295+
- React-featureflags
1296+
- React-graphics
1297+
- React-ImageManager
1298+
- react-native-keyboard-controller/common (= 1.16.8)
1299+
- React-NativeModulesApple
1300+
- React-RCTFabric
1301+
- React-rendererdebug
1302+
- React-utils
1303+
- ReactCodegen
1304+
- ReactCommon/turbomodule/bridging
1305+
- ReactCommon/turbomodule/core
1306+
- Yoga
1307+
- react-native-keyboard-controller/common (1.16.8):
12861308
- DoubleConversion
12871309
- glog
12881310
- hermes-engine
@@ -2164,7 +2186,7 @@ SPEC CHECKSUMS:
21642186
React-Mapbuffer: 0502faf46cab8fb89cfc7bf3e6c6109b6ef9b5de
21652187
React-microtasksnativemodule: 663bc64e3a96c5fc91081923ae7481adc1359a78
21662188
react-native-blur: b37343d4df1af48a17444156b674b26d5aec2425
2167-
react-native-keyboard-controller: b9280bc833465038d4805dbd4ca66660f7f0dcd3
2189+
react-native-keyboard-controller: ef23fc10134fc24ab13d331afe1fbc33374f185a
21682190
react-native-safe-area-context: 9c33120e9eac7741a5364cc2d9f74665049b76b3
21692191
React-NativeModulesApple: 16fbd5b040ff6c492dacc361d49e63cba7a6a7a1
21702192
React-perflogger: ab51b7592532a0ea45bf6eed7e6cae14a368b678

FabricExample/src/screens/Examples/Toolbar/index.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ const styles = StyleSheet.create({
231231
right: 0,
232232
},
233233
header: {
234+
color: "black",
234235
marginRight: 12,
235236
},
236237
modal: {

android/detekt.yml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
complexity:
2+
TooManyFunctions:
3+
ignoreOverridden: true

android/src/fabric/java/com/reactnativekeyboardcontroller/OverKeyboardViewManager.kt

+11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.reactnativekeyboardcontroller
22

33
import com.facebook.react.bridge.ReactApplicationContext
44
import com.facebook.react.uimanager.LayoutShadowNode
5+
import com.facebook.react.uimanager.ReactStylesDiffMap
6+
import com.facebook.react.uimanager.StateWrapper
57
import com.facebook.react.uimanager.ThemedReactContext
68
import com.facebook.react.uimanager.ViewGroupManager
79
import com.facebook.react.uimanager.ViewManagerDelegate
@@ -30,6 +32,15 @@ class OverKeyboardViewManager(
3032

3133
override fun getShadowNodeClass(): Class<out LayoutShadowNode> = OverKeyboardHostShadowNode::class.java
3234

35+
override fun updateState(
36+
view: OverKeyboardHostView,
37+
props: ReactStylesDiffMap,
38+
stateWrapper: StateWrapper,
39+
): Any? {
40+
view.stateWrapper = stateWrapper
41+
return null
42+
}
43+
3344
@ReactProp(name = "visible")
3445
override fun setVisible(
3546
view: OverKeyboardHostView,

android/src/main/java/com/reactnativekeyboardcontroller/views/overlay/OverKeyboardViewGroup.kt

+17
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@ import android.view.MotionEvent
77
import android.view.View
88
import android.view.WindowManager
99
import com.facebook.react.bridge.UiThreadUtil
10+
import com.facebook.react.bridge.WritableMap
11+
import com.facebook.react.bridge.WritableNativeMap
1012
import com.facebook.react.config.ReactFeatureFlags
1113
import com.facebook.react.uimanager.JSTouchDispatcher
14+
import com.facebook.react.uimanager.StateWrapper
1215
import com.facebook.react.uimanager.ThemedReactContext
1316
import com.facebook.react.uimanager.UIManagerHelper
1417
import com.facebook.react.uimanager.events.EventDispatcher
1518
import com.facebook.react.views.view.ReactViewGroup
19+
import com.reactnativekeyboardcontroller.extensions.dp
20+
import com.reactnativekeyboardcontroller.extensions.getDisplaySize
1621

1722
@SuppressLint("ViewConstructor")
1823
class OverKeyboardHostView(
@@ -22,6 +27,8 @@ class OverKeyboardHostView(
2227
private var windowManager: WindowManager = reactContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
2328
private var hostView: OverKeyboardRootViewGroup = OverKeyboardRootViewGroup(reactContext)
2429

30+
internal var stateWrapper: StateWrapper? = null
31+
2532
init {
2633
hostView.eventDispatcher = dispatcher
2734
}
@@ -82,14 +89,24 @@ class OverKeyboardHostView(
8289
PixelFormat.TRANSLUCENT,
8390
)
8491

92+
stretchTo(fullScreen = true)
8593
windowManager.addView(hostView, layoutParams)
8694
}
8795

8896
fun hide() {
8997
if (hostView.isAttached) {
9098
windowManager.removeView(hostView)
99+
stretchTo(fullScreen = false)
91100
}
92101
}
102+
103+
private fun stretchTo(fullScreen: Boolean) {
104+
val displaySize = reactContext.getDisplaySize()
105+
val newStateData: WritableMap = WritableNativeMap()
106+
newStateData.putDouble("screenWidth", if (fullScreen) displaySize.x.toFloat().dp else 0.0)
107+
newStateData.putDouble("screenHeight", if (fullScreen) displaySize.y.toFloat().dp else 0.0)
108+
stateWrapper?.updateState(newStateData)
109+
}
93110
}
94111

95112
@SuppressLint("ViewConstructor")

android/src/main/jni/CMakeLists.txt

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
cmake_minimum_required(VERSION 3.13)
2+
set(CMAKE_VERBOSE_MAKEFILE ON)
3+
4+
set(LIB_LITERAL reactnativekeyboardcontroller)
5+
set(LIB_TARGET_NAME react_codegen_${LIB_LITERAL})
6+
7+
set(LIB_ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..)
8+
set(LIB_COMMON_DIR ${LIB_ANDROID_DIR}/../common/cpp)
9+
set(LIB_COMMON_COMPONENTS_DIR ${LIB_COMMON_DIR}/react/renderer/components/${LIB_LITERAL})
10+
set(LIB_ANDROID_GENERATED_JNI_DIR ${LIB_ANDROID_DIR}/build/generated/source/codegen/jni)
11+
set(LIB_ANDROID_GENERATED_COMPONENTS_DIR ${LIB_ANDROID_GENERATED_JNI_DIR}/react/renderer/components/${LIB_LITERAL})
12+
13+
add_compile_options(
14+
-fexceptions
15+
-frtti
16+
-std=c++20
17+
-Wall
18+
-Wpedantic
19+
-Wno-gnu-zero-variadic-macro-arguments
20+
-Wno-dollar-in-identifier-extension
21+
)
22+
23+
file(GLOB LIB_CUSTOM_SRCS CONFIGURE_DEPENDS *.cpp ${LIB_COMMON_COMPONENTS_DIR}/*.cpp)
24+
file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS ${LIB_ANDROID_GENERATED_JNI_DIR}/*.cpp ${LIB_ANDROID_GENERATED_COMPONENTS_DIR}/*.cpp)
25+
26+
add_library(
27+
${LIB_TARGET_NAME}
28+
SHARED
29+
${LIB_CUSTOM_SRCS}
30+
${LIB_CODEGEN_SRCS}
31+
)
32+
33+
target_include_directories(
34+
${LIB_TARGET_NAME}
35+
PUBLIC
36+
.
37+
${LIB_COMMON_DIR}
38+
${LIB_ANDROID_GENERATED_JNI_DIR}
39+
${LIB_ANDROID_GENERATED_COMPONENTS_DIR}
40+
)
41+
42+
if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 76)
43+
target_link_libraries(
44+
${LIB_TARGET_NAME}
45+
ReactAndroid::reactnative
46+
ReactAndroid::jsi
47+
fbjni::fbjni
48+
)
49+
else()
50+
target_link_libraries(
51+
${LIB_TARGET_NAME}
52+
fbjni
53+
folly_runtime
54+
glog
55+
jsi
56+
react_codegen_rncore
57+
react_debug
58+
react_nativemodule_core
59+
react_render_core
60+
react_render_debug
61+
react_render_graphics
62+
react_render_mapbuffer
63+
react_render_componentregistry
64+
react_utils
65+
rrc_view
66+
turbomodulejsijni
67+
yoga
68+
)
69+
endif()
70+
71+
target_compile_options(
72+
${LIB_TARGET_NAME}
73+
PRIVATE
74+
-DLOG_TAG=\"ReactNative\"
75+
-fexceptions
76+
-frtti
77+
-std=c++20
78+
-Wall
79+
)
80+
81+
target_include_directories(
82+
${CMAKE_PROJECT_NAME}
83+
PUBLIC
84+
${CMAKE_CURRENT_SOURCE_DIR}
85+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#pragma once
2+
3+
#include <ReactCommon/JavaTurboModule.h>
4+
#include <ReactCommon/TurboModule.h>
5+
#include <jsi/jsi.h>
6+
7+
/**
8+
* Note this import and that it is not present in autogenerated header file
9+
* under android/build/generated/source/codegen/jni/reactnativekeyboardcontroller.h
10+
*
11+
* Here we are overriding autogenerated component descriptors by prioritizing our custom headers via include path setup.
12+
*/
13+
#include <react/renderer/components/reactnativekeyboardcontroller/RNKCKeyboardControllerViewComponentDescriptor.h>
14+
#include <react/renderer/components/reactnativekeyboardcontroller/RNKCKeyboardGestureAreaComponentDescriptor.h>
15+
#include <react/renderer/components/reactnativekeyboardcontroller/RNKCOverKeyboardViewComponentDescriptor.h>
16+
17+
#include <memory>
18+
#include <string>
19+
20+
namespace facebook::react {
21+
JSI_EXPORT
22+
std::shared_ptr<TurboModule> reactnativekeyboardcontroller_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams &params);
23+
} // namespace facebook::react
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#pragma once
2+
3+
#include "RNKCKeyboardControllerViewShadowNode.h"
4+
5+
#include <react/debug/react_native_assert.h>
6+
#include <react/renderer/components/reactnativekeyboardcontroller/Props.h>
7+
#include <react/renderer/core/ConcreteComponentDescriptor.h>
8+
9+
namespace facebook::react {
10+
11+
class KeyboardControllerViewComponentDescriptor final
12+
: public ConcreteComponentDescriptor<KeyboardControllerViewShadowNode> {
13+
public:
14+
using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
15+
void adopt(ShadowNode &shadowNode) const override {
16+
react_native_assert(dynamic_cast<KeyboardControllerViewShadowNode *>(&shadowNode));
17+
ConcreteComponentDescriptor::adopt(shadowNode);
18+
}
19+
};
20+
21+
} // namespace facebook::react
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#include "RNKCKeyboardControllerViewShadowNode.h"
2+
3+
namespace facebook::react {
4+
5+
extern const char KeyboardControllerViewComponentName[] = "KeyboardControllerView";
6+
7+
} // namespace facebook::react
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#pragma once
2+
3+
#include "RNKCKeyboardControllerViewState.h"
4+
5+
#include <react/renderer/components/reactnativekeyboardcontroller/EventEmitters.h>
6+
#include <react/renderer/components/reactnativekeyboardcontroller/Props.h>
7+
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
8+
#include <jsi/jsi.h>
9+
10+
namespace facebook::react {
11+
12+
JSI_EXPORT extern const char KeyboardControllerViewComponentName[];
13+
14+
/*
15+
* `ShadowNode` for <KeyboardControllerView> component.
16+
*/
17+
using KeyboardControllerViewShadowNode = ConcreteViewShadowNode<
18+
KeyboardControllerViewComponentName,
19+
KeyboardControllerViewProps,
20+
KeyboardControllerViewEventEmitter,
21+
KeyboardControllerViewState>;
22+
23+
} // namespace facebook::react
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#pragma once
2+
3+
#ifdef ANDROID
4+
#include <folly/dynamic.h>
5+
#endif
6+
7+
namespace facebook::react {
8+
9+
class KeyboardControllerViewState {
10+
public:
11+
KeyboardControllerViewState() = default;
12+
13+
#ifdef ANDROID
14+
KeyboardControllerViewState(KeyboardControllerViewState const &previousState, folly::dynamic data) {}
15+
folly::dynamic getDynamic() const {
16+
return {};
17+
}
18+
#endif
19+
};
20+
21+
} // namespace facebook::react
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#pragma once
2+
3+
#include "RNKCKeyboardGestureAreaShadowNode.h"
4+
5+
#include <react/debug/react_native_assert.h>
6+
#include <react/renderer/components/reactnativekeyboardcontroller/Props.h>
7+
#include <react/renderer/core/ConcreteComponentDescriptor.h>
8+
9+
namespace facebook::react {
10+
class KeyboardGestureAreaComponentDescriptor final
11+
: public ConcreteComponentDescriptor<KeyboardGestureAreaShadowNode> {
12+
public:
13+
using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
14+
void adopt(ShadowNode &shadowNode) const override {
15+
react_native_assert(dynamic_cast<KeyboardGestureAreaShadowNode *>(&shadowNode));
16+
ConcreteComponentDescriptor::adopt(shadowNode);
17+
}
18+
};
19+
20+
} // namespace facebook::react
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#include "RNKCKeyboardGestureAreaShadowNode.h"
2+
3+
namespace facebook::react {
4+
5+
extern const char KeyboardGestureAreaComponentName[] = "KeyboardGestureArea";
6+
7+
} // namespace facebook::react
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#pragma once
2+
3+
#include "RNKCKeyboardGestureAreaState.h"
4+
5+
#include <react/renderer/components/reactnativekeyboardcontroller/EventEmitters.h>
6+
#include <react/renderer/components/reactnativekeyboardcontroller/Props.h>
7+
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
8+
#include <jsi/jsi.h>
9+
10+
namespace facebook::react {
11+
12+
JSI_EXPORT extern const char KeyboardGestureAreaComponentName[];
13+
14+
/*
15+
* `ShadowNode` for <KeyboardGestureArea> component.
16+
*/
17+
using KeyboardGestureAreaShadowNode = ConcreteViewShadowNode<
18+
KeyboardGestureAreaComponentName,
19+
KeyboardGestureAreaProps,
20+
KeyboardGestureAreaEventEmitter,
21+
KeyboardGestureAreaState>;
22+
23+
} // namespace facebook::react

0 commit comments

Comments
 (0)