react-native-universal-keyboard-aware-scrollview 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +387 -0
  3. package/android/app/build.gradle +182 -0
  4. package/android/app/debug.keystore +0 -0
  5. package/android/app/proguard-rules.pro +14 -0
  6. package/android/app/src/debug/AndroidManifest.xml +7 -0
  7. package/android/app/src/debugOptimized/AndroidManifest.xml +7 -0
  8. package/android/app/src/main/AndroidManifest.xml +25 -0
  9. package/android/app/src/main/java/com/anonymous/reactnativeuniversalkeyboardawarescrollview/MainActivity.kt +61 -0
  10. package/android/app/src/main/java/com/anonymous/reactnativeuniversalkeyboardawarescrollview/MainApplication.kt +56 -0
  11. package/android/app/src/main/res/drawable/ic_launcher_background.xml +6 -0
  12. package/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  13. package/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png +0 -0
  14. package/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png +0 -0
  15. package/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png +0 -0
  16. package/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png +0 -0
  17. package/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png +0 -0
  18. package/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  19. package/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
  20. package/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  21. package/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp +0 -0
  22. package/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  23. package/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  24. package/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp +0 -0
  25. package/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  26. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  27. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp +0 -0
  28. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  29. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  30. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp +0 -0
  31. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  32. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  33. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp +0 -0
  34. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  35. package/android/app/src/main/res/values/colors.xml +6 -0
  36. package/android/app/src/main/res/values/strings.xml +5 -0
  37. package/android/app/src/main/res/values/styles.xml +11 -0
  38. package/android/app/src/main/res/values-night/colors.xml +1 -0
  39. package/android/build.gradle +89 -0
  40. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  41. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  42. package/android/gradle.properties +65 -0
  43. package/android/gradlew +251 -0
  44. package/android/gradlew.bat +94 -0
  45. package/android/settings.gradle +39 -0
  46. package/android/src/main/AndroidManifest.xml +3 -0
  47. package/android/src/main/java/com/universalkeyboard/UniversalKeyboardModule.kt +349 -0
  48. package/android/src/main/java/com/universalkeyboard/UniversalKeyboardPackage.kt +21 -0
  49. package/ios/.xcode.env +11 -0
  50. package/ios/Podfile +60 -0
  51. package/ios/Podfile.lock +2001 -0
  52. package/ios/Podfile.properties.json +5 -0
  53. package/ios/UniversalKeyboard.h +24 -0
  54. package/ios/UniversalKeyboard.m +413 -0
  55. package/ios/reactnativeuniversalkeyboardawarescrollview/AppDelegate.swift +70 -0
  56. package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png +0 -0
  57. package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/AppIcon.appiconset/Contents.json +14 -0
  58. package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/Contents.json +6 -0
  59. package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenBackground.colorset/Contents.json +20 -0
  60. package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +23 -0
  61. package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenLegacy.imageset/image.png +0 -0
  62. package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png +0 -0
  63. package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png +0 -0
  64. package/ios/reactnativeuniversalkeyboardawarescrollview/Info.plist +76 -0
  65. package/ios/reactnativeuniversalkeyboardawarescrollview/PrivacyInfo.xcprivacy +48 -0
  66. package/ios/reactnativeuniversalkeyboardawarescrollview/SplashScreen.storyboard +48 -0
  67. package/ios/reactnativeuniversalkeyboardawarescrollview/Supporting/Expo.plist +12 -0
  68. package/ios/reactnativeuniversalkeyboardawarescrollview/reactnativeuniversalkeyboardawarescrollview-Bridging-Header.h +3 -0
  69. package/ios/reactnativeuniversalkeyboardawarescrollview/reactnativeuniversalkeyboardawarescrollview.entitlements +5 -0
  70. package/ios/reactnativeuniversalkeyboardawarescrollview.xcodeproj/project.pbxproj +540 -0
  71. package/ios/reactnativeuniversalkeyboardawarescrollview.xcodeproj/xcshareddata/xcschemes/reactnativeuniversalkeyboardawarescrollview.xcscheme +88 -0
  72. package/ios/reactnativeuniversalkeyboardawarescrollview.xcworkspace/contents.xcworkspacedata +10 -0
  73. package/package.json +61 -0
  74. package/react-native-universal-keyboard-aware-scrollview.podspec +32 -0
  75. package/react-native.config.js +18 -0
  76. package/src/NativeModule.ts +61 -0
  77. package/src/components/KeyboardAwareScrollView.tsx +388 -0
  78. package/src/components/index.ts +5 -0
  79. package/src/hooks/index.ts +2 -0
  80. package/src/hooks/useKeyboard.ts +360 -0
  81. package/src/index.ts +27 -0
  82. package/src/types.ts +87 -0
  83. package/src/utils/KeyboardController.ts +112 -0
  84. package/src/utils/index.ts +1 -0
@@ -0,0 +1,88 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Scheme
3
+ LastUpgradeVersion = "1130"
4
+ version = "1.3">
5
+ <BuildAction
6
+ parallelizeBuildables = "YES"
7
+ buildImplicitDependencies = "YES">
8
+ <BuildActionEntries>
9
+ <BuildActionEntry
10
+ buildForTesting = "YES"
11
+ buildForRunning = "YES"
12
+ buildForProfiling = "YES"
13
+ buildForArchiving = "YES"
14
+ buildForAnalyzing = "YES">
15
+ <BuildableReference
16
+ BuildableIdentifier = "primary"
17
+ BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
18
+ BuildableName = "reactnativeuniversalkeyboardawarescrollview.app"
19
+ BlueprintName = "reactnativeuniversalkeyboardawarescrollview"
20
+ ReferencedContainer = "container:reactnativeuniversalkeyboardawarescrollview.xcodeproj">
21
+ </BuildableReference>
22
+ </BuildActionEntry>
23
+ </BuildActionEntries>
24
+ </BuildAction>
25
+ <TestAction
26
+ buildConfiguration = "Debug"
27
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
28
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29
+ shouldUseLaunchSchemeArgsEnv = "YES">
30
+ <Testables>
31
+ <TestableReference
32
+ skipped = "NO">
33
+ <BuildableReference
34
+ BuildableIdentifier = "primary"
35
+ BlueprintIdentifier = "00E356ED1AD99517003FC87E"
36
+ BuildableName = "reactnativeuniversalkeyboardawarescrollviewTests.xctest"
37
+ BlueprintName = "reactnativeuniversalkeyboardawarescrollviewTests"
38
+ ReferencedContainer = "container:reactnativeuniversalkeyboardawarescrollview.xcodeproj">
39
+ </BuildableReference>
40
+ </TestableReference>
41
+ </Testables>
42
+ </TestAction>
43
+ <LaunchAction
44
+ buildConfiguration = "Debug"
45
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
46
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
47
+ launchStyle = "0"
48
+ useCustomWorkingDirectory = "NO"
49
+ ignoresPersistentStateOnLaunch = "NO"
50
+ debugDocumentVersioning = "YES"
51
+ debugServiceExtension = "internal"
52
+ allowLocationSimulation = "YES">
53
+ <BuildableProductRunnable
54
+ runnableDebuggingMode = "0">
55
+ <BuildableReference
56
+ BuildableIdentifier = "primary"
57
+ BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
58
+ BuildableName = "reactnativeuniversalkeyboardawarescrollview.app"
59
+ BlueprintName = "reactnativeuniversalkeyboardawarescrollview"
60
+ ReferencedContainer = "container:reactnativeuniversalkeyboardawarescrollview.xcodeproj">
61
+ </BuildableReference>
62
+ </BuildableProductRunnable>
63
+ </LaunchAction>
64
+ <ProfileAction
65
+ buildConfiguration = "Release"
66
+ shouldUseLaunchSchemeArgsEnv = "YES"
67
+ savedToolIdentifier = ""
68
+ useCustomWorkingDirectory = "NO"
69
+ debugDocumentVersioning = "YES">
70
+ <BuildableProductRunnable
71
+ runnableDebuggingMode = "0">
72
+ <BuildableReference
73
+ BuildableIdentifier = "primary"
74
+ BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
75
+ BuildableName = "reactnativeuniversalkeyboardawarescrollview.app"
76
+ BlueprintName = "reactnativeuniversalkeyboardawarescrollview"
77
+ ReferencedContainer = "container:reactnativeuniversalkeyboardawarescrollview.xcodeproj">
78
+ </BuildableReference>
79
+ </BuildableProductRunnable>
80
+ </ProfileAction>
81
+ <AnalyzeAction
82
+ buildConfiguration = "Debug">
83
+ </AnalyzeAction>
84
+ <ArchiveAction
85
+ buildConfiguration = "Release"
86
+ revealArchiveInOrganizer = "YES">
87
+ </ArchiveAction>
88
+ </Scheme>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Workspace
3
+ version = "1.0">
4
+ <FileRef
5
+ location = "group:reactnativeuniversalkeyboardawarescrollview.xcodeproj">
6
+ </FileRef>
7
+ <FileRef
8
+ location = "group:Pods/Pods.xcodeproj">
9
+ </FileRef>
10
+ </Workspace>
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "react-native-universal-keyboard-aware-scrollview",
3
+ "version": "1.0.0",
4
+ "description": "A universal keyboard-aware ScrollView for React Native that works correctly in normal screens, modals, and bottom sheets on both Android and iOS",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "react-native": "src/index.ts",
8
+ "source": "src/index.ts",
9
+ "files": [
10
+ "src",
11
+ "android",
12
+ "ios",
13
+ "react-native.config.js",
14
+ "react-native-universal-keyboard-aware-scrollview.podspec",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "typescript": "tsc --noEmit",
19
+ "lint": "eslint \"**/*.{js,ts,tsx}\""
20
+ },
21
+ "keywords": [
22
+ "react-native",
23
+ "keyboard",
24
+ "keyboard-aware",
25
+ "scrollview",
26
+ "modal",
27
+ "bottom-sheet",
28
+ "android",
29
+ "ios",
30
+ "expo",
31
+ "typescript"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/AetherTechDev/react-native-universal-keyboard-aware-scrollview.git"
36
+ },
37
+ "author": "Vijay Kishan <vijay@aethertech.dev> (https://github.com/AetherTechDev)",
38
+ "license": "MIT",
39
+ "bugs": {
40
+ "url": "https://github.com/AetherTechDev/react-native-universal-keyboard-aware-scrollview/issues"
41
+ },
42
+ "homepage": "https://github.com/AetherTechDev/react-native-universal-keyboard-aware-scrollview#readme",
43
+ "publishConfig": {
44
+ "registry": "https://registry.npmjs.org/"
45
+ },
46
+ "peerDependencies": {
47
+ "react": ">=16.8.0",
48
+ "react-native": ">=0.60.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "react": {
52
+ "optional": false
53
+ },
54
+ "react-native": {
55
+ "optional": false
56
+ }
57
+ },
58
+ "engines": {
59
+ "node": ">=16"
60
+ }
61
+ }
@@ -0,0 +1,32 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "react-native-universal-keyboard-aware-scrollview"
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = <<-DESC
10
+ A universal keyboard-aware ScrollView for React Native that works correctly
11
+ in normal screens, modals, and bottom sheets on both Android and iOS.
12
+ Uses native keyboard listeners for reliable keyboard height detection.
13
+ DESC
14
+ s.homepage = package['homepage']
15
+ s.license = package['license']
16
+ s.author = package['author']
17
+ s.platforms = { :ios => "12.0" }
18
+ s.source = { :git => package['repository']['url'], :tag => "v#{s.version}" }
19
+
20
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
21
+
22
+ # Use frameworks for Swift support
23
+ s.pod_target_xcconfig = {
24
+ 'DEFINES_MODULE' => 'YES',
25
+ 'SWIFT_OPTIMIZATION_LEVEL' => '-Onone'
26
+ }
27
+
28
+ # React Native dependency
29
+ install_modules_dependencies(s) if defined?(install_modules_dependencies)
30
+
31
+ s.dependency "React-Core"
32
+ end
@@ -0,0 +1,18 @@
1
+ /**
2
+ * React Native Config for native module linking
3
+ * This configuration enables automatic linking for React Native CLI projects
4
+ */
5
+ module.exports = {
6
+ dependency: {
7
+ platforms: {
8
+ android: {
9
+ sourceDir: './android',
10
+ packageImportPath: 'import com.universalkeyboard.UniversalKeyboardPackage;',
11
+ packageInstance: 'new UniversalKeyboardPackage()',
12
+ },
13
+ ios: {
14
+ podspecPath: './react-native-universal-keyboard-aware-scrollview.podspec',
15
+ },
16
+ },
17
+ },
18
+ };
@@ -0,0 +1,61 @@
1
+ import { NativeModules, NativeEventEmitter, Platform } from 'react-native';
2
+ import type { UniversalKeyboardNativeModule, KeyboardEvent } from './types';
3
+
4
+ const LINKING_ERROR =
5
+ `The package 'react-native-universal-keyboard-aware-scrollview' doesn't seem to be linked. Make sure: \n\n` +
6
+ Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
7
+ '- You rebuilt the app after installing the package\n' +
8
+ '- You are not using Expo Go (use development build instead)\n';
9
+
10
+ /**
11
+ * Native module instance
12
+ */
13
+ const NativeModule: UniversalKeyboardNativeModule | undefined =
14
+ NativeModules.UniversalKeyboard;
15
+
16
+ /**
17
+ * Proxy that throws a helpful error if the native module is not linked
18
+ */
19
+ export const UniversalKeyboardModule: UniversalKeyboardNativeModule = NativeModule
20
+ ? NativeModule
21
+ : new Proxy({} as UniversalKeyboardNativeModule, {
22
+ get() {
23
+ throw new Error(LINKING_ERROR);
24
+ },
25
+ });
26
+
27
+ /**
28
+ * Event emitter for keyboard events
29
+ */
30
+ export const KeyboardEventEmitter = NativeModule
31
+ ? new NativeEventEmitter(NativeModule as any)
32
+ : null;
33
+
34
+ /**
35
+ * Subscribe to keyboard events
36
+ * @param eventName - Name of the event to subscribe to
37
+ * @param callback - Callback function to execute when event fires
38
+ * @returns Cleanup function to unsubscribe
39
+ */
40
+ export function subscribeToKeyboardEvent(
41
+ eventName: string,
42
+ callback: (event: KeyboardEvent) => void
43
+ ): () => void {
44
+ if (!KeyboardEventEmitter) {
45
+ console.warn(LINKING_ERROR);
46
+ return () => {};
47
+ }
48
+
49
+ const subscription = KeyboardEventEmitter.addListener(eventName, callback);
50
+
51
+ return () => {
52
+ subscription.remove();
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Check if the native module is available
58
+ */
59
+ export function isNativeModuleAvailable(): boolean {
60
+ return NativeModule != null;
61
+ }
@@ -0,0 +1,388 @@
1
+ import React, {
2
+ useCallback,
3
+ useRef,
4
+ useImperativeHandle,
5
+ forwardRef,
6
+ useState,
7
+ useEffect,
8
+ } from 'react';
9
+ import {
10
+ ScrollView,
11
+ ScrollViewProps,
12
+ View,
13
+ TextInput,
14
+ Platform,
15
+ Animated,
16
+ StyleSheet,
17
+ Dimensions,
18
+ findNodeHandle,
19
+ UIManager,
20
+ LayoutChangeEvent,
21
+ } from 'react-native';
22
+ import { useKeyboard } from '../hooks/useKeyboard';
23
+
24
+ const { height: SCREEN_HEIGHT } = Dimensions.get('window');
25
+
26
+ /**
27
+ * Props for KeyboardAwareScrollView
28
+ */
29
+ export interface KeyboardAwareScrollViewProps extends ScrollViewProps {
30
+ /** Enable keyboard handling on Android (default: true) */
31
+ enableOnAndroid?: boolean;
32
+ /** Enable keyboard handling on iOS (default: true) */
33
+ enableOnIOS?: boolean;
34
+ /** Extra space to add above the keyboard (default: 20) */
35
+ extraScrollHeight?: number;
36
+ /** Extra space for focused element (default: 75) */
37
+ extraHeight?: number;
38
+ /** Whether to use animations (default: true) */
39
+ enableAnimation?: boolean;
40
+ /** Animation duration in ms (default: 250) */
41
+ animationDuration?: number;
42
+ /** Whether to auto-scroll when keyboard shows (default: true) */
43
+ enableAutoScrollToFocused?: boolean;
44
+ /** Whether to reset scroll position when keyboard hides (default: false) */
45
+ resetScrollToCoords?: { x: number; y: number } | null;
46
+ /** Whether to enable keyboard should persist taps (default: 'handled') */
47
+ keyboardShouldPersistTaps?: 'always' | 'never' | 'handled';
48
+ /** Callback when keyboard will show */
49
+ onKeyboardWillShow?: () => void;
50
+ /** Callback when keyboard will hide */
51
+ onKeyboardWillHide?: () => void;
52
+ /** Callback when keyboard did show */
53
+ onKeyboardDidShow?: () => void;
54
+ /** Callback when keyboard did hide */
55
+ onKeyboardDidHide?: () => void;
56
+ /** Whether to treat as being inside a modal (improves behavior in modals) */
57
+ insideModal?: boolean;
58
+ /** Content container style */
59
+ contentContainerStyle?: ScrollViewProps['contentContainerStyle'];
60
+ /** Inner container style (wraps content) */
61
+ innerContentContainerStyle?: ScrollViewProps['contentContainerStyle'];
62
+ /** Whether to show keyboard spacer view (alternative approach) */
63
+ enableKeyboardSpacer?: boolean;
64
+ /** Use content inset instead of padding (iOS only) */
65
+ useContentInset?: boolean;
66
+ }
67
+
68
+ /**
69
+ * Ref methods exposed by KeyboardAwareScrollView
70
+ */
71
+ export interface KeyboardAwareScrollViewRef {
72
+ /** Scroll to a specific position */
73
+ scrollTo: (options: { x?: number; y?: number; animated?: boolean }) => void;
74
+ /** Scroll to end of content */
75
+ scrollToEnd: (options?: { animated?: boolean }) => void;
76
+ /** Scroll to make a specific input visible */
77
+ scrollToFocusedInput: (input: React.RefObject<TextInput>) => void;
78
+ /** Get the underlying ScrollView ref */
79
+ getScrollResponder: () => ScrollView | null;
80
+ /** Dismiss keyboard */
81
+ dismissKeyboard: () => Promise<void>;
82
+ }
83
+
84
+ /**
85
+ * KeyboardAwareScrollView - A ScrollView that automatically adjusts for the keyboard
86
+ *
87
+ * This component provides reliable keyboard avoidance that works in:
88
+ * - Normal React Native screens
89
+ * - React Native Modal components
90
+ * - BottomSheet components
91
+ * - Any overlay/presentation scenarios
92
+ *
93
+ * @example
94
+ * ```tsx
95
+ * <KeyboardAwareScrollView
96
+ * extraScrollHeight={50}
97
+ * enableOnAndroid={true}
98
+ * >
99
+ * <TextInput placeholder="Name" />
100
+ * <TextInput placeholder="Email" />
101
+ * <TextInput placeholder="Message" multiline />
102
+ * </KeyboardAwareScrollView>
103
+ * ```
104
+ */
105
+ export const KeyboardAwareScrollView = forwardRef<
106
+ KeyboardAwareScrollViewRef,
107
+ KeyboardAwareScrollViewProps
108
+ >((props, ref) => {
109
+ const {
110
+ enableOnAndroid = true,
111
+ enableOnIOS = true,
112
+ extraScrollHeight = 20,
113
+ extraHeight = 75,
114
+ enableAnimation = true,
115
+ animationDuration = 250,
116
+ enableAutoScrollToFocused = true,
117
+ resetScrollToCoords = null,
118
+ keyboardShouldPersistTaps = 'handled',
119
+ onKeyboardWillShow,
120
+ onKeyboardWillHide,
121
+ onKeyboardDidShow,
122
+ onKeyboardDidHide,
123
+ insideModal = false,
124
+ contentContainerStyle,
125
+ innerContentContainerStyle,
126
+ enableKeyboardSpacer = true,
127
+ useContentInset = Platform.OS === 'ios',
128
+ children,
129
+ style,
130
+ ...scrollViewProps
131
+ } = props;
132
+
133
+ const scrollViewRef = useRef<ScrollView>(null);
134
+ const contentRef = useRef<View>(null);
135
+ const focusedInputRef = useRef<any>(null);
136
+ const scrollPositionRef = useRef({ x: 0, y: 0 });
137
+ const contentHeightRef = useRef(0);
138
+ const scrollViewHeightRef = useRef(0);
139
+
140
+ const [bottomPadding, setBottomPadding] = useState(0);
141
+ const animatedPadding = useRef(new Animated.Value(0)).current;
142
+
143
+ const {
144
+ keyboardHeight,
145
+ isKeyboardVisible,
146
+ dismissKeyboard,
147
+ safeAreaBottom,
148
+ } = useKeyboard({
149
+ enableOnAndroid,
150
+ enableOnIOS,
151
+ onKeyboardWillShow: () => {
152
+ onKeyboardWillShow?.();
153
+ },
154
+ onKeyboardWillHide: () => {
155
+ onKeyboardWillHide?.();
156
+ },
157
+ onKeyboardDidShow: () => {
158
+ onKeyboardDidShow?.();
159
+ },
160
+ onKeyboardDidHide: () => {
161
+ onKeyboardDidHide?.();
162
+ // Reset scroll position if specified
163
+ if (resetScrollToCoords) {
164
+ scrollViewRef.current?.scrollTo({
165
+ x: resetScrollToCoords.x,
166
+ y: resetScrollToCoords.y,
167
+ animated: enableAnimation,
168
+ });
169
+ }
170
+ },
171
+ onKeyboardHeightChange: (height) => {
172
+ handleKeyboardHeightChange(height);
173
+ },
174
+ });
175
+
176
+ // Handle keyboard height changes
177
+ const handleKeyboardHeightChange = useCallback(
178
+ (height: number) => {
179
+ const adjustedHeight = height > 0 ? height + extraScrollHeight : 0;
180
+
181
+ if (enableAnimation) {
182
+ Animated.timing(animatedPadding, {
183
+ toValue: adjustedHeight,
184
+ duration: animationDuration,
185
+ useNativeDriver: false,
186
+ }).start();
187
+ } else {
188
+ animatedPadding.setValue(adjustedHeight);
189
+ }
190
+
191
+ setBottomPadding(adjustedHeight);
192
+
193
+ // Auto-scroll to focused input
194
+ if (height > 0 && enableAutoScrollToFocused) {
195
+ setTimeout(() => {
196
+ scrollToFocusedInput();
197
+ }, 100);
198
+ }
199
+ },
200
+ [
201
+ extraScrollHeight,
202
+ enableAnimation,
203
+ animationDuration,
204
+ animatedPadding,
205
+ enableAutoScrollToFocused,
206
+ ]
207
+ );
208
+
209
+ // Find and scroll to currently focused input
210
+ const scrollToFocusedInput = useCallback(() => {
211
+ const currentlyFocused = TextInput.State.currentlyFocusedInput?.();
212
+
213
+ if (!currentlyFocused) return;
214
+
215
+ focusedInputRef.current = currentlyFocused;
216
+
217
+ // Measure focused input position
218
+ const scrollView = scrollViewRef.current;
219
+ if (!scrollView) return;
220
+
221
+ const scrollNode = findNodeHandle(scrollView);
222
+ const inputNode = findNodeHandle(currentlyFocused);
223
+
224
+ if (!scrollNode || !inputNode) return;
225
+
226
+ // Get positions relative to scroll view
227
+ UIManager.measureLayout(
228
+ inputNode,
229
+ scrollNode,
230
+ () => {
231
+ // Error callback - ignore
232
+ },
233
+ (x, y, width, height) => {
234
+ // Calculate scroll position
235
+ const inputBottom = y + height + extraHeight;
236
+ const visibleHeight =
237
+ scrollViewHeightRef.current - keyboardHeight - safeAreaBottom;
238
+
239
+ if (inputBottom > scrollPositionRef.current.y + visibleHeight) {
240
+ const newScrollY = inputBottom - visibleHeight;
241
+ scrollView.scrollTo({
242
+ x: 0,
243
+ y: Math.max(0, newScrollY),
244
+ animated: enableAnimation,
245
+ });
246
+ } else if (y < scrollPositionRef.current.y) {
247
+ scrollView.scrollTo({
248
+ x: 0,
249
+ y: Math.max(0, y - extraHeight),
250
+ animated: enableAnimation,
251
+ });
252
+ }
253
+ }
254
+ );
255
+ }, [keyboardHeight, extraHeight, safeAreaBottom, enableAnimation]);
256
+
257
+ // Track scroll position
258
+ const handleScroll = useCallback(
259
+ (event: any) => {
260
+ scrollPositionRef.current = event.nativeEvent.contentOffset;
261
+ props.onScroll?.(event);
262
+ },
263
+ [props.onScroll]
264
+ );
265
+
266
+ // Track content size
267
+ const handleContentSizeChange = useCallback(
268
+ (contentWidth: number, contentHeight: number) => {
269
+ contentHeightRef.current = contentHeight;
270
+ props.onContentSizeChange?.(contentWidth, contentHeight);
271
+ },
272
+ [props.onContentSizeChange]
273
+ );
274
+
275
+ // Track scroll view layout
276
+ const handleLayout = useCallback(
277
+ (event: LayoutChangeEvent) => {
278
+ scrollViewHeightRef.current = event.nativeEvent.layout.height;
279
+ props.onLayout?.(event);
280
+ },
281
+ [props.onLayout]
282
+ );
283
+
284
+ // Scroll to a specific input
285
+ const scrollToFocusedInputRef = useCallback(
286
+ (inputRef: React.RefObject<TextInput>) => {
287
+ if (!inputRef.current || !scrollViewRef.current) return;
288
+
289
+ const scrollNode = findNodeHandle(scrollViewRef.current);
290
+ const inputNode = findNodeHandle(inputRef.current);
291
+
292
+ if (!scrollNode || !inputNode) return;
293
+
294
+ UIManager.measureLayout(
295
+ inputNode,
296
+ scrollNode,
297
+ () => {},
298
+ (x, y, width, height) => {
299
+ scrollViewRef.current?.scrollTo({
300
+ x: 0,
301
+ y: Math.max(0, y - extraHeight),
302
+ animated: enableAnimation,
303
+ });
304
+ }
305
+ );
306
+ },
307
+ [extraHeight, enableAnimation]
308
+ );
309
+
310
+ // Expose ref methods
311
+ useImperativeHandle(
312
+ ref,
313
+ () => ({
314
+ scrollTo: (options) => {
315
+ scrollViewRef.current?.scrollTo(options);
316
+ },
317
+ scrollToEnd: (options) => {
318
+ scrollViewRef.current?.scrollToEnd(options);
319
+ },
320
+ scrollToFocusedInput: scrollToFocusedInputRef,
321
+ getScrollResponder: () => scrollViewRef.current,
322
+ dismissKeyboard,
323
+ }),
324
+ [scrollToFocusedInputRef, dismissKeyboard]
325
+ );
326
+
327
+ // Calculate content inset for iOS
328
+ const contentInset = useContentInset
329
+ ? { bottom: keyboardHeight > 0 ? keyboardHeight + extraScrollHeight : 0 }
330
+ : undefined;
331
+
332
+ const scrollIndicatorInsets = useContentInset
333
+ ? { bottom: keyboardHeight > 0 ? keyboardHeight + extraScrollHeight : 0 }
334
+ : undefined;
335
+
336
+ // Merge content container styles
337
+ const mergedContentContainerStyle = [
338
+ contentContainerStyle,
339
+ !useContentInset && enableKeyboardSpacer
340
+ ? { paddingBottom: bottomPadding }
341
+ : undefined,
342
+ ];
343
+
344
+ return (
345
+ <ScrollView
346
+ ref={scrollViewRef}
347
+ style={style}
348
+ contentContainerStyle={mergedContentContainerStyle}
349
+ keyboardShouldPersistTaps={keyboardShouldPersistTaps}
350
+ keyboardDismissMode="interactive"
351
+ onScroll={handleScroll}
352
+ onContentSizeChange={handleContentSizeChange}
353
+ onLayout={handleLayout}
354
+ scrollEventThrottle={16}
355
+ contentInset={contentInset}
356
+ scrollIndicatorInsets={scrollIndicatorInsets}
357
+ automaticallyAdjustKeyboardInsets={false}
358
+ automaticallyAdjustContentInsets={false}
359
+ {...scrollViewProps}
360
+ >
361
+ <View ref={contentRef} style={innerContentContainerStyle}>
362
+ {children}
363
+ </View>
364
+
365
+ {/* Keyboard spacer - alternative approach for specific cases */}
366
+ {enableKeyboardSpacer && !useContentInset && (
367
+ <Animated.View
368
+ style={[
369
+ styles.keyboardSpacer,
370
+ {
371
+ height: animatedPadding,
372
+ },
373
+ ]}
374
+ />
375
+ )}
376
+ </ScrollView>
377
+ );
378
+ });
379
+
380
+ KeyboardAwareScrollView.displayName = 'KeyboardAwareScrollView';
381
+
382
+ const styles = StyleSheet.create({
383
+ keyboardSpacer: {
384
+ // Empty spacer view that expands when keyboard is shown
385
+ },
386
+ });
387
+
388
+ export default KeyboardAwareScrollView;
@@ -0,0 +1,5 @@
1
+ export { KeyboardAwareScrollView } from './KeyboardAwareScrollView';
2
+ export type {
3
+ KeyboardAwareScrollViewProps,
4
+ KeyboardAwareScrollViewRef,
5
+ } from './KeyboardAwareScrollView';
@@ -0,0 +1,2 @@
1
+ export { useKeyboard } from './useKeyboard';
2
+ export type { UseKeyboardOptions, UseKeyboardReturn } from './useKeyboard';