Skip to content

Commit ba2e187

Browse files
authored
fix: keep shadow nodes in sync (#950)
## 📜 Description Fixed a problem when `Pressable` doesn't trigger `onPress` callback if it's located in `KeyboardStickyView` (`KeyboardToolbar`). ## 💡 Motivation and Context The problem stems from the fact that C++ shadow nodes do not know about changes caused by `useKeyboardAnimation` hook (we drive animation by native driver, so C++ thinks that view hasn't changed its position). I found out that `ScrollView` had the same problem: facebook/react-native#36504 (comment) Basically the fix is next: - after finishing the animation we dispatch `onUserDrivenAnimationEnded`; - in `useAnimatedProps`: ```ts NativeAnimatedHelper.nativeEventEmitter.addListener( 'onUserDrivenAnimationEnded', data => { node.update(); }, ); ``` Where: ```ts new AnimatedProps( props, () => onUpdateRef.current?.(), allowlistIfEnabled, ), ``` Which in turns calls: ```ts const isFabricNode = isFabricInstance(instance); if (node.__isNative) { // Check 2: this is an animation driven by native. // In native driven animations, this callback is only called once the animation completes. if (isFabricNode) { // Call `scheduleUpdate` to synchronise Fiber and Shadow tree. // Must not be called in Paper. scheduleUpdate(); } return; } ``` And it triggers re-render and synchronizes C++ state with latest react state. Probably not the best solution, but it fixes the problem. Also we have to gather all connected nodes that depends on animated value, but at the moment this data is unused: ```ts data => { node.update(); }, ``` So for now it's safe to skip connected nodes 🙃 (maybe later I'll revisit this) Closes #947 #916 #588 ## 📢 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 --> ### Android - added `keepShadowNodesInSync` function extension for `ThemedReactContext`; - call `keepShadowNodesInSync` in the end of keyboard animation. ## 🤔 How Has This Been Tested? Tested manually in FabricExample. ## 📸 Screenshots (if appropriate): |KeyboardStickyView|KeyboardToolbar| |---------------------|----------------| |<video src="https://github.com/user-attachments/assets/3950ec69-338e-4f2e-9301-b5c64efb808e">|<video src="https://github.com/user-attachments/assets/291e846e-3bf8-46fd-9f64-d53ec30c452b">| ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
1 parent be7ef24 commit ba2e187

File tree

4 files changed

+28
-4
lines changed

4 files changed

+28
-4
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useEffect, useMemo, useState } from "react";
2-
import { Button, Text, View } from "react-native";
2+
import { Alert, Button, Text, View } from "react-native";
33
import {
44
KeyboardAwareScrollView,
55
KeyboardStickyView,
@@ -82,7 +82,7 @@ export default function AwareScrollViewStickyFooter({ navigation }: Props) {
8282
<View style={styles.footer} onLayout={handleLayout}>
8383
<Text style={styles.footerText}>A mocked sticky footer</Text>
8484
<TextInput placeholder="Amount" style={styles.inputInFooter} />
85-
<Button title="Click me" />
85+
<Button title="Click me" onPress={() => Alert.alert("Clicked")} />
8686
</View>
8787
</KeyboardStickyView>
8888
)}

android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.reactnativekeyboardcontroller.extensions
22

33
import android.content.Context
44
import android.os.Build
5+
import com.facebook.react.bridge.Arguments
56
import com.facebook.react.bridge.ReactContext
67
import com.facebook.react.bridge.WritableMap
78
import com.facebook.react.modules.core.DeviceEventManagerModule
@@ -37,6 +38,25 @@ fun ThemedReactContext?.emitEvent(
3738
Logger.i("ThemedReactContext", event)
3839
}
3940

41+
fun ThemedReactContext?.keepShadowNodesInSync(viewId: Int) {
42+
// originally by viewId we should lookup all connected nodes
43+
// and send them to JS
44+
// but at the moment JS side broadcasts events to all ViewType
45+
// instances, so we can send even empty array
46+
val tags = intArrayOf(viewId)
47+
48+
val tagsArray = Arguments.createArray()
49+
for (tag in tags) {
50+
tagsArray.pushInt(tag)
51+
}
52+
53+
// emit the event to JS to re-sync the trees
54+
val onAnimationEndedData = Arguments.createMap()
55+
onAnimationEndedData.putArray("tags", tagsArray)
56+
57+
this?.reactApplicationContext?.emitDeviceEvent("onUserDrivenAnimationEnded", onAnimationEndedData)
58+
}
59+
4060
val ThemedReactContext?.appearance: String
4161
get() =
4262
when {

android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.reactnativekeyboardcontroller.extensions.dispatchEvent
2020
import com.reactnativekeyboardcontroller.extensions.dp
2121
import com.reactnativekeyboardcontroller.extensions.emitEvent
2222
import com.reactnativekeyboardcontroller.extensions.isKeyboardAnimation
23+
import com.reactnativekeyboardcontroller.extensions.keepShadowNodesInSync
2324
import com.reactnativekeyboardcontroller.extensions.keyboardType
2425
import com.reactnativekeyboardcontroller.interactive.InteractiveKeyboardProvider
2526
import com.reactnativekeyboardcontroller.log.Logger
@@ -327,6 +328,8 @@ class KeyboardAnimationCallback(
327328

328329
// reset to initial state
329330
duration = 0
331+
332+
context.keepShadowNodesInSync(eventPropagationView.id)
330333
}
331334

332335
if (isKeyboardInteractive) {
@@ -401,6 +404,7 @@ class KeyboardAnimationCallback(
401404
)
402405
}
403406
context.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight))
407+
context.keepShadowNodesInSync(eventPropagationView.id)
404408

405409
this.persistentKeyboardHeight = keyboardHeight
406410
}

example/src/screens/Examples/AwareScrollViewStickyFooter/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useEffect, useMemo, useState } from "react";
2-
import { Button, Text, View } from "react-native";
2+
import { Alert, Button, Text, View } from "react-native";
33
import {
44
KeyboardAwareScrollView,
55
KeyboardStickyView,
@@ -82,7 +82,7 @@ export default function AwareScrollViewStickyFooter({ navigation }: Props) {
8282
<View style={styles.footer} onLayout={handleLayout}>
8383
<Text style={styles.footerText}>A mocked sticky footer</Text>
8484
<TextInput placeholder="Amount" style={styles.inputInFooter} />
85-
<Button title="Click me" />
85+
<Button title="Click me" onPress={() => Alert.alert("Clicked")} />
8686
</View>
8787
</KeyboardStickyView>
8888
)}

0 commit comments

Comments
 (0)