react-native-keyboard-controller 1.21.0-beta.3 → 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 (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 +12 -3
  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 +12 -3
  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 +15 -3
  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
@@ -3,17 +3,13 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.clampedScrollTarget = clampedScrollTarget;
7
- exports.computeIOSContentOffset = computeIOSContentOffset;
8
- exports.getEffectiveHeight = getEffectiveHeight;
9
- exports.isScrollAtEnd = isScrollAtEnd;
10
- exports.shouldShiftContent = shouldShiftContent;
6
+ exports.shouldShiftContent = exports.isScrollAtEnd = exports.getVisibleMinimumPaddingFraction = exports.getScrollEffective = exports.getMinimumPaddingAbsorbed = exports.getEffectiveHeight = exports.computeIOSContentOffset = exports.clampedScrollTarget = void 0;
11
7
  var _reactNativeReanimated = require("react-native-reanimated");
12
8
  const AT_END_THRESHOLD = 20;
13
9
 
14
10
  /**
15
11
  * Map the current keyboard height to an effective height that accounts for a
16
- * fixed offset (e.g. bottom safe-area or tab-bar height).
12
+ * fixed offset (e.g. Bottom safe-area or tab-bar height)..
17
13
  *
18
14
  * @param height - Current keyboard height.
19
15
  * @param targetKeyboardHeight - Full target keyboard height (captured on keyboard open).
@@ -25,14 +21,14 @@ const AT_END_THRESHOLD = 20;
25
21
  * getEffectiveHeight(150, 300, 50); // 125
26
22
  * ```
27
23
  */
28
- function getEffectiveHeight(height, targetKeyboardHeight, offset) {
24
+ const getEffectiveHeight = (height, targetKeyboardHeight, offset) => {
29
25
  "worklet";
30
26
 
31
27
  if (offset === 0 || targetKeyboardHeight === 0) {
32
28
  return height;
33
29
  }
34
30
  return (0, _reactNativeReanimated.interpolate)(height, [0, targetKeyboardHeight], [0, Math.max(targetKeyboardHeight - offset, 0)]);
35
- }
31
+ };
36
32
 
37
33
  /**
38
34
  * Check whether the scroll view is at the end of its content.
@@ -52,14 +48,15 @@ function getEffectiveHeight(height, targetKeyboardHeight, offset) {
52
48
  * const atEndInverted = isScrollAtEnd(5, 800, 2000, true); // true (5 <= 20)
53
49
  * ```
54
50
  */
55
- function isScrollAtEnd(scrollOffset, layoutHeight, contentHeight, inverted = false) {
51
+ exports.getEffectiveHeight = getEffectiveHeight;
52
+ const isScrollAtEnd = (scrollOffset, layoutHeight, contentHeight, inverted = false) => {
56
53
  "worklet";
57
54
 
58
55
  if (inverted) {
59
56
  return scrollOffset <= AT_END_THRESHOLD;
60
57
  }
61
58
  return scrollOffset + layoutHeight >= contentHeight - AT_END_THRESHOLD;
62
- }
59
+ };
63
60
 
64
61
  /**
65
62
  * Decide whether content should be shifted based on the keyboard lift behavior.
@@ -73,7 +70,8 @@ function isScrollAtEnd(scrollOffset, layoutHeight, contentHeight, inverted = fal
73
70
  * shouldShiftContent("whenAtEnd", false); // false
74
71
  * ```
75
72
  */
76
- function shouldShiftContent(behavior, isAtEnd) {
73
+ exports.isScrollAtEnd = isScrollAtEnd;
74
+ const shouldShiftContent = (behavior, isAtEnd) => {
77
75
  "worklet";
78
76
 
79
77
  switch (behavior) {
@@ -86,50 +84,138 @@ function shouldShiftContent(behavior, isAtEnd) {
86
84
  case "persistent":
87
85
  return true;
88
86
  }
89
- }
87
+ };
88
+
89
+ /**
90
+ * Compute the fraction of minimum padding space currently visible in the viewport (0–1).
91
+ *
92
+ * The minimum padding space lives in the scroll view's contentInset, NOT in the
93
+ * content itself. So `contentHeight` (from onContentSizeChange / scroll
94
+ * events) does **not** include it. The visible portion is how far the
95
+ * viewport extends past the content boundary into the inset area.
96
+ *
97
+ * For non-inverted lists the padding is in contentInset.bottom.
98
+ * For inverted lists the padding is in contentInset.top (negative scroll).
99
+ *
100
+ * @param scrollOffset - Current vertical scroll offset.
101
+ * @param layoutHeight - Visible height of the scroll view.
102
+ * @param contentHeight - Height of the scroll content (excludes insets).
103
+ * @param blankSpace - Size of the minimum padding inset area.
104
+ * @param inverted - Whether the list is inverted.
105
+ * @returns A value between 0 (padding fully off-screen) and 1 (padding fully visible).
106
+ * @example
107
+ * ```ts
108
+ * // Non-inverted: contentHeight=1500, layout=800, blankSpace=300
109
+ * getVisibleMinimumPaddingFraction(1500, 800, 1500, 300, false); // 1 (at end, viewport past content)
110
+ * getVisibleMinimumPaddingFraction(850, 800, 1500, 300, false); // 0.5 (half padding visible)
111
+ * getVisibleMinimumPaddingFraction(700, 800, 1500, 300, false); // 0 (padding off-screen)
112
+ * ```
113
+ */
114
+ exports.shouldShiftContent = shouldShiftContent;
115
+ const getVisibleMinimumPaddingFraction = (scrollOffset, layoutHeight, contentHeight, blankSpace, inverted) => {
116
+ "worklet";
117
+
118
+ if (blankSpace <= 0) {
119
+ return 0;
120
+ }
121
+ if (inverted) {
122
+ // Minimum padding is in contentInset.top; visible when scroll < 0
123
+ return Math.max(0, Math.min(1, -scrollOffset / blankSpace));
124
+ }
125
+
126
+ // Minimum padding is in contentInset.bottom; visible when viewport extends past content
127
+ const pastContentEnd = scrollOffset + layoutHeight - contentHeight;
128
+ return Math.max(0, Math.min(1, pastContentEnd / blankSpace));
129
+ };
130
+
131
+ /**
132
+ * Compute how much of the minimum content padding absorbs the keyboard + extraContentPadding.
133
+ *
134
+ * @param blankSpace - Minimum inset floor.
135
+ * @param extraContentPadding - Extra content padding from external elements.
136
+ * @returns The portion of blankSpace that absorbs keyboard displacement.
137
+ * @example
138
+ * ```ts
139
+ * getMinimumPaddingAbsorbed(500, 20); // 480
140
+ * getMinimumPaddingAbsorbed(0, 20); // 0
141
+ * ```
142
+ */
143
+ exports.getVisibleMinimumPaddingFraction = getVisibleMinimumPaddingFraction;
144
+ const getMinimumPaddingAbsorbed = (blankSpace, extraContentPadding) => {
145
+ "worklet";
146
+
147
+ return Math.max(0, blankSpace - extraContentPadding);
148
+ };
149
+
150
+ /**
151
+ * Compute the effective scroll displacement after minimum padding absorption.
152
+ *
153
+ * @param rawEffective - Raw effective keyboard height.
154
+ * @param minimumPaddingAbsorbed - Amount absorbed by minimum content padding.
155
+ * @returns The scroll displacement after subtracting the absorbed portion.
156
+ * @example
157
+ * ```ts
158
+ * getScrollEffective(300, 200); // 100
159
+ * getScrollEffective(300, 400); // 0
160
+ * ```
161
+ */
162
+ exports.getMinimumPaddingAbsorbed = getMinimumPaddingAbsorbed;
163
+ const getScrollEffective = (rawEffective, minimumPaddingAbsorbed) => {
164
+ "worklet";
165
+
166
+ return Math.max(0, rawEffective - minimumPaddingAbsorbed);
167
+ };
90
168
 
91
169
  /**
92
170
  * Compute the clamped scroll target for non-inverted lists.
93
171
  *
94
172
  * @param offsetBeforeScroll - Scroll position before keyboard appeared.
95
- * @param keyboardHeight - Current keyboard height.
173
+ * @param keyboardHeight - Current keyboard height (used for scroll displacement).
96
174
  * @param contentHeight - Total height of the scrollable content.
97
175
  * @param layoutHeight - Visible height of the scroll view.
176
+ * @param totalPaddingForMaxScroll - Total padding to use for maxScroll calculation. When provided, used instead of keyboardHeight for the scrollable range. Defaults to keyboardHeight.
98
177
  * @returns Clamped scroll target between 0 and maxScroll.
99
178
  * @example
100
179
  * ```ts
101
180
  * clampedScrollTarget(100, 300, 1000, 800); // 400
181
+ * clampedScrollTarget(100, 100, 1000, 800, 500); // 200, maxScroll uses 500
102
182
  * ```
103
183
  */
104
- function clampedScrollTarget(offsetBeforeScroll, keyboardHeight, contentHeight, layoutHeight) {
184
+ exports.getScrollEffective = getScrollEffective;
185
+ const clampedScrollTarget = (offsetBeforeScroll, keyboardHeight, contentHeight, layoutHeight, totalPaddingForMaxScroll) => {
105
186
  "worklet";
106
187
 
107
- const maxScroll = Math.max(contentHeight - layoutHeight + keyboardHeight, 0);
188
+ const paddingForMax = totalPaddingForMaxScroll !== undefined ? totalPaddingForMaxScroll : keyboardHeight;
189
+ const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0);
108
190
  return Math.min(Math.max(offsetBeforeScroll + keyboardHeight, 0), maxScroll);
109
- }
191
+ };
110
192
 
111
193
  /**
112
194
  * Compute contentOffset.y for iOS lists.
113
195
  *
114
196
  * @param relativeScroll - Scroll position relative to current inset.
115
- * @param keyboardHeight - Target keyboard height.
197
+ * @param keyboardHeight - Target keyboard height (used for scroll displacement).
116
198
  * @param contentHeight - Total height of the scrollable content.
117
199
  * @param layoutHeight - Visible height of the scroll view.
118
200
  * @param inverted - Whether the list is inverted.
201
+ * @param totalPaddingForMaxScroll - Total padding to use for maxScroll calculation. When provided, used instead of keyboardHeight for the scrollable range. Defaults to keyboardHeight.
119
202
  * @returns The absolute contentOffset.y to set.
120
203
  * @example
121
204
  * ```ts
122
205
  * computeIOSContentOffset(100, 300, 1000, 800, false); // 400
123
206
  * ```
124
207
  */
125
- function computeIOSContentOffset(relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted) {
208
+ exports.clampedScrollTarget = clampedScrollTarget;
209
+ const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll) => {
126
210
  "worklet";
127
211
 
212
+ const paddingForMax = totalPaddingForMaxScroll !== undefined ? totalPaddingForMaxScroll : keyboardHeight;
128
213
  if (inverted) {
129
214
  const maxScroll = Math.max(contentHeight - layoutHeight, 0);
130
- return Math.max(Math.min(relativeScroll - keyboardHeight, maxScroll), -keyboardHeight);
215
+ return Math.max(Math.min(relativeScroll - keyboardHeight, maxScroll), -paddingForMax);
131
216
  }
132
- const maxScroll = Math.max(contentHeight - layoutHeight + keyboardHeight, 0);
217
+ const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0);
133
218
  return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll);
134
- }
219
+ };
220
+ exports.computeIOSContentOffset = computeIOSContentOffset;
135
221
  //# sourceMappingURL=helpers.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["_reactNativeReanimated","require","AT_END_THRESHOLD","getEffectiveHeight","height","targetKeyboardHeight","offset","interpolate","Math","max","isScrollAtEnd","scrollOffset","layoutHeight","contentHeight","inverted","shouldShiftContent","behavior","isAtEnd","clampedScrollTarget","offsetBeforeScroll","keyboardHeight","maxScroll","min","computeIOSContentOffset","relativeScroll"],"sources":["helpers.ts"],"sourcesContent":["import { interpolate } from \"react-native-reanimated\";\n\nimport type { KeyboardLiftBehavior } from \"./types\";\n\nconst AT_END_THRESHOLD = 20;\n\n/**\n * Map the current keyboard height to an effective height that accounts for a\n * fixed offset (e.g. bottom safe-area or tab-bar height).\n *\n * @param height - Current keyboard height.\n * @param targetKeyboardHeight - Full target keyboard height (captured on keyboard open).\n * @param offset - Fixed distance between the scroll-view bottom and the screen bottom.\n * @returns Effective height after subtracting the offset proportionally.\n * @example\n * ```ts\n * getEffectiveHeight(300, 300, 50); // 250\n * getEffectiveHeight(150, 300, 50); // 125\n * ```\n */\nexport function getEffectiveHeight(\n height: number,\n targetKeyboardHeight: number,\n offset: number,\n): number {\n \"worklet\";\n\n if (offset === 0 || targetKeyboardHeight === 0) {\n return height;\n }\n\n return interpolate(\n height,\n [0, targetKeyboardHeight],\n [0, Math.max(targetKeyboardHeight - offset, 0)],\n );\n}\n\n/**\n * Check whether the scroll view is at the end of its content.\n *\n * For non-inverted lists the \"end\" is the bottom of the content.\n * For inverted lists the \"end\" is the top (scroll offset near 0),\n * because that is where the latest messages are displayed.\n *\n * @param scrollOffset - Current vertical scroll offset.\n * @param layoutHeight - Visible height of the scroll view.\n * @param contentHeight - Total height of the scrollable content.\n * @param inverted - Whether the list is inverted.\n * @returns `true` if the scroll position is within the threshold of the content end.\n * @example\n * ```ts\n * const atEnd = isScrollAtEnd(100, 800, 920); // true (100 + 800 >= 920 - 20)\n * const atEndInverted = isScrollAtEnd(5, 800, 2000, true); // true (5 <= 20)\n * ```\n */\nexport function isScrollAtEnd(\n scrollOffset: number,\n layoutHeight: number,\n contentHeight: number,\n inverted: boolean = false,\n): boolean {\n \"worklet\";\n\n if (inverted) {\n return scrollOffset <= AT_END_THRESHOLD;\n }\n\n return scrollOffset + layoutHeight >= contentHeight - AT_END_THRESHOLD;\n}\n\n/**\n * Decide whether content should be shifted based on the keyboard lift behavior.\n *\n * @param behavior - The configured keyboard lift behavior.\n * @param isAtEnd - Whether the scroll view is currently at the end.\n * @returns `true` if content should be shifted.\n * @example\n * ```ts\n * shouldShiftContent(\"always\", false); // true\n * shouldShiftContent(\"whenAtEnd\", false); // false\n * ```\n */\nexport function shouldShiftContent(\n behavior: KeyboardLiftBehavior,\n isAtEnd: boolean,\n): boolean {\n \"worklet\";\n\n switch (behavior) {\n case \"always\":\n return true;\n case \"never\":\n return false;\n case \"whenAtEnd\":\n return isAtEnd;\n case \"persistent\":\n return true;\n }\n}\n\n/**\n * Compute the clamped scroll target for non-inverted lists.\n *\n * @param offsetBeforeScroll - Scroll position before keyboard appeared.\n * @param keyboardHeight - Current keyboard height.\n * @param contentHeight - Total height of the scrollable content.\n * @param layoutHeight - Visible height of the scroll view.\n * @returns Clamped scroll target between 0 and maxScroll.\n * @example\n * ```ts\n * clampedScrollTarget(100, 300, 1000, 800); // 400\n * ```\n */\nexport function clampedScrollTarget(\n offsetBeforeScroll: number,\n keyboardHeight: number,\n contentHeight: number,\n layoutHeight: number,\n): number {\n \"worklet\";\n\n const maxScroll = Math.max(contentHeight - layoutHeight + keyboardHeight, 0);\n\n return Math.min(Math.max(offsetBeforeScroll + keyboardHeight, 0), maxScroll);\n}\n\n/**\n * Compute contentOffset.y for iOS lists.\n *\n * @param relativeScroll - Scroll position relative to current inset.\n * @param keyboardHeight - Target keyboard height.\n * @param contentHeight - Total height of the scrollable content.\n * @param layoutHeight - Visible height of the scroll view.\n * @param inverted - Whether the list is inverted.\n * @returns The absolute contentOffset.y to set.\n * @example\n * ```ts\n * computeIOSContentOffset(100, 300, 1000, 800, false); // 400\n * ```\n */\nexport function computeIOSContentOffset(\n relativeScroll: number,\n keyboardHeight: number,\n contentHeight: number,\n layoutHeight: number,\n inverted: boolean,\n): number {\n \"worklet\";\n\n if (inverted) {\n const maxScroll = Math.max(contentHeight - layoutHeight, 0);\n\n return Math.max(\n Math.min(relativeScroll - keyboardHeight, maxScroll),\n -keyboardHeight,\n );\n }\n\n const maxScroll = Math.max(contentHeight - layoutHeight + keyboardHeight, 0);\n\n return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll);\n}\n"],"mappings":";;;;;;;;;;AAAA,IAAAA,sBAAA,GAAAC,OAAA;AAIA,MAAMC,gBAAgB,GAAG,EAAE;;AAE3B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,kBAAkBA,CAChCC,MAAc,EACdC,oBAA4B,EAC5BC,MAAc,EACN;EACR,SAAS;;EAET,IAAIA,MAAM,KAAK,CAAC,IAAID,oBAAoB,KAAK,CAAC,EAAE;IAC9C,OAAOD,MAAM;EACf;EAEA,OAAO,IAAAG,kCAAW,EAChBH,MAAM,EACN,CAAC,CAAC,EAAEC,oBAAoB,CAAC,EACzB,CAAC,CAAC,EAAEG,IAAI,CAACC,GAAG,CAACJ,oBAAoB,GAAGC,MAAM,EAAE,CAAC,CAAC,CAChD,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASI,aAAaA,CAC3BC,YAAoB,EACpBC,YAAoB,EACpBC,aAAqB,EACrBC,QAAiB,GAAG,KAAK,EAChB;EACT,SAAS;;EAET,IAAIA,QAAQ,EAAE;IACZ,OAAOH,YAAY,IAAIT,gBAAgB;EACzC;EAEA,OAAOS,YAAY,GAAGC,YAAY,IAAIC,aAAa,GAAGX,gBAAgB;AACxE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASa,kBAAkBA,CAChCC,QAA8B,EAC9BC,OAAgB,EACP;EACT,SAAS;;EAET,QAAQD,QAAQ;IACd,KAAK,QAAQ;MACX,OAAO,IAAI;IACb,KAAK,OAAO;MACV,OAAO,KAAK;IACd,KAAK,WAAW;MACd,OAAOC,OAAO;IAChB,KAAK,YAAY;MACf,OAAO,IAAI;EACf;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,mBAAmBA,CACjCC,kBAA0B,EAC1BC,cAAsB,EACtBP,aAAqB,EACrBD,YAAoB,EACZ;EACR,SAAS;;EAET,MAAMS,SAAS,GAAGb,IAAI,CAACC,GAAG,CAACI,aAAa,GAAGD,YAAY,GAAGQ,cAAc,EAAE,CAAC,CAAC;EAE5E,OAAOZ,IAAI,CAACc,GAAG,CAACd,IAAI,CAACC,GAAG,CAACU,kBAAkB,GAAGC,cAAc,EAAE,CAAC,CAAC,EAAEC,SAAS,CAAC;AAC9E;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASE,uBAAuBA,CACrCC,cAAsB,EACtBJ,cAAsB,EACtBP,aAAqB,EACrBD,YAAoB,EACpBE,QAAiB,EACT;EACR,SAAS;;EAET,IAAIA,QAAQ,EAAE;IACZ,MAAMO,SAAS,GAAGb,IAAI,CAACC,GAAG,CAACI,aAAa,GAAGD,YAAY,EAAE,CAAC,CAAC;IAE3D,OAAOJ,IAAI,CAACC,GAAG,CACbD,IAAI,CAACc,GAAG,CAACE,cAAc,GAAGJ,cAAc,EAAEC,SAAS,CAAC,EACpD,CAACD,cACH,CAAC;EACH;EAEA,MAAMC,SAAS,GAAGb,IAAI,CAACC,GAAG,CAACI,aAAa,GAAGD,YAAY,GAAGQ,cAAc,EAAE,CAAC,CAAC;EAE5E,OAAOZ,IAAI,CAACc,GAAG,CAACd,IAAI,CAACC,GAAG,CAACW,cAAc,GAAGI,cAAc,EAAE,CAAC,CAAC,EAAEH,SAAS,CAAC;AAC1E","ignoreList":[]}
1
+ {"version":3,"names":["_reactNativeReanimated","require","AT_END_THRESHOLD","getEffectiveHeight","height","targetKeyboardHeight","offset","interpolate","Math","max","exports","isScrollAtEnd","scrollOffset","layoutHeight","contentHeight","inverted","shouldShiftContent","behavior","isAtEnd","getVisibleMinimumPaddingFraction","blankSpace","min","pastContentEnd","getMinimumPaddingAbsorbed","extraContentPadding","getScrollEffective","rawEffective","minimumPaddingAbsorbed","clampedScrollTarget","offsetBeforeScroll","keyboardHeight","totalPaddingForMaxScroll","paddingForMax","undefined","maxScroll","computeIOSContentOffset","relativeScroll"],"sources":["helpers.ts"],"sourcesContent":["import { interpolate } from \"react-native-reanimated\";\n\nimport type { KeyboardLiftBehavior } from \"./types\";\n\nconst AT_END_THRESHOLD = 20;\n\n/**\n * Map the current keyboard height to an effective height that accounts for a\n * fixed offset (e.g. Bottom safe-area or tab-bar height)..\n *\n * @param height - Current keyboard height.\n * @param targetKeyboardHeight - Full target keyboard height (captured on keyboard open).\n * @param offset - Fixed distance between the scroll-view bottom and the screen bottom.\n * @returns Effective height after subtracting the offset proportionally.\n * @example\n * ```ts\n * getEffectiveHeight(300, 300, 50); // 250\n * getEffectiveHeight(150, 300, 50); // 125\n * ```\n */\nexport const getEffectiveHeight = (\n height: number,\n targetKeyboardHeight: number,\n offset: number,\n): number => {\n \"worklet\";\n\n if (offset === 0 || targetKeyboardHeight === 0) {\n return height;\n }\n\n return interpolate(\n height,\n [0, targetKeyboardHeight],\n [0, Math.max(targetKeyboardHeight - offset, 0)],\n );\n};\n\n/**\n * Check whether the scroll view is at the end of its content.\n *\n * For non-inverted lists the \"end\" is the bottom of the content.\n * For inverted lists the \"end\" is the top (scroll offset near 0),\n * because that is where the latest messages are displayed.\n *\n * @param scrollOffset - Current vertical scroll offset.\n * @param layoutHeight - Visible height of the scroll view.\n * @param contentHeight - Total height of the scrollable content.\n * @param inverted - Whether the list is inverted.\n * @returns `true` if the scroll position is within the threshold of the content end.\n * @example\n * ```ts\n * const atEnd = isScrollAtEnd(100, 800, 920); // true (100 + 800 >= 920 - 20)\n * const atEndInverted = isScrollAtEnd(5, 800, 2000, true); // true (5 <= 20)\n * ```\n */\nexport const isScrollAtEnd = (\n scrollOffset: number,\n layoutHeight: number,\n contentHeight: number,\n inverted: boolean = false,\n): boolean => {\n \"worklet\";\n\n if (inverted) {\n return scrollOffset <= AT_END_THRESHOLD;\n }\n\n return scrollOffset + layoutHeight >= contentHeight - AT_END_THRESHOLD;\n};\n\n/**\n * Decide whether content should be shifted based on the keyboard lift behavior.\n *\n * @param behavior - The configured keyboard lift behavior.\n * @param isAtEnd - Whether the scroll view is currently at the end.\n * @returns `true` if content should be shifted.\n * @example\n * ```ts\n * shouldShiftContent(\"always\", false); // true\n * shouldShiftContent(\"whenAtEnd\", false); // false\n * ```\n */\nexport const shouldShiftContent = (\n behavior: KeyboardLiftBehavior,\n isAtEnd: boolean,\n): boolean => {\n \"worklet\";\n\n switch (behavior) {\n case \"always\":\n return true;\n case \"never\":\n return false;\n case \"whenAtEnd\":\n return isAtEnd;\n case \"persistent\":\n return true;\n }\n};\n\n/**\n * Compute the fraction of minimum padding space currently visible in the viewport (0–1).\n *\n * The minimum padding space lives in the scroll view's contentInset, NOT in the\n * content itself. So `contentHeight` (from onContentSizeChange / scroll\n * events) does **not** include it. The visible portion is how far the\n * viewport extends past the content boundary into the inset area.\n *\n * For non-inverted lists the padding is in contentInset.bottom.\n * For inverted lists the padding is in contentInset.top (negative scroll).\n *\n * @param scrollOffset - Current vertical scroll offset.\n * @param layoutHeight - Visible height of the scroll view.\n * @param contentHeight - Height of the scroll content (excludes insets).\n * @param blankSpace - Size of the minimum padding inset area.\n * @param inverted - Whether the list is inverted.\n * @returns A value between 0 (padding fully off-screen) and 1 (padding fully visible).\n * @example\n * ```ts\n * // Non-inverted: contentHeight=1500, layout=800, blankSpace=300\n * getVisibleMinimumPaddingFraction(1500, 800, 1500, 300, false); // 1 (at end, viewport past content)\n * getVisibleMinimumPaddingFraction(850, 800, 1500, 300, false); // 0.5 (half padding visible)\n * getVisibleMinimumPaddingFraction(700, 800, 1500, 300, false); // 0 (padding off-screen)\n * ```\n */\nexport const getVisibleMinimumPaddingFraction = (\n scrollOffset: number,\n layoutHeight: number,\n contentHeight: number,\n blankSpace: number,\n inverted: boolean,\n): number => {\n \"worklet\";\n\n if (blankSpace <= 0) {\n return 0;\n }\n\n if (inverted) {\n // Minimum padding is in contentInset.top; visible when scroll < 0\n return Math.max(0, Math.min(1, -scrollOffset / blankSpace));\n }\n\n // Minimum padding is in contentInset.bottom; visible when viewport extends past content\n const pastContentEnd = scrollOffset + layoutHeight - contentHeight;\n\n return Math.max(0, Math.min(1, pastContentEnd / blankSpace));\n};\n\n/**\n * Compute how much of the minimum content padding absorbs the keyboard + extraContentPadding.\n *\n * @param blankSpace - Minimum inset floor.\n * @param extraContentPadding - Extra content padding from external elements.\n * @returns The portion of blankSpace that absorbs keyboard displacement.\n * @example\n * ```ts\n * getMinimumPaddingAbsorbed(500, 20); // 480\n * getMinimumPaddingAbsorbed(0, 20); // 0\n * ```\n */\nexport const getMinimumPaddingAbsorbed = (\n blankSpace: number,\n extraContentPadding: number,\n): number => {\n \"worklet\";\n\n return Math.max(0, blankSpace - extraContentPadding);\n};\n\n/**\n * Compute the effective scroll displacement after minimum padding absorption.\n *\n * @param rawEffective - Raw effective keyboard height.\n * @param minimumPaddingAbsorbed - Amount absorbed by minimum content padding.\n * @returns The scroll displacement after subtracting the absorbed portion.\n * @example\n * ```ts\n * getScrollEffective(300, 200); // 100\n * getScrollEffective(300, 400); // 0\n * ```\n */\nexport const getScrollEffective = (\n rawEffective: number,\n minimumPaddingAbsorbed: number,\n): number => {\n \"worklet\";\n\n return Math.max(0, rawEffective - minimumPaddingAbsorbed);\n};\n\n/**\n * Compute the clamped scroll target for non-inverted lists.\n *\n * @param offsetBeforeScroll - Scroll position before keyboard appeared.\n * @param keyboardHeight - Current keyboard height (used for scroll displacement).\n * @param contentHeight - Total height of the scrollable content.\n * @param layoutHeight - Visible height of the scroll view.\n * @param totalPaddingForMaxScroll - Total padding to use for maxScroll calculation. When provided, used instead of keyboardHeight for the scrollable range. Defaults to keyboardHeight.\n * @returns Clamped scroll target between 0 and maxScroll.\n * @example\n * ```ts\n * clampedScrollTarget(100, 300, 1000, 800); // 400\n * clampedScrollTarget(100, 100, 1000, 800, 500); // 200, maxScroll uses 500\n * ```\n */\nexport const clampedScrollTarget = (\n offsetBeforeScroll: number,\n keyboardHeight: number,\n contentHeight: number,\n layoutHeight: number,\n totalPaddingForMaxScroll?: number,\n): number => {\n \"worklet\";\n\n const paddingForMax =\n totalPaddingForMaxScroll !== undefined\n ? totalPaddingForMaxScroll\n : keyboardHeight;\n const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0);\n\n return Math.min(Math.max(offsetBeforeScroll + keyboardHeight, 0), maxScroll);\n};\n\n/**\n * Compute contentOffset.y for iOS lists.\n *\n * @param relativeScroll - Scroll position relative to current inset.\n * @param keyboardHeight - Target keyboard height (used for scroll displacement).\n * @param contentHeight - Total height of the scrollable content.\n * @param layoutHeight - Visible height of the scroll view.\n * @param inverted - Whether the list is inverted.\n * @param totalPaddingForMaxScroll - Total padding to use for maxScroll calculation. When provided, used instead of keyboardHeight for the scrollable range. Defaults to keyboardHeight.\n * @returns The absolute contentOffset.y to set.\n * @example\n * ```ts\n * computeIOSContentOffset(100, 300, 1000, 800, false); // 400\n * ```\n */\nexport const computeIOSContentOffset = (\n relativeScroll: number,\n keyboardHeight: number,\n contentHeight: number,\n layoutHeight: number,\n inverted: boolean,\n totalPaddingForMaxScroll?: number,\n): number => {\n \"worklet\";\n\n const paddingForMax =\n totalPaddingForMaxScroll !== undefined\n ? totalPaddingForMaxScroll\n : keyboardHeight;\n\n if (inverted) {\n const maxScroll = Math.max(contentHeight - layoutHeight, 0);\n\n return Math.max(\n Math.min(relativeScroll - keyboardHeight, maxScroll),\n -paddingForMax,\n );\n }\n\n const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0);\n\n return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll);\n};\n"],"mappings":"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","ignoreList":[]}
@@ -30,12 +30,15 @@ function useChatKeyboard(scrollViewRef, options) {
30
30
  inverted,
31
31
  keyboardLiftBehavior,
32
32
  freeze,
33
- offset
33
+ offset,
34
+ blankSpace,
35
+ extraContentPadding
34
36
  } = options;
35
37
  const padding = (0, _reactNativeReanimated.useSharedValue)(0);
36
38
  const currentHeight = (0, _reactNativeReanimated.useSharedValue)(0);
37
39
  const contentOffsetY = (0, _reactNativeReanimated.useSharedValue)(0);
38
40
  const targetKeyboardHeight = (0, _reactNativeReanimated.useSharedValue)(0);
41
+ const prevAbsorption = (0, _reactNativeReanimated.useSharedValue)(0);
39
42
  const {
40
43
  layout,
41
44
  size,
@@ -57,43 +60,65 @@ function useChatKeyboard(scrollViewRef, options) {
57
60
  const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset);
58
61
  const atEnd = (0, _helpers.isScrollAtEnd)(scroll.value, layout.value.height, size.value.height, inverted);
59
62
 
60
- // persistent mode: when keyboard shrinks, snap to end or hold position
63
+ // Scale minimum padding absorption by how much of it is visible.
64
+ // Fully visible → full absorption; fully off-screen → no absorption.
65
+ const visibleFraction = (0, _helpers.getVisibleMinimumPaddingFraction)(scroll.value, layout.value.height, size.value.height, blankSpace.value, inverted);
66
+ const visiblePadding = visibleFraction * blankSpace.value;
67
+ const minimumPaddingAbsorbed = Math.max(0, visiblePadding - extraContentPadding.value);
68
+ const scrollEffective = (0, _helpers.getScrollEffective)(effective, minimumPaddingAbsorbed);
69
+ const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value);
70
+
71
+ // persistent mode: when keyboard shrinks, clamp to valid range
61
72
  if (keyboardLiftBehavior === "persistent" && effective < padding.value) {
62
73
  padding.value = effective;
63
- if (atEnd) {
64
- if (inverted) {
65
- contentOffsetY.value = -effective;
66
- } else {
67
- contentOffsetY.value = Math.max(size.value.height - layout.value.height + effective, 0);
68
- }
74
+ prevAbsorption.value = minimumPaddingAbsorbed;
75
+ if (inverted) {
76
+ const maxScroll = Math.max(size.value.height - layout.value.height, 0);
77
+ contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll));
69
78
  } else {
70
- // Preserve current scroll position so the animated props
71
- // don't re-apply the stale contentOffset from keyboard open
72
- contentOffsetY.value = scroll.value;
79
+ const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0);
80
+ contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll));
73
81
  }
74
82
  return;
75
83
  }
76
84
 
77
- // never mode: when keyboard shrinks and at end, snap to end
85
+ // never mode: when keyboard shrinks, clamp to valid range
78
86
  // to avoid ghost padding
79
87
  if (keyboardLiftBehavior === "never" && effective < padding.value && atEnd) {
80
88
  padding.value = effective;
89
+ prevAbsorption.value = minimumPaddingAbsorbed;
81
90
  if (inverted) {
82
- contentOffsetY.value = -effective;
91
+ const maxScroll = Math.max(size.value.height - layout.value.height, 0);
92
+ contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll));
83
93
  } else {
84
- contentOffsetY.value = Math.max(size.value.height - layout.value.height + effective, 0);
94
+ const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0);
95
+ contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll));
85
96
  }
86
97
  return;
87
98
  }
88
- const relativeScroll = inverted ? scroll.value + padding.value : scroll.value - padding.value;
99
+
100
+ // Undo only the scroll displacement that was actually applied
101
+ // (not the full padding, which includes the absorbed portion).
102
+ // Use the stored absorption from the previous event so that
103
+ // the unwind matches the shift that was originally applied.
104
+ const prevScrollEffective = (0, _helpers.getScrollEffective)(padding.value, prevAbsorption.value);
105
+ const relativeScroll = inverted ? scroll.value + prevScrollEffective : scroll.value - prevScrollEffective;
89
106
  padding.value = effective;
107
+ prevAbsorption.value = minimumPaddingAbsorbed;
90
108
  if (!(0, _helpers.shouldShiftContent)(keyboardLiftBehavior, atEnd)) {
91
109
  // Preserve current scroll position so animated props
92
110
  // don't re-apply a stale contentOffset when padding changes
93
111
  contentOffsetY.value = scroll.value;
94
112
  return;
95
113
  }
96
- contentOffsetY.value = (0, _helpers.computeIOSContentOffset)(relativeScroll, effective, size.value.height, layout.value.height, inverted);
114
+
115
+ // When blankSpace fully absorbs the keyboard opening, preserve current scroll position
116
+ // (only when keyboard is open — effective > 0 — not when closing)
117
+ if (scrollEffective === 0 && minimumPaddingAbsorbed > 0 && effective > 0) {
118
+ contentOffsetY.value = scroll.value;
119
+ return;
120
+ }
121
+ contentOffsetY.value = (0, _helpers.computeIOSContentOffset)(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding);
97
122
  },
98
123
  onMove: () => {
99
124
  "worklet";
@@ -109,11 +134,14 @@ function useChatKeyboard(scrollViewRef, options) {
109
134
  const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset);
110
135
  padding.value = effective;
111
136
  }
112
- }, [inverted, keyboardLiftBehavior, freeze, offset]);
137
+ }, [inverted, keyboardLiftBehavior, freeze, offset, extraContentPadding]);
113
138
  return {
114
139
  padding,
115
140
  currentHeight,
116
141
  contentOffsetY,
142
+ scroll,
143
+ layout,
144
+ size,
117
145
  onLayout,
118
146
  onContentSizeChange
119
147
  };
@@ -1 +1 @@
1
- {"version":3,"names":["_reactNativeReanimated","require","_hooks","_useScrollState","_interopRequireDefault","_helpers","e","__esModule","default","useChatKeyboard","scrollViewRef","options","inverted","keyboardLiftBehavior","freeze","offset","padding","useSharedValue","currentHeight","contentOffsetY","targetKeyboardHeight","layout","size","scroll","onLayout","onContentSizeChange","useScrollState","useKeyboardHandler","onStart","height","value","effective","getEffectiveHeight","atEnd","isScrollAtEnd","Math","max","relativeScroll","shouldShiftContent","computeIOSContentOffset","onMove","onEnd"],"sources":["index.ios.ts"],"sourcesContent":["import { useSharedValue } from \"react-native-reanimated\";\n\nimport { useKeyboardHandler } from \"../../../hooks\";\nimport useScrollState from \"../../hooks/useScrollState\";\n\nimport {\n computeIOSContentOffset,\n getEffectiveHeight,\n isScrollAtEnd,\n shouldShiftContent,\n} from \"./helpers\";\n\nimport type { UseChatKeyboardOptions, UseChatKeyboardReturn } from \"./types\";\nimport type { AnimatedRef } from \"react-native-reanimated\";\nimport type Reanimated from \"react-native-reanimated\";\n\n/**\n * Hook that manages keyboard-driven scrolling for chat-style scroll views.\n * Calculates padding (extra scrollable space) and content shift values,\n * using iOS-specific strategy (contentOffset set once in onStart).\n *\n * @param scrollViewRef - Animated ref to the scroll view.\n * @param options - Configuration for inverted and keyboardLiftBehavior.\n * @returns Shared values for padding and contentOffsetY.\n * @example\n * ```tsx\n * const { padding, contentOffsetY } = useChatKeyboard(ref, {\n * inverted: false,\n * keyboardLiftBehavior: \"always\",\n * });\n * ```\n */\nfunction useChatKeyboard(\n scrollViewRef: AnimatedRef<Reanimated.ScrollView>,\n options: UseChatKeyboardOptions,\n): UseChatKeyboardReturn {\n const { inverted, keyboardLiftBehavior, freeze, offset } = options;\n\n const padding = useSharedValue(0);\n const currentHeight = useSharedValue(0);\n const contentOffsetY = useSharedValue(0);\n const targetKeyboardHeight = useSharedValue(0);\n\n const {\n layout,\n size,\n offset: scroll,\n onLayout,\n onContentSizeChange,\n } = useScrollState(scrollViewRef);\n\n useKeyboardHandler(\n {\n onStart: (e) => {\n \"worklet\";\n\n if (freeze) {\n return;\n }\n\n if (e.height > 0) {\n // eslint-disable-next-line react-compiler/react-compiler\n targetKeyboardHeight.value = e.height;\n }\n\n const effective = getEffectiveHeight(\n e.height,\n targetKeyboardHeight.value,\n offset,\n );\n\n const atEnd = isScrollAtEnd(\n scroll.value,\n layout.value.height,\n size.value.height,\n inverted,\n );\n\n // persistent mode: when keyboard shrinks, snap to end or hold position\n if (\n keyboardLiftBehavior === \"persistent\" &&\n effective < padding.value\n ) {\n padding.value = effective;\n\n if (atEnd) {\n if (inverted) {\n contentOffsetY.value = -effective;\n } else {\n contentOffsetY.value = Math.max(\n size.value.height - layout.value.height + effective,\n 0,\n );\n }\n } else {\n // Preserve current scroll position so the animated props\n // don't re-apply the stale contentOffset from keyboard open\n contentOffsetY.value = scroll.value;\n }\n\n return;\n }\n\n // never mode: when keyboard shrinks and at end, snap to end\n // to avoid ghost padding\n if (\n keyboardLiftBehavior === \"never\" &&\n effective < padding.value &&\n atEnd\n ) {\n padding.value = effective;\n\n if (inverted) {\n contentOffsetY.value = -effective;\n } else {\n contentOffsetY.value = Math.max(\n size.value.height - layout.value.height + effective,\n 0,\n );\n }\n\n return;\n }\n\n const relativeScroll = inverted\n ? scroll.value + padding.value\n : scroll.value - padding.value;\n\n padding.value = effective;\n\n if (!shouldShiftContent(keyboardLiftBehavior, atEnd)) {\n // Preserve current scroll position so animated props\n // don't re-apply a stale contentOffset when padding changes\n contentOffsetY.value = scroll.value;\n\n return;\n }\n\n contentOffsetY.value = computeIOSContentOffset(\n relativeScroll,\n effective,\n size.value.height,\n layout.value.height,\n inverted,\n );\n },\n onMove: () => {\n \"worklet\";\n\n // iOS doesn't need per-frame updates (contentOffset handles it)\n },\n onEnd: (e) => {\n \"worklet\";\n\n if (freeze) {\n return;\n }\n\n const effective = getEffectiveHeight(\n e.height,\n targetKeyboardHeight.value,\n offset,\n );\n\n padding.value = effective;\n },\n },\n [inverted, keyboardLiftBehavior, freeze, offset],\n );\n\n return {\n padding,\n currentHeight,\n contentOffsetY,\n onLayout,\n onContentSizeChange,\n };\n}\n\nexport { useChatKeyboard };\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["_reactNativeReanimated","require","_hooks","_useScrollState","_interopRequireDefault","_helpers","e","__esModule","default","useChatKeyboard","scrollViewRef","options","inverted","keyboardLiftBehavior","freeze","offset","blankSpace","extraContentPadding","padding","useSharedValue","currentHeight","contentOffsetY","targetKeyboardHeight","prevAbsorption","layout","size","scroll","onLayout","onContentSizeChange","useScrollState","useKeyboardHandler","onStart","height","value","effective","getEffectiveHeight","atEnd","isScrollAtEnd","visibleFraction","getVisibleMinimumPaddingFraction","visiblePadding","minimumPaddingAbsorbed","Math","max","scrollEffective","getScrollEffective","actualTotalPadding","maxScroll","min","prevScrollEffective","relativeScroll","shouldShiftContent","computeIOSContentOffset","onMove","onEnd"],"sources":["index.ios.ts"],"sourcesContent":["import { useSharedValue } from \"react-native-reanimated\";\n\nimport { useKeyboardHandler } from \"../../../hooks\";\nimport useScrollState from \"../../hooks/useScrollState\";\n\nimport {\n computeIOSContentOffset,\n getEffectiveHeight,\n getScrollEffective,\n getVisibleMinimumPaddingFraction,\n isScrollAtEnd,\n shouldShiftContent,\n} from \"./helpers\";\n\nimport type { UseChatKeyboardOptions, UseChatKeyboardReturn } from \"./types\";\nimport type { AnimatedRef } from \"react-native-reanimated\";\nimport type Reanimated from \"react-native-reanimated\";\n\n/**\n * Hook that manages keyboard-driven scrolling for chat-style scroll views.\n * Calculates padding (extra scrollable space) and content shift values,\n * using iOS-specific strategy (contentOffset set once in onStart).\n *\n * @param scrollViewRef - Animated ref to the scroll view.\n * @param options - Configuration for inverted and keyboardLiftBehavior.\n * @returns Shared values for padding and contentOffsetY.\n * @example\n * ```tsx\n * const { padding, contentOffsetY } = useChatKeyboard(ref, {\n * inverted: false,\n * keyboardLiftBehavior: \"always\",\n * });\n * ```\n */\nfunction useChatKeyboard(\n scrollViewRef: AnimatedRef<Reanimated.ScrollView>,\n options: UseChatKeyboardOptions,\n): UseChatKeyboardReturn {\n const {\n inverted,\n keyboardLiftBehavior,\n freeze,\n offset,\n blankSpace,\n extraContentPadding,\n } = options;\n\n const padding = useSharedValue(0);\n const currentHeight = useSharedValue(0);\n const contentOffsetY = useSharedValue(0);\n const targetKeyboardHeight = useSharedValue(0);\n const prevAbsorption = useSharedValue(0);\n\n const {\n layout,\n size,\n offset: scroll,\n onLayout,\n onContentSizeChange,\n } = useScrollState(scrollViewRef);\n\n useKeyboardHandler(\n {\n onStart: (e) => {\n \"worklet\";\n\n if (freeze) {\n return;\n }\n\n if (e.height > 0) {\n // eslint-disable-next-line react-compiler/react-compiler\n targetKeyboardHeight.value = e.height;\n }\n\n const effective = getEffectiveHeight(\n e.height,\n targetKeyboardHeight.value,\n offset,\n );\n\n const atEnd = isScrollAtEnd(\n scroll.value,\n layout.value.height,\n size.value.height,\n inverted,\n );\n\n // Scale minimum padding absorption by how much of it is visible.\n // Fully visible → full absorption; fully off-screen → no absorption.\n const visibleFraction = getVisibleMinimumPaddingFraction(\n scroll.value,\n layout.value.height,\n size.value.height,\n blankSpace.value,\n inverted,\n );\n const visiblePadding = visibleFraction * blankSpace.value;\n const minimumPaddingAbsorbed = Math.max(\n 0,\n visiblePadding - extraContentPadding.value,\n );\n const scrollEffective = getScrollEffective(\n effective,\n minimumPaddingAbsorbed,\n );\n const actualTotalPadding = Math.max(\n blankSpace.value,\n effective + extraContentPadding.value,\n );\n\n // persistent mode: when keyboard shrinks, clamp to valid range\n if (\n keyboardLiftBehavior === \"persistent\" &&\n effective < padding.value\n ) {\n padding.value = effective;\n prevAbsorption.value = minimumPaddingAbsorbed;\n\n if (inverted) {\n const maxScroll = Math.max(\n size.value.height - layout.value.height,\n 0,\n );\n\n contentOffsetY.value = Math.max(\n -actualTotalPadding,\n Math.min(scroll.value, maxScroll),\n );\n } else {\n const maxScroll = Math.max(\n size.value.height - layout.value.height + actualTotalPadding,\n 0,\n );\n\n contentOffsetY.value = Math.max(\n 0,\n Math.min(scroll.value, maxScroll),\n );\n }\n\n return;\n }\n\n // never mode: when keyboard shrinks, clamp to valid range\n // to avoid ghost padding\n if (\n keyboardLiftBehavior === \"never\" &&\n effective < padding.value &&\n atEnd\n ) {\n padding.value = effective;\n prevAbsorption.value = minimumPaddingAbsorbed;\n\n if (inverted) {\n const maxScroll = Math.max(\n size.value.height - layout.value.height,\n 0,\n );\n\n contentOffsetY.value = Math.max(\n -actualTotalPadding,\n Math.min(scroll.value, maxScroll),\n );\n } else {\n const maxScroll = Math.max(\n size.value.height - layout.value.height + actualTotalPadding,\n 0,\n );\n\n contentOffsetY.value = Math.max(\n 0,\n Math.min(scroll.value, maxScroll),\n );\n }\n\n return;\n }\n\n // Undo only the scroll displacement that was actually applied\n // (not the full padding, which includes the absorbed portion).\n // Use the stored absorption from the previous event so that\n // the unwind matches the shift that was originally applied.\n const prevScrollEffective = getScrollEffective(\n padding.value,\n prevAbsorption.value,\n );\n const relativeScroll = inverted\n ? scroll.value + prevScrollEffective\n : scroll.value - prevScrollEffective;\n\n padding.value = effective;\n prevAbsorption.value = minimumPaddingAbsorbed;\n\n if (!shouldShiftContent(keyboardLiftBehavior, atEnd)) {\n // Preserve current scroll position so animated props\n // don't re-apply a stale contentOffset when padding changes\n contentOffsetY.value = scroll.value;\n\n return;\n }\n\n // When blankSpace fully absorbs the keyboard opening, preserve current scroll position\n // (only when keyboard is open — effective > 0 — not when closing)\n if (\n scrollEffective === 0 &&\n minimumPaddingAbsorbed > 0 &&\n effective > 0\n ) {\n contentOffsetY.value = scroll.value;\n\n return;\n }\n\n contentOffsetY.value = computeIOSContentOffset(\n relativeScroll,\n scrollEffective,\n size.value.height,\n layout.value.height,\n inverted,\n actualTotalPadding,\n );\n },\n onMove: () => {\n \"worklet\";\n\n // iOS doesn't need per-frame updates (contentOffset handles it)\n },\n onEnd: (e) => {\n \"worklet\";\n\n if (freeze) {\n return;\n }\n\n const effective = getEffectiveHeight(\n e.height,\n targetKeyboardHeight.value,\n offset,\n );\n\n padding.value = effective;\n },\n },\n [inverted, keyboardLiftBehavior, freeze, offset, extraContentPadding],\n );\n\n return {\n padding,\n currentHeight,\n contentOffsetY,\n scroll,\n layout,\n size,\n onLayout,\n onContentSizeChange,\n };\n}\n\nexport { useChatKeyboard };\n"],"mappings":"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","ignoreList":[]}
@@ -19,10 +19,7 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
19
19
  * @returns Shared values for padding and contentOffsetY (always `undefined`).
20
20
  * @example
21
21
  * ```tsx
22
- * const { padding, contentOffsetY } = useChatKeyboard(ref, {
23
- * inverted: false,
24
- * keyboardLiftBehavior: "always",
25
- * });
22
+ * const { padding } = useChatKeyboard(ref, { inverted: false, keyboardLiftBehavior: "always" });
26
23
  * ```
27
24
  */
28
25
  function useChatKeyboard(scrollViewRef, options) {
@@ -30,13 +27,17 @@ function useChatKeyboard(scrollViewRef, options) {
30
27
  inverted,
31
28
  keyboardLiftBehavior,
32
29
  freeze,
33
- offset
30
+ offset,
31
+ blankSpace,
32
+ extraContentPadding
34
33
  } = options;
35
34
  const padding = (0, _reactNativeReanimated.useSharedValue)(0);
36
35
  const currentHeight = (0, _reactNativeReanimated.useSharedValue)(0);
37
36
  const offsetBeforeScroll = (0, _reactNativeReanimated.useSharedValue)(0);
38
37
  const targetKeyboardHeight = (0, _reactNativeReanimated.useSharedValue)(0);
39
38
  const closing = (0, _reactNativeReanimated.useSharedValue)(false);
39
+ const minimumPaddingFractionOnOpen = (0, _reactNativeReanimated.useSharedValue)(0);
40
+ const actualOpenShift = (0, _reactNativeReanimated.useSharedValue)(0);
40
41
  const {
41
42
  layout,
42
43
  size,
@@ -44,10 +45,11 @@ function useChatKeyboard(scrollViewRef, options) {
44
45
  onLayout,
45
46
  onContentSizeChange
46
47
  } = (0, _useScrollState.default)(scrollViewRef);
47
- const clampScrollIfNeeded = effective => {
48
+ const clampScrollIfNeeded = (effective, totalPaddingForMaxScroll) => {
48
49
  "worklet";
49
50
 
50
- const maxScroll = Math.max(size.value.height - layout.value.height + effective, 0);
51
+ const paddingForMax = totalPaddingForMaxScroll !== undefined ? totalPaddingForMaxScroll : effective;
52
+ const maxScroll = Math.max(size.value.height - layout.value.height + paddingForMax, 0);
51
53
  if (scroll.value > maxScroll) {
52
54
  (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, maxScroll, false);
53
55
  }
@@ -68,6 +70,12 @@ function useChatKeyboard(scrollViewRef, options) {
68
70
  }
69
71
  const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset);
70
72
  const atEnd = (0, _helpers.isScrollAtEnd)(scroll.value, layout.value.height, size.value.height, inverted);
73
+
74
+ // Scale minimum padding absorption by how much of it is visible.
75
+ // Fully visible → full absorption; fully off-screen → no absorption.
76
+ const visibleFraction = (0, _helpers.getVisibleMinimumPaddingFraction)(scroll.value, layout.value.height, size.value.height, blankSpace.value, inverted);
77
+ const minimumPaddingAbsorbed = visibleFraction >= 1 ? (0, _helpers.getMinimumPaddingAbsorbed)(blankSpace.value, extraContentPadding.value) : 0;
78
+ const scrollEffective = (0, _helpers.getScrollEffective)(effective, minimumPaddingAbsorbed);
71
79
  if (inverted && e.duration === -1) {
72
80
  // Android inverted: skip post-interactive snap-back events
73
81
  // (duration === -1 means the keyboard is re-establishing its
@@ -75,11 +83,18 @@ function useChatKeyboard(scrollViewRef, options) {
75
83
  return;
76
84
  } else if (e.height > 0) {
77
85
  // Android: keyboard opening — set padding + capture scroll position
86
+ minimumPaddingFractionOnOpen.value = visibleFraction >= 1 ? 1 : 0;
78
87
  padding.value = effective;
79
88
  offsetBeforeScroll.value = scroll.value;
80
89
  if (!inverted && keyboardLiftBehavior === "whenAtEnd" && !atEnd) {
81
90
  // Sentinel: don't scroll in onMove (non-inverted only)
82
91
  offsetBeforeScroll.value = -1;
92
+ } else if (!inverted && scrollEffective === 0) {
93
+ // blankSpace fully absorbs the keyboard — prevent scroll
94
+ offsetBeforeScroll.value = -1;
95
+ } else if (inverted && scrollEffective === 0) {
96
+ // blankSpace fully absorbs the keyboard — guard for inverted
97
+ offsetBeforeScroll.value = scroll.value;
83
98
  }
84
99
  } else {
85
100
  // Android: keyboard closing — re-capture scroll position
@@ -88,9 +103,9 @@ function useChatKeyboard(scrollViewRef, options) {
88
103
  } else {
89
104
  // Preserve "whenAtEnd" sentinel: if open didn't shift, close shouldn't either
90
105
  if (offsetBeforeScroll.value !== -1) {
91
- // Non-inverted: subtract padding to get the "natural" position
92
- // so onMove smoothly scrolls back from where the user is now
93
- offsetBeforeScroll.value = scroll.value - padding.value;
106
+ // Use the actual displacement recorded at end of open animation
107
+ // (not the theoretical value) so close is symmetric with open
108
+ offsetBeforeScroll.value = scroll.value - actualOpenShift.value;
94
109
  }
95
110
  }
96
111
  }
@@ -108,6 +123,9 @@ function useChatKeyboard(scrollViewRef, options) {
108
123
  return;
109
124
  }
110
125
  const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset);
126
+ const minimumPaddingAbsorbed = (0, _helpers.getMinimumPaddingAbsorbed)(blankSpace.value, extraContentPadding.value) * minimumPaddingFractionOnOpen.value;
127
+ const scrollEffective = (0, _helpers.getScrollEffective)(effective, minimumPaddingAbsorbed);
128
+ const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value);
111
129
 
112
130
  // Check if we should shift content based on position when keyboard started
113
131
  const wasAtEnd = (0, _helpers.isScrollAtEnd)(offsetBeforeScroll.value, layout.value.height, size.value.height, inverted);
@@ -115,6 +133,9 @@ function useChatKeyboard(scrollViewRef, options) {
115
133
  // "never" at end: scroll along when keyboard closes to avoid jump
116
134
  if (keyboardLiftBehavior === "never" && wasAtEnd && effective < padding.value) {
117
135
  padding.value = effective;
136
+ if (scrollEffective === 0 && minimumPaddingAbsorbed > 0) {
137
+ return;
138
+ }
118
139
  (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, 0, false);
119
140
  return;
120
141
  }
@@ -122,11 +143,16 @@ function useChatKeyboard(scrollViewRef, options) {
122
143
  // Closing, not shifting: reduce padding to avoid gap
123
144
  if (closing.value && effective < padding.value) {
124
145
  padding.value = effective;
125
- clampScrollIfNeeded(effective);
146
+ clampScrollIfNeeded(effective, actualTotalPadding);
126
147
  }
127
148
  return;
128
149
  }
129
150
 
151
+ // When blankSpace fully absorbs the keyboard, skip scroll
152
+ if (scrollEffective === 0 && minimumPaddingAbsorbed > 0) {
153
+ return;
154
+ }
155
+
130
156
  // Persistent: don't let shift decrease
131
157
  if (keyboardLiftBehavior === "persistent") {
132
158
  const currentShift = offsetBeforeScroll.value + padding.value - scroll.value;
@@ -138,37 +164,33 @@ function useChatKeyboard(scrollViewRef, options) {
138
164
  } else if (closing.value) {
139
165
  // Not at end: reduce padding to avoid gap
140
166
  padding.value = effective;
141
- clampScrollIfNeeded(effective);
167
+ clampScrollIfNeeded(effective, actualTotalPadding);
142
168
  }
143
169
  return;
144
170
  }
145
171
  }
146
- const target = offsetBeforeScroll.value + padding.value - effective;
172
+ const target = offsetBeforeScroll.value + padding.value - scrollEffective;
147
173
  (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, target, false);
148
174
  } else {
149
175
  const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset);
176
+ const minimumPaddingAbsorbed = (0, _helpers.getMinimumPaddingAbsorbed)(blankSpace.value, extraContentPadding.value) * minimumPaddingFractionOnOpen.value;
177
+ const scrollEffective = (0, _helpers.getScrollEffective)(effective, minimumPaddingAbsorbed);
178
+ const actualTotalPadding = Math.max(blankSpace.value, effective + extraContentPadding.value);
150
179
 
151
- // "never" closing: scroll along when at end to avoid jump
180
+ // "never" closing: clamp scroll to valid range as inset shrinks
152
181
  if (keyboardLiftBehavior === "never" && closing.value && effective < padding.value) {
153
- const wasAtEnd = (0, _helpers.isScrollAtEnd)(offsetBeforeScroll.value + padding.value, layout.value.height, size.value.height, false);
154
- if (wasAtEnd) {
155
- const target = (0, _helpers.clampedScrollTarget)(offsetBeforeScroll.value, effective, size.value.height, layout.value.height);
156
- (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, target, false);
157
- } else {
158
- // Clamp to valid range as padding shrinks
159
- clampScrollIfNeeded(effective);
160
- }
182
+ clampScrollIfNeeded(effective, actualTotalPadding);
161
183
  return;
162
184
  }
163
185
  if (!(0, _helpers.shouldShiftContent)(keyboardLiftBehavior, true)) {
164
186
  return;
165
187
  }
166
188
 
167
- // "whenAtEnd" sentinel check
189
+ // "whenAtEnd" sentinel check (also used for blankSpace full absorption)
168
190
  if (offsetBeforeScroll.value === -1) {
169
191
  if (closing.value) {
170
192
  // Keyboard didn't shift on open; ensure valid position on close
171
- clampScrollIfNeeded(effective);
193
+ clampScrollIfNeeded(effective, actualTotalPadding);
172
194
  }
173
195
  return;
174
196
  }
@@ -176,12 +198,17 @@ function useChatKeyboard(scrollViewRef, options) {
176
198
  // "persistent" closing: maintain position, clamped to valid range
177
199
  if (keyboardLiftBehavior === "persistent" && closing.value) {
178
200
  const keepAt = offsetBeforeScroll.value + padding.value;
179
- const maxScroll = Math.max(size.value.height - layout.value.height + effective, 0);
201
+ const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0);
180
202
  (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, Math.min(keepAt, maxScroll), false);
181
203
  return;
182
204
  }
183
- const target = (0, _helpers.clampedScrollTarget)(offsetBeforeScroll.value, effective, size.value.height, layout.value.height);
205
+ const target = (0, _helpers.clampedScrollTarget)(offsetBeforeScroll.value, scrollEffective, size.value.height, layout.value.height, actualTotalPadding);
184
206
  (0, _reactNativeReanimated.scrollTo)(scrollViewRef, 0, target, false);
207
+
208
+ // Track actual (clamped) displacement during open for symmetric close
209
+ if (!closing.value) {
210
+ actualOpenShift.value = target - offsetBeforeScroll.value;
211
+ }
185
212
  }
186
213
  },
187
214
  onEnd: e => {
@@ -192,12 +219,20 @@ function useChatKeyboard(scrollViewRef, options) {
192
219
  }
193
220
  const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset);
194
221
  padding.value = effective;
222
+
223
+ // Record actual scroll displacement so close can be symmetric
224
+ if (effective > 0 && offsetBeforeScroll.value !== -1) {
225
+ actualOpenShift.value = scroll.value - offsetBeforeScroll.value;
226
+ }
195
227
  }
196
228
  }, [inverted, keyboardLiftBehavior, freeze, offset]);
197
229
  return {
198
230
  padding,
199
231
  currentHeight,
200
232
  contentOffsetY: undefined,
233
+ scroll,
234
+ layout,
235
+ size,
201
236
  onLayout,
202
237
  onContentSizeChange
203
238
  };