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.
Files changed (126) hide show
  1. package/android/src/fabric/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +7 -0
  2. package/android/src/main/java/com/reactnativekeyboardcontroller/views/ClippingScrollViewDecoratorView.kt +28 -1
  3. package/android/src/paper/java/com/reactnativekeyboardcontroller/ClippingScrollViewDecoratorViewManager.kt +8 -0
  4. package/ios/animations/KeyboardAnimation.swift +2 -2
  5. package/ios/animations/SpringAnimation.swift +1 -1
  6. package/ios/animations/TimingAnimation.swift +1 -3
  7. package/ios/delegates/KCTextInputCompositeDelegate.swift +2 -2
  8. package/ios/extensions/CGFloat.swift +1 -3
  9. package/ios/interactive/KeyboardAreaExtender.swift +2 -1
  10. package/ios/observers/FocusedInputObserver.swift +4 -2
  11. package/ios/observers/movement/KeyboardTrackingView.swift +4 -1
  12. package/ios/observers/movement/observer/KeyboardMovementObserver+Interactive.swift +2 -0
  13. package/ios/observers/movement/observer/KeyboardMovementObserver+Listeners.swift +30 -7
  14. package/ios/observers/movement/observer/KeyboardMovementObserver.swift +2 -48
  15. package/ios/protocols/TextInput.swift +1 -1
  16. package/ios/swizzling/UIResponderSwizzle.swift +2 -4
  17. package/ios/traversal/FocusedInputHolder.swift +3 -3
  18. package/ios/traversal/KeyboardView.swift +1 -1
  19. package/ios/traversal/ViewHierarchyNavigator.swift +2 -2
  20. package/ios/views/KeyboardControllerViewManager.swift +5 -5
  21. package/jest/index.js +1 -3
  22. package/lib/commonjs/components/KeyboardAwareScrollView/index.js +43 -11
  23. package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
  24. package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/hooks.js.map +1 -1
  25. package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/index.js +37 -12
  26. package/lib/commonjs/components/KeyboardChatScrollView/index.js.map +1 -0
  27. package/lib/commonjs/components/KeyboardChatScrollView/types.js.map +1 -0
  28. package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/useChatKeyboard/helpers.js +35 -1
  29. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js.map +1 -0
  30. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +121 -0
  31. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -0
  32. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js +205 -0
  33. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -0
  34. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/types.js +6 -0
  35. package/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -0
  36. package/lib/commonjs/components/KeyboardStickyView/index.js +3 -1
  37. package/lib/commonjs/components/KeyboardStickyView/index.js.map +1 -1
  38. package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js +18 -9
  39. package/lib/commonjs/components/ScrollViewWithBottomPadding/index.js.map +1 -1
  40. package/lib/commonjs/components/hooks/useScrollState.js +20 -1
  41. package/lib/commonjs/components/hooks/useScrollState.js.map +1 -1
  42. package/lib/commonjs/components/index.js +7 -7
  43. package/lib/commonjs/components/index.js.map +1 -1
  44. package/lib/commonjs/index.js +7 -7
  45. package/lib/commonjs/index.js.map +1 -1
  46. package/lib/commonjs/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -1
  47. package/lib/commonjs/types/views.js.map +1 -1
  48. package/lib/module/components/KeyboardAwareScrollView/index.js +43 -11
  49. package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
  50. package/lib/module/components/{ChatKit → KeyboardChatScrollView}/hooks.js.map +1 -1
  51. package/lib/module/components/KeyboardChatScrollView/index.js +73 -0
  52. package/lib/module/components/KeyboardChatScrollView/index.js.map +1 -0
  53. package/lib/module/components/KeyboardChatScrollView/types.js.map +1 -0
  54. package/lib/module/components/{ChatKit → KeyboardChatScrollView}/useChatKeyboard/helpers.js +34 -1
  55. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js.map +1 -0
  56. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js +115 -0
  57. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js.map +1 -0
  58. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js +199 -0
  59. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.js.map +1 -0
  60. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/types.js +2 -0
  61. package/lib/module/components/KeyboardChatScrollView/useChatKeyboard/types.js.map +1 -0
  62. package/lib/module/components/KeyboardStickyView/index.js +3 -1
  63. package/lib/module/components/KeyboardStickyView/index.js.map +1 -1
  64. package/lib/module/components/ScrollViewWithBottomPadding/index.js +19 -10
  65. package/lib/module/components/ScrollViewWithBottomPadding/index.js.map +1 -1
  66. package/lib/module/components/hooks/useScrollState.js +21 -2
  67. package/lib/module/components/hooks/useScrollState.js.map +1 -1
  68. package/lib/module/components/index.js +1 -1
  69. package/lib/module/components/index.js.map +1 -1
  70. package/lib/module/index.js +1 -1
  71. package/lib/module/index.js.map +1 -1
  72. package/lib/module/specs/ClippingScrollViewDecoratorViewNativeComponent.js.map +1 -1
  73. package/lib/module/types/views.js.map +1 -1
  74. package/lib/typescript/components/KeyboardChatScrollView/hooks.d.ts +2 -0
  75. package/lib/typescript/components/KeyboardChatScrollView/index.d.ts +12 -0
  76. package/lib/typescript/components/{ChatKit → KeyboardChatScrollView}/types.d.ts +5 -6
  77. package/lib/typescript/components/{ChatKit → KeyboardChatScrollView}/useChatKeyboard/helpers.d.ts +23 -3
  78. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/index.d.ts +21 -0
  79. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/index.ios.d.ts +21 -0
  80. package/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts +22 -0
  81. package/lib/typescript/components/ScrollViewWithBottomPadding/index.d.ts +2 -4
  82. package/lib/typescript/components/hooks/useScrollState.d.ts +3 -0
  83. package/lib/typescript/components/index.d.ts +2 -2
  84. package/lib/typescript/index.d.ts +2 -2
  85. package/lib/typescript/specs/ClippingScrollViewDecoratorViewNativeComponent.d.ts +1 -0
  86. package/lib/typescript/types/views.d.ts +2 -0
  87. package/package.json +1 -1
  88. package/src/components/KeyboardAwareScrollView/index.tsx +53 -10
  89. package/src/components/KeyboardChatScrollView/hooks.ts +2 -0
  90. package/src/components/KeyboardChatScrollView/index.tsx +106 -0
  91. package/src/components/{ChatKit → KeyboardChatScrollView}/types.ts +5 -6
  92. package/src/components/{ChatKit → KeyboardChatScrollView}/useChatKeyboard/helpers.ts +46 -1
  93. package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts +180 -0
  94. package/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts +300 -0
  95. package/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts +30 -0
  96. package/src/components/KeyboardStickyView/index.tsx +4 -3
  97. package/src/components/ScrollViewWithBottomPadding/index.tsx +22 -25
  98. package/src/components/hooks/useScrollState.ts +24 -2
  99. package/src/components/index.ts +2 -2
  100. package/src/index.ts +2 -2
  101. package/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts +1 -0
  102. package/src/types/views.ts +2 -0
  103. package/lib/commonjs/components/ChatKit/TODO.md +0 -20
  104. package/lib/commonjs/components/ChatKit/index.js.map +0 -1
  105. package/lib/commonjs/components/ChatKit/types.js.map +0 -1
  106. package/lib/commonjs/components/ChatKit/useChatKeyboard/helpers.js.map +0 -1
  107. package/lib/commonjs/components/ChatKit/useChatKeyboard/index.js +0 -147
  108. package/lib/commonjs/components/ChatKit/useChatKeyboard/index.js.map +0 -1
  109. package/lib/module/components/ChatKit/TODO.md +0 -20
  110. package/lib/module/components/ChatKit/index.js +0 -48
  111. package/lib/module/components/ChatKit/index.js.map +0 -1
  112. package/lib/module/components/ChatKit/types.js.map +0 -1
  113. package/lib/module/components/ChatKit/useChatKeyboard/helpers.js.map +0 -1
  114. package/lib/module/components/ChatKit/useChatKeyboard/index.js +0 -141
  115. package/lib/module/components/ChatKit/useChatKeyboard/index.js.map +0 -1
  116. package/lib/typescript/components/ChatKit/hooks.d.ts +0 -2
  117. package/lib/typescript/components/ChatKit/index.d.ts +0 -14
  118. package/lib/typescript/components/ChatKit/useChatKeyboard/index.d.ts +0 -36
  119. package/src/components/ChatKit/TODO.md +0 -20
  120. package/src/components/ChatKit/hooks.ts +0 -2
  121. package/src/components/ChatKit/index.tsx +0 -63
  122. package/src/components/ChatKit/useChatKeyboard/index.ts +0 -228
  123. /package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/hooks.js +0 -0
  124. /package/lib/commonjs/components/{ChatKit → KeyboardChatScrollView}/types.js +0 -0
  125. /package/lib/module/components/{ChatKit → KeyboardChatScrollView}/hooks.js +0 -0
  126. /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, StyleProp, ViewStyle } from "react-native";
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 ChatKit). */
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 ChatKit } from "./ChatKit";
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 { ChatKitScrollViewProps } from "./ChatKit/types";
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 { ChatKit, KeyboardAvoidingView, KeyboardStickyView, KeyboardAwareScrollView, KeyboardToolbar, DefaultKeyboardToolbarTheme, } from "./components";
10
- export type { ChatKitScrollViewProps, KeyboardAvoidingViewProps, KeyboardStickyViewProps, KeyboardAwareScrollViewProps, KeyboardAwareScrollViewRef, KeyboardToolbarProps, } from "./components";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-keyboard-controller",
3
- "version": "1.21.0-beta.2",
3
+ "version": "1.21.0-beta.3",
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",
@@ -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
- // ignore this event, because "focus changed" event handled in `useSmoothKeyboardHandler`
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
- [scrollFromCurrentPosition, onChangeTextHandler],
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
- // save position of focused text input when keyboard starts to move
395
- updateLayoutFromSelection();
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
- // update position on scroll value, so `onEnd` handler
403
- // will pick up correct values
404
- position.value += maybeScroll(e.height, true);
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
- const existingMethods = scrollView ? { ...scrollView } : {};
500
+ if (scrollView) {
501
+ const scrollViewWithMethods =
502
+ scrollView as KeyboardAwareScrollViewRef;
465
503
 
466
- return {
467
- ...existingMethods,
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,2 @@
1
+ export { useChatKeyboard } from "./useChatKeyboard";
2
+ export type { KeyboardLiftBehavior } from "./useChatKeyboard/types";
@@ -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 KeyboardLiftBehavior = "always" | "whenAtEnd" | "persistent" | "never";
5
-
6
- export type ChatKitScrollViewProps = {
5
+ export type KeyboardChatScrollViewProps = {
7
6
  /** Custom component for `ScrollView`. Default is `ScrollView`. */
8
- ScrollViewComponent: AnimatedScrollViewComponent;
7
+ ScrollViewComponent?: AnimatedScrollViewComponent;
9
8
  /** Whether list are using `inverted` prop. Default is `false`. */
10
9
  inverted?: boolean;
11
10
  /**
12
- * The distance (in pixels) between the bottom of the screen and the `ScrollView`.
13
- * When the keyboard appears, the scroll view will only push content by the effective
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
- type KeyboardLiftBehavior = "always" | "whenAtEnd" | "persistent" | "never";
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 };