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.
- package/README.md +1 -0
- package/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +1 -0
- package/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +7 -0
- package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +8 -0
- package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt +23 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ReactContext.kt +12 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt +2 -1
- package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/WindowDimensionListener.kt +5 -1
- package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardToolbarGroupViewManagerImpl.kt +13 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt +4 -6
- package/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt +26 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt +31 -4
- package/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt +5 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardToolbarGroupReactViewGroup.kt +13 -0
- package/android/src/main/jni/RNKC.h +2 -0
- package/android/src/paper/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +8 -0
- package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +9 -0
- package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardToolbarGroupViewManager.kt +15 -0
- package/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +2 -0
- package/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewComponentDescriptor.h +27 -0
- package/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewShadowNode.cpp +14 -0
- package/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewShadowNode.h +30 -0
- package/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewState.h +28 -0
- package/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewComponentDescriptor.h +20 -0
- package/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.cpp +7 -0
- package/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewShadowNode.h +23 -0
- package/common/cpp/react/renderer/components/RNKC/RNKCKeyboardToolbarGroupViewState.h +21 -0
- package/ios/KeyboardControllerModule.mm +36 -0
- package/ios/extensions/UIApplication.swift +1 -1
- package/ios/observers/FocusedInputObserver.swift +2 -1
- package/ios/traversal/ViewHierarchyNavigator.swift +49 -3
- package/ios/views/ClippingScrollViewDecoratorViewManager.h +26 -0
- package/ios/views/ClippingScrollViewDecoratorViewManager.mm +231 -0
- package/ios/views/KeyboardToolbarGroupViewManager.h +28 -0
- package/ios/views/KeyboardToolbarGroupViewManager.mm +88 -0
- package/jest/index.js +11 -1
- package/lib/commonjs/bindings.js +18 -2
- package/lib/commonjs/bindings.js.map +1 -1
- package/lib/commonjs/bindings.native.js +3 -4
- package/lib/commonjs/bindings.native.js.map +1 -1
- package/lib/commonjs/components/KeyboardAvoidingView/index.js +30 -4
- package/lib/commonjs/components/KeyboardAvoidingView/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js +17 -4
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/index.js +33 -2
- package/lib/commonjs/components/KeyboardChatScrollView/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/types.js.map +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js +107 -21
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js.map +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +45 -17
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js +61 -26
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -1
- package/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js +72 -0
- package/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js.map +1 -0
- package/lib/commonjs/components/KeyboardToolbar/index.js +1 -0
- package/lib/commonjs/components/KeyboardToolbar/index.js.map +1 -1
- package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js +18 -6
- package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js.map +1 -1
- package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js +1 -2
- package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -1
- package/lib/commonjs/specs/KeyboardToolbarGroupViewNativeComponent.js +12 -0
- package/lib/commonjs/specs/KeyboardToolbarGroupViewNativeComponent.js.map +1 -0
- package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
- package/lib/commonjs/types/module.js.map +1 -1
- package/lib/commonjs/types/views.js.map +1 -1
- package/lib/module/bindings.js +17 -1
- package/lib/module/bindings.js.map +1 -1
- package/lib/module/bindings.native.js +2 -3
- package/lib/module/bindings.native.js.map +1 -1
- package/lib/module/components/KeyboardAvoidingView/index.js +29 -4
- package/lib/module/components/KeyboardAvoidingView/index.js.map +1 -1
- package/lib/module/components/KeyboardAwareScrollView/index.js +17 -4
- package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/index.js +34 -3
- package/lib/module/components/KeyboardChatScrollView/index.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/types.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js +98 -16
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +46 -18
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js +62 -27
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js +67 -0
- package/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js.map +1 -0
- package/lib/module/components/KeyboardToolbar/index.js +2 -1
- package/lib/module/components/KeyboardToolbar/index.js.map +1 -1
- package/lib/module/components/ScrollViewWithBottomPadding/index.js +19 -7
- package/lib/module/components/ScrollViewWithBottomPadding/index.js.map +1 -1
- package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js +1 -2
- package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -1
- package/lib/module/specs/KeyboardToolbarGroupViewNativeComponent.js +5 -0
- package/lib/module/specs/KeyboardToolbarGroupViewNativeComponent.js.map +1 -0
- package/lib/module/specs/NativeKeyboardController.js.map +1 -1
- package/lib/module/types/module.js.map +1 -1
- package/lib/module/types/views.js.map +1 -1
- package/lib/typescript/bindings.d.ts +12 -2
- package/lib/typescript/bindings.native.d.ts +2 -1
- package/lib/typescript/components/KeyboardAvoidingView/index.d.ts +44 -13
- package/lib/typescript/components/KeyboardChatScrollView/index.d.ts +3 -0
- package/lib/typescript/components/KeyboardChatScrollView/types.d.ts +41 -0
- package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts +63 -8
- package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/index.d.ts +1 -4
- package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts +15 -0
- package/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts +43 -0
- package/lib/typescript/components/KeyboardToolbar/index.d.ts +2 -0
- package/lib/typescript/components/ScrollViewWithBottomPadding/index.d.ts +3 -0
- package/lib/typescript/specs/ClippingScrollViewDecoratorViewNativeComponent.d.ts +1 -0
- package/lib/typescript/specs/KeyboardToolbarGroupViewNativeComponent.d.ts +6 -0
- package/lib/typescript/specs/NativeKeyboardController.d.ts +1 -0
- package/lib/typescript/types/module.d.ts +7 -0
- package/lib/typescript/types/views.d.ts +2 -0
- package/package.json +6 -2
- package/react-native.config.js +2 -0
- package/src/bindings.native.ts +4 -4
- package/src/bindings.ts +15 -1
- package/src/components/KeyboardAvoidingView/index.tsx +56 -29
- package/src/components/KeyboardAwareScrollView/index.tsx +20 -5
- package/src/components/KeyboardChatScrollView/index.tsx +47 -3
- package/src/components/KeyboardChatScrollView/types.ts +41 -0
- package/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts +126 -21
- package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts +101 -21
- package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts +116 -40
- package/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts +9 -0
- package/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts +123 -0
- package/src/components/KeyboardToolbar/index.tsx +6 -1
- package/src/components/ScrollViewWithBottomPadding/index.tsx +31 -4
- package/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts +1 -1
- package/src/specs/KeyboardToolbarGroupViewNativeComponent.ts +10 -0
- package/src/specs/NativeKeyboardController.ts +1 -0
- package/src/types/module.ts +9 -0
- package/src/types/views.ts +2 -0
- package/android/.settings/org.eclipse.buildship.core.prefs +0 -2
- package/ios/KeyboardController.xcodeproj/xcuserdata/kirylziusko.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/ios/Tests/Tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/Tests/Tests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/ios/Tests/Tests.xcodeproj/project.xcworkspace/xcuserdata/kirylziusko.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- 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 {
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
208
|
+
export const clampedScrollTarget = (
|
|
116
209
|
offsetBeforeScroll: number,
|
|
117
210
|
keyboardHeight: number,
|
|
118
211
|
contentHeight: number,
|
|
119
212
|
layoutHeight: number,
|
|
120
|
-
|
|
213
|
+
totalPaddingForMaxScroll?: number,
|
|
214
|
+
): number => {
|
|
121
215
|
"worklet";
|
|
122
216
|
|
|
123
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
-
|
|
261
|
+
-paddingForMax,
|
|
157
262
|
);
|
|
158
263
|
}
|
|
159
264
|
|
|
160
|
-
const maxScroll = Math.max(contentHeight - layoutHeight +
|
|
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 {
|
|
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
|
-
//
|
|
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 (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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
|
-
|
|
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 +
|
|
127
|
-
: scroll.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
|
-
|
|
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
|
};
|