react-native-keyboard-controller 1.20.6 → 1.21.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/.settings/org.eclipse.buildship.core.prefs +2 -0
- package/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +2 -0
- package/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +30 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt +1 -1
- package/android/src/main/java/com/reactnativekeyboardcontroller/managers/ClippingScrollViewDecoratorViewManagerImpl.kt +13 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt +37 -0
- package/android/src/paper/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +24 -0
- package/ios/KeyboardController.xcodeproj/xcuserdata/kirylziusko.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
- package/ios/Tests/Tests.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/Tests/Tests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- 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 +32 -0
- package/ios/animations/KeyboardAnimation.swift +2 -2
- package/ios/animations/SpringAnimation.swift +1 -1
- package/ios/animations/TimingAnimation.swift +1 -3
- package/ios/delegates/KCTextInputCompositeDelegate.swift +2 -2
- package/ios/extensions/CGFloat.swift +1 -3
- package/ios/interactive/KeyboardAreaExtender.swift +2 -1
- package/ios/observers/FocusedInputObserver.swift +4 -2
- package/ios/observers/movement/KeyboardTrackingView.swift +4 -1
- package/ios/observers/movement/observer/KeyboardMovementObserver+Interactive.swift +4 -0
- package/ios/protocols/TextInput.swift +1 -1
- package/ios/swizzling/UIResponderSwizzle.swift +2 -4
- package/ios/traversal/FocusedInputHolder.swift +3 -3
- package/ios/traversal/KeyboardView.swift +1 -1
- package/ios/traversal/ViewHierarchyNavigator.swift +2 -2
- package/ios/views/KeyboardControllerViewManager.swift +5 -5
- package/lib/commonjs/bindings.js +7 -1
- package/lib/commonjs/bindings.js.map +1 -1
- package/lib/commonjs/bindings.native.js +4 -1
- package/lib/commonjs/bindings.native.js.map +1 -1
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js +80 -26
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js +43 -0
- package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js.map +1 -0
- package/lib/commonjs/components/ScrollViewWithBottomPadding/styles.js +14 -0
- package/lib/commonjs/components/ScrollViewWithBottomPadding/styles.js.map +1 -0
- package/lib/commonjs/components/hooks/useCombinedRef.js +24 -0
- package/lib/commonjs/components/hooks/useCombinedRef.js.map +1 -0
- package/lib/commonjs/components/hooks/useScrollState.js +43 -0
- package/lib/commonjs/components/hooks/useScrollState.js.map +1 -0
- package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js +12 -0
- package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -0
- package/lib/commonjs/types/views.js.map +1 -1
- package/lib/module/bindings.js +6 -0
- package/lib/module/bindings.js.map +1 -1
- package/lib/module/bindings.native.js +3 -0
- package/lib/module/bindings.native.js.map +1 -1
- package/lib/module/components/KeyboardAwareScrollView/index.js +80 -27
- package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/module/components/ScrollViewWithBottomPadding/index.js +35 -0
- package/lib/module/components/ScrollViewWithBottomPadding/index.js.map +1 -0
- package/lib/module/components/ScrollViewWithBottomPadding/styles.js +8 -0
- package/lib/module/components/ScrollViewWithBottomPadding/styles.js.map +1 -0
- package/lib/module/components/hooks/useCombinedRef.js +18 -0
- package/lib/module/components/hooks/useCombinedRef.js.map +1 -0
- package/lib/module/components/hooks/useScrollState.js +37 -0
- package/lib/module/components/hooks/useScrollState.js.map +1 -0
- package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js +6 -0
- package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -0
- package/lib/module/types/views.js.map +1 -1
- package/lib/typescript/bindings.d.ts +7 -1
- package/lib/typescript/bindings.native.d.ts +1 -0
- package/lib/typescript/components/KeyboardAwareScrollView/index.d.ts +3 -2
- package/lib/typescript/components/ScrollViewWithBottomPadding/index.d.ts +12 -0
- package/lib/typescript/components/ScrollViewWithBottomPadding/styles.d.ts +6 -0
- package/lib/typescript/components/hooks/useCombinedRef.d.ts +3 -0
- package/lib/typescript/components/hooks/useScrollState.d.ts +14 -0
- package/lib/typescript/specs/ClippingScrollViewDecoratorViewNativeComponent.d.ts +8 -0
- package/lib/typescript/types/views.d.ts +4 -0
- package/package.json +4 -3
- package/src/bindings.native.ts +5 -0
- package/src/bindings.ts +8 -0
- package/src/components/KeyboardAwareScrollView/index.tsx +113 -33
- package/src/components/ScrollViewWithBottomPadding/index.tsx +72 -0
- package/src/components/ScrollViewWithBottomPadding/styles.ts +9 -0
- package/src/components/hooks/useCombinedRef.ts +22 -0
- package/src/components/hooks/useScrollState.ts +59 -0
- package/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts +17 -0
- package/src/types/views.ts +6 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnimatedRef } from "react-native-reanimated";
|
|
2
|
+
import type Reanimated from "react-native-reanimated";
|
|
3
|
+
declare const useScrollState: (ref: AnimatedRef<Reanimated.ScrollView>) => {
|
|
4
|
+
offset: import("react-native-reanimated").SharedValue<number>;
|
|
5
|
+
layout: import("react-native-reanimated").SharedValue<{
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}>;
|
|
9
|
+
size: import("react-native-reanimated").SharedValue<{
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
export default useScrollState;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HostComponent } from "react-native";
|
|
2
|
+
import type { ViewProps } from "react-native";
|
|
3
|
+
import type { Double } from "react-native/Libraries/Types/CodegenTypes";
|
|
4
|
+
export interface NativeProps extends ViewProps {
|
|
5
|
+
contentInsetBottom: Double;
|
|
6
|
+
}
|
|
7
|
+
declare const _default: HostComponent<NativeProps>;
|
|
8
|
+
export default _default;
|
|
@@ -48,3 +48,7 @@ export type KeyboardExtenderProps = PropsWithChildren<{
|
|
|
48
48
|
/** Controls whether this `KeyboardExtender` instance should take an effect. Default is `true`. */
|
|
49
49
|
enabled?: boolean;
|
|
50
50
|
}>;
|
|
51
|
+
export type ClippingScrollViewProps = PropsWithChildren<ViewProps & {
|
|
52
|
+
/** An additional space that gets applied to the bottom of the `ScrollView` (inside a scrollable content). Default is `0`. */
|
|
53
|
+
contentInsetBottom?: number;
|
|
54
|
+
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-keyboard-controller",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0-beta.0",
|
|
4
4
|
"description": "Keyboard manager which works in identical way on both iOS and Android",
|
|
5
5
|
"main": "lib/commonjs/index",
|
|
6
6
|
"module": "lib/module/index",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"typescript": "tsc --noEmit --project tsconfig.build.json",
|
|
42
42
|
"lint": "eslint --quiet \"**/*.{js,ts,tsx}\"",
|
|
43
43
|
"lint-clang": "find ios/ -iname *.h -o -iname *.m -o -iname *.mm | grep -v -e Pods -e build | xargs clang-format -i -n --Werror",
|
|
44
|
-
"prepare": "bob build
|
|
44
|
+
"prepare": "bob build",
|
|
45
45
|
"release": "release-it",
|
|
46
46
|
"example": "yarn --cwd example",
|
|
47
47
|
"pods": "cd example && pod-install --quiet",
|
|
@@ -187,5 +187,6 @@
|
|
|
187
187
|
"KeyboardExtender": "KeyboardExtender"
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
}
|
|
190
|
+
},
|
|
191
|
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
191
192
|
}
|
package/src/bindings.native.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NativeEventEmitter, Platform } from "react-native";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
+
ClippingScrollViewProps,
|
|
4
5
|
FocusedInputEventsModule,
|
|
5
6
|
KeyboardBackgroundViewProps,
|
|
6
7
|
KeyboardControllerNativeModule,
|
|
@@ -71,3 +72,7 @@ export const RCTKeyboardExtender: React.FC<KeyboardExtenderProps> =
|
|
|
71
72
|
Platform.OS === "ios"
|
|
72
73
|
? require("./specs/KeyboardExtenderNativeComponent").default
|
|
73
74
|
: ({ children }: KeyboardExtenderProps) => children;
|
|
75
|
+
export const ClippingScrollView: React.FC<KeyboardBackgroundViewProps> =
|
|
76
|
+
Platform.OS === "android"
|
|
77
|
+
? require("./specs/ClippingScrollViewDecoratorViewNativeComponent").default
|
|
78
|
+
: ({ children }: ClippingScrollViewProps) => children;
|
package/src/bindings.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { View } from "react-native";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
+
ClippingScrollViewProps,
|
|
4
5
|
FocusedInputEventsModule,
|
|
5
6
|
KeyboardBackgroundViewProps,
|
|
6
7
|
KeyboardControllerNativeModule,
|
|
@@ -82,3 +83,10 @@ export const KeyboardBackgroundView =
|
|
|
82
83
|
*/
|
|
83
84
|
export const RCTKeyboardExtender =
|
|
84
85
|
View as unknown as React.FC<KeyboardExtenderProps>;
|
|
86
|
+
/**
|
|
87
|
+
* A decorator that will clip the content of the `ScrollView`. It helps to simulate `contentInset` behavior on Android.
|
|
88
|
+
* Supports only `bottom` property (`paddingBottom` is not supported property of `ScrollView.style`).
|
|
89
|
+
* Using this component we can modify bottom inset without having a fake view.
|
|
90
|
+
*/
|
|
91
|
+
export const ClippingScrollView =
|
|
92
|
+
View as unknown as React.FC<ClippingScrollViewProps>;
|
|
@@ -12,8 +12,7 @@ import Reanimated, {
|
|
|
12
12
|
scrollTo,
|
|
13
13
|
useAnimatedReaction,
|
|
14
14
|
useAnimatedRef,
|
|
15
|
-
|
|
16
|
-
useScrollViewOffset,
|
|
15
|
+
useDerivedValue,
|
|
17
16
|
useSharedValue,
|
|
18
17
|
} from "react-native-reanimated";
|
|
19
18
|
|
|
@@ -23,10 +22,14 @@ import {
|
|
|
23
22
|
useWindowDimensions,
|
|
24
23
|
} from "../../hooks";
|
|
25
24
|
import { findNodeHandle } from "../../utils/findNodeHandle";
|
|
25
|
+
import useCombinedRef from "../hooks/useCombinedRef";
|
|
26
|
+
import useScrollState from "../hooks/useScrollState";
|
|
27
|
+
import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding";
|
|
26
28
|
|
|
27
29
|
import { useSmoothKeyboardHandler } from "./useSmoothKeyboardHandler";
|
|
28
30
|
import { debounce, scrollDistanceWithRespectToSnapPoints } from "./utils";
|
|
29
31
|
|
|
32
|
+
import type { AnimatedScrollViewComponent } from "../ScrollViewWithBottomPadding";
|
|
30
33
|
import type {
|
|
31
34
|
LayoutChangeEvent,
|
|
32
35
|
ScrollView,
|
|
@@ -48,7 +51,7 @@ export type KeyboardAwareScrollViewProps = {
|
|
|
48
51
|
/** Adjusting the bottom spacing of KeyboardAwareScrollView. Default is `0`. */
|
|
49
52
|
extraKeyboardSpace?: number;
|
|
50
53
|
/** Custom component for `ScrollView`. Default is `ScrollView`. */
|
|
51
|
-
ScrollViewComponent?:
|
|
54
|
+
ScrollViewComponent?: AnimatedScrollViewComponent;
|
|
52
55
|
} & ScrollViewProps;
|
|
53
56
|
export type KeyboardAwareScrollViewRef = {
|
|
54
57
|
assureFocusedInputVisible: () => void;
|
|
@@ -127,9 +130,14 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
127
130
|
) => {
|
|
128
131
|
const scrollViewAnimatedRef = useAnimatedRef<Reanimated.ScrollView>();
|
|
129
132
|
const scrollViewRef = React.useRef<ScrollView>(null);
|
|
133
|
+
const onRef = useCombinedRef(scrollViewAnimatedRef, scrollViewRef);
|
|
130
134
|
const scrollViewTarget = useSharedValue<number | null>(null);
|
|
131
135
|
const scrollPosition = useSharedValue(0);
|
|
132
|
-
const
|
|
136
|
+
const {
|
|
137
|
+
offset: position,
|
|
138
|
+
layout: scrollViewLayout,
|
|
139
|
+
size: scrollViewContentSize,
|
|
140
|
+
} = useScrollState(scrollViewAnimatedRef);
|
|
133
141
|
const currentKeyboardFrameHeight = useSharedValue(0);
|
|
134
142
|
const keyboardHeight = useSharedValue(0);
|
|
135
143
|
const keyboardWillAppear = useSharedValue(false);
|
|
@@ -140,14 +148,11 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
140
148
|
const layout = useSharedValue<FocusedInputLayoutChangedEvent | null>(null);
|
|
141
149
|
const lastSelection =
|
|
142
150
|
useSharedValue<FocusedInputSelectionChangedEvent | null>(null);
|
|
151
|
+
const ghostViewSpace = useSharedValue(-1);
|
|
152
|
+
const pendingSelectionForFocus = useSharedValue(false);
|
|
143
153
|
|
|
144
154
|
const { height } = useWindowDimensions();
|
|
145
155
|
|
|
146
|
-
const onRef = useCallback((assignedRef: Reanimated.ScrollView) => {
|
|
147
|
-
scrollViewRef.current = assignedRef;
|
|
148
|
-
|
|
149
|
-
scrollViewAnimatedRef(assignedRef);
|
|
150
|
-
}, []);
|
|
151
156
|
const onScrollViewLayout = useCallback(
|
|
152
157
|
(e: LayoutChangeEvent) => {
|
|
153
158
|
scrollViewTarget.value = findNodeHandle(scrollViewAnimatedRef.current);
|
|
@@ -216,6 +221,30 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
216
221
|
},
|
|
217
222
|
[bottomOffset, enabled, height, snapToOffsets],
|
|
218
223
|
);
|
|
224
|
+
const removeGhostPadding = useCallback((e: number) => {
|
|
225
|
+
"worklet";
|
|
226
|
+
|
|
227
|
+
// new `ScrollViewWithBottomPadding` behavior: if we hide keyboard and we are in the end of `ScrollView`
|
|
228
|
+
// then we always need to scroll back, because we apply a padding that doesn't change layout, so we will
|
|
229
|
+
// not have auto scroll back in this case
|
|
230
|
+
if (!keyboardWillAppear.value && ghostViewSpace.value > 0) {
|
|
231
|
+
scrollTo(
|
|
232
|
+
scrollViewAnimatedRef,
|
|
233
|
+
0,
|
|
234
|
+
scrollPosition.value -
|
|
235
|
+
interpolate(
|
|
236
|
+
e,
|
|
237
|
+
[initialKeyboardSize.value, keyboardHeight.value],
|
|
238
|
+
[ghostViewSpace.value, 0],
|
|
239
|
+
),
|
|
240
|
+
false,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return false;
|
|
247
|
+
}, []);
|
|
219
248
|
const performScrollWithPositionRestoration = useCallback(
|
|
220
249
|
(newPosition: number) => {
|
|
221
250
|
"worklet";
|
|
@@ -296,7 +325,18 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
296
325
|
lastSelection.value = e;
|
|
297
326
|
|
|
298
327
|
if (e.target !== lastTarget) {
|
|
299
|
-
|
|
328
|
+
if (pendingSelectionForFocus.value) {
|
|
329
|
+
// selection arrived after onStart - complete the deferred setup
|
|
330
|
+
pendingSelectionForFocus.value = false;
|
|
331
|
+
updateLayoutFromSelection();
|
|
332
|
+
|
|
333
|
+
// if keyboard was already visible (focus change, no onMove expected),
|
|
334
|
+
// perform the deferred scroll now
|
|
335
|
+
if (!keyboardWillAppear.value && keyboardHeight.value > 0) {
|
|
336
|
+
position.value += maybeScroll(keyboardHeight.value, true);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
300
340
|
return;
|
|
301
341
|
}
|
|
302
342
|
// caret in the end + end coordinates has been changed -> we moved to a new line
|
|
@@ -314,7 +354,12 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
314
354
|
|
|
315
355
|
onChangeTextHandler();
|
|
316
356
|
},
|
|
317
|
-
[
|
|
357
|
+
[
|
|
358
|
+
scrollFromCurrentPosition,
|
|
359
|
+
onChangeTextHandler,
|
|
360
|
+
updateLayoutFromSelection,
|
|
361
|
+
maybeScroll,
|
|
362
|
+
],
|
|
318
363
|
);
|
|
319
364
|
|
|
320
365
|
useFocusedInputHandler(
|
|
@@ -347,6 +392,7 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
347
392
|
// on back transition need to interpolate as [0, keyboardHeight]
|
|
348
393
|
initialKeyboardSize.value = 0;
|
|
349
394
|
scrollPosition.value = scrollBeforeKeyboardMovement.value;
|
|
395
|
+
pendingSelectionForFocus.value = false;
|
|
350
396
|
}
|
|
351
397
|
|
|
352
398
|
if (
|
|
@@ -363,17 +409,40 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
363
409
|
// focus was changed
|
|
364
410
|
if (focusWasChanged) {
|
|
365
411
|
tag.value = e.target;
|
|
366
|
-
|
|
367
|
-
|
|
412
|
+
|
|
413
|
+
if (lastSelection.value?.target === e.target) {
|
|
414
|
+
// selection arrived before onStart - use it to update layout
|
|
415
|
+
updateLayoutFromSelection();
|
|
416
|
+
pendingSelectionForFocus.value = false;
|
|
417
|
+
} else {
|
|
418
|
+
// selection hasn't arrived yet for the new target.
|
|
419
|
+
// use input layout as-is; will be refined when selection arrives.
|
|
420
|
+
if (input.value) {
|
|
421
|
+
layout.value = input.value;
|
|
422
|
+
}
|
|
423
|
+
pendingSelectionForFocus.value = true;
|
|
424
|
+
}
|
|
425
|
+
|
|
368
426
|
// save current scroll position - when keyboard will hide we'll reuse
|
|
369
427
|
// this value to achieve smooth hide effect
|
|
370
428
|
scrollBeforeKeyboardMovement.value = position.value;
|
|
371
429
|
}
|
|
372
430
|
|
|
373
431
|
if (focusWasChanged && !keyboardWillAppear.value) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
432
|
+
if (!pendingSelectionForFocus.value) {
|
|
433
|
+
// update position on scroll value, so `onEnd` handler
|
|
434
|
+
// will pick up correct values
|
|
435
|
+
position.value += maybeScroll(e.height, true);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
ghostViewSpace.value =
|
|
440
|
+
position.value +
|
|
441
|
+
scrollViewLayout.value.height -
|
|
442
|
+
scrollViewContentSize.value.height;
|
|
443
|
+
|
|
444
|
+
if (ghostViewSpace.value > 0) {
|
|
445
|
+
scrollPosition.value = position.value;
|
|
377
446
|
}
|
|
378
447
|
},
|
|
379
448
|
onMove: (e) => {
|
|
@@ -381,6 +450,10 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
381
450
|
|
|
382
451
|
syncKeyboardFrame(e);
|
|
383
452
|
|
|
453
|
+
if (removeGhostPadding(e.height)) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
384
457
|
// if the user has set disableScrollOnKeyboardHide, only auto-scroll when the keyboard opens
|
|
385
458
|
if (!disableScrollOnKeyboardHide || keyboardWillAppear.value) {
|
|
386
459
|
maybeScroll(e.height);
|
|
@@ -389,13 +462,24 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
389
462
|
onEnd: (e) => {
|
|
390
463
|
"worklet";
|
|
391
464
|
|
|
465
|
+
removeGhostPadding(e.height);
|
|
466
|
+
|
|
392
467
|
keyboardHeight.value = e.height;
|
|
393
468
|
scrollPosition.value = position.value;
|
|
394
469
|
|
|
470
|
+
if (e.height === 0) {
|
|
471
|
+
lastSelection.value = null;
|
|
472
|
+
}
|
|
473
|
+
|
|
395
474
|
syncKeyboardFrame(e);
|
|
396
475
|
},
|
|
397
476
|
},
|
|
398
|
-
[
|
|
477
|
+
[
|
|
478
|
+
maybeScroll,
|
|
479
|
+
removeGhostPadding,
|
|
480
|
+
disableScrollOnKeyboardHide,
|
|
481
|
+
syncKeyboardFrame,
|
|
482
|
+
],
|
|
399
483
|
);
|
|
400
484
|
|
|
401
485
|
const synchronize = useCallback(async () => {
|
|
@@ -446,32 +530,28 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
446
530
|
[],
|
|
447
531
|
);
|
|
448
532
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
// re-calculation on every animation frame and it helps to achieve smooth animation.
|
|
458
|
-
// see: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/342
|
|
459
|
-
paddingBottom: currentKeyboardFrameHeight.value + 1,
|
|
460
|
-
}
|
|
461
|
-
: {},
|
|
533
|
+
// animations become choppy when scrolling to the end of the `ScrollView` (when the last input is focused)
|
|
534
|
+
// this happens because the layout recalculates on every frame. To avoid this we slightly increase padding
|
|
535
|
+
// by `+1`. In this way we assure, that `scrollTo` will never scroll to the end, because it uses interpolation
|
|
536
|
+
// from 0 to `keyboardHeight`, and here our padding is `keyboardHeight + 1`. It allows us not to re-run layout
|
|
537
|
+
// re-calculation on every animation frame and it helps to achieve smooth animation.
|
|
538
|
+
// see: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/342
|
|
539
|
+
const padding = useDerivedValue(
|
|
540
|
+
() => (enabled ? currentKeyboardFrameHeight.value + 1 : 0),
|
|
462
541
|
[enabled],
|
|
463
542
|
);
|
|
464
543
|
|
|
465
544
|
return (
|
|
466
|
-
<
|
|
545
|
+
<ScrollViewWithBottomPadding
|
|
467
546
|
ref={onRef}
|
|
468
547
|
{...rest}
|
|
548
|
+
bottomPadding={padding}
|
|
469
549
|
scrollEventThrottle={16}
|
|
550
|
+
ScrollViewComponent={ScrollViewComponent}
|
|
470
551
|
onLayout={onScrollViewLayout}
|
|
471
552
|
>
|
|
472
553
|
{children}
|
|
473
|
-
|
|
474
|
-
</ScrollViewComponent>
|
|
554
|
+
</ScrollViewWithBottomPadding>
|
|
475
555
|
);
|
|
476
556
|
},
|
|
477
557
|
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { forwardRef } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
import Reanimated, { useAnimatedProps } from "react-native-reanimated";
|
|
4
|
+
|
|
5
|
+
import { ClippingScrollView } from "../../bindings";
|
|
6
|
+
|
|
7
|
+
import styles from "./styles";
|
|
8
|
+
|
|
9
|
+
import type { ScrollViewProps } from "react-native";
|
|
10
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
11
|
+
|
|
12
|
+
const ReanimatedClippingScrollView =
|
|
13
|
+
Platform.OS === "android"
|
|
14
|
+
? Reanimated.createAnimatedComponent(ClippingScrollView)
|
|
15
|
+
: ClippingScrollView;
|
|
16
|
+
|
|
17
|
+
type AnimatedScrollViewProps = React.ComponentProps<
|
|
18
|
+
typeof Reanimated.ScrollView
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
export type AnimatedScrollViewComponent = React.ForwardRefExoticComponent<
|
|
22
|
+
AnimatedScrollViewProps & React.RefAttributes<Reanimated.ScrollView>
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
type ScrollViewWithBottomPaddingProps = {
|
|
26
|
+
ScrollViewComponent: AnimatedScrollViewComponent;
|
|
27
|
+
children?: React.ReactNode;
|
|
28
|
+
bottomPadding: SharedValue<number>;
|
|
29
|
+
} & ScrollViewProps;
|
|
30
|
+
|
|
31
|
+
const ScrollViewWithBottomPadding = forwardRef<
|
|
32
|
+
Reanimated.ScrollView,
|
|
33
|
+
ScrollViewWithBottomPaddingProps
|
|
34
|
+
>(
|
|
35
|
+
(
|
|
36
|
+
{ ScrollViewComponent, bottomPadding, contentInset, children, ...rest },
|
|
37
|
+
ref,
|
|
38
|
+
) => {
|
|
39
|
+
const animatedProps = useAnimatedProps(
|
|
40
|
+
() => ({
|
|
41
|
+
// iOS prop
|
|
42
|
+
contentInset: {
|
|
43
|
+
bottom: bottomPadding.value + (contentInset?.bottom || 0),
|
|
44
|
+
top: contentInset?.top,
|
|
45
|
+
right: contentInset?.right,
|
|
46
|
+
left: contentInset?.left,
|
|
47
|
+
},
|
|
48
|
+
// Android prop
|
|
49
|
+
contentInsetBottom: bottomPadding.value,
|
|
50
|
+
}),
|
|
51
|
+
[
|
|
52
|
+
contentInset?.bottom,
|
|
53
|
+
contentInset?.top,
|
|
54
|
+
contentInset?.right,
|
|
55
|
+
contentInset?.left,
|
|
56
|
+
],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<ReanimatedClippingScrollView
|
|
61
|
+
animatedProps={animatedProps}
|
|
62
|
+
style={styles.container}
|
|
63
|
+
>
|
|
64
|
+
<ScrollViewComponent ref={ref} animatedProps={animatedProps} {...rest}>
|
|
65
|
+
{children}
|
|
66
|
+
</ScrollViewComponent>
|
|
67
|
+
</ReanimatedClippingScrollView>
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
export default ScrollViewWithBottomPadding;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
import type { Ref, RefCallback } from "react";
|
|
4
|
+
|
|
5
|
+
const useCombinedRef = <T>(...refs: Ref<T>[]): RefCallback<T> => {
|
|
6
|
+
return useCallback((value: T | null) => {
|
|
7
|
+
for (const ref of refs) {
|
|
8
|
+
if (!ref) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof ref === "function") {
|
|
13
|
+
ref(value);
|
|
14
|
+
} else {
|
|
15
|
+
ref.current = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// eslint-disable-next-line react-compiler/react-compiler
|
|
19
|
+
}, refs);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default useCombinedRef;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useEvent, useSharedValue } from "react-native-reanimated";
|
|
3
|
+
|
|
4
|
+
import { useEventHandlerRegistration } from "../../internal";
|
|
5
|
+
|
|
6
|
+
import type { AnimatedRef } from "react-native-reanimated";
|
|
7
|
+
import type Reanimated from "react-native-reanimated";
|
|
8
|
+
|
|
9
|
+
const NATIVE_SCROLL_EVENT_NAMES = [
|
|
10
|
+
"onScroll",
|
|
11
|
+
"onScrollBeginDrag",
|
|
12
|
+
"onScrollEndDrag",
|
|
13
|
+
"onMomentumScrollBegin",
|
|
14
|
+
"onMomentumScrollEnd",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
type ScrollEvent = {
|
|
18
|
+
contentOffset: {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
};
|
|
22
|
+
layoutMeasurement: {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
26
|
+
contentSize: {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const useScrollState = (ref: AnimatedRef<Reanimated.ScrollView>) => {
|
|
33
|
+
const offset = useSharedValue(0);
|
|
34
|
+
const layout = useSharedValue({ width: 0, height: 0 });
|
|
35
|
+
const size = useSharedValue({ width: 0, height: 0 });
|
|
36
|
+
|
|
37
|
+
const register = useEventHandlerRegistration(ref);
|
|
38
|
+
|
|
39
|
+
const eventHandler = useEvent((event: ScrollEvent) => {
|
|
40
|
+
"worklet";
|
|
41
|
+
|
|
42
|
+
// eslint-disable-next-line react-compiler/react-compiler
|
|
43
|
+
offset.value = event.contentOffset.y;
|
|
44
|
+
layout.value = event.layoutMeasurement;
|
|
45
|
+
size.value = event.contentSize;
|
|
46
|
+
}, NATIVE_SCROLL_EVENT_NAMES);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const cleanup = register(eventHandler);
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
cleanup();
|
|
53
|
+
};
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
return { offset, layout, size };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default useScrollState;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { codegenNativeComponent } from "react-native";
|
|
2
|
+
|
|
3
|
+
import type { HostComponent } from "react-native";
|
|
4
|
+
import type { ViewProps } from "react-native";
|
|
5
|
+
import type { Double } from "react-native/Libraries/Types/CodegenTypes";
|
|
6
|
+
|
|
7
|
+
export interface NativeProps extends ViewProps {
|
|
8
|
+
contentInsetBottom: Double;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default codegenNativeComponent<NativeProps>(
|
|
12
|
+
"ClippingScrollViewDecoratorView",
|
|
13
|
+
{
|
|
14
|
+
interfaceOnly: true,
|
|
15
|
+
excludedPlatforms: ["iOS"],
|
|
16
|
+
},
|
|
17
|
+
) as HostComponent<NativeProps>;
|
package/src/types/views.ts
CHANGED
|
@@ -49,3 +49,9 @@ 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 ClippingScrollViewProps = PropsWithChildren<
|
|
53
|
+
ViewProps & {
|
|
54
|
+
/** An additional space that gets applied to the bottom of the `ScrollView` (inside a scrollable content). Default is `0`. */
|
|
55
|
+
contentInsetBottom?: number;
|
|
56
|
+
}
|
|
57
|
+
>;
|