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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +1 -0
  2. package/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +1 -0
  3. package/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +7 -0
  4. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +8 -0
  5. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt +23 -0
  6. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ReactContext.kt +12 -0
  7. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt +2 -1
  8. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/WindowDimensionListener.kt +5 -1
  9. package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarGroupViewManagerImpl.kt +13 -0
  10. package/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt +4 -6
  11. package/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt +26 -0
  12. package/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt +31 -4
  13. package/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt +5 -0
  14. package/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarGroupReactViewGroup.kt +13 -0
  15. package/android/src/main/jni/RNKC.h +2 -0
  16. package/android/src/paper/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +8 -0
  17. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +9 -0
  18. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt +15 -0
  19. package/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +2 -0
  20. package/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewComponentDescriptor.h +27 -0
  21. package/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewShadowNode.cpp +14 -0
  22. package/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewShadowNode.h +30 -0
  23. package/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewState.h +28 -0
  24. package/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewComponentDescriptor.h +20 -0
  25. package/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.cpp +7 -0
  26. package/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.h +23 -0
  27. package/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewState.h +21 -0
  28. package/ios/KeyboardControllerModule.mm +36 -0
  29. package/ios/extensions/UIApplication.swift +1 -1
  30. package/ios/observers/FocusedInputObserver.swift +2 -1
  31. package/ios/traversal/ViewHierarchyNavigator.swift +49 -3
  32. package/ios/views/ClippingScrollViewDecoratorViewManager.h +26 -0
  33. package/ios/views/ClippingScrollViewDecoratorViewManager.mm +231 -0
  34. package/ios/views/KeyboardToolbarGroupViewManager.h +28 -0
  35. package/ios/views/KeyboardToolbarGroupViewManager.mm +88 -0
  36. package/jest/index.js +11 -1
  37. package/lib/commonjs/bindings.js +18 -2
  38. package/lib/commonjs/bindings.js.map +1 -1
  39. package/lib/commonjs/bindings.native.js +3 -4
  40. package/lib/commonjs/bindings.native.js.map +1 -1
  41. package/lib/commonjs/components/KeyboardAvoidingView/index.js +30 -4
  42. package/lib/commonjs/components/KeyboardAvoidingView/index.js.map +1 -1
  43. package/lib/commonjs/components/KeyboardAwareScrollView/index.js +17 -4
  44. package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
  45. package/lib/commonjs/components/KeyboardChatScrollView/index.js +33 -2
  46. package/lib/commonjs/components/KeyboardChatScrollView/index.js.map +1 -1
  47. package/lib/commonjs/components/KeyboardChatScrollView/types.js.map +1 -1
  48. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js +107 -21
  49. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js.map +1 -1
  50. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +45 -17
  51. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -1
  52. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js +61 -26
  53. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -1
  54. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -1
  55. package/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js +72 -0
  56. package/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js.map +1 -0
  57. package/lib/commonjs/components/KeyboardToolbar/index.js +1 -0
  58. package/lib/commonjs/components/KeyboardToolbar/index.js.map +1 -1
  59. package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js +18 -6
  60. package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js.map +1 -1
  61. package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js +1 -2
  62. package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -1
  63. package/lib/commonjs/specs/KeyboardToolbarGroupViewNativeComponent.js +12 -0
  64. package/lib/commonjs/specs/KeyboardToolbarGroupViewNativeComponent.js.map +1 -0
  65. package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
  66. package/lib/commonjs/types/module.js.map +1 -1
  67. package/lib/commonjs/types/views.js.map +1 -1
  68. package/lib/module/bindings.js +17 -1
  69. package/lib/module/bindings.js.map +1 -1
  70. package/lib/module/bindings.native.js +2 -3
  71. package/lib/module/bindings.native.js.map +1 -1
  72. package/lib/module/components/KeyboardAvoidingView/index.js +29 -4
  73. package/lib/module/components/KeyboardAvoidingView/index.js.map +1 -1
  74. package/lib/module/components/KeyboardAwareScrollView/index.js +17 -4
  75. package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
  76. package/lib/module/components/KeyboardChatScrollView/index.js +34 -3
  77. package/lib/module/components/KeyboardChatScrollView/index.js.map +1 -1
  78. package/lib/module/components/KeyboardChatScrollView/types.js.map +1 -1
  79. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js +98 -16
  80. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js.map +1 -1
  81. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +46 -18
  82. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -1
  83. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js +62 -27
  84. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -1
  85. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -1
  86. package/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js +67 -0
  87. package/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js.map +1 -0
  88. package/lib/module/components/KeyboardToolbar/index.js +2 -1
  89. package/lib/module/components/KeyboardToolbar/index.js.map +1 -1
  90. package/lib/module/components/ScrollViewWithBottomPadding/index.js +19 -7
  91. package/lib/module/components/ScrollViewWithBottomPadding/index.js.map +1 -1
  92. package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js +1 -2
  93. package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -1
  94. package/lib/module/specs/KeyboardToolbarGroupViewNativeComponent.js +5 -0
  95. package/lib/module/specs/KeyboardToolbarGroupViewNativeComponent.js.map +1 -0
  96. package/lib/module/specs/NativeKeyboardController.js.map +1 -1
  97. package/lib/module/types/module.js.map +1 -1
  98. package/lib/module/types/views.js.map +1 -1
  99. package/lib/typescript/bindings.d.ts +12 -2
  100. package/lib/typescript/bindings.native.d.ts +2 -1
  101. package/lib/typescript/components/KeyboardAvoidingView/index.d.ts +44 -13
  102. package/lib/typescript/components/KeyboardChatScrollView/index.d.ts +3 -0
  103. package/lib/typescript/components/KeyboardChatScrollView/types.d.ts +41 -0
  104. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts +63 -8
  105. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/index.d.ts +1 -4
  106. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts +15 -0
  107. package/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts +43 -0
  108. package/lib/typescript/components/KeyboardToolbar/index.d.ts +2 -0
  109. package/lib/typescript/components/ScrollViewWithBottomPadding/index.d.ts +3 -0
  110. package/lib/typescript/specs/ClippingScrollViewDecoratorViewNativeComponent.d.ts +1 -0
  111. package/lib/typescript/specs/KeyboardToolbarGroupViewNativeComponent.d.ts +6 -0
  112. package/lib/typescript/specs/NativeKeyboardController.d.ts +1 -0
  113. package/lib/typescript/types/module.d.ts +7 -0
  114. package/lib/typescript/types/views.d.ts +2 -0
  115. package/package.json +6 -2
  116. package/react-native.config.js +2 -0
  117. package/src/bindings.native.ts +4 -4
  118. package/src/bindings.ts +15 -1
  119. package/src/components/KeyboardAvoidingView/index.tsx +56 -29
  120. package/src/components/KeyboardAwareScrollView/index.tsx +20 -5
  121. package/src/components/KeyboardChatScrollView/index.tsx +47 -3
  122. package/src/components/KeyboardChatScrollView/types.ts +41 -0
  123. package/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts +126 -21
  124. package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts +101 -21
  125. package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts +116 -40
  126. package/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts +9 -0
  127. package/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts +123 -0
  128. package/src/components/KeyboardToolbar/index.tsx +6 -1
  129. package/src/components/ScrollViewWithBottomPadding/index.tsx +31 -4
  130. package/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts +1 -1
  131. package/src/specs/KeyboardToolbarGroupViewNativeComponent.ts +10 -0
  132. package/src/specs/NativeKeyboardController.ts +1 -0
  133. package/src/types/module.ts +9 -0
  134. package/src/types/views.ts +2 -0
  135. package/android/.settings/org.eclipse.buildship.core.prefs +0 -2
  136. package/ios/KeyboardController.xcodeproj/xcuserdata/kirylziusko.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  137. package/ios/Tests/Tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  138. package/ios/Tests/Tests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  139. package/ios/Tests/Tests.xcodeproj/project.xcworkspace/xcuserdata/kirylziusko.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  140. package/ios/Tests/Tests.xcodeproj/xcuserdata/kirylziusko.xcuserdatad/xcschemes/xcschememanagement.plist +0 -32
@@ -1,16 +1,25 @@
1
1
  import React, { forwardRef, useCallback, useMemo } from "react";
2
2
  import { StyleSheet } from "react-native";
3
- import { useAnimatedRef, useAnimatedStyle } from "react-native-reanimated";
3
+ import {
4
+ makeMutable,
5
+ useAnimatedRef,
6
+ useAnimatedStyle,
7
+ useDerivedValue,
8
+ } from "react-native-reanimated";
4
9
  import Reanimated from "react-native-reanimated";
5
10
 
6
11
  import useCombinedRef from "../hooks/useCombinedRef";
7
12
  import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding";
8
13
 
9
14
  import { useChatKeyboard } from "./useChatKeyboard";
15
+ import { useExtraContentPadding } from "./useExtraContentPadding";
10
16
 
11
17
  import type { KeyboardChatScrollViewProps } from "./types";
12
18
  import type { LayoutChangeEvent } from "react-native";
13
19
 
20
+ const ZERO_CONTENT_PADDING = makeMutable(0);
21
+ const ZERO_BLANK_SPACE = makeMutable(0);
22
+
14
23
  const KeyboardChatScrollView = forwardRef<
15
24
  Reanimated.ScrollView,
16
25
  React.PropsWithChildren<KeyboardChatScrollViewProps>
@@ -23,6 +32,9 @@ const KeyboardChatScrollView = forwardRef<
23
32
  keyboardLiftBehavior = "always",
24
33
  freeze = false,
25
34
  offset = 0,
35
+ extraContentPadding = ZERO_CONTENT_PADDING,
36
+ blankSpace = ZERO_BLANK_SPACE,
37
+ applyWorkaroundForContentInsetHitTestBug = false,
26
38
  onLayout: onLayoutProp,
27
39
  onContentSizeChange: onContentSizeChangeProp,
28
40
  ...rest
@@ -31,11 +43,13 @@ const KeyboardChatScrollView = forwardRef<
31
43
  ) => {
32
44
  const scrollViewRef = useAnimatedRef<Reanimated.ScrollView>();
33
45
  const onRef = useCombinedRef(ref, scrollViewRef);
34
-
35
46
  const {
36
47
  padding,
37
48
  currentHeight,
38
49
  contentOffsetY,
50
+ scroll,
51
+ layout,
52
+ size,
39
53
  onLayout: onLayoutInternal,
40
54
  onContentSizeChange: onContentSizeChangeInternal,
41
55
  } = useChatKeyboard(scrollViewRef, {
@@ -43,8 +57,34 @@ const KeyboardChatScrollView = forwardRef<
43
57
  keyboardLiftBehavior,
44
58
  freeze,
45
59
  offset,
60
+ blankSpace,
61
+ extraContentPadding,
46
62
  });
47
63
 
64
+ useExtraContentPadding({
65
+ scrollViewRef,
66
+ extraContentPadding,
67
+ keyboardPadding: padding,
68
+ blankSpace,
69
+ scroll,
70
+ layout,
71
+ size,
72
+ inverted,
73
+ keyboardLiftBehavior,
74
+ freeze,
75
+ });
76
+
77
+ const totalPadding = useDerivedValue(() =>
78
+ Math.max(blankSpace.value, padding.value + extraContentPadding.value),
79
+ );
80
+
81
+ // Scroll indicator inset = keyboard + extraContentPadding (excludes blankSpace).
82
+ // Apps that render into the unsafe area can supply a negative
83
+ // scrollIndicatorInsets adjustment at the application layer.
84
+ const indicatorPadding = useDerivedValue(
85
+ () => padding.value + extraContentPadding.value,
86
+ );
87
+
48
88
  const onLayout = useCallback(
49
89
  (e: LayoutChangeEvent) => {
50
90
  onLayoutInternal(e);
@@ -81,9 +121,13 @@ const KeyboardChatScrollView = forwardRef<
81
121
  <ScrollViewWithBottomPadding
82
122
  ref={onRef}
83
123
  {...rest}
84
- bottomPadding={padding}
124
+ applyWorkaroundForContentInsetHitTestBug={
125
+ applyWorkaroundForContentInsetHitTestBug
126
+ }
127
+ bottomPadding={totalPadding}
85
128
  contentOffsetY={contentOffsetY}
86
129
  inverted={inverted}
130
+ scrollIndicatorPadding={indicatorPadding}
87
131
  ScrollViewComponent={ScrollViewComponent}
88
132
  onContentSizeChange={onContentSizeChange}
89
133
  onLayout={onLayout}
@@ -1,6 +1,7 @@
1
1
  import type { AnimatedScrollViewComponent } from "../ScrollViewWithBottomPadding";
2
2
  import type { KeyboardLiftBehavior } from "./useChatKeyboard/types";
3
3
  import type { ScrollViewProps } from "react-native";
4
+ import type { SharedValue } from "react-native-reanimated";
4
5
 
5
6
  export type KeyboardChatScrollViewProps = {
6
7
  /** Custom component for `ScrollView`. Default is `ScrollView`. */
@@ -46,4 +47,44 @@ export type KeyboardChatScrollViewProps = {
46
47
  * Default is `false`.
47
48
  */
48
49
  freeze?: boolean;
50
+ /**
51
+ * A shared value representing additional padding from external elements
52
+ * (e.g., a growing multiline `TextInput` in a `KeyboardStickyView`).
53
+ *
54
+ * When this value changes:
55
+ * - The scrollable range is always extended/contracted (via `contentInset`).
56
+ * - The scroll position is conditionally adjusted based on `keyboardLiftBehavior`.
57
+ *
58
+ * Default is `undefined` (no extra padding).
59
+ */
60
+ extraContentPadding?: SharedValue<number>;
61
+ /**
62
+ * When `true`, applies a runtime workaround for a React Native 0.81+ bug
63
+ * where the ScrollView's `contentInset` area does not respond to touch/scroll
64
+ * gestures (facebook/react-native#54123).
65
+ *
66
+ * This uses Objective-C runtime method swizzling on the ScrollView's container
67
+ * view, which is inherently fragile. Only enable if you are affected by the
68
+ * upstream bug and understand the risks.
69
+ *
70
+ * iOS only. Default is `false`.
71
+ */
72
+ applyWorkaroundForContentInsetHitTestBug?: boolean;
73
+ /**
74
+ * A shared value representing a minimum inset floor (in pixels).
75
+ *
76
+ * When set, the total bottom padding is computed as:
77
+ * `max(blankSpace, keyboardPadding + extraContentPadding)`
78
+ *
79
+ * This means the keyboard "absorbs" into the minimum padding rather than adding to it:
80
+ * - When `blankSpace >= keyboard + extraContentPadding`: content does NOT move on keyboard open/close.
81
+ * - When `blankSpace < keyboard + extraContentPadding`: content moves, but only by the excess amount.
82
+ *
83
+ * Useful in AI chat apps where a sent message needs space below it (to push it to the top
84
+ * of the viewport) while the AI response streams in, without that space causing extra movement
85
+ * when the keyboard opens.
86
+ *
87
+ * Default is `undefined` (equivalent to `0` — no minimum floor).
88
+ */
89
+ blankSpace?: SharedValue<number>;
49
90
  } & ScrollViewProps;
@@ -6,7 +6,7 @@ const AT_END_THRESHOLD = 20;
6
6
 
7
7
  /**
8
8
  * Map the current keyboard height to an effective height that accounts for a
9
- * fixed offset (e.g. bottom safe-area or tab-bar height).
9
+ * fixed offset (e.g. Bottom safe-area or tab-bar height)..
10
10
  *
11
11
  * @param height - Current keyboard height.
12
12
  * @param targetKeyboardHeight - Full target keyboard height (captured on keyboard open).
@@ -18,11 +18,11 @@ const AT_END_THRESHOLD = 20;
18
18
  * getEffectiveHeight(150, 300, 50); // 125
19
19
  * ```
20
20
  */
21
- export function getEffectiveHeight(
21
+ export const getEffectiveHeight = (
22
22
  height: number,
23
23
  targetKeyboardHeight: number,
24
24
  offset: number,
25
- ): number {
25
+ ): number => {
26
26
  "worklet";
27
27
 
28
28
  if (offset === 0 || targetKeyboardHeight === 0) {
@@ -34,7 +34,7 @@ export function getEffectiveHeight(
34
34
  [0, targetKeyboardHeight],
35
35
  [0, Math.max(targetKeyboardHeight - offset, 0)],
36
36
  );
37
- }
37
+ };
38
38
 
39
39
  /**
40
40
  * Check whether the scroll view is at the end of its content.
@@ -54,12 +54,12 @@ export function getEffectiveHeight(
54
54
  * const atEndInverted = isScrollAtEnd(5, 800, 2000, true); // true (5 <= 20)
55
55
  * ```
56
56
  */
57
- export function isScrollAtEnd(
57
+ export const isScrollAtEnd = (
58
58
  scrollOffset: number,
59
59
  layoutHeight: number,
60
60
  contentHeight: number,
61
61
  inverted: boolean = false,
62
- ): boolean {
62
+ ): boolean => {
63
63
  "worklet";
64
64
 
65
65
  if (inverted) {
@@ -67,7 +67,7 @@ export function isScrollAtEnd(
67
67
  }
68
68
 
69
69
  return scrollOffset + layoutHeight >= contentHeight - AT_END_THRESHOLD;
70
- }
70
+ };
71
71
 
72
72
  /**
73
73
  * Decide whether content should be shifted based on the keyboard lift behavior.
@@ -81,10 +81,10 @@ export function isScrollAtEnd(
81
81
  * shouldShiftContent("whenAtEnd", false); // false
82
82
  * ```
83
83
  */
84
- export function shouldShiftContent(
84
+ export const shouldShiftContent = (
85
85
  behavior: KeyboardLiftBehavior,
86
86
  isAtEnd: boolean,
87
- ): boolean {
87
+ ): boolean => {
88
88
  "worklet";
89
89
 
90
90
  switch (behavior) {
@@ -97,67 +97,172 @@ export function shouldShiftContent(
97
97
  case "persistent":
98
98
  return true;
99
99
  }
100
- }
100
+ };
101
+
102
+ /**
103
+ * Compute the fraction of minimum padding space currently visible in the viewport (0–1).
104
+ *
105
+ * The minimum padding space lives in the scroll view's contentInset, NOT in the
106
+ * content itself. So `contentHeight` (from onContentSizeChange / scroll
107
+ * events) does **not** include it. The visible portion is how far the
108
+ * viewport extends past the content boundary into the inset area.
109
+ *
110
+ * For non-inverted lists the padding is in contentInset.bottom.
111
+ * For inverted lists the padding is in contentInset.top (negative scroll).
112
+ *
113
+ * @param scrollOffset - Current vertical scroll offset.
114
+ * @param layoutHeight - Visible height of the scroll view.
115
+ * @param contentHeight - Height of the scroll content (excludes insets).
116
+ * @param blankSpace - Size of the minimum padding inset area.
117
+ * @param inverted - Whether the list is inverted.
118
+ * @returns A value between 0 (padding fully off-screen) and 1 (padding fully visible).
119
+ * @example
120
+ * ```ts
121
+ * // Non-inverted: contentHeight=1500, layout=800, blankSpace=300
122
+ * getVisibleMinimumPaddingFraction(1500, 800, 1500, 300, false); // 1 (at end, viewport past content)
123
+ * getVisibleMinimumPaddingFraction(850, 800, 1500, 300, false); // 0.5 (half padding visible)
124
+ * getVisibleMinimumPaddingFraction(700, 800, 1500, 300, false); // 0 (padding off-screen)
125
+ * ```
126
+ */
127
+ export const getVisibleMinimumPaddingFraction = (
128
+ scrollOffset: number,
129
+ layoutHeight: number,
130
+ contentHeight: number,
131
+ blankSpace: number,
132
+ inverted: boolean,
133
+ ): number => {
134
+ "worklet";
135
+
136
+ if (blankSpace <= 0) {
137
+ return 0;
138
+ }
139
+
140
+ if (inverted) {
141
+ // Minimum padding is in contentInset.top; visible when scroll < 0
142
+ return Math.max(0, Math.min(1, -scrollOffset / blankSpace));
143
+ }
144
+
145
+ // Minimum padding is in contentInset.bottom; visible when viewport extends past content
146
+ const pastContentEnd = scrollOffset + layoutHeight - contentHeight;
147
+
148
+ return Math.max(0, Math.min(1, pastContentEnd / blankSpace));
149
+ };
150
+
151
+ /**
152
+ * Compute how much of the minimum content padding absorbs the keyboard + extraContentPadding.
153
+ *
154
+ * @param blankSpace - Minimum inset floor.
155
+ * @param extraContentPadding - Extra content padding from external elements.
156
+ * @returns The portion of blankSpace that absorbs keyboard displacement.
157
+ * @example
158
+ * ```ts
159
+ * getMinimumPaddingAbsorbed(500, 20); // 480
160
+ * getMinimumPaddingAbsorbed(0, 20); // 0
161
+ * ```
162
+ */
163
+ export const getMinimumPaddingAbsorbed = (
164
+ blankSpace: number,
165
+ extraContentPadding: number,
166
+ ): number => {
167
+ "worklet";
168
+
169
+ return Math.max(0, blankSpace - extraContentPadding);
170
+ };
171
+
172
+ /**
173
+ * Compute the effective scroll displacement after minimum padding absorption.
174
+ *
175
+ * @param rawEffective - Raw effective keyboard height.
176
+ * @param minimumPaddingAbsorbed - Amount absorbed by minimum content padding.
177
+ * @returns The scroll displacement after subtracting the absorbed portion.
178
+ * @example
179
+ * ```ts
180
+ * getScrollEffective(300, 200); // 100
181
+ * getScrollEffective(300, 400); // 0
182
+ * ```
183
+ */
184
+ export const getScrollEffective = (
185
+ rawEffective: number,
186
+ minimumPaddingAbsorbed: number,
187
+ ): number => {
188
+ "worklet";
189
+
190
+ return Math.max(0, rawEffective - minimumPaddingAbsorbed);
191
+ };
101
192
 
102
193
  /**
103
194
  * Compute the clamped scroll target for non-inverted lists.
104
195
  *
105
196
  * @param offsetBeforeScroll - Scroll position before keyboard appeared.
106
- * @param keyboardHeight - Current keyboard height.
197
+ * @param keyboardHeight - Current keyboard height (used for scroll displacement).
107
198
  * @param contentHeight - Total height of the scrollable content.
108
199
  * @param layoutHeight - Visible height of the scroll view.
200
+ * @param totalPaddingForMaxScroll - Total padding to use for maxScroll calculation. When provided, used instead of keyboardHeight for the scrollable range. Defaults to keyboardHeight.
109
201
  * @returns Clamped scroll target between 0 and maxScroll.
110
202
  * @example
111
203
  * ```ts
112
204
  * clampedScrollTarget(100, 300, 1000, 800); // 400
205
+ * clampedScrollTarget(100, 100, 1000, 800, 500); // 200, maxScroll uses 500
113
206
  * ```
114
207
  */
115
- export function clampedScrollTarget(
208
+ export const clampedScrollTarget = (
116
209
  offsetBeforeScroll: number,
117
210
  keyboardHeight: number,
118
211
  contentHeight: number,
119
212
  layoutHeight: number,
120
- ): number {
213
+ totalPaddingForMaxScroll?: number,
214
+ ): number => {
121
215
  "worklet";
122
216
 
123
- const maxScroll = Math.max(contentHeight - layoutHeight + keyboardHeight, 0);
217
+ const paddingForMax =
218
+ totalPaddingForMaxScroll !== undefined
219
+ ? totalPaddingForMaxScroll
220
+ : keyboardHeight;
221
+ const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0);
124
222
 
125
223
  return Math.min(Math.max(offsetBeforeScroll + keyboardHeight, 0), maxScroll);
126
- }
224
+ };
127
225
 
128
226
  /**
129
227
  * Compute contentOffset.y for iOS lists.
130
228
  *
131
229
  * @param relativeScroll - Scroll position relative to current inset.
132
- * @param keyboardHeight - Target keyboard height.
230
+ * @param keyboardHeight - Target keyboard height (used for scroll displacement).
133
231
  * @param contentHeight - Total height of the scrollable content.
134
232
  * @param layoutHeight - Visible height of the scroll view.
135
233
  * @param inverted - Whether the list is inverted.
234
+ * @param totalPaddingForMaxScroll - Total padding to use for maxScroll calculation. When provided, used instead of keyboardHeight for the scrollable range. Defaults to keyboardHeight.
136
235
  * @returns The absolute contentOffset.y to set.
137
236
  * @example
138
237
  * ```ts
139
238
  * computeIOSContentOffset(100, 300, 1000, 800, false); // 400
140
239
  * ```
141
240
  */
142
- export function computeIOSContentOffset(
241
+ export const computeIOSContentOffset = (
143
242
  relativeScroll: number,
144
243
  keyboardHeight: number,
145
244
  contentHeight: number,
146
245
  layoutHeight: number,
147
246
  inverted: boolean,
148
- ): number {
247
+ totalPaddingForMaxScroll?: number,
248
+ ): number => {
149
249
  "worklet";
150
250
 
251
+ const paddingForMax =
252
+ totalPaddingForMaxScroll !== undefined
253
+ ? totalPaddingForMaxScroll
254
+ : keyboardHeight;
255
+
151
256
  if (inverted) {
152
257
  const maxScroll = Math.max(contentHeight - layoutHeight, 0);
153
258
 
154
259
  return Math.max(
155
260
  Math.min(relativeScroll - keyboardHeight, maxScroll),
156
- -keyboardHeight,
261
+ -paddingForMax,
157
262
  );
158
263
  }
159
264
 
160
- const maxScroll = Math.max(contentHeight - layoutHeight + keyboardHeight, 0);
265
+ const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0);
161
266
 
162
267
  return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll);
163
- }
268
+ };
@@ -6,6 +6,8 @@ import useScrollState from "../../hooks/useScrollState";
6
6
  import {
7
7
  computeIOSContentOffset,
8
8
  getEffectiveHeight,
9
+ getScrollEffective,
10
+ getVisibleMinimumPaddingFraction,
9
11
  isScrollAtEnd,
10
12
  shouldShiftContent,
11
13
  } from "./helpers";
@@ -34,12 +36,20 @@ function useChatKeyboard(
34
36
  scrollViewRef: AnimatedRef<Reanimated.ScrollView>,
35
37
  options: UseChatKeyboardOptions,
36
38
  ): UseChatKeyboardReturn {
37
- const { inverted, keyboardLiftBehavior, freeze, offset } = options;
39
+ const {
40
+ inverted,
41
+ keyboardLiftBehavior,
42
+ freeze,
43
+ offset,
44
+ blankSpace,
45
+ extraContentPadding,
46
+ } = options;
38
47
 
39
48
  const padding = useSharedValue(0);
40
49
  const currentHeight = useSharedValue(0);
41
50
  const contentOffsetY = useSharedValue(0);
42
51
  const targetKeyboardHeight = useSharedValue(0);
52
+ const prevAbsorption = useSharedValue(0);
43
53
 
44
54
  const {
45
55
  layout,
@@ -76,32 +86,63 @@ function useChatKeyboard(
76
86
  inverted,
77
87
  );
78
88
 
79
- // persistent mode: when keyboard shrinks, snap to end or hold position
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
80
113
  if (
81
114
  keyboardLiftBehavior === "persistent" &&
82
115
  effective < padding.value
83
116
  ) {
84
117
  padding.value = effective;
118
+ prevAbsorption.value = minimumPaddingAbsorbed;
85
119
 
86
- if (atEnd) {
87
- if (inverted) {
88
- contentOffsetY.value = -effective;
89
- } else {
90
- contentOffsetY.value = Math.max(
91
- size.value.height - layout.value.height + effective,
92
- 0,
93
- );
94
- }
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
+ );
95
130
  } else {
96
- // Preserve current scroll position so the animated props
97
- // don't re-apply the stale contentOffset from keyboard open
98
- contentOffsetY.value = scroll.value;
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
+ );
99
140
  }
100
141
 
101
142
  return;
102
143
  }
103
144
 
104
- // never mode: when keyboard shrinks and at end, snap to end
145
+ // never mode: when keyboard shrinks, clamp to valid range
105
146
  // to avoid ghost padding
106
147
  if (
107
148
  keyboardLiftBehavior === "never" &&
@@ -109,24 +150,47 @@ function useChatKeyboard(
109
150
  atEnd
110
151
  ) {
111
152
  padding.value = effective;
153
+ prevAbsorption.value = minimumPaddingAbsorbed;
112
154
 
113
155
  if (inverted) {
114
- contentOffsetY.value = -effective;
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
+ );
115
165
  } else {
166
+ const maxScroll = Math.max(
167
+ size.value.height - layout.value.height + actualTotalPadding,
168
+ 0,
169
+ );
170
+
116
171
  contentOffsetY.value = Math.max(
117
- size.value.height - layout.value.height + effective,
118
172
  0,
173
+ Math.min(scroll.value, maxScroll),
119
174
  );
120
175
  }
121
176
 
122
177
  return;
123
178
  }
124
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
+ );
125
188
  const relativeScroll = inverted
126
- ? scroll.value + padding.value
127
- : scroll.value - padding.value;
189
+ ? scroll.value + prevScrollEffective
190
+ : scroll.value - prevScrollEffective;
128
191
 
129
192
  padding.value = effective;
193
+ prevAbsorption.value = minimumPaddingAbsorbed;
130
194
 
131
195
  if (!shouldShiftContent(keyboardLiftBehavior, atEnd)) {
132
196
  // Preserve current scroll position so animated props
@@ -136,12 +200,25 @@ function useChatKeyboard(
136
200
  return;
137
201
  }
138
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
+
139
215
  contentOffsetY.value = computeIOSContentOffset(
140
216
  relativeScroll,
141
- effective,
217
+ scrollEffective,
142
218
  size.value.height,
143
219
  layout.value.height,
144
220
  inverted,
221
+ actualTotalPadding,
145
222
  );
146
223
  },
147
224
  onMove: () => {
@@ -165,13 +242,16 @@ function useChatKeyboard(
165
242
  padding.value = effective;
166
243
  },
167
244
  },
168
- [inverted, keyboardLiftBehavior, freeze, offset],
245
+ [inverted, keyboardLiftBehavior, freeze, offset, extraContentPadding],
169
246
  );
170
247
 
171
248
  return {
172
249
  padding,
173
250
  currentHeight,
174
251
  contentOffsetY,
252
+ scroll,
253
+ layout,
254
+ size,
175
255
  onLayout,
176
256
  onContentSizeChange,
177
257
  };