react-native-keyboard-controller 1.21.0-beta.3 → 1.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +1 -0
  2. package/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +1 -0
  3. package/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +7 -0
  4. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +8 -0
  5. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt +23 -0
  6. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ReactContext.kt +12 -0
  7. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt +2 -1
  8. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/WindowDimensionListener.kt +5 -1
  9. package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarGroupViewManagerImpl.kt +13 -0
  10. package/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt +4 -6
  11. package/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt +26 -0
  12. package/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt +31 -4
  13. package/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt +5 -0
  14. package/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarGroupReactViewGroup.kt +13 -0
  15. package/android/src/main/jni/RNKC.h +2 -0
  16. package/android/src/paper/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +8 -0
  17. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +9 -0
  18. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt +15 -0
  19. package/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +2 -0
  20. package/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewComponentDescriptor.h +27 -0
  21. package/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewShadowNode.cpp +14 -0
  22. package/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewShadowNode.h +30 -0
  23. package/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewState.h +28 -0
  24. package/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewComponentDescriptor.h +20 -0
  25. package/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.cpp +7 -0
  26. package/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.h +23 -0
  27. package/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewState.h +21 -0
  28. package/ios/KeyboardControllerModule.mm +36 -0
  29. package/ios/extensions/UIApplication.swift +1 -1
  30. package/ios/observers/FocusedInputObserver.swift +2 -1
  31. package/ios/traversal/ViewHierarchyNavigator.swift +49 -3
  32. package/ios/views/ClippingScrollViewDecoratorViewManager.h +26 -0
  33. package/ios/views/ClippingScrollViewDecoratorViewManager.mm +231 -0
  34. package/ios/views/KeyboardToolbarGroupViewManager.h +28 -0
  35. package/ios/views/KeyboardToolbarGroupViewManager.mm +88 -0
  36. package/jest/index.js +11 -1
  37. package/lib/commonjs/bindings.js +18 -2
  38. package/lib/commonjs/bindings.js.map +1 -1
  39. package/lib/commonjs/bindings.native.js +3 -4
  40. package/lib/commonjs/bindings.native.js.map +1 -1
  41. package/lib/commonjs/components/KeyboardAvoidingView/index.js +30 -4
  42. package/lib/commonjs/components/KeyboardAvoidingView/index.js.map +1 -1
  43. package/lib/commonjs/components/KeyboardAwareScrollView/index.js +17 -4
  44. package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
  45. package/lib/commonjs/components/KeyboardChatScrollView/index.js +33 -2
  46. package/lib/commonjs/components/KeyboardChatScrollView/index.js.map +1 -1
  47. package/lib/commonjs/components/KeyboardChatScrollView/types.js.map +1 -1
  48. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js +107 -21
  49. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js.map +1 -1
  50. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +45 -17
  51. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -1
  52. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js +61 -26
  53. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -1
  54. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -1
  55. package/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js +72 -0
  56. package/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js.map +1 -0
  57. package/lib/commonjs/components/KeyboardToolbar/index.js +1 -0
  58. package/lib/commonjs/components/KeyboardToolbar/index.js.map +1 -1
  59. package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js +18 -6
  60. package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js.map +1 -1
  61. package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js +1 -2
  62. package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -1
  63. package/lib/commonjs/specs/KeyboardToolbarGroupViewNativeComponent.js +12 -0
  64. package/lib/commonjs/specs/KeyboardToolbarGroupViewNativeComponent.js.map +1 -0
  65. package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
  66. package/lib/commonjs/types/module.js.map +1 -1
  67. package/lib/commonjs/types/views.js.map +1 -1
  68. package/lib/module/bindings.js +17 -1
  69. package/lib/module/bindings.js.map +1 -1
  70. package/lib/module/bindings.native.js +2 -3
  71. package/lib/module/bindings.native.js.map +1 -1
  72. package/lib/module/components/KeyboardAvoidingView/index.js +29 -4
  73. package/lib/module/components/KeyboardAvoidingView/index.js.map +1 -1
  74. package/lib/module/components/KeyboardAwareScrollView/index.js +17 -4
  75. package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
  76. package/lib/module/components/KeyboardChatScrollView/index.js +34 -3
  77. package/lib/module/components/KeyboardChatScrollView/index.js.map +1 -1
  78. package/lib/module/components/KeyboardChatScrollView/types.js.map +1 -1
  79. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js +98 -16
  80. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js.map +1 -1
  81. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +46 -18
  82. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -1
  83. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js +62 -27
  84. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -1
  85. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -1
  86. package/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js +67 -0
  87. package/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js.map +1 -0
  88. package/lib/module/components/KeyboardToolbar/index.js +2 -1
  89. package/lib/module/components/KeyboardToolbar/index.js.map +1 -1
  90. package/lib/module/components/ScrollViewWithBottomPadding/index.js +19 -7
  91. package/lib/module/components/ScrollViewWithBottomPadding/index.js.map +1 -1
  92. package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js +1 -2
  93. package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -1
  94. package/lib/module/specs/KeyboardToolbarGroupViewNativeComponent.js +5 -0
  95. package/lib/module/specs/KeyboardToolbarGroupViewNativeComponent.js.map +1 -0
  96. package/lib/module/specs/NativeKeyboardController.js.map +1 -1
  97. package/lib/module/types/module.js.map +1 -1
  98. package/lib/module/types/views.js.map +1 -1
  99. package/lib/typescript/bindings.d.ts +12 -2
  100. package/lib/typescript/bindings.native.d.ts +2 -1
  101. package/lib/typescript/components/KeyboardAvoidingView/index.d.ts +44 -13
  102. package/lib/typescript/components/KeyboardChatScrollView/index.d.ts +3 -0
  103. package/lib/typescript/components/KeyboardChatScrollView/types.d.ts +41 -0
  104. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts +63 -8
  105. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/index.d.ts +1 -4
  106. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts +15 -0
  107. package/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts +43 -0
  108. package/lib/typescript/components/KeyboardToolbar/index.d.ts +2 -0
  109. package/lib/typescript/components/ScrollViewWithBottomPadding/index.d.ts +3 -0
  110. package/lib/typescript/specs/ClippingScrollViewDecoratorViewNativeComponent.d.ts +1 -0
  111. package/lib/typescript/specs/KeyboardToolbarGroupViewNativeComponent.d.ts +6 -0
  112. package/lib/typescript/specs/NativeKeyboardController.d.ts +1 -0
  113. package/lib/typescript/types/module.d.ts +7 -0
  114. package/lib/typescript/types/views.d.ts +2 -0
  115. package/package.json +6 -2
  116. package/react-native.config.js +2 -0
  117. package/src/bindings.native.ts +4 -4
  118. package/src/bindings.ts +15 -1
  119. package/src/components/KeyboardAvoidingView/index.tsx +56 -29
  120. package/src/components/KeyboardAwareScrollView/index.tsx +20 -5
  121. package/src/components/KeyboardChatScrollView/index.tsx +47 -3
  122. package/src/components/KeyboardChatScrollView/types.ts +41 -0
  123. package/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts +126 -21
  124. package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts +101 -21
  125. package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts +116 -40
  126. package/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts +9 -0
  127. package/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts +123 -0
  128. package/src/components/KeyboardToolbar/index.tsx +6 -1
  129. package/src/components/ScrollViewWithBottomPadding/index.tsx +31 -4
  130. package/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts +1 -1
  131. package/src/specs/KeyboardToolbarGroupViewNativeComponent.ts +10 -0
  132. package/src/specs/NativeKeyboardController.ts +1 -0
  133. package/src/types/module.ts +9 -0
  134. package/src/types/views.ts +2 -0
  135. package/android/.settings/org.eclipse.buildship.core.prefs +0 -2
  136. package/ios/KeyboardController.xcodeproj/xcuserdata/kirylziusko.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  137. package/ios/Tests/Tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  138. package/ios/Tests/Tests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  139. package/ios/Tests/Tests.xcodeproj/project.xcworkspace/xcuserdata/kirylziusko.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  140. package/ios/Tests/Tests.xcodeproj/xcuserdata/kirylziusko.xcuserdatad/xcschemes/xcschememanagement.plist +0 -32
@@ -6,6 +6,9 @@ import useScrollState from "../../hooks/useScrollState";
6
6
  import {
7
7
  clampedScrollTarget,
8
8
  getEffectiveHeight,
9
+ getMinimumPaddingAbsorbed,
10
+ getScrollEffective,
11
+ getVisibleMinimumPaddingFraction,
9
12
  isScrollAtEnd,
10
13
  shouldShiftContent,
11
14
  } from "./helpers";
@@ -24,24 +27,29 @@ import type Reanimated from "react-native-reanimated";
24
27
  * @returns Shared values for padding and contentOffsetY (always `undefined`).
25
28
  * @example
26
29
  * ```tsx
27
- * const { padding, contentOffsetY } = useChatKeyboard(ref, {
28
- * inverted: false,
29
- * keyboardLiftBehavior: "always",
30
- * });
30
+ * const { padding } = useChatKeyboard(ref, { inverted: false, keyboardLiftBehavior: "always" });
31
31
  * ```
32
32
  */
33
33
  function useChatKeyboard(
34
34
  scrollViewRef: AnimatedRef<Reanimated.ScrollView>,
35
35
  options: UseChatKeyboardOptions,
36
36
  ): UseChatKeyboardReturn {
37
- const { inverted, keyboardLiftBehavior, freeze, offset } = options;
37
+ const {
38
+ inverted,
39
+ keyboardLiftBehavior,
40
+ freeze,
41
+ offset,
42
+ blankSpace,
43
+ extraContentPadding,
44
+ } = options;
38
45
 
39
46
  const padding = useSharedValue(0);
40
47
  const currentHeight = useSharedValue(0);
41
48
  const offsetBeforeScroll = useSharedValue(0);
42
49
  const targetKeyboardHeight = useSharedValue(0);
43
50
  const closing = useSharedValue(false);
44
-
51
+ const minimumPaddingFractionOnOpen = useSharedValue(0);
52
+ const actualOpenShift = useSharedValue(0);
45
53
  const {
46
54
  layout,
47
55
  size,
@@ -49,12 +57,18 @@ function useChatKeyboard(
49
57
  onLayout,
50
58
  onContentSizeChange,
51
59
  } = useScrollState(scrollViewRef);
52
-
53
- const clampScrollIfNeeded = (effective: number) => {
60
+ const clampScrollIfNeeded = (
61
+ effective: number,
62
+ totalPaddingForMaxScroll?: number,
63
+ ) => {
54
64
  "worklet";
55
65
 
66
+ const paddingForMax =
67
+ totalPaddingForMaxScroll !== undefined
68
+ ? totalPaddingForMaxScroll
69
+ : effective;
56
70
  const maxScroll = Math.max(
57
- size.value.height - layout.value.height + effective,
71
+ size.value.height - layout.value.height + paddingForMax,
58
72
  0,
59
73
  );
60
74
 
@@ -93,6 +107,27 @@ function useChatKeyboard(
93
107
  inverted,
94
108
  );
95
109
 
110
+ // Scale minimum padding absorption by how much of it is visible.
111
+ // Fully visible → full absorption; fully off-screen → no absorption.
112
+ const visibleFraction = getVisibleMinimumPaddingFraction(
113
+ scroll.value,
114
+ layout.value.height,
115
+ size.value.height,
116
+ blankSpace.value,
117
+ inverted,
118
+ );
119
+ const minimumPaddingAbsorbed =
120
+ visibleFraction >= 1
121
+ ? getMinimumPaddingAbsorbed(
122
+ blankSpace.value,
123
+ extraContentPadding.value,
124
+ )
125
+ : 0;
126
+ const scrollEffective = getScrollEffective(
127
+ effective,
128
+ minimumPaddingAbsorbed,
129
+ );
130
+
96
131
  if (inverted && e.duration === -1) {
97
132
  // Android inverted: skip post-interactive snap-back events
98
133
  // (duration === -1 means the keyboard is re-establishing its
@@ -100,12 +135,19 @@ function useChatKeyboard(
100
135
  return;
101
136
  } else if (e.height > 0) {
102
137
  // Android: keyboard opening — set padding + capture scroll position
138
+ minimumPaddingFractionOnOpen.value = visibleFraction >= 1 ? 1 : 0;
103
139
  padding.value = effective;
104
140
  offsetBeforeScroll.value = scroll.value;
105
141
 
106
142
  if (!inverted && keyboardLiftBehavior === "whenAtEnd" && !atEnd) {
107
143
  // Sentinel: don't scroll in onMove (non-inverted only)
108
144
  offsetBeforeScroll.value = -1;
145
+ } else if (!inverted && scrollEffective === 0) {
146
+ // blankSpace fully absorbs the keyboard — prevent scroll
147
+ offsetBeforeScroll.value = -1;
148
+ } else if (inverted && scrollEffective === 0) {
149
+ // blankSpace fully absorbs the keyboard — guard for inverted
150
+ offsetBeforeScroll.value = scroll.value;
109
151
  }
110
152
  } else {
111
153
  // Android: keyboard closing — re-capture scroll position
@@ -114,9 +156,9 @@ function useChatKeyboard(
114
156
  } else {
115
157
  // Preserve "whenAtEnd" sentinel: if open didn't shift, close shouldn't either
116
158
  if (offsetBeforeScroll.value !== -1) {
117
- // Non-inverted: subtract padding to get the "natural" position
118
- // so onMove smoothly scrolls back from where the user is now
119
- offsetBeforeScroll.value = scroll.value - padding.value;
159
+ // Use the actual displacement recorded at end of open animation
160
+ // (not the theoretical value) so close is symmetric with open
161
+ offsetBeforeScroll.value = scroll.value - actualOpenShift.value;
120
162
  }
121
163
  }
122
164
  }
@@ -142,6 +184,20 @@ function useChatKeyboard(
142
184
  offset,
143
185
  );
144
186
 
187
+ const minimumPaddingAbsorbed =
188
+ getMinimumPaddingAbsorbed(
189
+ blankSpace.value,
190
+ extraContentPadding.value,
191
+ ) * minimumPaddingFractionOnOpen.value;
192
+ const scrollEffective = getScrollEffective(
193
+ effective,
194
+ minimumPaddingAbsorbed,
195
+ );
196
+ const actualTotalPadding = Math.max(
197
+ blankSpace.value,
198
+ effective + extraContentPadding.value,
199
+ );
200
+
145
201
  // Check if we should shift content based on position when keyboard started
146
202
  const wasAtEnd = isScrollAtEnd(
147
203
  offsetBeforeScroll.value,
@@ -157,6 +213,11 @@ function useChatKeyboard(
157
213
  effective < padding.value
158
214
  ) {
159
215
  padding.value = effective;
216
+
217
+ if (scrollEffective === 0 && minimumPaddingAbsorbed > 0) {
218
+ return;
219
+ }
220
+
160
221
  scrollTo(scrollViewRef, 0, 0, false);
161
222
 
162
223
  return;
@@ -166,12 +227,17 @@ function useChatKeyboard(
166
227
  // Closing, not shifting: reduce padding to avoid gap
167
228
  if (closing.value && effective < padding.value) {
168
229
  padding.value = effective;
169
- clampScrollIfNeeded(effective);
230
+ clampScrollIfNeeded(effective, actualTotalPadding);
170
231
  }
171
232
 
172
233
  return;
173
234
  }
174
235
 
236
+ // When blankSpace fully absorbs the keyboard, skip scroll
237
+ if (scrollEffective === 0 && minimumPaddingAbsorbed > 0) {
238
+ return;
239
+ }
240
+
175
241
  // Persistent: don't let shift decrease
176
242
  if (keyboardLiftBehavior === "persistent") {
177
243
  const currentShift =
@@ -185,14 +251,15 @@ function useChatKeyboard(
185
251
  } else if (closing.value) {
186
252
  // Not at end: reduce padding to avoid gap
187
253
  padding.value = effective;
188
- clampScrollIfNeeded(effective);
254
+ clampScrollIfNeeded(effective, actualTotalPadding);
189
255
  }
190
256
 
191
257
  return;
192
258
  }
193
259
  }
194
260
 
195
- const target = offsetBeforeScroll.value + padding.value - effective;
261
+ const target =
262
+ offsetBeforeScroll.value + padding.value - scrollEffective;
196
263
 
197
264
  scrollTo(scrollViewRef, 0, target, false);
198
265
  } else {
@@ -202,32 +269,27 @@ function useChatKeyboard(
202
269
  offset,
203
270
  );
204
271
 
205
- // "never" closing: scroll along when at end to avoid jump
272
+ const minimumPaddingAbsorbed =
273
+ getMinimumPaddingAbsorbed(
274
+ blankSpace.value,
275
+ extraContentPadding.value,
276
+ ) * minimumPaddingFractionOnOpen.value;
277
+ const scrollEffective = getScrollEffective(
278
+ effective,
279
+ minimumPaddingAbsorbed,
280
+ );
281
+ const actualTotalPadding = Math.max(
282
+ blankSpace.value,
283
+ effective + extraContentPadding.value,
284
+ );
285
+
286
+ // "never" closing: clamp scroll to valid range as inset shrinks
206
287
  if (
207
288
  keyboardLiftBehavior === "never" &&
208
289
  closing.value &&
209
290
  effective < padding.value
210
291
  ) {
211
- const wasAtEnd = isScrollAtEnd(
212
- offsetBeforeScroll.value + padding.value,
213
- layout.value.height,
214
- size.value.height,
215
- false,
216
- );
217
-
218
- if (wasAtEnd) {
219
- const target = clampedScrollTarget(
220
- offsetBeforeScroll.value,
221
- effective,
222
- size.value.height,
223
- layout.value.height,
224
- );
225
-
226
- scrollTo(scrollViewRef, 0, target, false);
227
- } else {
228
- // Clamp to valid range as padding shrinks
229
- clampScrollIfNeeded(effective);
230
- }
292
+ clampScrollIfNeeded(effective, actualTotalPadding);
231
293
 
232
294
  return;
233
295
  }
@@ -236,11 +298,11 @@ function useChatKeyboard(
236
298
  return;
237
299
  }
238
300
 
239
- // "whenAtEnd" sentinel check
301
+ // "whenAtEnd" sentinel check (also used for blankSpace full absorption)
240
302
  if (offsetBeforeScroll.value === -1) {
241
303
  if (closing.value) {
242
304
  // Keyboard didn't shift on open; ensure valid position on close
243
- clampScrollIfNeeded(effective);
305
+ clampScrollIfNeeded(effective, actualTotalPadding);
244
306
  }
245
307
 
246
308
  return;
@@ -250,7 +312,7 @@ function useChatKeyboard(
250
312
  if (keyboardLiftBehavior === "persistent" && closing.value) {
251
313
  const keepAt = offsetBeforeScroll.value + padding.value;
252
314
  const maxScroll = Math.max(
253
- size.value.height - layout.value.height + effective,
315
+ size.value.height - layout.value.height + actualTotalPadding,
254
316
  0,
255
317
  );
256
318
 
@@ -261,12 +323,18 @@ function useChatKeyboard(
261
323
 
262
324
  const target = clampedScrollTarget(
263
325
  offsetBeforeScroll.value,
264
- effective,
326
+ scrollEffective,
265
327
  size.value.height,
266
328
  layout.value.height,
329
+ actualTotalPadding,
267
330
  );
268
331
 
269
332
  scrollTo(scrollViewRef, 0, target, false);
333
+
334
+ // Track actual (clamped) displacement during open for symmetric close
335
+ if (!closing.value) {
336
+ actualOpenShift.value = target - offsetBeforeScroll.value;
337
+ }
270
338
  }
271
339
  },
272
340
  onEnd: (e) => {
@@ -283,6 +351,11 @@ function useChatKeyboard(
283
351
  );
284
352
 
285
353
  padding.value = effective;
354
+
355
+ // Record actual scroll displacement so close can be symmetric
356
+ if (effective > 0 && offsetBeforeScroll.value !== -1) {
357
+ actualOpenShift.value = scroll.value - offsetBeforeScroll.value;
358
+ }
286
359
  },
287
360
  },
288
361
  [inverted, keyboardLiftBehavior, freeze, offset],
@@ -292,6 +365,9 @@ function useChatKeyboard(
292
365
  padding,
293
366
  currentHeight,
294
367
  contentOffsetY: undefined,
368
+ scroll,
369
+ layout,
370
+ size,
295
371
  onLayout,
296
372
  onContentSizeChange,
297
373
  };
@@ -8,6 +8,9 @@ type UseChatKeyboardOptions = {
8
8
  keyboardLiftBehavior: KeyboardLiftBehavior;
9
9
  freeze: boolean;
10
10
  offset: number;
11
+ blankSpace: SharedValue<number>;
12
+ /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */
13
+ extraContentPadding: SharedValue<number>;
11
14
  };
12
15
 
13
16
  type UseChatKeyboardReturn = {
@@ -17,6 +20,12 @@ type UseChatKeyboardReturn = {
17
20
  currentHeight: SharedValue<number>;
18
21
  /** Absolute Y content offset for iOS (set once in onStart). `undefined` on Android. */
19
22
  contentOffsetY: SharedValue<number> | undefined;
23
+ /** Current vertical scroll offset. */
24
+ scroll: SharedValue<number>;
25
+ /** Visible viewport dimensions. */
26
+ layout: SharedValue<{ width: number; height: number }>;
27
+ /** Total content dimensions. */
28
+ size: SharedValue<{ width: number; height: number }>;
20
29
  /** Callback to attach to ScrollView's onLayout prop to capture initial viewport dimensions. */
21
30
  onLayout: (e: LayoutChangeEvent) => void;
22
31
  /** Callback to attach to ScrollView's onContentSizeChange prop to capture initial content dimensions. */
@@ -0,0 +1,123 @@
1
+ import { scrollTo, useAnimatedReaction } from "react-native-reanimated";
2
+
3
+ import { isScrollAtEnd, shouldShiftContent } from "../useChatKeyboard/helpers";
4
+
5
+ import type { KeyboardLiftBehavior } from "../useChatKeyboard/types";
6
+ import type { AnimatedRef, SharedValue } from "react-native-reanimated";
7
+ import type Reanimated from "react-native-reanimated";
8
+
9
+ type UseExtraContentPaddingOptions = {
10
+ scrollViewRef: AnimatedRef<Reanimated.ScrollView>;
11
+ extraContentPadding: SharedValue<number>;
12
+ /** Keyboard-only padding from useChatKeyboard — used to compute total padding for clamping. */
13
+ keyboardPadding: SharedValue<number>;
14
+ /** Minimum inset floor — used to absorb keyboard and extraContentPadding changes. */
15
+ blankSpace: SharedValue<number>;
16
+ /** Current vertical scroll offset. */
17
+ scroll: SharedValue<number>;
18
+ /** Visible viewport dimensions. */
19
+ layout: SharedValue<{ width: number; height: number }>;
20
+ /** Total content dimensions. */
21
+ size: SharedValue<{ width: number; height: number }>;
22
+ inverted: boolean;
23
+ keyboardLiftBehavior: KeyboardLiftBehavior;
24
+ freeze: boolean;
25
+ };
26
+
27
+ /**
28
+ * Hook that reacts to `extraContentPadding` changes and conditionally
29
+ * adjusts the scroll position using `scrollTo` on both iOS and Android.
30
+ *
31
+ * Padding extension (scrollable range) is handled externally via a
32
+ * `useDerivedValue` that sums keyboard padding + extra content padding.
33
+ * This hook only handles the scroll correction.
34
+ *
35
+ * @param options - Configuration and shared values.
36
+ * @example
37
+ * ```tsx
38
+ * useExtraContentPadding({ scrollViewRef, extraContentPadding, ... });
39
+ * ```
40
+ */
41
+ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void {
42
+ const {
43
+ scrollViewRef,
44
+ extraContentPadding,
45
+ keyboardPadding,
46
+ blankSpace,
47
+ scroll,
48
+ layout,
49
+ size,
50
+ inverted,
51
+ keyboardLiftBehavior,
52
+ freeze,
53
+ } = options;
54
+
55
+ useAnimatedReaction(
56
+ () => extraContentPadding.value,
57
+ (current, previous) => {
58
+ if (freeze || previous === null) {
59
+ return;
60
+ }
61
+
62
+ const rawDelta = current - previous;
63
+
64
+ if (rawDelta === 0) {
65
+ return;
66
+ }
67
+
68
+ // Compute effective delta considering blankSpace floor
69
+ const previousTotal = Math.max(
70
+ blankSpace.value,
71
+ keyboardPadding.value + previous,
72
+ );
73
+ const currentTotal = Math.max(
74
+ blankSpace.value,
75
+ keyboardPadding.value + current,
76
+ );
77
+ const effectiveDelta = currentTotal - previousTotal;
78
+
79
+ if (effectiveDelta === 0) {
80
+ // blankSpace absorbed the change
81
+ return;
82
+ }
83
+
84
+ const atEnd = isScrollAtEnd(
85
+ scroll.value,
86
+ layout.value.height,
87
+ size.value.height,
88
+ inverted,
89
+ );
90
+
91
+ // "persistent": scroll on grow, hold position on shrink (unless at end)
92
+ if (
93
+ keyboardLiftBehavior === "persistent" &&
94
+ effectiveDelta < 0 &&
95
+ !atEnd
96
+ ) {
97
+ return;
98
+ }
99
+
100
+ if (!shouldShiftContent(keyboardLiftBehavior, atEnd)) {
101
+ return;
102
+ }
103
+
104
+ if (inverted) {
105
+ const target = Math.max(scroll.value - effectiveDelta, -currentTotal);
106
+
107
+ scrollTo(scrollViewRef, 0, target, false);
108
+ } else {
109
+ const maxScroll = Math.max(
110
+ size.value.height - layout.value.height + currentTotal,
111
+ 0,
112
+ );
113
+ const target = Math.min(scroll.value + effectiveDelta, maxScroll);
114
+
115
+ scrollTo(scrollViewRef, 0, target, false);
116
+ }
117
+ },
118
+ [inverted, keyboardLiftBehavior, freeze],
119
+ );
120
+ }
121
+
122
+ export { useExtraContentPadding };
123
+ export type { UseExtraContentPaddingOptions };
@@ -1,7 +1,10 @@
1
1
  import React, { useEffect, useMemo, useState } from "react";
2
2
  import { StyleSheet, View } from "react-native";
3
3
 
4
- import { FocusedInputEvents } from "../../bindings";
4
+ import {
5
+ FocusedInputEvents,
6
+ RCTKeyboardToolbarGroupView,
7
+ } from "../../bindings";
5
8
  import { useKeyboardState } from "../../hooks";
6
9
  import KeyboardStickyView from "../KeyboardStickyView";
7
10
 
@@ -41,6 +44,7 @@ const KeyboardToolbar: React.FC<KeyboardToolbarProps> & {
41
44
  Prev: typeof Prev;
42
45
  Next: typeof Next;
43
46
  Done: typeof Done;
47
+ Group: typeof RCTKeyboardToolbarGroupView;
44
48
  } = (props) => {
45
49
  const {
46
50
  children,
@@ -227,6 +231,7 @@ KeyboardToolbar.Content = Content;
227
231
  KeyboardToolbar.Prev = Prev;
228
232
  KeyboardToolbar.Next = Next;
229
233
  KeyboardToolbar.Done = Done;
234
+ KeyboardToolbar.Group = RCTKeyboardToolbarGroupView;
230
235
 
231
236
  export { colors as DefaultKeyboardToolbarTheme, KeyboardToolbarProps };
232
237
  export default KeyboardToolbar;
@@ -1,6 +1,9 @@
1
1
  import React, { forwardRef } from "react";
2
2
  import { Platform, View } from "react-native";
3
- import Reanimated, { useAnimatedProps } from "react-native-reanimated";
3
+ import Reanimated, {
4
+ useAnimatedProps,
5
+ useSharedValue,
6
+ } from "react-native-reanimated";
4
7
 
5
8
  import { ClippingScrollView } from "../../bindings";
6
9
 
@@ -28,8 +31,11 @@ type ScrollViewWithBottomPaddingProps = {
28
31
  children?: React.ReactNode;
29
32
  inverted?: boolean;
30
33
  bottomPadding: SharedValue<number>;
34
+ /** Padding for scroll indicator insets (excludes blankSpace). Falls back to bottomPadding when not provided. */
35
+ scrollIndicatorPadding?: SharedValue<number>;
31
36
  /** Absolute Y content offset (iOS only, for KeyboardChatScrollView). */
32
37
  contentOffsetY?: SharedValue<number>;
38
+ applyWorkaroundForContentInsetHitTestBug?: boolean;
33
39
  } & ScrollViewProps;
34
40
 
35
41
  const ScrollViewWithBottomPadding = forwardRef<
@@ -40,21 +46,33 @@ const ScrollViewWithBottomPadding = forwardRef<
40
46
  {
41
47
  ScrollViewComponent,
42
48
  bottomPadding,
49
+ scrollIndicatorPadding,
43
50
  contentInset,
44
51
  scrollIndicatorInsets,
45
52
  inverted,
46
53
  contentOffsetY,
54
+ applyWorkaroundForContentInsetHitTestBug,
47
55
  children,
48
56
  ...rest
49
57
  },
50
58
  ref,
51
59
  ) => {
60
+ const prevContentOffsetY = useSharedValue<number | null>(null);
61
+
52
62
  const animatedProps = useAnimatedProps(() => {
53
63
  const insetTop = inverted ? bottomPadding.value : 0;
54
64
  const insetBottom = !inverted ? bottomPadding.value : 0;
55
65
  const bottom = insetBottom + (contentInset?.bottom || 0);
56
66
  const top = insetTop + (contentInset?.top || 0);
57
67
 
68
+ const indicatorPadding = scrollIndicatorPadding ?? bottomPadding;
69
+ const indicatorTop =
70
+ (inverted ? indicatorPadding.value : 0) +
71
+ (scrollIndicatorInsets?.top || 0);
72
+ const indicatorBottom =
73
+ (!inverted ? indicatorPadding.value : 0) +
74
+ (scrollIndicatorInsets?.bottom || 0);
75
+
58
76
  const result: Record<string, unknown> = {
59
77
  // iOS prop
60
78
  contentInset: {
@@ -64,8 +82,8 @@ const ScrollViewWithBottomPadding = forwardRef<
64
82
  left: contentInset?.left,
65
83
  },
66
84
  scrollIndicatorInsets: {
67
- bottom: bottom,
68
- top: top,
85
+ bottom: indicatorBottom,
86
+ top: indicatorTop,
69
87
  right: scrollIndicatorInsets?.right,
70
88
  left: scrollIndicatorInsets?.left,
71
89
  },
@@ -75,7 +93,13 @@ const ScrollViewWithBottomPadding = forwardRef<
75
93
  };
76
94
 
77
95
  if (contentOffsetY) {
78
- result.contentOffset = { x: 0, y: contentOffsetY.value };
96
+ const curr = contentOffsetY.value;
97
+
98
+ if (curr !== prevContentOffsetY.value) {
99
+ // eslint-disable-next-line react-compiler/react-compiler
100
+ prevContentOffsetY.value = curr;
101
+ result.contentOffset = { x: 0, y: curr };
102
+ }
79
103
  }
80
104
 
81
105
  return result;
@@ -95,6 +119,9 @@ const ScrollViewWithBottomPadding = forwardRef<
95
119
  return (
96
120
  <ReanimatedClippingScrollView
97
121
  animatedProps={animatedProps}
122
+ applyWorkaroundForContentInsetHitTestBug={
123
+ applyWorkaroundForContentInsetHitTestBug
124
+ }
98
125
  style={styles.container}
99
126
  >
100
127
  <ScrollViewComponent ref={ref} animatedProps={animatedProps} {...rest}>
@@ -7,12 +7,12 @@ import type { Double } from "react-native/Libraries/Types/CodegenTypes";
7
7
  export interface NativeProps extends ViewProps {
8
8
  contentInsetBottom: Double;
9
9
  contentInsetTop: Double;
10
+ applyWorkaroundForContentInsetHitTestBug?: boolean;
10
11
  }
11
12
 
12
13
  export default codegenNativeComponent<NativeProps>(
13
14
  "ClippingScrollViewDecoratorView",
14
15
  {
15
16
  interfaceOnly: true,
16
- excludedPlatforms: ["iOS"],
17
17
  },
18
18
  ) as HostComponent<NativeProps>;
@@ -0,0 +1,10 @@
1
+ import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent";
2
+
3
+ import type { HostComponent } from "react-native";
4
+ import type { ViewProps } from "react-native/Libraries/Components/View/ViewPropTypes";
5
+
6
+ export interface NativeProps extends ViewProps {}
7
+
8
+ export default codegenNativeComponent<NativeProps>("KeyboardToolbarGroupView", {
9
+ interfaceOnly: true,
10
+ }) as HostComponent<NativeProps>;
@@ -11,6 +11,7 @@ export interface Spec extends TurboModule {
11
11
  preload(): void;
12
12
  dismiss(keepFocus: boolean, animated: boolean): void;
13
13
  setFocusTo(direction: string): void;
14
+ viewPositionInWindow(viewTag: number): Promise<object>;
14
15
 
15
16
  // event emitter
16
17
  addListener: (eventName: string) => void;
@@ -109,6 +109,12 @@ export type KeyboardControllerModule = {
109
109
  */
110
110
  state: () => KeyboardEventData;
111
111
  };
112
+ export type ViewPositionInWindowResult = {
113
+ x: number;
114
+ y: number;
115
+ width: number;
116
+ height: number;
117
+ };
112
118
  export type KeyboardControllerNativeModule = {
113
119
  // android only
114
120
  setDefaultMode: () => void;
@@ -118,6 +124,9 @@ export type KeyboardControllerNativeModule = {
118
124
  // all platforms
119
125
  dismiss: (keepFocus: boolean, animated: boolean) => void;
120
126
  setFocusTo: (direction: Direction) => void;
127
+ viewPositionInWindow: (
128
+ viewTag: number,
129
+ ) => Promise<ViewPositionInWindowResult>;
121
130
  // native event module stuff
122
131
  addListener: (eventName: string) => void;
123
132
  removeListeners: (count: number) => void;
@@ -49,11 +49,13 @@ export type KeyboardExtenderProps = PropsWithChildren<{
49
49
  /** Controls whether this `KeyboardExtender` instance should take an effect. Default is `true`. */
50
50
  enabled?: boolean;
51
51
  }>;
52
+ export type KeyboardToolbarGroupViewProps = PropsWithChildren<ViewProps>;
52
53
  export type ClippingScrollViewProps = PropsWithChildren<
53
54
  ViewProps & {
54
55
  /** An additional space that gets applied to the bottom of the `ScrollView` (inside a scrollable content). Default is `0`. */
55
56
  contentInsetBottom?: number;
56
57
  /** An additional space that gets applied to the top of the `ScrollView` (inside a scrollable content). Default is `0`. */
57
58
  contentInsetTop?: number;
59
+ applyWorkaroundForContentInsetHitTestBug?: boolean;
58
60
  }
59
61
  >;
@@ -1,2 +0,0 @@
1
- connection.project.dir=../../../android
2
- eclipse.preferences.version=1
@@ -1,14 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>SchemeUserState</key>
6
- <dict>
7
- <key>KeyboardController.xcscheme_^#shared#^_</key>
8
- <dict>
9
- <key>orderHint</key>
10
- <integer>0</integer>
11
- </dict>
12
- </dict>
13
- </dict>
14
- </plist>
@@ -1,7 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <Workspace
3
- version = "1.0">
4
- <FileRef
5
- location = "self:">
6
- </FileRef>
7
- </Workspace>
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>IDEDidComputeMac32BitWarning</key>
6
- <true/>
7
- </dict>
8
- </plist>