react-native-keyboard-controller 1.21.0-beta.2 → 1.21.0

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 (214) 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 +14 -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 +33 -1
  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 +16 -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/animations/KeyboardAnimation.swift +2 -2
  30. package/ios/animations/SpringAnimation.swift +1 -1
  31. package/ios/animations/TimingAnimation.swift +1 -3
  32. package/ios/delegates/KCTextInputCompositeDelegate.swift +2 -2
  33. package/ios/extensions/CGFloat.swift +1 -3
  34. package/ios/extensions/UIApplication.swift +1 -1
  35. package/ios/interactive/KeyboardAreaExtender.swift +2 -1
  36. package/ios/observers/FocusedInputObserver.swift +6 -3
  37. package/ios/observers/movement/KeyboardTrackingView.swift +4 -1
  38. package/ios/observers/movement/observer/KeyboardMovementObserver+Interactive.swift +2 -0
  39. package/ios/observers/movement/observer/KeyboardMovementObserver+Listeners.swift +30 -7
  40. package/ios/observers/movement/observer/KeyboardMovementObserver.swift +2 -48
  41. package/ios/protocols/TextInput.swift +1 -1
  42. package/ios/swizzling/UIResponderSwizzle.swift +2 -4
  43. package/ios/traversal/FocusedInputHolder.swift +3 -3
  44. package/ios/traversal/KeyboardView.swift +1 -1
  45. package/ios/traversal/ViewHierarchyNavigator.swift +51 -5
  46. package/ios/views/ClippingScrollViewDecoratorViewManager.h +26 -0
  47. package/ios/views/ClippingScrollViewDecoratorViewManager.mm +231 -0
  48. package/ios/views/KeyboardControllerViewManager.swift +5 -5
  49. package/ios/views/KeyboardToolbarGroupViewManager.h +28 -0
  50. package/ios/views/KeyboardToolbarGroupViewManager.mm +88 -0
  51. package/jest/index.js +12 -4
  52. package/lib/commonjs/bindings.js +18 -2
  53. package/lib/commonjs/bindings.js.map +1 -1
  54. package/lib/commonjs/bindings.native.js +3 -4
  55. package/lib/commonjs/bindings.native.js.map +1 -1
  56. package/lib/commonjs/components/KeyboardAvoidingView/index.js +30 -4
  57. package/lib/commonjs/components/KeyboardAvoidingView/index.js.map +1 -1
  58. package/lib/commonjs/components/KeyboardAwareScrollView/index.js +55 -14
  59. package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
  60. package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/hooks.js.map +1 -1
  61. package/lib/commonjs/components/KeyboardChatScrollView/index.js +111 -0
  62. package/lib/commonjs/components/KeyboardChatScrollView/index.js.map +1 -0
  63. package/lib/commonjs/components/KeyboardChatScrollView/types.js.map +1 -0
  64. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js +221 -0
  65. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js.map +1 -0
  66. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +149 -0
  67. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -0
  68. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js +240 -0
  69. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -0
  70. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/types.js +6 -0
  71. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -0
  72. package/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js +72 -0
  73. package/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js.map +1 -0
  74. package/lib/commonjs/components/KeyboardStickyView/index.js +3 -1
  75. package/lib/commonjs/components/KeyboardStickyView/index.js.map +1 -1
  76. package/lib/commonjs/components/KeyboardToolbar/index.js +1 -0
  77. package/lib/commonjs/components/KeyboardToolbar/index.js.map +1 -1
  78. package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js +36 -15
  79. package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js.map +1 -1
  80. package/lib/commonjs/components/hooks/useScrollState.js +20 -1
  81. package/lib/commonjs/components/hooks/useScrollState.js.map +1 -1
  82. package/lib/commonjs/components/index.js +7 -7
  83. package/lib/commonjs/components/index.js.map +1 -1
  84. package/lib/commonjs/index.js +7 -7
  85. package/lib/commonjs/index.js.map +1 -1
  86. package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js +1 -2
  87. package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -1
  88. package/lib/commonjs/specs/KeyboardToolbarGroupViewNativeComponent.js +12 -0
  89. package/lib/commonjs/specs/KeyboardToolbarGroupViewNativeComponent.js.map +1 -0
  90. package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
  91. package/lib/commonjs/types/module.js.map +1 -1
  92. package/lib/commonjs/types/views.js.map +1 -1
  93. package/lib/module/bindings.js +17 -1
  94. package/lib/module/bindings.js.map +1 -1
  95. package/lib/module/bindings.native.js +2 -3
  96. package/lib/module/bindings.native.js.map +1 -1
  97. package/lib/module/components/KeyboardAvoidingView/index.js +29 -4
  98. package/lib/module/components/KeyboardAvoidingView/index.js.map +1 -1
  99. package/lib/module/components/KeyboardAwareScrollView/index.js +55 -14
  100. package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
  101. package/lib/module/components/{ChatKit → KeyboardChatScrollView}/hooks.js.map +1 -1
  102. package/lib/module/components/KeyboardChatScrollView/index.js +104 -0
  103. package/lib/module/components/KeyboardChatScrollView/index.js.map +1 -0
  104. package/lib/module/components/KeyboardChatScrollView/types.js.map +1 -0
  105. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js +207 -0
  106. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js.map +1 -0
  107. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +143 -0
  108. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -0
  109. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js +234 -0
  110. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -0
  111. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/types.js +2 -0
  112. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -0
  113. package/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js +67 -0
  114. package/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js.map +1 -0
  115. package/lib/module/components/KeyboardStickyView/index.js +3 -1
  116. package/lib/module/components/KeyboardStickyView/index.js.map +1 -1
  117. package/lib/module/components/KeyboardToolbar/index.js +2 -1
  118. package/lib/module/components/KeyboardToolbar/index.js.map +1 -1
  119. package/lib/module/components/ScrollViewWithBottomPadding/index.js +38 -17
  120. package/lib/module/components/ScrollViewWithBottomPadding/index.js.map +1 -1
  121. package/lib/module/components/hooks/useScrollState.js +21 -2
  122. package/lib/module/components/hooks/useScrollState.js.map +1 -1
  123. package/lib/module/components/index.js +1 -1
  124. package/lib/module/components/index.js.map +1 -1
  125. package/lib/module/index.js +1 -1
  126. package/lib/module/index.js.map +1 -1
  127. package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js +1 -2
  128. package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -1
  129. package/lib/module/specs/KeyboardToolbarGroupViewNativeComponent.js +5 -0
  130. package/lib/module/specs/KeyboardToolbarGroupViewNativeComponent.js.map +1 -0
  131. package/lib/module/specs/NativeKeyboardController.js.map +1 -1
  132. package/lib/module/types/module.js.map +1 -1
  133. package/lib/module/types/views.js.map +1 -1
  134. package/lib/typescript/bindings.d.ts +12 -2
  135. package/lib/typescript/bindings.native.d.ts +2 -1
  136. package/lib/typescript/components/KeyboardAvoidingView/index.d.ts +44 -13
  137. package/lib/typescript/components/KeyboardChatScrollView/hooks.d.ts +2 -0
  138. package/lib/typescript/components/KeyboardChatScrollView/index.d.ts +15 -0
  139. package/lib/typescript/components/{ChatKit → KeyboardChatScrollView}/types.d.ts +46 -6
  140. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts +132 -0
  141. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/index.d.ts +18 -0
  142. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/index.ios.d.ts +21 -0
  143. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts +37 -0
  144. package/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts +43 -0
  145. package/lib/typescript/components/KeyboardToolbar/index.d.ts +2 -0
  146. package/lib/typescript/components/ScrollViewWithBottomPadding/index.d.ts +5 -4
  147. package/lib/typescript/components/hooks/useScrollState.d.ts +3 -0
  148. package/lib/typescript/components/index.d.ts +2 -2
  149. package/lib/typescript/index.d.ts +2 -2
  150. package/lib/typescript/specs/ClippingScrollViewDecoratorViewNativeComponent.d.ts +2 -0
  151. package/lib/typescript/specs/KeyboardToolbarGroupViewNativeComponent.d.ts +6 -0
  152. package/lib/typescript/specs/NativeKeyboardController.d.ts +1 -0
  153. package/lib/typescript/types/module.d.ts +7 -0
  154. package/lib/typescript/types/views.d.ts +4 -0
  155. package/package.json +6 -2
  156. package/react-native.config.js +2 -0
  157. package/src/bindings.native.ts +4 -4
  158. package/src/bindings.ts +15 -1
  159. package/src/components/KeyboardAvoidingView/index.tsx +56 -29
  160. package/src/components/KeyboardAwareScrollView/index.tsx +68 -13
  161. package/src/components/KeyboardChatScrollView/hooks.ts +2 -0
  162. package/src/components/KeyboardChatScrollView/index.tsx +150 -0
  163. package/src/components/{ChatKit → KeyboardChatScrollView}/types.ts +46 -6
  164. package/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts +268 -0
  165. package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts +260 -0
  166. package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts +376 -0
  167. package/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts +39 -0
  168. package/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts +123 -0
  169. package/src/components/KeyboardStickyView/index.tsx +4 -3
  170. package/src/components/KeyboardToolbar/index.tsx +6 -1
  171. package/src/components/ScrollViewWithBottomPadding/index.tsx +53 -29
  172. package/src/components/hooks/useScrollState.ts +24 -2
  173. package/src/components/index.ts +2 -2
  174. package/src/index.ts +2 -2
  175. package/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts +2 -1
  176. package/src/specs/KeyboardToolbarGroupViewNativeComponent.ts +10 -0
  177. package/src/specs/NativeKeyboardController.ts +1 -0
  178. package/src/types/module.ts +9 -0
  179. package/src/types/views.ts +4 -0
  180. package/android/.settings/org.eclipse.buildship.core.prefs +0 -2
  181. package/ios/KeyboardController.xcodeproj/xcuserdata/kirylziusko.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  182. package/ios/Tests/Tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  183. package/ios/Tests/Tests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  184. package/ios/Tests/Tests.xcodeproj/project.xcworkspace/xcuserdata/kirylziusko.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  185. package/ios/Tests/Tests.xcodeproj/xcuserdata/kirylziusko.xcuserdatad/xcschemes/xcschememanagement.plist +0 -32
  186. package/lib/commonjs/components/ChatKit/TODO.md +0 -20
  187. package/lib/commonjs/components/ChatKit/index.js +0 -55
  188. package/lib/commonjs/components/ChatKit/index.js.map +0 -1
  189. package/lib/commonjs/components/ChatKit/types.js.map +0 -1
  190. package/lib/commonjs/components/ChatKit/useChatKeyboard/helpers.js +0 -101
  191. package/lib/commonjs/components/ChatKit/useChatKeyboard/helpers.js.map +0 -1
  192. package/lib/commonjs/components/ChatKit/useChatKeyboard/index.js +0 -147
  193. package/lib/commonjs/components/ChatKit/useChatKeyboard/index.js.map +0 -1
  194. package/lib/module/components/ChatKit/TODO.md +0 -20
  195. package/lib/module/components/ChatKit/index.js +0 -48
  196. package/lib/module/components/ChatKit/index.js.map +0 -1
  197. package/lib/module/components/ChatKit/types.js.map +0 -1
  198. package/lib/module/components/ChatKit/useChatKeyboard/helpers.js +0 -92
  199. package/lib/module/components/ChatKit/useChatKeyboard/helpers.js.map +0 -1
  200. package/lib/module/components/ChatKit/useChatKeyboard/index.js +0 -141
  201. package/lib/module/components/ChatKit/useChatKeyboard/index.js.map +0 -1
  202. package/lib/typescript/components/ChatKit/hooks.d.ts +0 -2
  203. package/lib/typescript/components/ChatKit/index.d.ts +0 -14
  204. package/lib/typescript/components/ChatKit/useChatKeyboard/helpers.d.ts +0 -57
  205. package/lib/typescript/components/ChatKit/useChatKeyboard/index.d.ts +0 -36
  206. package/src/components/ChatKit/TODO.md +0 -20
  207. package/src/components/ChatKit/hooks.ts +0 -2
  208. package/src/components/ChatKit/index.tsx +0 -63
  209. package/src/components/ChatKit/useChatKeyboard/helpers.ts +0 -118
  210. package/src/components/ChatKit/useChatKeyboard/index.ts +0 -228
  211. /package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/hooks.js +0 -0
  212. /package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/types.js +0 -0
  213. /package/lib/module/components/{ChatKit → KeyboardChatScrollView}/hooks.js +0 -0
  214. /package/lib/module/components/{ChatKit → KeyboardChatScrollView}/types.js +0 -0
@@ -0,0 +1,260 @@
1
+ import { useSharedValue } from "react-native-reanimated";
2
+
3
+ import { useKeyboardHandler } from "../../../hooks";
4
+ import useScrollState from "../../hooks/useScrollState";
5
+
6
+ import {
7
+ computeIOSContentOffset,
8
+ getEffectiveHeight,
9
+ getScrollEffective,
10
+ getVisibleMinimumPaddingFraction,
11
+ isScrollAtEnd,
12
+ shouldShiftContent,
13
+ } from "./helpers";
14
+
15
+ import type { UseChatKeyboardOptions, UseChatKeyboardReturn } from "./types";
16
+ import type { AnimatedRef } from "react-native-reanimated";
17
+ import type Reanimated from "react-native-reanimated";
18
+
19
+ /**
20
+ * Hook that manages keyboard-driven scrolling for chat-style scroll views.
21
+ * Calculates padding (extra scrollable space) and content shift values,
22
+ * using iOS-specific strategy (contentOffset set once in onStart).
23
+ *
24
+ * @param scrollViewRef - Animated ref to the scroll view.
25
+ * @param options - Configuration for inverted and keyboardLiftBehavior.
26
+ * @returns Shared values for padding and contentOffsetY.
27
+ * @example
28
+ * ```tsx
29
+ * const { padding, contentOffsetY } = useChatKeyboard(ref, {
30
+ * inverted: false,
31
+ * keyboardLiftBehavior: "always",
32
+ * });
33
+ * ```
34
+ */
35
+ function useChatKeyboard(
36
+ scrollViewRef: AnimatedRef<Reanimated.ScrollView>,
37
+ options: UseChatKeyboardOptions,
38
+ ): UseChatKeyboardReturn {
39
+ const {
40
+ inverted,
41
+ keyboardLiftBehavior,
42
+ freeze,
43
+ offset,
44
+ blankSpace,
45
+ extraContentPadding,
46
+ } = options;
47
+
48
+ const padding = useSharedValue(0);
49
+ const currentHeight = useSharedValue(0);
50
+ const contentOffsetY = useSharedValue(0);
51
+ const targetKeyboardHeight = useSharedValue(0);
52
+ const prevAbsorption = useSharedValue(0);
53
+
54
+ const {
55
+ layout,
56
+ size,
57
+ offset: scroll,
58
+ onLayout,
59
+ onContentSizeChange,
60
+ } = useScrollState(scrollViewRef);
61
+
62
+ useKeyboardHandler(
63
+ {
64
+ onStart: (e) => {
65
+ "worklet";
66
+
67
+ if (freeze) {
68
+ return;
69
+ }
70
+
71
+ if (e.height > 0) {
72
+ // eslint-disable-next-line react-compiler/react-compiler
73
+ targetKeyboardHeight.value = e.height;
74
+ }
75
+
76
+ const effective = getEffectiveHeight(
77
+ e.height,
78
+ targetKeyboardHeight.value,
79
+ offset,
80
+ );
81
+
82
+ const atEnd = isScrollAtEnd(
83
+ scroll.value,
84
+ layout.value.height,
85
+ size.value.height,
86
+ inverted,
87
+ );
88
+
89
+ // Scale minimum padding absorption by how much of it is visible.
90
+ // Fully visible → full absorption; fully off-screen → no absorption.
91
+ const visibleFraction = getVisibleMinimumPaddingFraction(
92
+ scroll.value,
93
+ layout.value.height,
94
+ size.value.height,
95
+ blankSpace.value,
96
+ inverted,
97
+ );
98
+ const visiblePadding = visibleFraction * blankSpace.value;
99
+ const minimumPaddingAbsorbed = Math.max(
100
+ 0,
101
+ visiblePadding - extraContentPadding.value,
102
+ );
103
+ const scrollEffective = getScrollEffective(
104
+ effective,
105
+ minimumPaddingAbsorbed,
106
+ );
107
+ const actualTotalPadding = Math.max(
108
+ blankSpace.value,
109
+ effective + extraContentPadding.value,
110
+ );
111
+
112
+ // persistent mode: when keyboard shrinks, clamp to valid range
113
+ if (
114
+ keyboardLiftBehavior === "persistent" &&
115
+ effective < padding.value
116
+ ) {
117
+ padding.value = effective;
118
+ prevAbsorption.value = minimumPaddingAbsorbed;
119
+
120
+ if (inverted) {
121
+ const maxScroll = Math.max(
122
+ size.value.height - layout.value.height,
123
+ 0,
124
+ );
125
+
126
+ contentOffsetY.value = Math.max(
127
+ -actualTotalPadding,
128
+ Math.min(scroll.value, maxScroll),
129
+ );
130
+ } else {
131
+ const maxScroll = Math.max(
132
+ size.value.height - layout.value.height + actualTotalPadding,
133
+ 0,
134
+ );
135
+
136
+ contentOffsetY.value = Math.max(
137
+ 0,
138
+ Math.min(scroll.value, maxScroll),
139
+ );
140
+ }
141
+
142
+ return;
143
+ }
144
+
145
+ // never mode: when keyboard shrinks, clamp to valid range
146
+ // to avoid ghost padding
147
+ if (
148
+ keyboardLiftBehavior === "never" &&
149
+ effective < padding.value &&
150
+ atEnd
151
+ ) {
152
+ padding.value = effective;
153
+ prevAbsorption.value = minimumPaddingAbsorbed;
154
+
155
+ if (inverted) {
156
+ const maxScroll = Math.max(
157
+ size.value.height - layout.value.height,
158
+ 0,
159
+ );
160
+
161
+ contentOffsetY.value = Math.max(
162
+ -actualTotalPadding,
163
+ Math.min(scroll.value, maxScroll),
164
+ );
165
+ } else {
166
+ const maxScroll = Math.max(
167
+ size.value.height - layout.value.height + actualTotalPadding,
168
+ 0,
169
+ );
170
+
171
+ contentOffsetY.value = Math.max(
172
+ 0,
173
+ Math.min(scroll.value, maxScroll),
174
+ );
175
+ }
176
+
177
+ return;
178
+ }
179
+
180
+ // Undo only the scroll displacement that was actually applied
181
+ // (not the full padding, which includes the absorbed portion).
182
+ // Use the stored absorption from the previous event so that
183
+ // the unwind matches the shift that was originally applied.
184
+ const prevScrollEffective = getScrollEffective(
185
+ padding.value,
186
+ prevAbsorption.value,
187
+ );
188
+ const relativeScroll = inverted
189
+ ? scroll.value + prevScrollEffective
190
+ : scroll.value - prevScrollEffective;
191
+
192
+ padding.value = effective;
193
+ prevAbsorption.value = minimumPaddingAbsorbed;
194
+
195
+ if (!shouldShiftContent(keyboardLiftBehavior, atEnd)) {
196
+ // Preserve current scroll position so animated props
197
+ // don't re-apply a stale contentOffset when padding changes
198
+ contentOffsetY.value = scroll.value;
199
+
200
+ return;
201
+ }
202
+
203
+ // When blankSpace fully absorbs the keyboard opening, preserve current scroll position
204
+ // (only when keyboard is open — effective > 0 — not when closing)
205
+ if (
206
+ scrollEffective === 0 &&
207
+ minimumPaddingAbsorbed > 0 &&
208
+ effective > 0
209
+ ) {
210
+ contentOffsetY.value = scroll.value;
211
+
212
+ return;
213
+ }
214
+
215
+ contentOffsetY.value = computeIOSContentOffset(
216
+ relativeScroll,
217
+ scrollEffective,
218
+ size.value.height,
219
+ layout.value.height,
220
+ inverted,
221
+ actualTotalPadding,
222
+ );
223
+ },
224
+ onMove: () => {
225
+ "worklet";
226
+
227
+ // iOS doesn't need per-frame updates (contentOffset handles it)
228
+ },
229
+ onEnd: (e) => {
230
+ "worklet";
231
+
232
+ if (freeze) {
233
+ return;
234
+ }
235
+
236
+ const effective = getEffectiveHeight(
237
+ e.height,
238
+ targetKeyboardHeight.value,
239
+ offset,
240
+ );
241
+
242
+ padding.value = effective;
243
+ },
244
+ },
245
+ [inverted, keyboardLiftBehavior, freeze, offset, extraContentPadding],
246
+ );
247
+
248
+ return {
249
+ padding,
250
+ currentHeight,
251
+ contentOffsetY,
252
+ scroll,
253
+ layout,
254
+ size,
255
+ onLayout,
256
+ onContentSizeChange,
257
+ };
258
+ }
259
+
260
+ export { useChatKeyboard };
@@ -0,0 +1,376 @@
1
+ import { scrollTo, useSharedValue } from "react-native-reanimated";
2
+
3
+ import { useKeyboardHandler } from "../../../hooks";
4
+ import useScrollState from "../../hooks/useScrollState";
5
+
6
+ import {
7
+ clampedScrollTarget,
8
+ getEffectiveHeight,
9
+ getMinimumPaddingAbsorbed,
10
+ getScrollEffective,
11
+ getVisibleMinimumPaddingFraction,
12
+ isScrollAtEnd,
13
+ shouldShiftContent,
14
+ } from "./helpers";
15
+
16
+ import type { UseChatKeyboardOptions, UseChatKeyboardReturn } from "./types";
17
+ import type { AnimatedRef } from "react-native-reanimated";
18
+ import type Reanimated from "react-native-reanimated";
19
+
20
+ /**
21
+ * Hook that manages keyboard-driven scrolling for chat-style scroll views.
22
+ * Calculates padding (extra scrollable space) and content shift values,
23
+ * using per-frame scrollTo updates (Android and other platforms).
24
+ *
25
+ * @param scrollViewRef - Animated ref to the scroll view.
26
+ * @param options - Configuration for inverted and keyboardLiftBehavior.
27
+ * @returns Shared values for padding and contentOffsetY (always `undefined`).
28
+ * @example
29
+ * ```tsx
30
+ * const { padding } = useChatKeyboard(ref, { inverted: false, keyboardLiftBehavior: "always" });
31
+ * ```
32
+ */
33
+ function useChatKeyboard(
34
+ scrollViewRef: AnimatedRef<Reanimated.ScrollView>,
35
+ options: UseChatKeyboardOptions,
36
+ ): UseChatKeyboardReturn {
37
+ const {
38
+ inverted,
39
+ keyboardLiftBehavior,
40
+ freeze,
41
+ offset,
42
+ blankSpace,
43
+ extraContentPadding,
44
+ } = options;
45
+
46
+ const padding = useSharedValue(0);
47
+ const currentHeight = useSharedValue(0);
48
+ const offsetBeforeScroll = useSharedValue(0);
49
+ const targetKeyboardHeight = useSharedValue(0);
50
+ const closing = useSharedValue(false);
51
+ const minimumPaddingFractionOnOpen = useSharedValue(0);
52
+ const actualOpenShift = useSharedValue(0);
53
+ const {
54
+ layout,
55
+ size,
56
+ offset: scroll,
57
+ onLayout,
58
+ onContentSizeChange,
59
+ } = useScrollState(scrollViewRef);
60
+ const clampScrollIfNeeded = (
61
+ effective: number,
62
+ totalPaddingForMaxScroll?: number,
63
+ ) => {
64
+ "worklet";
65
+
66
+ const paddingForMax =
67
+ totalPaddingForMaxScroll !== undefined
68
+ ? totalPaddingForMaxScroll
69
+ : effective;
70
+ const maxScroll = Math.max(
71
+ size.value.height - layout.value.height + paddingForMax,
72
+ 0,
73
+ );
74
+
75
+ if (scroll.value > maxScroll) {
76
+ scrollTo(scrollViewRef, 0, maxScroll, false);
77
+ }
78
+ };
79
+
80
+ useKeyboardHandler(
81
+ {
82
+ onStart: (e) => {
83
+ "worklet";
84
+
85
+ if (freeze) {
86
+ return;
87
+ }
88
+
89
+ if (e.height > 0) {
90
+ // eslint-disable-next-line react-compiler/react-compiler
91
+ targetKeyboardHeight.value = e.height;
92
+ closing.value = false;
93
+ } else {
94
+ closing.value = true;
95
+ }
96
+
97
+ const effective = getEffectiveHeight(
98
+ e.height,
99
+ targetKeyboardHeight.value,
100
+ offset,
101
+ );
102
+
103
+ const atEnd = isScrollAtEnd(
104
+ scroll.value,
105
+ layout.value.height,
106
+ size.value.height,
107
+ inverted,
108
+ );
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
+
131
+ if (inverted && e.duration === -1) {
132
+ // Android inverted: skip post-interactive snap-back events
133
+ // (duration === -1 means the keyboard is re-establishing its
134
+ // position after an interactive gesture, not a real animation)
135
+ return;
136
+ } else if (e.height > 0) {
137
+ // Android: keyboard opening — set padding + capture scroll position
138
+ minimumPaddingFractionOnOpen.value = visibleFraction >= 1 ? 1 : 0;
139
+ padding.value = effective;
140
+ offsetBeforeScroll.value = scroll.value;
141
+
142
+ if (!inverted && keyboardLiftBehavior === "whenAtEnd" && !atEnd) {
143
+ // Sentinel: don't scroll in onMove (non-inverted only)
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;
151
+ }
152
+ } else {
153
+ // Android: keyboard closing — re-capture scroll position
154
+ if (inverted) {
155
+ offsetBeforeScroll.value = scroll.value;
156
+ } else {
157
+ // Preserve "whenAtEnd" sentinel: if open didn't shift, close shouldn't either
158
+ if (offsetBeforeScroll.value !== -1) {
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;
162
+ }
163
+ }
164
+ }
165
+ },
166
+ onMove: (e) => {
167
+ "worklet";
168
+
169
+ if (freeze) {
170
+ return;
171
+ }
172
+
173
+ currentHeight.value = e.height;
174
+
175
+ if (inverted) {
176
+ // Skip post-interactive snap-back (duration === -1)
177
+ if (e.duration === -1) {
178
+ return;
179
+ }
180
+
181
+ const effective = getEffectiveHeight(
182
+ e.height,
183
+ targetKeyboardHeight.value,
184
+ offset,
185
+ );
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
+
201
+ // Check if we should shift content based on position when keyboard started
202
+ const wasAtEnd = isScrollAtEnd(
203
+ offsetBeforeScroll.value,
204
+ layout.value.height,
205
+ size.value.height,
206
+ inverted,
207
+ );
208
+
209
+ // "never" at end: scroll along when keyboard closes to avoid jump
210
+ if (
211
+ keyboardLiftBehavior === "never" &&
212
+ wasAtEnd &&
213
+ effective < padding.value
214
+ ) {
215
+ padding.value = effective;
216
+
217
+ if (scrollEffective === 0 && minimumPaddingAbsorbed > 0) {
218
+ return;
219
+ }
220
+
221
+ scrollTo(scrollViewRef, 0, 0, false);
222
+
223
+ return;
224
+ }
225
+
226
+ if (!shouldShiftContent(keyboardLiftBehavior, wasAtEnd)) {
227
+ // Closing, not shifting: reduce padding to avoid gap
228
+ if (closing.value && effective < padding.value) {
229
+ padding.value = effective;
230
+ clampScrollIfNeeded(effective, actualTotalPadding);
231
+ }
232
+
233
+ return;
234
+ }
235
+
236
+ // When blankSpace fully absorbs the keyboard, skip scroll
237
+ if (scrollEffective === 0 && minimumPaddingAbsorbed > 0) {
238
+ return;
239
+ }
240
+
241
+ // Persistent: don't let shift decrease
242
+ if (keyboardLiftBehavior === "persistent") {
243
+ const currentShift =
244
+ offsetBeforeScroll.value + padding.value - scroll.value;
245
+
246
+ if (effective < currentShift) {
247
+ // When at end, allow scrolling back (snap to end + reduce padding)
248
+ if (wasAtEnd) {
249
+ padding.value = effective;
250
+ scrollTo(scrollViewRef, 0, 0, false);
251
+ } else if (closing.value) {
252
+ // Not at end: reduce padding to avoid gap
253
+ padding.value = effective;
254
+ clampScrollIfNeeded(effective, actualTotalPadding);
255
+ }
256
+
257
+ return;
258
+ }
259
+ }
260
+
261
+ const target =
262
+ offsetBeforeScroll.value + padding.value - scrollEffective;
263
+
264
+ scrollTo(scrollViewRef, 0, target, false);
265
+ } else {
266
+ const effective = getEffectiveHeight(
267
+ e.height,
268
+ targetKeyboardHeight.value,
269
+ offset,
270
+ );
271
+
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
287
+ if (
288
+ keyboardLiftBehavior === "never" &&
289
+ closing.value &&
290
+ effective < padding.value
291
+ ) {
292
+ clampScrollIfNeeded(effective, actualTotalPadding);
293
+
294
+ return;
295
+ }
296
+
297
+ if (!shouldShiftContent(keyboardLiftBehavior, true)) {
298
+ return;
299
+ }
300
+
301
+ // "whenAtEnd" sentinel check (also used for blankSpace full absorption)
302
+ if (offsetBeforeScroll.value === -1) {
303
+ if (closing.value) {
304
+ // Keyboard didn't shift on open; ensure valid position on close
305
+ clampScrollIfNeeded(effective, actualTotalPadding);
306
+ }
307
+
308
+ return;
309
+ }
310
+
311
+ // "persistent" closing: maintain position, clamped to valid range
312
+ if (keyboardLiftBehavior === "persistent" && closing.value) {
313
+ const keepAt = offsetBeforeScroll.value + padding.value;
314
+ const maxScroll = Math.max(
315
+ size.value.height - layout.value.height + actualTotalPadding,
316
+ 0,
317
+ );
318
+
319
+ scrollTo(scrollViewRef, 0, Math.min(keepAt, maxScroll), false);
320
+
321
+ return;
322
+ }
323
+
324
+ const target = clampedScrollTarget(
325
+ offsetBeforeScroll.value,
326
+ scrollEffective,
327
+ size.value.height,
328
+ layout.value.height,
329
+ actualTotalPadding,
330
+ );
331
+
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
+ }
338
+ }
339
+ },
340
+ onEnd: (e) => {
341
+ "worklet";
342
+
343
+ if (freeze) {
344
+ return;
345
+ }
346
+
347
+ const effective = getEffectiveHeight(
348
+ e.height,
349
+ targetKeyboardHeight.value,
350
+ offset,
351
+ );
352
+
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
+ }
359
+ },
360
+ },
361
+ [inverted, keyboardLiftBehavior, freeze, offset],
362
+ );
363
+
364
+ return {
365
+ padding,
366
+ currentHeight,
367
+ contentOffsetY: undefined,
368
+ scroll,
369
+ layout,
370
+ size,
371
+ onLayout,
372
+ onContentSizeChange,
373
+ };
374
+ }
375
+
376
+ export { useChatKeyboard };
@@ -0,0 +1,39 @@
1
+ import type { LayoutChangeEvent } from "react-native";
2
+ import type { SharedValue } from "react-native-reanimated";
3
+
4
+ type KeyboardLiftBehavior = "always" | "whenAtEnd" | "persistent" | "never";
5
+
6
+ type UseChatKeyboardOptions = {
7
+ inverted: boolean;
8
+ keyboardLiftBehavior: KeyboardLiftBehavior;
9
+ freeze: boolean;
10
+ offset: number;
11
+ blankSpace: SharedValue<number>;
12
+ /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */
13
+ extraContentPadding: SharedValue<number>;
14
+ };
15
+
16
+ type UseChatKeyboardReturn = {
17
+ /** Extra scrollable space (= keyboard height). Used as contentInset on iOS, contentInsetBottom/contentInsetTop on Android. */
18
+ padding: SharedValue<number>;
19
+ /** Raw keyboard height updated every frame in onMove. Used to force Reanimated commits on Fabric. */
20
+ currentHeight: SharedValue<number>;
21
+ /** Absolute Y content offset for iOS (set once in onStart). `undefined` on Android. */
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 }>;
29
+ /** Callback to attach to ScrollView's onLayout prop to capture initial viewport dimensions. */
30
+ onLayout: (e: LayoutChangeEvent) => void;
31
+ /** Callback to attach to ScrollView's onContentSizeChange prop to capture initial content dimensions. */
32
+ onContentSizeChange: (w: number, h: number) => void;
33
+ };
34
+
35
+ export type {
36
+ KeyboardLiftBehavior,
37
+ UseChatKeyboardOptions,
38
+ UseChatKeyboardReturn,
39
+ };