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