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.
- package/LICENSE +21 -0
- package/README.md +387 -0
- package/android/app/build.gradle +182 -0
- package/android/app/debug.keystore +0 -0
- package/android/app/proguard-rules.pro +14 -0
- package/android/app/src/debug/AndroidManifest.xml +7 -0
- package/android/app/src/debugOptimized/AndroidManifest.xml +7 -0
- package/android/app/src/main/AndroidManifest.xml +25 -0
- package/android/app/src/main/java/com/anonymous/reactnativeuniversalkeyboardawarescrollview/MainActivity.kt +61 -0
- package/android/app/src/main/java/com/anonymous/reactnativeuniversalkeyboardawarescrollview/MainApplication.kt +56 -0
- package/android/app/src/main/res/drawable/ic_launcher_background.xml +6 -0
- package/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png +0 -0
- package/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png +0 -0
- package/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png +0 -0
- package/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png +0 -0
- package/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png +0 -0
- package/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp +0 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- package/android/app/src/main/res/values/colors.xml +6 -0
- package/android/app/src/main/res/values/strings.xml +5 -0
- package/android/app/src/main/res/values/styles.xml +11 -0
- package/android/app/src/main/res/values-night/colors.xml +1 -0
- package/android/build.gradle +89 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +65 -0
- package/android/gradlew +251 -0
- package/android/gradlew.bat +94 -0
- package/android/settings.gradle +39 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/universalkeyboard/UniversalKeyboardModule.kt +349 -0
- package/android/src/main/java/com/universalkeyboard/UniversalKeyboardPackage.kt +21 -0
- package/ios/.xcode.env +11 -0
- package/ios/Podfile +60 -0
- package/ios/Podfile.lock +2001 -0
- package/ios/Podfile.properties.json +5 -0
- package/ios/UniversalKeyboard.h +24 -0
- package/ios/UniversalKeyboard.m +413 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/AppDelegate.swift +70 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png +0 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/Contents.json +6 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenBackground.colorset/Contents.json +20 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +23 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenLegacy.imageset/image.png +0 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png +0 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png +0 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Info.plist +76 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/PrivacyInfo.xcprivacy +48 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/SplashScreen.storyboard +48 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Supporting/Expo.plist +12 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/reactnativeuniversalkeyboardawarescrollview-Bridging-Header.h +3 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/reactnativeuniversalkeyboardawarescrollview.entitlements +5 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview.xcodeproj/project.pbxproj +540 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview.xcodeproj/xcshareddata/xcschemes/reactnativeuniversalkeyboardawarescrollview.xcscheme +88 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview.xcworkspace/contents.xcworkspacedata +10 -0
- package/package.json +61 -0
- package/react-native-universal-keyboard-aware-scrollview.podspec +32 -0
- package/react-native.config.js +18 -0
- package/src/NativeModule.ts +61 -0
- package/src/components/KeyboardAwareScrollView.tsx +388 -0
- package/src/components/index.ts +5 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useKeyboard.ts +360 -0
- package/src/index.ts +27 -0
- package/src/types.ts +87 -0
- package/src/utils/KeyboardController.ts +112 -0
- 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>
|
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;
|