react-native-keyboard-controller 1.17.5 → 1.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +4 -1
  2. package/android/src/base/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +3 -2
  3. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardBackgroundViewManager.kt +19 -0
  4. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +4 -0
  5. package/android/src/fabric/java/com/reactnativekeyboardcontroller/StatusBarManagerCompatModule.kt +3 -1
  6. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/Context.kt +20 -0
  7. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt +2 -12
  8. package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardBackgroundViewManagerImpl.kt +13 -0
  9. package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt +2 -1
  10. package/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt +4 -0
  11. package/android/src/main/java/com/reactnativekeyboardcontroller/modules/{StatusBarManagerCompatModuleImpl.kt → statusbar/StatusBarManagerCompatModuleImpl.kt} +31 -5
  12. package/android/src/main/java/com/reactnativekeyboardcontroller/modules/statusbar/StatusBarModuleProxy.kt +86 -0
  13. package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +9 -11
  14. package/android/src/main/java/com/reactnativekeyboardcontroller/views/background/KeyboardBackgroundViewGroup.kt +35 -0
  15. package/android/src/main/java/com/reactnativekeyboardcontroller/views/background/Skins.kt +91 -0
  16. package/android/src/main/jni/reactnativekeyboardcontroller.h +1 -0
  17. package/android/src/main/res/values/attrs.xml +7 -0
  18. package/android/src/main/res/values/colors.xml +35 -0
  19. package/android/src/main/res/values-v29/colors.xml +5 -0
  20. package/android/src/main/res/values-v30/colors.xml +17 -0
  21. package/android/src/main/res/values-v33/colors.xml +16 -0
  22. package/android/src/main/res/values-v34/colors.xml +18 -0
  23. package/android/src/main/res/values-v35/colors.xml +10 -0
  24. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardBackgroundViewManager.kt +15 -0
  25. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +5 -0
  26. package/android/src/paper/java/com/reactnativekeyboardcontroller/StatusBarManagerCompatModule.kt +3 -1
  27. package/android/src/turbo/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +2 -2
  28. package/common/cpp/react/renderer/components/reactnativekeyboardcontroller/RNKCKeyboardBackgroundViewComponentDescriptor.h +27 -0
  29. package/common/cpp/react/renderer/components/reactnativekeyboardcontroller/RNKCKeyboardBackgroundViewShadowNode.cpp +14 -0
  30. package/common/cpp/react/renderer/components/reactnativekeyboardcontroller/RNKCKeyboardBackgroundViewShadowNode.h +30 -0
  31. package/common/cpp/react/renderer/components/reactnativekeyboardcontroller/RNKCKeyboardBackgroundViewState.h +28 -0
  32. package/common/cpp/react/renderer/components/reactnativekeyboardcontroller/RNKCKeyboardExtenderComponentDescriptor.h +27 -0
  33. package/common/cpp/react/renderer/components/reactnativekeyboardcontroller/RNKCKeyboardExtenderShadowNode.cpp +14 -0
  34. package/common/cpp/react/renderer/components/reactnativekeyboardcontroller/RNKCKeyboardExtenderShadowNode.h +30 -0
  35. package/common/cpp/react/renderer/components/reactnativekeyboardcontroller/RNKCKeyboardExtenderState.h +28 -0
  36. package/ios/KeyboardControllerModule.mm +9 -0
  37. package/ios/delegates/KCTextInputCompositeDelegate.swift +5 -5
  38. package/ios/events/KeyboardEventEmitterPayload.swift +1 -1
  39. package/ios/extensions/UIResponder.swift +27 -0
  40. package/ios/interactive/KeyboardAreaExtender.swift +5 -5
  41. package/ios/observers/FocusedInputObserver.swift +14 -5
  42. package/ios/observers/KeyboardMovementObserver.swift +7 -1
  43. package/ios/protocols/TextInput.swift +22 -0
  44. package/ios/swizzling/UIResponderSwizzle.swift +3 -0
  45. package/ios/views/KeyboardBackgroundViewManager.h +29 -0
  46. package/ios/views/KeyboardBackgroundViewManager.mm +175 -0
  47. package/ios/views/KeyboardExtenderContainerView.swift +59 -0
  48. package/ios/views/KeyboardExtenderManager.h +29 -0
  49. package/ios/views/KeyboardExtenderManager.mm +287 -0
  50. package/jest/index.js +6 -1
  51. package/lib/commonjs/animated.js +7 -10
  52. package/lib/commonjs/animated.js.map +1 -1
  53. package/lib/commonjs/bindings.js +15 -1
  54. package/lib/commonjs/bindings.js.map +1 -1
  55. package/lib/commonjs/bindings.native.js +5 -1
  56. package/lib/commonjs/bindings.native.js.map +1 -1
  57. package/lib/commonjs/components/KeyboardAwareScrollView/index.js +49 -29
  58. package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
  59. package/lib/commonjs/components/KeyboardToolbar/Arrow.js +2 -3
  60. package/lib/commonjs/components/KeyboardToolbar/Arrow.js.map +1 -1
  61. package/lib/commonjs/components/KeyboardToolbar/Button.js +2 -3
  62. package/lib/commonjs/components/KeyboardToolbar/Button.js.map +1 -1
  63. package/lib/commonjs/components/KeyboardToolbar/index.js +2 -2
  64. package/lib/commonjs/components/KeyboardToolbar/index.js.map +1 -1
  65. package/lib/commonjs/hooks/useKeyboardState/index.js +18 -8
  66. package/lib/commonjs/hooks/useKeyboardState/index.js.map +1 -1
  67. package/lib/commonjs/index.js +8 -1
  68. package/lib/commonjs/index.js.map +1 -1
  69. package/lib/commonjs/module.js +2 -1
  70. package/lib/commonjs/module.js.map +1 -1
  71. package/lib/commonjs/specs/KeyboardBackgroundViewNativeComponent.js +12 -0
  72. package/lib/commonjs/specs/KeyboardBackgroundViewNativeComponent.js.map +1 -0
  73. package/lib/commonjs/specs/KeyboardExtenderNativeComponent.js +13 -0
  74. package/lib/commonjs/specs/KeyboardExtenderNativeComponent.js.map +1 -0
  75. package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
  76. package/lib/commonjs/types/module.js.map +1 -1
  77. package/lib/commonjs/types/views.js.map +1 -1
  78. package/lib/commonjs/views/KeyboardExtender/index.ios.js +43 -0
  79. package/lib/commonjs/views/KeyboardExtender/index.ios.js.map +1 -0
  80. package/lib/commonjs/views/KeyboardExtender/index.js +46 -0
  81. package/lib/commonjs/views/KeyboardExtender/index.js.map +1 -0
  82. package/lib/commonjs/views/index.js +7 -0
  83. package/lib/commonjs/views/index.js.map +1 -1
  84. package/lib/module/animated.js +8 -11
  85. package/lib/module/animated.js.map +1 -1
  86. package/lib/module/bindings.js +14 -0
  87. package/lib/module/bindings.js.map +1 -1
  88. package/lib/module/bindings.native.js +4 -0
  89. package/lib/module/bindings.native.js.map +1 -1
  90. package/lib/module/components/KeyboardAwareScrollView/index.js +50 -30
  91. package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
  92. package/lib/module/components/KeyboardToolbar/Arrow.js +2 -2
  93. package/lib/module/components/KeyboardToolbar/Arrow.js.map +1 -1
  94. package/lib/module/components/KeyboardToolbar/Button.js +2 -2
  95. package/lib/module/components/KeyboardToolbar/Button.js.map +1 -1
  96. package/lib/module/components/KeyboardToolbar/index.js +2 -2
  97. package/lib/module/components/KeyboardToolbar/index.js.map +1 -1
  98. package/lib/module/hooks/useKeyboardState/index.js +18 -6
  99. package/lib/module/hooks/useKeyboardState/index.js.map +1 -1
  100. package/lib/module/index.js +1 -1
  101. package/lib/module/index.js.map +1 -1
  102. package/lib/module/module.js +2 -1
  103. package/lib/module/module.js.map +1 -1
  104. package/lib/module/specs/KeyboardBackgroundViewNativeComponent.js +5 -0
  105. package/lib/module/specs/KeyboardBackgroundViewNativeComponent.js.map +1 -0
  106. package/lib/module/specs/KeyboardExtenderNativeComponent.js +6 -0
  107. package/lib/module/specs/KeyboardExtenderNativeComponent.js.map +1 -0
  108. package/lib/module/specs/NativeKeyboardController.js.map +1 -1
  109. package/lib/module/types/module.js.map +1 -1
  110. package/lib/module/types/views.js.map +1 -1
  111. package/lib/module/views/KeyboardExtender/index.ios.js +36 -0
  112. package/lib/module/views/KeyboardExtender/index.ios.js.map +1 -0
  113. package/lib/module/views/KeyboardExtender/index.js +39 -0
  114. package/lib/module/views/KeyboardExtender/index.js.map +1 -0
  115. package/lib/module/views/index.js +1 -0
  116. package/lib/module/views/index.js.map +1 -1
  117. package/lib/typescript/animated.d.ts +7 -0
  118. package/lib/typescript/bindings.d.ts +14 -1
  119. package/lib/typescript/bindings.native.d.ts +3 -1
  120. package/lib/typescript/components/KeyboardAwareScrollView/index.d.ts +2 -2
  121. package/lib/typescript/hooks/useKeyboardState/index.d.ts +8 -2
  122. package/lib/typescript/index.d.ts +1 -1
  123. package/lib/typescript/specs/KeyboardBackgroundViewNativeComponent.d.ts +6 -0
  124. package/lib/typescript/specs/KeyboardExtenderNativeComponent.d.ts +7 -0
  125. package/lib/typescript/specs/NativeKeyboardController.d.ts +1 -0
  126. package/lib/typescript/types/module.d.ts +15 -3
  127. package/lib/typescript/types/views.d.ts +5 -0
  128. package/lib/typescript/views/KeyboardExtender/index.d.ts +19 -0
  129. package/lib/typescript/views/KeyboardExtender/index.ios.d.ts +19 -0
  130. package/lib/typescript/views/index.d.ts +1 -0
  131. package/package.json +7 -3
  132. package/react-native.config.js +1 -0
  133. package/src/animated.tsx +14 -9
  134. package/src/bindings.native.ts +8 -0
  135. package/src/bindings.ts +18 -0
  136. package/src/components/KeyboardAwareScrollView/index.tsx +72 -47
  137. package/src/components/KeyboardToolbar/Arrow.tsx +2 -2
  138. package/src/components/KeyboardToolbar/Button.tsx +2 -2
  139. package/src/components/KeyboardToolbar/index.tsx +2 -2
  140. package/src/hooks/useKeyboardState/index.ts +25 -6
  141. package/src/index.ts +1 -1
  142. package/src/module.ts +2 -1
  143. package/src/specs/KeyboardBackgroundViewNativeComponent.ts +10 -0
  144. package/src/specs/KeyboardExtenderNativeComponent.ts +13 -0
  145. package/src/specs/NativeKeyboardController.ts +1 -0
  146. package/src/types/module.ts +17 -3
  147. package/src/types/views.ts +5 -0
  148. package/src/views/KeyboardExtender/index.ios.tsx +35 -0
  149. package/src/views/KeyboardExtender/index.tsx +42 -0
  150. package/src/views/index.ts +1 -0
  151. package/ios/extensions/UIKeyboardAppearance.swift +0 -21
  152. package/lib/commonjs/components/hooks/useColorScheme.js +0 -10
  153. package/lib/commonjs/components/hooks/useColorScheme.js.map +0 -1
  154. package/lib/commonjs/monkey-patch.android.js +0 -53
  155. package/lib/commonjs/monkey-patch.android.js.map +0 -1
  156. package/lib/commonjs/monkey-patch.js +0 -11
  157. package/lib/commonjs/monkey-patch.js.map +0 -1
  158. package/lib/module/components/hooks/useColorScheme.js +0 -4
  159. package/lib/module/components/hooks/useColorScheme.js.map +0 -1
  160. package/lib/module/monkey-patch.android.js +0 -42
  161. package/lib/module/monkey-patch.android.js.map +0 -1
  162. package/lib/module/monkey-patch.js +0 -5
  163. package/lib/module/monkey-patch.js.map +0 -1
  164. package/lib/typescript/components/hooks/useColorScheme.d.ts +0 -2
  165. package/lib/typescript/monkey-patch.android.d.ts +0 -2
  166. package/lib/typescript/monkey-patch.d.ts +0 -2
  167. package/src/components/hooks/useColorScheme.ts +0 -5
  168. package/src/monkey-patch.android.ts +0 -41
  169. package/src/monkey-patch.ts +0 -5
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import type { KeyboardExtenderProps } from "../../types";
3
+ import type { PropsWithChildren } from "react";
4
+ /**
5
+ * A component that embeds its children into the keyboard thus enhancing keyboard functionality.
6
+ *
7
+ * @param props - Component props.
8
+ * @returns A view component that renders inside the keyboard above all system buttons.
9
+ * @example
10
+ * ```tsx
11
+ * <KeyboardExtender>
12
+ * <Button>10$</Button>
13
+ * <Button>20$</Button>
14
+ * <Button>50$</Button>
15
+ * </KeyboardExtender>
16
+ * ```
17
+ */
18
+ declare const KeyboardExtender: (props: PropsWithChildren<KeyboardExtenderProps>) => React.JSX.Element;
19
+ export default KeyboardExtender;
@@ -1 +1,2 @@
1
1
  export { default as OverKeyboardView } from "./OverKeyboardView";
2
+ export { default as KeyboardExtender } from "./KeyboardExtender";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-keyboard-controller",
3
- "version": "1.17.5",
3
+ "version": "1.18.1",
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",
@@ -60,11 +60,13 @@
60
60
  "avoid keyboard",
61
61
  "sticky view",
62
62
  "over keyboard view",
63
+ "keyboard background view",
63
64
  "keyboard aware scroll view",
64
65
  "keyboard toolbar",
65
66
  "keyboard done button",
66
67
  "keyboard next button",
67
68
  "keyboard previous button",
69
+ "extend keyboard",
68
70
  "ios",
69
71
  "android"
70
72
  ],
@@ -79,7 +81,7 @@
79
81
  "registry": "https://registry.npmjs.org/"
80
82
  },
81
83
  "dependencies": {
82
- "react-native-is-edge-to-edge": "^1.1.6"
84
+ "react-native-is-edge-to-edge": "^1.2.1"
83
85
  },
84
86
  "devDependencies": {
85
87
  "@commitlint/config-conventional": "^11.0.0",
@@ -180,7 +182,9 @@
180
182
  "componentProvider": {
181
183
  "KeyboardControllerView": "KeyboardControllerView",
182
184
  "KeyboardGestureArea": "KeyboardGestureArea",
183
- "OverKeyboardView": "OverKeyboardView"
185
+ "OverKeyboardView": "OverKeyboardView",
186
+ "KeyboardBackgroundView": "KeyboardBackgroundView",
187
+ "KeyboardExtender": "KeyboardExtender"
184
188
  }
185
189
  }
186
190
  }
@@ -6,6 +6,7 @@ module.exports = {
6
6
  "KeyboardControllerViewComponentDescriptor",
7
7
  "KeyboardGestureAreaComponentDescriptor",
8
8
  "OverKeyboardViewComponentDescriptor",
9
+ "KeyboardBackgroundViewComponentDescriptor",
9
10
  ],
10
11
  cmakeListsPath: "../android/src/main/jni/CMakeLists.txt",
11
12
  },
package/src/animated.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  /* eslint react/jsx-sort-props: off */
2
- import React, { useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import React, { useEffect, useMemo, useRef, useState } from "react";
3
3
  import { Animated, Platform, StyleSheet } from "react-native";
4
4
  import {
5
5
  controlEdgeToEdgeValues,
@@ -11,7 +11,7 @@ import { KeyboardControllerView } from "./bindings";
11
11
  import { KeyboardContext } from "./context";
12
12
  import { focusedInputEventsMap, keyboardEventsMap } from "./event-mappings";
13
13
  import { useAnimatedValue, useEventHandlerRegistration } from "./internal";
14
- import { applyMonkeyPatch, revertMonkeyPatch } from "./monkey-patch";
14
+ import { KeyboardController } from "./module";
15
15
  import {
16
16
  useAnimatedKeyboardHandler,
17
17
  useFocusedInputLayoutHandler,
@@ -83,6 +83,13 @@ type KeyboardProviderProps = {
83
83
  * Defaults to `true`.
84
84
  */
85
85
  enabled?: boolean;
86
+ /**
87
+ * A boolean prop indicating whether to preload the keyboard to reduce time-to-interaction (TTI) on first input focus.
88
+ * Defaults to `true`.
89
+ *
90
+ * @platform ios
91
+ */
92
+ preload?: boolean;
86
93
  };
87
94
 
88
95
  // capture `Platform.OS` in separate variable to avoid deep workletization of entire RN package
@@ -110,6 +117,7 @@ export const KeyboardProvider = (props: KeyboardProviderProps) => {
110
117
  navigationBarTranslucent,
111
118
  preserveEdgeToEdge,
112
119
  enabled: initiallyEnabled = true,
120
+ preload = true,
113
121
  } = props;
114
122
  // ref
115
123
  const viewTagRef = useRef<React.Component<KeyboardControllerProps>>(null);
@@ -216,14 +224,11 @@ export const KeyboardProvider = (props: KeyboardProviderProps) => {
216
224
  [],
217
225
  );
218
226
 
219
- // layout effects
220
- useLayoutEffect(() => {
221
- if (enabled) {
222
- applyMonkeyPatch();
223
- } else {
224
- revertMonkeyPatch();
227
+ useEffect(() => {
228
+ if (preload) {
229
+ KeyboardController.preload();
225
230
  }
226
- }, [enabled]);
231
+ }, [preload]);
227
232
 
228
233
  if (__DEV__) {
229
234
  controlEdgeToEdgeValues({
@@ -2,9 +2,11 @@ import { NativeEventEmitter, Platform } from "react-native";
2
2
 
3
3
  import type {
4
4
  FocusedInputEventsModule,
5
+ KeyboardBackgroundViewProps,
5
6
  KeyboardControllerNativeModule,
6
7
  KeyboardControllerProps,
7
8
  KeyboardEventsModule,
9
+ KeyboardExtenderProps,
8
10
  KeyboardGestureAreaProps,
9
11
  OverKeyboardViewProps,
10
12
  WindowDimensionsEventsModule,
@@ -60,3 +62,9 @@ export const KeyboardGestureArea: React.FC<KeyboardGestureAreaProps> =
60
62
  : ({ children }: KeyboardGestureAreaProps) => children;
61
63
  export const RCTOverKeyboardView: React.FC<OverKeyboardViewProps> =
62
64
  require("./specs/OverKeyboardViewNativeComponent").default;
65
+ export const KeyboardBackgroundView: React.FC<KeyboardBackgroundViewProps> =
66
+ require("./specs/KeyboardBackgroundViewNativeComponent").default;
67
+ export const RCTKeyboardExtender: React.FC<KeyboardExtenderProps> =
68
+ Platform.OS === "ios"
69
+ ? require("./specs/KeyboardExtenderNativeComponent").default
70
+ : ({ children }: KeyboardExtenderProps) => children;
package/src/bindings.ts CHANGED
@@ -2,9 +2,11 @@ import { View } from "react-native";
2
2
 
3
3
  import type {
4
4
  FocusedInputEventsModule,
5
+ KeyboardBackgroundViewProps,
5
6
  KeyboardControllerNativeModule,
6
7
  KeyboardControllerProps,
7
8
  KeyboardEventsModule,
9
+ KeyboardExtenderProps,
8
10
  KeyboardGestureAreaProps,
9
11
  OverKeyboardViewProps,
10
12
  WindowDimensionsEventsModule,
@@ -16,6 +18,7 @@ const NOOP = () => {};
16
18
  export const KeyboardControllerNative: KeyboardControllerNativeModule = {
17
19
  setDefaultMode: NOOP,
18
20
  setInputMode: NOOP,
21
+ preload: NOOP,
19
22
  dismiss: NOOP,
20
23
  setFocusTo: NOOP,
21
24
  addListener: NOOP,
@@ -59,3 +62,18 @@ export const KeyboardGestureArea =
59
62
  View as unknown as React.FC<KeyboardGestureAreaProps>;
60
63
  export const RCTOverKeyboardView =
61
64
  View as unknown as React.FC<OverKeyboardViewProps>;
65
+ /**
66
+ * A view that matches keyboard background.
67
+ *
68
+ * @see {@link https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/keyboard-background-view|Documentation} page for more details.
69
+ */
70
+ export const KeyboardBackgroundView =
71
+ View as unknown as React.FC<KeyboardBackgroundViewProps>;
72
+ /**
73
+ * A container that will embed its children into the keyboard
74
+ * and will always show them above the keyboard.
75
+ *
76
+ * @see {@link https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/keyboard-extender|Documentation} page for more details.
77
+ */
78
+ export const RCTKeyboardExtender =
79
+ View as unknown as React.FC<KeyboardExtenderProps>;
@@ -1,5 +1,6 @@
1
1
  import React, { forwardRef, useCallback, useEffect, useMemo } from "react";
2
2
  import Reanimated, {
3
+ clamp,
3
4
  interpolate,
4
5
  runOnUI,
5
6
  scrollTo,
@@ -32,7 +33,7 @@ import type {
32
33
  } from "react-native-keyboard-controller";
33
34
 
34
35
  export type KeyboardAwareScrollViewProps = {
35
- /** The distance between keyboard and focused `TextInput` when keyboard is shown. Default is `0`. */
36
+ /** The distance between the keyboard and the caret inside a focused `TextInput` when the keyboard is shown. Default is `0`. */
36
37
  bottomOffset?: number;
37
38
  /** Prevents automatic scrolling of the `ScrollView` when the keyboard gets hidden, maintaining the current screen position. Default is `false`. */
38
39
  disableScrollOnKeyboardHide?: boolean;
@@ -127,6 +128,8 @@ const KeyboardAwareScrollView = forwardRef<
127
128
  const scrollBeforeKeyboardMovement = useSharedValue(0);
128
129
  const { input } = useReanimatedFocusedInput();
129
130
  const layout = useSharedValue<FocusedInputLayoutChangedEvent | null>(null);
131
+ const lastSelection =
132
+ useSharedValue<FocusedInputSelectionChangedEvent | null>(null);
130
133
 
131
134
  const { height } = useWindowDimensions();
132
135
 
@@ -191,9 +194,9 @@ const KeyboardAwareScrollView = forwardRef<
191
194
  return interpolatedScrollTo;
192
195
  }
193
196
 
194
- if (absoluteY < 0) {
195
- const positionOnScreen = visibleRect - inputHeight - bottomOffset;
196
- const topOfScreen = scrollPosition.value + absoluteY;
197
+ if (point < 0) {
198
+ const positionOnScreen = visibleRect - bottomOffset;
199
+ const topOfScreen = scrollPosition.value + point;
197
200
 
198
201
  scrollTo(
199
202
  scrollViewAnimatedRef,
@@ -222,65 +225,89 @@ const KeyboardAwareScrollView = forwardRef<
222
225
  [extraKeyboardSpace],
223
226
  );
224
227
 
225
- const scrollFromCurrentPosition = useCallback(
226
- (customHeight?: number) => {
227
- "worklet";
228
+ const updateLayoutFromSelection = useCallback(() => {
229
+ "worklet";
228
230
 
229
- const prevScrollPosition = scrollPosition.value;
230
- const prevLayout = layout.value;
231
+ const customHeight = lastSelection.value?.selection.end.y;
231
232
 
232
- if (!input.value?.layout) {
233
- return;
234
- }
233
+ if (!input.value?.layout || !customHeight) {
234
+ return false;
235
+ }
235
236
 
236
- // eslint-disable-next-line react-compiler/react-compiler
237
- layout.value = {
238
- ...input.value,
239
- layout: {
240
- ...input.value.layout,
241
- height: customHeight ?? input.value.layout.height,
242
- },
243
- };
244
- scrollPosition.value = position.value;
245
- maybeScroll(keyboardHeight.value, true);
246
- scrollPosition.value = prevScrollPosition;
247
- layout.value = prevLayout;
248
- },
249
- [maybeScroll],
250
- );
251
- const onChangeText = useCallback(() => {
237
+ layout.value = {
238
+ ...input.value,
239
+ layout: {
240
+ ...input.value.layout,
241
+ // when we have multiline input with limited amount of lines, then custom height can be very big
242
+ // so we clamp it to max input height
243
+ height: clamp(customHeight, 0, input.value.layout.height),
244
+ },
245
+ };
246
+
247
+ return true;
248
+ }, [input, lastSelection, layout]);
249
+ const scrollFromCurrentPosition = useCallback(() => {
252
250
  "worklet";
253
251
 
254
- // if typing a text caused layout shift, then we need to ignore this handler
255
- // because this event will be handled in `useAnimatedReaction` below
256
- if (layout.value?.layout.height !== input.value?.layout.height) {
252
+ const prevScrollPosition = scrollPosition.value;
253
+ const prevLayout = layout.value;
254
+
255
+ if (!updateLayoutFromSelection()) {
257
256
  return;
258
257
  }
259
258
 
259
+ // eslint-disable-next-line react-compiler/react-compiler
260
+ scrollPosition.value = position.value;
261
+ maybeScroll(keyboardHeight.value, true);
262
+ scrollPosition.value = prevScrollPosition;
263
+ layout.value = prevLayout;
264
+ }, [maybeScroll]);
265
+ const onChangeText = useCallback(() => {
266
+ "worklet";
267
+
268
+ console.debug("maybeScroll - onChangeText");
260
269
  scrollFromCurrentPosition();
261
270
  }, [scrollFromCurrentPosition]);
271
+ const onChangeTextHandler = useMemo(
272
+ () => debounce(onChangeText, 200),
273
+ [onChangeText],
274
+ );
262
275
  const onSelectionChange = useCallback(
263
276
  (e: FocusedInputSelectionChangedEvent) => {
264
277
  "worklet";
265
278
 
279
+ const lastTarget = lastSelection.value?.target;
280
+ const latestSelection = lastSelection.value?.selection;
281
+
282
+ lastSelection.value = e;
283
+
284
+ if (e.target !== lastTarget) {
285
+ // ignore this event, because "focus changed" event handled in `useSmoothKeyboardHandler`
286
+ return;
287
+ }
288
+ // caret in the end + end coordinates has been changed -> we moved to a new line
289
+ // so input may grow
290
+ if (
291
+ e.selection.end.position === e.selection.start.position &&
292
+ latestSelection?.end.y !== e.selection.end.y
293
+ ) {
294
+ return scrollFromCurrentPosition();
295
+ }
296
+ // selection has been changed
266
297
  if (e.selection.start.position !== e.selection.end.position) {
267
- scrollFromCurrentPosition(e.selection.end.y);
298
+ return scrollFromCurrentPosition();
268
299
  }
269
- },
270
- [scrollFromCurrentPosition],
271
- );
272
300
 
273
- const onChangeTextHandler = useMemo(
274
- () => debounce(onChangeText, 200),
275
- [onChangeText],
301
+ onChangeTextHandler();
302
+ },
303
+ [scrollFromCurrentPosition, onChangeTextHandler],
276
304
  );
277
305
 
278
306
  useFocusedInputHandler(
279
307
  {
280
- onChangeText: onChangeTextHandler,
281
308
  onSelectionChange: onSelectionChange,
282
309
  },
283
- [onChangeTextHandler, onSelectionChange],
310
+ [onSelectionChange],
284
311
  );
285
312
 
286
313
  useSmoothKeyboardHandler(
@@ -322,9 +349,8 @@ const KeyboardAwareScrollView = forwardRef<
322
349
  // focus was changed
323
350
  if (focusWasChanged) {
324
351
  tag.value = e.target;
325
-
326
352
  // save position of focused text input when keyboard starts to move
327
- layout.value = input.value;
353
+ updateLayoutFromSelection();
328
354
  // save current scroll position - when keyboard will hide we'll reuse
329
355
  // this value to achieve smooth hide effect
330
356
  scrollBeforeKeyboardMovement.value = position.value;
@@ -369,11 +395,10 @@ const KeyboardAwareScrollView = forwardRef<
369
395
  current?.target === previous?.target &&
370
396
  current?.layout.height !== previous?.layout.height
371
397
  ) {
372
- const prevLayout = layout.value;
373
-
374
- layout.value = input.value;
375
- scrollPosition.value += maybeScroll(keyboardHeight.value, true);
376
- layout.value = prevLayout;
398
+ // input has changed layout - let's check if we need to scroll
399
+ // may happen when you paste text, then onSelectionChange will be
400
+ // fired earlier than text actually changes its layout
401
+ scrollFromCurrentPosition();
377
402
  }
378
403
  },
379
404
  [],
@@ -1,7 +1,7 @@
1
1
  import React, { useMemo } from "react";
2
2
  import { Animated, StyleSheet, View } from "react-native";
3
3
 
4
- import useColorScheme from "../hooks/useColorScheme";
4
+ import { useKeyboardState } from "../../hooks";
5
5
 
6
6
  import type { KeyboardToolbarTheme } from "./types";
7
7
  import type { ViewStyle } from "react-native";
@@ -13,7 +13,7 @@ type ArrowProps = {
13
13
  };
14
14
 
15
15
  const ArrowComponent: React.FC<ArrowProps> = ({ type, disabled, theme }) => {
16
- const colorScheme = useColorScheme();
16
+ const colorScheme = useKeyboardState((state) => state.appearance);
17
17
 
18
18
  const color = useMemo(
19
19
  () => ({
@@ -6,7 +6,7 @@ import {
6
6
  View,
7
7
  } from "react-native";
8
8
 
9
- import useColorScheme from "../hooks/useColorScheme";
9
+ import { useKeyboardState } from "../../hooks";
10
10
 
11
11
  import type { KeyboardToolbarTheme } from "./types";
12
12
  import type { PropsWithChildren } from "react";
@@ -64,7 +64,7 @@ const ButtonAndroid = ({
64
64
  style,
65
65
  theme,
66
66
  }: PropsWithChildren<ButtonProps>) => {
67
- const colorScheme = useColorScheme();
67
+ const colorScheme = useKeyboardState((state) => state.appearance);
68
68
  const accessibilityState = useMemo(() => ({ disabled }), [disabled]);
69
69
  const ripple = useMemo(
70
70
  () =>
@@ -2,8 +2,8 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { StyleSheet, Text, View } from "react-native";
3
3
 
4
4
  import { FocusedInputEvents } from "../../bindings";
5
+ import { useKeyboardState } from "../../hooks";
5
6
  import { KeyboardController } from "../../module";
6
- import useColorScheme from "../hooks/useColorScheme";
7
7
  import KeyboardStickyView from "../KeyboardStickyView";
8
8
 
9
9
  import Arrow from "./Arrow";
@@ -106,7 +106,7 @@ const KeyboardToolbar: React.FC<KeyboardToolbarProps> = (props) => {
106
106
  insets,
107
107
  ...rest
108
108
  } = props;
109
- const colorScheme = useColorScheme();
109
+ const colorScheme = useKeyboardState((state) => state.appearance);
110
110
  const [inputs, setInputs] = useState({
111
111
  current: 0,
112
112
  count: 0,
@@ -12,17 +12,25 @@ const getLatestState = () => ({
12
12
  isVisible: KeyboardController.isVisible(),
13
13
  });
14
14
 
15
+ type KeyboardStateSelector<T> = (state: KeyboardState) => T;
16
+
17
+ const defaultSelector: KeyboardStateSelector<KeyboardState> = (state) => state;
18
+
15
19
  /**
16
20
  * React Hook that represents the current keyboard state on iOS and Android.
17
21
  * It tracks keyboard visibility, height, appearance, type and other properties.
18
22
  * This hook subscribes to keyboard events and updates the state reactively.
19
23
  *
24
+ * @template T - A type of the returned object from the `selector`.
25
+ * @param selector - A function that receives the current keyboard state and picks only necessary properties to avoid frequent re-renders.
20
26
  * @returns Object {@link KeyboardState|containing} keyboard state information.
21
27
  * @see {@link https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/hooks/keyboard/use-keyboard-state|Documentation} page for more details.
22
28
  * @example
23
29
  * ```tsx
24
30
  * function MyComponent() {
25
- * const { isVisible, height } = useKeyboardState();
31
+ * const isVisible = useKeyboardState((state) => state.isVisible);
32
+ * const height = useKeyboardState((state) => state.height);
33
+ *
26
34
  * return (
27
35
  * <View>
28
36
  * <Text>Keyboard is {isVisible ? 'visible' : 'hidden'}</Text>
@@ -32,27 +40,38 @@ const getLatestState = () => ({
32
40
  * }
33
41
  * ```
34
42
  */
35
- export const useKeyboardState = (): KeyboardState => {
36
- const [state, setState] = useState(getLatestState);
43
+ function useKeyboardState<T = KeyboardState>(
44
+ selector: KeyboardStateSelector<T> = defaultSelector as KeyboardStateSelector<T>,
45
+ ): T {
46
+ const [state, setState] = useState<T>(() => selector(getLatestState()));
37
47
 
38
48
  useEffect(() => {
39
49
  const subscriptions = EVENTS.map((event) =>
40
50
  KeyboardEvents.addListener(event, () =>
41
51
  // state will be updated by global listener first,
42
52
  // so we simply read it and don't derive data from the event
43
- setState(getLatestState),
53
+ setState(selector(getLatestState())),
44
54
  ),
45
55
  );
56
+ // update `appearance` prematurely
57
+ const willShowSubscription = KeyboardEvents.addListener(
58
+ "keyboardWillShow",
59
+ (e) =>
60
+ setState(selector({ ...getLatestState(), appearance: e.appearance })),
61
+ );
46
62
 
47
63
  // we might have missed an update between reading a value in render and
48
64
  // `addListener` in this handler, so we set it here. If there was
49
65
  // no change, React will filter out this update as a no-op.
50
- setState(getLatestState);
66
+ setState(selector(getLatestState()));
51
67
 
52
68
  return () => {
53
69
  subscriptions.forEach((subscription) => subscription.remove());
70
+ willShowSubscription.remove();
54
71
  };
55
72
  }, []);
56
73
 
57
74
  return state;
58
- };
75
+ }
76
+
77
+ export { useKeyboardState };
package/src/index.ts CHANGED
@@ -20,4 +20,4 @@ export type {
20
20
  KeyboardAwareScrollViewProps,
21
21
  KeyboardToolbarProps,
22
22
  } from "./components";
23
- export { OverKeyboardView } from "./views";
23
+ export { OverKeyboardView, KeyboardExtender } from "./views";
package/src/module.ts CHANGED
@@ -13,7 +13,7 @@ let lastState: KeyboardEventData = {
13
13
  timestamp: new Date().getTime(),
14
14
  target: -1,
15
15
  type: "default",
16
- appearance: "default",
16
+ appearance: "light",
17
17
  };
18
18
 
19
19
  KeyboardEvents.addListener("keyboardDidHide", (e) => {
@@ -54,6 +54,7 @@ export const KeyboardController: KeyboardControllerModule = {
54
54
  setDefaultMode: KeyboardControllerNative.setDefaultMode,
55
55
  setInputMode: KeyboardControllerNative.setInputMode,
56
56
  setFocusTo: KeyboardControllerNative.setFocusTo,
57
+ preload: KeyboardControllerNative.preload,
57
58
  dismiss,
58
59
  isVisible,
59
60
  state,
@@ -0,0 +1,10 @@
1
+ import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent";
2
+
3
+ import type { HostComponent } from "react-native";
4
+ import type { ViewProps } from "react-native/Libraries/Components/View/ViewPropTypes";
5
+
6
+ export interface NativeProps extends ViewProps {}
7
+
8
+ export default codegenNativeComponent<NativeProps>("KeyboardBackgroundView", {
9
+ interfaceOnly: true,
10
+ }) as HostComponent<NativeProps>;
@@ -0,0 +1,13 @@
1
+ import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent";
2
+
3
+ import type { HostComponent } from "react-native";
4
+ import type { ViewProps } from "react-native/Libraries/Components/View/ViewPropTypes";
5
+
6
+ export interface NativeProps extends ViewProps {
7
+ enabled?: boolean;
8
+ }
9
+
10
+ export default codegenNativeComponent<NativeProps>("KeyboardExtender", {
11
+ interfaceOnly: true,
12
+ excludedPlatforms: ["android"],
13
+ }) as HostComponent<NativeProps>;
@@ -8,6 +8,7 @@ export interface Spec extends TurboModule {
8
8
  // methods
9
9
  setInputMode(mode: number): void;
10
10
  setDefaultMode(): void;
11
+ preload(): void;
11
12
  dismiss(keepFocus: boolean): void;
12
13
  setFocusTo(direction: string): void;
13
14
 
@@ -17,8 +17,8 @@ export type KeyboardEventData = {
17
17
  target: number;
18
18
  /** `keyboardType` property from focused `TextInput`. */
19
19
  type: NonNullable<TextInputProps["keyboardType"]>;
20
- /** Keyboard appearance. Can be one of `default`, `dark` or `light`. */
21
- appearance: NonNullable<TextInputProps["keyboardAppearance"]>;
20
+ /** Keyboard appearance. Can be one of `dark` or `light`. */
21
+ appearance: "dark" | "light";
22
22
  };
23
23
  /**
24
24
  * An object that represent current keyboard state.
@@ -59,13 +59,25 @@ export type KeyboardControllerModule = {
59
59
  /**
60
60
  * Sets default `windowSoftInputMode` (the one that declared in `AndroidManifest.xml`).
61
61
  *
62
+ * @platform android
62
63
  * @see {@link https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/keyboard-controller#setdefaultmode-|docs} page for more details.
63
64
  */
64
65
  setDefaultMode: () => void;
65
66
  /**
66
- * Changes `windowSoftInputMode` on Android. @see AndroidSoftInputModes for all possible values and {@link https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/keyboard-controller#setinputmode-|docs} page for more details.
67
+ * Changes `windowSoftInputMode` on Android.
68
+ *
69
+ * @platform android
70
+ * @see AndroidSoftInputModes for all possible values and {@link https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/keyboard-controller#setinputmode-|docs} page for more details.
67
71
  */
68
72
  setInputMode: (mode: number) => void;
73
+ // ios only
74
+ /**
75
+ * A method that preloads the keyboard. It's useful when you want to avoid a delay when the user focuses the first input.
76
+ *
77
+ * @platform ios
78
+ * @see {@link https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/keyboard-controller#preload-|docs} page for more details.
79
+ */
80
+ preload: () => void;
69
81
  // all platforms
70
82
  /**
71
83
  * Dismisses the active keyboard. Removes a focus by default, but allows to pass `{keepFocus: true}` to keep focus.
@@ -96,6 +108,8 @@ export type KeyboardControllerNativeModule = {
96
108
  // android only
97
109
  setDefaultMode: () => void;
98
110
  setInputMode: (mode: number) => void;
111
+ // ios only
112
+ preload: () => void;
99
113
  // all platforms
100
114
  dismiss: (keepFocus: boolean) => void;
101
115
  setFocusTo: (direction: Direction) => void;
@@ -44,3 +44,8 @@ export type OverKeyboardViewProps = PropsWithChildren<{
44
44
  */
45
45
  visible: boolean;
46
46
  }>;
47
+ export type KeyboardBackgroundViewProps = PropsWithChildren<ViewProps>;
48
+ export type KeyboardExtenderProps = PropsWithChildren<{
49
+ /** Controls whether this `KeyboardExtender` instance should take an effect. Default is `true`. */
50
+ enabled: boolean;
51
+ }>;
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+
4
+ import { RCTKeyboardExtender } from "../../bindings";
5
+ import { useWindowDimensions } from "../../hooks";
6
+
7
+ import type { KeyboardExtenderProps } from "../../types";
8
+ import type { PropsWithChildren } from "react";
9
+
10
+ /**
11
+ * A component that embeds its children into the keyboard thus enhancing keyboard functionality.
12
+ *
13
+ * @param props - Component props.
14
+ * @returns A view component that renders inside the keyboard above all system buttons.
15
+ * @example
16
+ * ```tsx
17
+ * <KeyboardExtender>
18
+ * <Button>10$</Button>
19
+ * <Button>20$</Button>
20
+ * <Button>50$</Button>
21
+ * </KeyboardExtender>
22
+ * ```
23
+ */
24
+ const KeyboardExtender = (props: PropsWithChildren<KeyboardExtenderProps>) => {
25
+ const { children, enabled = true } = props;
26
+ const { width } = useWindowDimensions();
27
+
28
+ return (
29
+ <RCTKeyboardExtender enabled={enabled}>
30
+ <View style={{ width }}>{children}</View>
31
+ </RCTKeyboardExtender>
32
+ );
33
+ };
34
+
35
+ export default KeyboardExtender;