react-native-keyboard-controller 1.21.0-beta.2 → 1.21.0-beta.3
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/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +7 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt +28 -1
- package/android/src/paper/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +8 -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 +2 -0
- package/ios/observers/movement/observer/KeyboardMovementObserver+Listeners.swift +30 -7
- package/ios/observers/movement/observer/KeyboardMovementObserver.swift +2 -48
- 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/jest/index.js +1 -3
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js +43 -11
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/hooks.js.map +1 -1
- package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/index.js +37 -12
- package/lib/commonjs/components/KeyboardChatScrollView/index.js.map +1 -0
- package/lib/commonjs/components/KeyboardChatScrollView/types.js.map +1 -0
- package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/useChatKeyboard/helpers.js +35 -1
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js.map +1 -0
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +121 -0
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -0
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js +205 -0
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -0
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/types.js +6 -0
- package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -0
- package/lib/commonjs/components/KeyboardStickyView/index.js +3 -1
- package/lib/commonjs/components/KeyboardStickyView/index.js.map +1 -1
- package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js +18 -9
- package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js.map +1 -1
- package/lib/commonjs/components/hooks/useScrollState.js +20 -1
- package/lib/commonjs/components/hooks/useScrollState.js.map +1 -1
- package/lib/commonjs/components/index.js +7 -7
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/index.js +7 -7
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -1
- package/lib/commonjs/types/views.js.map +1 -1
- package/lib/module/components/KeyboardAwareScrollView/index.js +43 -11
- package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/module/components/{ChatKit → KeyboardChatScrollView}/hooks.js.map +1 -1
- package/lib/module/components/KeyboardChatScrollView/index.js +73 -0
- package/lib/module/components/KeyboardChatScrollView/index.js.map +1 -0
- package/lib/module/components/KeyboardChatScrollView/types.js.map +1 -0
- package/lib/module/components/{ChatKit → KeyboardChatScrollView}/useChatKeyboard/helpers.js +34 -1
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js.map +1 -0
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +115 -0
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -0
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js +199 -0
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -0
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/types.js +2 -0
- package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -0
- package/lib/module/components/KeyboardStickyView/index.js +3 -1
- package/lib/module/components/KeyboardStickyView/index.js.map +1 -1
- package/lib/module/components/ScrollViewWithBottomPadding/index.js +19 -10
- package/lib/module/components/ScrollViewWithBottomPadding/index.js.map +1 -1
- package/lib/module/components/hooks/useScrollState.js +21 -2
- package/lib/module/components/hooks/useScrollState.js.map +1 -1
- package/lib/module/components/index.js +1 -1
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -1
- package/lib/module/types/views.js.map +1 -1
- package/lib/typescript/components/KeyboardChatScrollView/hooks.d.ts +2 -0
- package/lib/typescript/components/KeyboardChatScrollView/index.d.ts +12 -0
- package/lib/typescript/components/{ChatKit → KeyboardChatScrollView}/types.d.ts +5 -6
- package/lib/typescript/components/{ChatKit → KeyboardChatScrollView}/useChatKeyboard/helpers.d.ts +23 -3
- package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/index.d.ts +21 -0
- package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/index.ios.d.ts +21 -0
- package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts +22 -0
- package/lib/typescript/components/ScrollViewWithBottomPadding/index.d.ts +2 -4
- package/lib/typescript/components/hooks/useScrollState.d.ts +3 -0
- package/lib/typescript/components/index.d.ts +2 -2
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/specs/ClippingScrollViewDecoratorViewNativeComponent.d.ts +1 -0
- package/lib/typescript/types/views.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/KeyboardAwareScrollView/index.tsx +53 -10
- package/src/components/KeyboardChatScrollView/hooks.ts +2 -0
- package/src/components/KeyboardChatScrollView/index.tsx +106 -0
- package/src/components/{ChatKit → KeyboardChatScrollView}/types.ts +5 -6
- package/src/components/{ChatKit → KeyboardChatScrollView}/useChatKeyboard/helpers.ts +46 -1
- package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts +180 -0
- package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts +300 -0
- package/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts +30 -0
- package/src/components/KeyboardStickyView/index.tsx +4 -3
- package/src/components/ScrollViewWithBottomPadding/index.tsx +22 -25
- package/src/components/hooks/useScrollState.ts +24 -2
- package/src/components/index.ts +2 -2
- package/src/index.ts +2 -2
- package/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts +1 -0
- package/src/types/views.ts +2 -0
- package/lib/commonjs/components/ChatKit/TODO.md +0 -20
- package/lib/commonjs/components/ChatKit/index.js.map +0 -1
- package/lib/commonjs/components/ChatKit/types.js.map +0 -1
- package/lib/commonjs/components/ChatKit/useChatKeyboard/helpers.js.map +0 -1
- package/lib/commonjs/components/ChatKit/useChatKeyboard/index.js +0 -147
- package/lib/commonjs/components/ChatKit/useChatKeyboard/index.js.map +0 -1
- package/lib/module/components/ChatKit/TODO.md +0 -20
- package/lib/module/components/ChatKit/index.js +0 -48
- package/lib/module/components/ChatKit/index.js.map +0 -1
- package/lib/module/components/ChatKit/types.js.map +0 -1
- package/lib/module/components/ChatKit/useChatKeyboard/helpers.js.map +0 -1
- package/lib/module/components/ChatKit/useChatKeyboard/index.js +0 -141
- package/lib/module/components/ChatKit/useChatKeyboard/index.js.map +0 -1
- package/lib/typescript/components/ChatKit/hooks.d.ts +0 -2
- package/lib/typescript/components/ChatKit/index.d.ts +0 -14
- package/lib/typescript/components/ChatKit/useChatKeyboard/index.d.ts +0 -36
- package/src/components/ChatKit/TODO.md +0 -20
- package/src/components/ChatKit/hooks.ts +0 -2
- package/src/components/ChatKit/index.tsx +0 -63
- package/src/components/ChatKit/useChatKeyboard/index.ts +0 -228
- /package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/hooks.js +0 -0
- /package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/types.js +0 -0
- /package/lib/module/components/{ChatKit → KeyboardChatScrollView}/hooks.js +0 -0
- /package/lib/module/components/{ChatKit → KeyboardChatScrollView}/types.js +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { UseChatKeyboardOptions, UseChatKeyboardReturn } from "./types";
|
|
2
|
+
import type { AnimatedRef } from "react-native-reanimated";
|
|
3
|
+
import type Reanimated from "react-native-reanimated";
|
|
4
|
+
/**
|
|
5
|
+
* Hook that manages keyboard-driven scrolling for chat-style scroll views.
|
|
6
|
+
* Calculates padding (extra scrollable space) and content shift values,
|
|
7
|
+
* using iOS-specific strategy (contentOffset set once in onStart).
|
|
8
|
+
*
|
|
9
|
+
* @param scrollViewRef - Animated ref to the scroll view.
|
|
10
|
+
* @param options - Configuration for inverted and keyboardLiftBehavior.
|
|
11
|
+
* @returns Shared values for padding and contentOffsetY.
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const { padding, contentOffsetY } = useChatKeyboard(ref, {
|
|
15
|
+
* inverted: false,
|
|
16
|
+
* keyboardLiftBehavior: "always",
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare function useChatKeyboard(scrollViewRef: AnimatedRef<Reanimated.ScrollView>, options: UseChatKeyboardOptions): UseChatKeyboardReturn;
|
|
21
|
+
export { useChatKeyboard };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LayoutChangeEvent } from "react-native";
|
|
2
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
3
|
+
type KeyboardLiftBehavior = "always" | "whenAtEnd" | "persistent" | "never";
|
|
4
|
+
type UseChatKeyboardOptions = {
|
|
5
|
+
inverted: boolean;
|
|
6
|
+
keyboardLiftBehavior: KeyboardLiftBehavior;
|
|
7
|
+
freeze: boolean;
|
|
8
|
+
offset: number;
|
|
9
|
+
};
|
|
10
|
+
type UseChatKeyboardReturn = {
|
|
11
|
+
/** Extra scrollable space (= keyboard height). Used as contentInset on iOS, contentInsetBottom/contentInsetTop on Android. */
|
|
12
|
+
padding: SharedValue<number>;
|
|
13
|
+
/** Raw keyboard height updated every frame in onMove. Used to force Reanimated commits on Fabric. */
|
|
14
|
+
currentHeight: SharedValue<number>;
|
|
15
|
+
/** Absolute Y content offset for iOS (set once in onStart). `undefined` on Android. */
|
|
16
|
+
contentOffsetY: SharedValue<number> | undefined;
|
|
17
|
+
/** Callback to attach to ScrollView's onLayout prop to capture initial viewport dimensions. */
|
|
18
|
+
onLayout: (e: LayoutChangeEvent) => void;
|
|
19
|
+
/** Callback to attach to ScrollView's onContentSizeChange prop to capture initial content dimensions. */
|
|
20
|
+
onContentSizeChange: (w: number, h: number) => void;
|
|
21
|
+
};
|
|
22
|
+
export type { KeyboardLiftBehavior, UseChatKeyboardOptions, UseChatKeyboardReturn, };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import Reanimated from "react-native-reanimated";
|
|
3
|
-
import type { ScrollViewProps
|
|
3
|
+
import type { ScrollViewProps } from "react-native";
|
|
4
4
|
import type { SharedValue } from "react-native-reanimated";
|
|
5
5
|
type AnimatedScrollViewProps = React.ComponentProps<typeof Reanimated.ScrollView>;
|
|
6
6
|
export type AnimatedScrollViewComponent = React.ForwardRefExoticComponent<AnimatedScrollViewProps & React.RefAttributes<Reanimated.ScrollView>>;
|
|
@@ -9,9 +9,7 @@ declare const ScrollViewWithBottomPadding: React.ForwardRefExoticComponent<{
|
|
|
9
9
|
children?: React.ReactNode;
|
|
10
10
|
inverted?: boolean;
|
|
11
11
|
bottomPadding: SharedValue<number>;
|
|
12
|
-
/** Absolute Y content offset (iOS only, for
|
|
12
|
+
/** Absolute Y content offset (iOS only, for KeyboardChatScrollView). */
|
|
13
13
|
contentOffsetY?: SharedValue<number>;
|
|
14
|
-
/** Style applied to the container wrapper (Android only, for ChatKit translateY). */
|
|
15
|
-
containerStyle?: StyleProp<ViewStyle>;
|
|
16
14
|
} & ScrollViewProps & React.RefAttributes<Reanimated.ScrollView>>;
|
|
17
15
|
export default ScrollViewWithBottomPadding;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LayoutChangeEvent } from "react-native";
|
|
1
2
|
import type { AnimatedRef } from "react-native-reanimated";
|
|
2
3
|
import type Reanimated from "react-native-reanimated";
|
|
3
4
|
declare const useScrollState: (ref: AnimatedRef<Reanimated.ScrollView>) => {
|
|
@@ -10,5 +11,7 @@ declare const useScrollState: (ref: AnimatedRef<Reanimated.ScrollView>) => {
|
|
|
10
11
|
width: number;
|
|
11
12
|
height: number;
|
|
12
13
|
}>;
|
|
14
|
+
onLayout: (e: LayoutChangeEvent) => void;
|
|
15
|
+
onContentSizeChange: (w: number, h: number) => void;
|
|
13
16
|
};
|
|
14
17
|
export default useScrollState;
|
|
@@ -2,9 +2,9 @@ export { default as KeyboardAvoidingView } from "./KeyboardAvoidingView";
|
|
|
2
2
|
export { default as KeyboardStickyView } from "./KeyboardStickyView";
|
|
3
3
|
export { default as KeyboardAwareScrollView } from "./KeyboardAwareScrollView";
|
|
4
4
|
export { default as KeyboardToolbar, DefaultKeyboardToolbarTheme, } from "./KeyboardToolbar";
|
|
5
|
-
export { default as
|
|
5
|
+
export { default as KeyboardChatScrollView } from "./KeyboardChatScrollView";
|
|
6
6
|
export type { KeyboardAvoidingViewProps } from "./KeyboardAvoidingView";
|
|
7
7
|
export type { KeyboardStickyViewProps } from "./KeyboardStickyView";
|
|
8
8
|
export type { KeyboardAwareScrollViewProps, KeyboardAwareScrollViewRef, } from "./KeyboardAwareScrollView";
|
|
9
9
|
export type { KeyboardToolbarProps } from "./KeyboardToolbar";
|
|
10
|
-
export type {
|
|
10
|
+
export type { KeyboardChatScrollViewProps } from "./KeyboardChatScrollView/types";
|
|
@@ -6,6 +6,6 @@ export * from "./constants";
|
|
|
6
6
|
export * from "./module";
|
|
7
7
|
export * from "./types";
|
|
8
8
|
export * from "./compat";
|
|
9
|
-
export {
|
|
10
|
-
export type {
|
|
9
|
+
export { KeyboardChatScrollView, KeyboardAvoidingView, KeyboardStickyView, KeyboardAwareScrollView, KeyboardToolbar, DefaultKeyboardToolbarTheme, } from "./components";
|
|
10
|
+
export type { KeyboardChatScrollViewProps, KeyboardAvoidingViewProps, KeyboardStickyViewProps, KeyboardAwareScrollViewProps, KeyboardAwareScrollViewRef, KeyboardToolbarProps, } from "./components";
|
|
11
11
|
export { OverKeyboardView, KeyboardExtender } from "./views";
|
|
@@ -3,6 +3,7 @@ import type { ViewProps } from "react-native";
|
|
|
3
3
|
import type { Double } from "react-native/Libraries/Types/CodegenTypes";
|
|
4
4
|
export interface NativeProps extends ViewProps {
|
|
5
5
|
contentInsetBottom: Double;
|
|
6
|
+
contentInsetTop: Double;
|
|
6
7
|
}
|
|
7
8
|
declare const _default: HostComponent<NativeProps>;
|
|
8
9
|
export default _default;
|
|
@@ -51,4 +51,6 @@ export type KeyboardExtenderProps = PropsWithChildren<{
|
|
|
51
51
|
export type ClippingScrollViewProps = PropsWithChildren<ViewProps & {
|
|
52
52
|
/** An additional space that gets applied to the bottom of the `ScrollView` (inside a scrollable content). Default is `0`. */
|
|
53
53
|
contentInsetBottom?: number;
|
|
54
|
+
/** An additional space that gets applied to the top of the `ScrollView` (inside a scrollable content). Default is `0`. */
|
|
55
|
+
contentInsetTop?: number;
|
|
54
56
|
}>;
|
package/package.json
CHANGED
|
@@ -149,6 +149,7 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
149
149
|
const lastSelection =
|
|
150
150
|
useSharedValue<FocusedInputSelectionChangedEvent | null>(null);
|
|
151
151
|
const ghostViewSpace = useSharedValue(-1);
|
|
152
|
+
const pendingSelectionForFocus = useSharedValue(false);
|
|
152
153
|
|
|
153
154
|
const { height } = useWindowDimensions();
|
|
154
155
|
|
|
@@ -324,7 +325,18 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
324
325
|
lastSelection.value = e;
|
|
325
326
|
|
|
326
327
|
if (e.target !== lastTarget) {
|
|
327
|
-
|
|
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
|
+
|
|
328
340
|
return;
|
|
329
341
|
}
|
|
330
342
|
// caret in the end + end coordinates has been changed -> we moved to a new line
|
|
@@ -342,7 +354,12 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
342
354
|
|
|
343
355
|
onChangeTextHandler();
|
|
344
356
|
},
|
|
345
|
-
[
|
|
357
|
+
[
|
|
358
|
+
scrollFromCurrentPosition,
|
|
359
|
+
onChangeTextHandler,
|
|
360
|
+
updateLayoutFromSelection,
|
|
361
|
+
maybeScroll,
|
|
362
|
+
],
|
|
346
363
|
);
|
|
347
364
|
|
|
348
365
|
useFocusedInputHandler(
|
|
@@ -375,6 +392,7 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
375
392
|
// on back transition need to interpolate as [0, keyboardHeight]
|
|
376
393
|
initialKeyboardSize.value = 0;
|
|
377
394
|
scrollPosition.value = scrollBeforeKeyboardMovement.value;
|
|
395
|
+
pendingSelectionForFocus.value = false;
|
|
378
396
|
}
|
|
379
397
|
|
|
380
398
|
if (
|
|
@@ -391,17 +409,31 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
391
409
|
// focus was changed
|
|
392
410
|
if (focusWasChanged) {
|
|
393
411
|
tag.value = e.target;
|
|
394
|
-
|
|
395
|
-
|
|
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
|
+
|
|
396
426
|
// save current scroll position - when keyboard will hide we'll reuse
|
|
397
427
|
// this value to achieve smooth hide effect
|
|
398
428
|
scrollBeforeKeyboardMovement.value = position.value;
|
|
399
429
|
}
|
|
400
430
|
|
|
401
431
|
if (focusWasChanged && !keyboardWillAppear.value) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
+
}
|
|
405
437
|
}
|
|
406
438
|
|
|
407
439
|
ghostViewSpace.value =
|
|
@@ -435,6 +467,10 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
435
467
|
keyboardHeight.value = e.height;
|
|
436
468
|
scrollPosition.value = position.value;
|
|
437
469
|
|
|
470
|
+
if (e.height === 0) {
|
|
471
|
+
lastSelection.value = null;
|
|
472
|
+
}
|
|
473
|
+
|
|
438
474
|
syncKeyboardFrame(e);
|
|
439
475
|
},
|
|
440
476
|
},
|
|
@@ -461,11 +497,18 @@ const KeyboardAwareScrollView = forwardRef<
|
|
|
461
497
|
() => {
|
|
462
498
|
const scrollView = scrollViewRef.current;
|
|
463
499
|
|
|
464
|
-
|
|
500
|
+
if (scrollView) {
|
|
501
|
+
const scrollViewWithMethods =
|
|
502
|
+
scrollView as KeyboardAwareScrollViewRef;
|
|
465
503
|
|
|
466
|
-
|
|
467
|
-
|
|
504
|
+
scrollViewWithMethods.assureFocusedInputVisible = () => {
|
|
505
|
+
synchronize();
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
return scrollViewWithMethods;
|
|
509
|
+
}
|
|
468
510
|
|
|
511
|
+
return {
|
|
469
512
|
assureFocusedInputVisible: () => {
|
|
470
513
|
synchronize();
|
|
471
514
|
},
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useMemo } from "react";
|
|
2
|
+
import { StyleSheet } from "react-native";
|
|
3
|
+
import { useAnimatedRef, useAnimatedStyle } from "react-native-reanimated";
|
|
4
|
+
import Reanimated from "react-native-reanimated";
|
|
5
|
+
|
|
6
|
+
import useCombinedRef from "../hooks/useCombinedRef";
|
|
7
|
+
import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding";
|
|
8
|
+
|
|
9
|
+
import { useChatKeyboard } from "./useChatKeyboard";
|
|
10
|
+
|
|
11
|
+
import type { KeyboardChatScrollViewProps } from "./types";
|
|
12
|
+
import type { LayoutChangeEvent } from "react-native";
|
|
13
|
+
|
|
14
|
+
const KeyboardChatScrollView = forwardRef<
|
|
15
|
+
Reanimated.ScrollView,
|
|
16
|
+
React.PropsWithChildren<KeyboardChatScrollViewProps>
|
|
17
|
+
>(
|
|
18
|
+
(
|
|
19
|
+
{
|
|
20
|
+
children,
|
|
21
|
+
ScrollViewComponent = Reanimated.ScrollView,
|
|
22
|
+
inverted = false,
|
|
23
|
+
keyboardLiftBehavior = "always",
|
|
24
|
+
freeze = false,
|
|
25
|
+
offset = 0,
|
|
26
|
+
onLayout: onLayoutProp,
|
|
27
|
+
onContentSizeChange: onContentSizeChangeProp,
|
|
28
|
+
...rest
|
|
29
|
+
},
|
|
30
|
+
ref,
|
|
31
|
+
) => {
|
|
32
|
+
const scrollViewRef = useAnimatedRef<Reanimated.ScrollView>();
|
|
33
|
+
const onRef = useCombinedRef(ref, scrollViewRef);
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
padding,
|
|
37
|
+
currentHeight,
|
|
38
|
+
contentOffsetY,
|
|
39
|
+
onLayout: onLayoutInternal,
|
|
40
|
+
onContentSizeChange: onContentSizeChangeInternal,
|
|
41
|
+
} = useChatKeyboard(scrollViewRef, {
|
|
42
|
+
inverted,
|
|
43
|
+
keyboardLiftBehavior,
|
|
44
|
+
freeze,
|
|
45
|
+
offset,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const onLayout = useCallback(
|
|
49
|
+
(e: LayoutChangeEvent) => {
|
|
50
|
+
onLayoutInternal(e);
|
|
51
|
+
onLayoutProp?.(e);
|
|
52
|
+
},
|
|
53
|
+
[onLayoutInternal, onLayoutProp],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const onContentSizeChange = useCallback(
|
|
57
|
+
(w: number, h: number) => {
|
|
58
|
+
onContentSizeChangeInternal(w, h);
|
|
59
|
+
onContentSizeChangeProp?.(w, h);
|
|
60
|
+
},
|
|
61
|
+
[onContentSizeChangeInternal, onContentSizeChangeProp],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Invisible view whose animated style changes every frame during keyboard
|
|
65
|
+
// animation. On Fabric, this forces Reanimated to schedule a commit,
|
|
66
|
+
// which flushes the scrollTo call in the same frame (fixing de-synchronization).
|
|
67
|
+
// see https://github.com/software-mansion/react-native-reanimated/issues/9000
|
|
68
|
+
const commitStyle = useAnimatedStyle(
|
|
69
|
+
() => ({
|
|
70
|
+
transform: [{ translateY: -currentHeight.value }],
|
|
71
|
+
}),
|
|
72
|
+
[],
|
|
73
|
+
);
|
|
74
|
+
const commit = useMemo(
|
|
75
|
+
() => [styles.commitView, commitStyle],
|
|
76
|
+
[commitStyle],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<ScrollViewWithBottomPadding
|
|
82
|
+
ref={onRef}
|
|
83
|
+
{...rest}
|
|
84
|
+
bottomPadding={padding}
|
|
85
|
+
contentOffsetY={contentOffsetY}
|
|
86
|
+
inverted={inverted}
|
|
87
|
+
ScrollViewComponent={ScrollViewComponent}
|
|
88
|
+
onContentSizeChange={onContentSizeChange}
|
|
89
|
+
onLayout={onLayout}
|
|
90
|
+
>
|
|
91
|
+
{children}
|
|
92
|
+
</ScrollViewWithBottomPadding>
|
|
93
|
+
<Reanimated.View style={commit} />
|
|
94
|
+
</>
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const styles = StyleSheet.create({
|
|
100
|
+
commitView: {
|
|
101
|
+
display: "none",
|
|
102
|
+
position: "absolute",
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export default KeyboardChatScrollView;
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { AnimatedScrollViewComponent } from "../ScrollViewWithBottomPadding";
|
|
2
|
+
import type { KeyboardLiftBehavior } from "./useChatKeyboard/types";
|
|
2
3
|
import type { ScrollViewProps } from "react-native";
|
|
3
4
|
|
|
4
|
-
type
|
|
5
|
-
|
|
6
|
-
export type ChatKitScrollViewProps = {
|
|
5
|
+
export type KeyboardChatScrollViewProps = {
|
|
7
6
|
/** Custom component for `ScrollView`. Default is `ScrollView`. */
|
|
8
|
-
ScrollViewComponent
|
|
7
|
+
ScrollViewComponent?: AnimatedScrollViewComponent;
|
|
9
8
|
/** Whether list are using `inverted` prop. Default is `false`. */
|
|
10
9
|
inverted?: boolean;
|
|
11
10
|
/**
|
|
12
|
-
* The distance
|
|
13
|
-
* When the keyboard appears, the
|
|
11
|
+
* The distance between the bottom of the screen and the `ScrollView`.
|
|
12
|
+
* When the keyboard appears, the `ScrollView` will only push content by the effective
|
|
14
13
|
* distance (`keyboardHeight - offset`) instead of the full keyboard height.
|
|
15
14
|
*
|
|
16
15
|
* Useful when the input is not at the very bottom of the screen (e.g., above safe area, above
|
|
@@ -1,26 +1,71 @@
|
|
|
1
|
-
|
|
1
|
+
import { interpolate } from "react-native-reanimated";
|
|
2
|
+
|
|
3
|
+
import type { KeyboardLiftBehavior } from "./types";
|
|
2
4
|
|
|
3
5
|
const AT_END_THRESHOLD = 20;
|
|
4
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Map the current keyboard height to an effective height that accounts for a
|
|
9
|
+
* fixed offset (e.g. bottom safe-area or tab-bar height).
|
|
10
|
+
*
|
|
11
|
+
* @param height - Current keyboard height.
|
|
12
|
+
* @param targetKeyboardHeight - Full target keyboard height (captured on keyboard open).
|
|
13
|
+
* @param offset - Fixed distance between the scroll-view bottom and the screen bottom.
|
|
14
|
+
* @returns Effective height after subtracting the offset proportionally.
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* getEffectiveHeight(300, 300, 50); // 250
|
|
18
|
+
* getEffectiveHeight(150, 300, 50); // 125
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function getEffectiveHeight(
|
|
22
|
+
height: number,
|
|
23
|
+
targetKeyboardHeight: number,
|
|
24
|
+
offset: number,
|
|
25
|
+
): number {
|
|
26
|
+
"worklet";
|
|
27
|
+
|
|
28
|
+
if (offset === 0 || targetKeyboardHeight === 0) {
|
|
29
|
+
return height;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return interpolate(
|
|
33
|
+
height,
|
|
34
|
+
[0, targetKeyboardHeight],
|
|
35
|
+
[0, Math.max(targetKeyboardHeight - offset, 0)],
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
5
39
|
/**
|
|
6
40
|
* Check whether the scroll view is at the end of its content.
|
|
7
41
|
*
|
|
42
|
+
* For non-inverted lists the "end" is the bottom of the content.
|
|
43
|
+
* For inverted lists the "end" is the top (scroll offset near 0),
|
|
44
|
+
* because that is where the latest messages are displayed.
|
|
45
|
+
*
|
|
8
46
|
* @param scrollOffset - Current vertical scroll offset.
|
|
9
47
|
* @param layoutHeight - Visible height of the scroll view.
|
|
10
48
|
* @param contentHeight - Total height of the scrollable content.
|
|
49
|
+
* @param inverted - Whether the list is inverted.
|
|
11
50
|
* @returns `true` if the scroll position is within the threshold of the content end.
|
|
12
51
|
* @example
|
|
13
52
|
* ```ts
|
|
14
53
|
* const atEnd = isScrollAtEnd(100, 800, 920); // true (100 + 800 >= 920 - 20)
|
|
54
|
+
* const atEndInverted = isScrollAtEnd(5, 800, 2000, true); // true (5 <= 20)
|
|
15
55
|
* ```
|
|
16
56
|
*/
|
|
17
57
|
export function isScrollAtEnd(
|
|
18
58
|
scrollOffset: number,
|
|
19
59
|
layoutHeight: number,
|
|
20
60
|
contentHeight: number,
|
|
61
|
+
inverted: boolean = false,
|
|
21
62
|
): boolean {
|
|
22
63
|
"worklet";
|
|
23
64
|
|
|
65
|
+
if (inverted) {
|
|
66
|
+
return scrollOffset <= AT_END_THRESHOLD;
|
|
67
|
+
}
|
|
68
|
+
|
|
24
69
|
return scrollOffset + layoutHeight >= contentHeight - AT_END_THRESHOLD;
|
|
25
70
|
}
|
|
26
71
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { useSharedValue } from "react-native-reanimated";
|
|
2
|
+
|
|
3
|
+
import { useKeyboardHandler } from "../../../hooks";
|
|
4
|
+
import useScrollState from "../../hooks/useScrollState";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
computeIOSContentOffset,
|
|
8
|
+
getEffectiveHeight,
|
|
9
|
+
isScrollAtEnd,
|
|
10
|
+
shouldShiftContent,
|
|
11
|
+
} from "./helpers";
|
|
12
|
+
|
|
13
|
+
import type { UseChatKeyboardOptions, UseChatKeyboardReturn } from "./types";
|
|
14
|
+
import type { AnimatedRef } from "react-native-reanimated";
|
|
15
|
+
import type Reanimated from "react-native-reanimated";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hook that manages keyboard-driven scrolling for chat-style scroll views.
|
|
19
|
+
* Calculates padding (extra scrollable space) and content shift values,
|
|
20
|
+
* using iOS-specific strategy (contentOffset set once in onStart).
|
|
21
|
+
*
|
|
22
|
+
* @param scrollViewRef - Animated ref to the scroll view.
|
|
23
|
+
* @param options - Configuration for inverted and keyboardLiftBehavior.
|
|
24
|
+
* @returns Shared values for padding and contentOffsetY.
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const { padding, contentOffsetY } = useChatKeyboard(ref, {
|
|
28
|
+
* inverted: false,
|
|
29
|
+
* keyboardLiftBehavior: "always",
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
function useChatKeyboard(
|
|
34
|
+
scrollViewRef: AnimatedRef<Reanimated.ScrollView>,
|
|
35
|
+
options: UseChatKeyboardOptions,
|
|
36
|
+
): UseChatKeyboardReturn {
|
|
37
|
+
const { inverted, keyboardLiftBehavior, freeze, offset } = options;
|
|
38
|
+
|
|
39
|
+
const padding = useSharedValue(0);
|
|
40
|
+
const currentHeight = useSharedValue(0);
|
|
41
|
+
const contentOffsetY = useSharedValue(0);
|
|
42
|
+
const targetKeyboardHeight = useSharedValue(0);
|
|
43
|
+
|
|
44
|
+
const {
|
|
45
|
+
layout,
|
|
46
|
+
size,
|
|
47
|
+
offset: scroll,
|
|
48
|
+
onLayout,
|
|
49
|
+
onContentSizeChange,
|
|
50
|
+
} = useScrollState(scrollViewRef);
|
|
51
|
+
|
|
52
|
+
useKeyboardHandler(
|
|
53
|
+
{
|
|
54
|
+
onStart: (e) => {
|
|
55
|
+
"worklet";
|
|
56
|
+
|
|
57
|
+
if (freeze) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (e.height > 0) {
|
|
62
|
+
// eslint-disable-next-line react-compiler/react-compiler
|
|
63
|
+
targetKeyboardHeight.value = e.height;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const effective = getEffectiveHeight(
|
|
67
|
+
e.height,
|
|
68
|
+
targetKeyboardHeight.value,
|
|
69
|
+
offset,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const atEnd = isScrollAtEnd(
|
|
73
|
+
scroll.value,
|
|
74
|
+
layout.value.height,
|
|
75
|
+
size.value.height,
|
|
76
|
+
inverted,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// persistent mode: when keyboard shrinks, snap to end or hold position
|
|
80
|
+
if (
|
|
81
|
+
keyboardLiftBehavior === "persistent" &&
|
|
82
|
+
effective < padding.value
|
|
83
|
+
) {
|
|
84
|
+
padding.value = effective;
|
|
85
|
+
|
|
86
|
+
if (atEnd) {
|
|
87
|
+
if (inverted) {
|
|
88
|
+
contentOffsetY.value = -effective;
|
|
89
|
+
} else {
|
|
90
|
+
contentOffsetY.value = Math.max(
|
|
91
|
+
size.value.height - layout.value.height + effective,
|
|
92
|
+
0,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// Preserve current scroll position so the animated props
|
|
97
|
+
// don't re-apply the stale contentOffset from keyboard open
|
|
98
|
+
contentOffsetY.value = scroll.value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// never mode: when keyboard shrinks and at end, snap to end
|
|
105
|
+
// to avoid ghost padding
|
|
106
|
+
if (
|
|
107
|
+
keyboardLiftBehavior === "never" &&
|
|
108
|
+
effective < padding.value &&
|
|
109
|
+
atEnd
|
|
110
|
+
) {
|
|
111
|
+
padding.value = effective;
|
|
112
|
+
|
|
113
|
+
if (inverted) {
|
|
114
|
+
contentOffsetY.value = -effective;
|
|
115
|
+
} else {
|
|
116
|
+
contentOffsetY.value = Math.max(
|
|
117
|
+
size.value.height - layout.value.height + effective,
|
|
118
|
+
0,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const relativeScroll = inverted
|
|
126
|
+
? scroll.value + padding.value
|
|
127
|
+
: scroll.value - padding.value;
|
|
128
|
+
|
|
129
|
+
padding.value = effective;
|
|
130
|
+
|
|
131
|
+
if (!shouldShiftContent(keyboardLiftBehavior, atEnd)) {
|
|
132
|
+
// Preserve current scroll position so animated props
|
|
133
|
+
// don't re-apply a stale contentOffset when padding changes
|
|
134
|
+
contentOffsetY.value = scroll.value;
|
|
135
|
+
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
contentOffsetY.value = computeIOSContentOffset(
|
|
140
|
+
relativeScroll,
|
|
141
|
+
effective,
|
|
142
|
+
size.value.height,
|
|
143
|
+
layout.value.height,
|
|
144
|
+
inverted,
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
onMove: () => {
|
|
148
|
+
"worklet";
|
|
149
|
+
|
|
150
|
+
// iOS doesn't need per-frame updates (contentOffset handles it)
|
|
151
|
+
},
|
|
152
|
+
onEnd: (e) => {
|
|
153
|
+
"worklet";
|
|
154
|
+
|
|
155
|
+
if (freeze) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const effective = getEffectiveHeight(
|
|
160
|
+
e.height,
|
|
161
|
+
targetKeyboardHeight.value,
|
|
162
|
+
offset,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
padding.value = effective;
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
[inverted, keyboardLiftBehavior, freeze, offset],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
padding,
|
|
173
|
+
currentHeight,
|
|
174
|
+
contentOffsetY,
|
|
175
|
+
onLayout,
|
|
176
|
+
onContentSizeChange,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { useChatKeyboard };
|