├── .editorconfig
├── .gitattributes
├── .github
├── actions
│ └── setup
│ │ └── action.yml
├── images
│ ├── rnek-android-example.gif
│ └── rnek-ios-example.gif
└── workflows
│ └── ci.yml
├── .gitignore
├── .nvmrc
├── .watchmanconfig
├── .yarn
└── patches
│ └── react-native-builder-bob-npm-0.35.2-4e84215963.patch
├── .yarnrc.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── android
├── build.gradle
├── gradle.properties
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── AndroidManifestNew.xml
│ └── java
│ │ └── com
│ │ └── externalkeyboard
│ │ ├── ExternalKeyboardViewPackage.java
│ │ ├── events
│ │ ├── EventHelper.java
│ │ ├── FocusChangeEvent.java
│ │ ├── KeyPressDownEvent.java
│ │ ├── KeyPressUpEvent.java
│ │ └── MultiplyTextSubmit.java
│ │ ├── helper
│ │ └── FocusHelper.java
│ │ ├── modules
│ │ └── ExternalKeyboardModule.java
│ │ ├── services
│ │ ├── KeyboardKeyPressHandler.java
│ │ └── KeyboardService.java
│ │ └── views
│ │ ├── ExternalKeyboardView
│ │ ├── ExternalKeyboardView.java
│ │ └── ExternalKeyboardViewManager.java
│ │ ├── KeyboardFocusGroup
│ │ ├── KeyboardFocusGroup.java
│ │ └── KeyboardFocusGroupManager.java
│ │ └── TextInputFocusWrapper
│ │ ├── TextInputFocusWrapper.java
│ │ └── TextInputFocusWrapperManager.java
│ ├── newarch
│ ├── ExternalKeyboardModuleSpec.java
│ ├── ExternalKeyboardViewManagerSpec.java
│ ├── KeyboardFocusGroupManagerSpec.java
│ └── TextInputFocusWrapperManagerSpec.java
│ └── oldarch
│ ├── ExternalKeyboardModuleSpec.java
│ ├── ExternalKeyboardViewManagerSpec.java
│ ├── KeyboardFocusGroupManagerSpec.java
│ └── TextInputFocusWrapperManagerSpec.java
├── babel.config.js
├── example
├── .bundle
│ └── config
├── .node-version
├── .watchmanconfig
├── .yarnrc.yml
├── Gemfile
├── Gemfile.lock
├── android
│ ├── app
│ │ ├── build.gradle
│ │ ├── debug.keystore
│ │ ├── proguard-rules.pro
│ │ └── src
│ │ │ ├── debug
│ │ │ └── AndroidManifest.xml
│ │ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java
│ │ │ └── externalkeyboard
│ │ │ │ └── example
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── MainApplication.kt
│ │ │ └── res
│ │ │ ├── drawable
│ │ │ └── rn_edit_text_material.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ └── values
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ ├── build.gradle
│ ├── gradle.properties
│ ├── gradle
│ │ └── wrapper
│ │ │ ├── gradle-wrapper.jar
│ │ │ └── gradle-wrapper.properties
│ ├── gradlew
│ ├── gradlew.bat
│ └── settings.gradle
├── app.json
├── babel.config.js
├── index.js
├── ios
│ ├── .xcode.env
│ ├── ExternalKeyboardExample-Bridging-Header.h
│ ├── ExternalKeyboardExample.xcodeproj
│ │ ├── project.pbxproj
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── ExternalKeyboardExample.xcscheme
│ ├── ExternalKeyboardExample.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── ExternalKeyboardExample
│ │ ├── AppDelegate.h
│ │ ├── AppDelegate.mm
│ │ ├── Images.xcassets
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── Info.plist
│ │ ├── LaunchScreen.storyboard
│ │ ├── PrivacyInfo.xcprivacy
│ │ └── main.m
│ ├── ExternalKeyboardExampleTests
│ │ ├── ExternalKeyboardExampleTests.m
│ │ └── Info.plist
│ ├── File.swift
│ ├── Podfile
│ └── Podfile.lock
├── metro.config.js
├── package-lock.json
├── package.json
├── react-native.config.js
└── src
│ ├── App.tsx
│ ├── BaseExample.tsx
│ ├── ModalExample.tsx
│ ├── components
│ ├── Cats
│ │ ├── Cats.tsx
│ │ ├── FocusableImage.tsx
│ │ └── assets
│ │ │ ├── 01.png
│ │ │ ├── 02.png
│ │ │ ├── 03.png
│ │ │ ├── 06.png
│ │ │ ├── 07.png
│ │ │ ├── 08.png
│ │ │ ├── 09.png
│ │ │ ├── 10.png
│ │ │ ├── 11.png
│ │ │ ├── 12.png
│ │ │ ├── 13.png
│ │ │ ├── 14.png
│ │ │ ├── 15.png
│ │ │ ├── 16.png
│ │ │ ├── 17.png
│ │ │ ├── 18.png
│ │ │ ├── 19.png
│ │ │ └── 20.png
│ ├── ComponentsExample
│ │ └── ComponentsExample.tsx
│ ├── ContrastColors
│ │ ├── Color
│ │ │ └── Color.tsx
│ │ ├── ContrastColors.tsx
│ │ └── FocusGroupExample.tsx
│ └── LineButton
│ │ └── LineButton.tsx
│ └── screens
│ └── Home
│ └── Home.tsx
├── ios
├── Delegates
│ ├── RNCEKVFocusDelegate
│ │ ├── RNCEKVFocusDelegate.h
│ │ ├── RNCEKVFocusDelegate.mm
│ │ └── RNCEKVFocusProtocol.h
│ ├── RNCEKVGroupIdentifierDelegate
│ │ ├── RNCEKVGroupIdentifierDelegate.h
│ │ ├── RNCEKVGroupIdentifierDelegate.mm
│ │ └── RNCEKVGroupIdentifierProtocol.h
│ └── RNCEKVHaloDelegate
│ │ ├── RNCEKVHaloDelegate.h
│ │ ├── RNCEKVHaloDelegate.mm
│ │ └── RNCEKVHaloProtocol.h
├── Extensions
│ ├── RCTTextInputComponentView+RNCEKVExternalKeyboard.h
│ ├── RCTTextInputComponentView+RNCEKVExternalKeyboard.mm
│ ├── UIViewController+RNCEKVExternalKeyboard.h
│ └── UIViewController+RNCEKVExternalKeyboard.mm
├── Modules
│ ├── RNCEKVExternalKeyboardModule.h
│ └── RNCEKVExternalKeyboardModule.mm
└── Views
│ ├── RNCEKVExternalKeyboardView
│ ├── Helpers
│ │ ├── RNCEKVFabricEventHelper
│ │ │ ├── RNCEKVFabricEventHelper.h
│ │ │ └── RNCEKVFabricEventHelper.mm
│ │ ├── RNCEKVFocusEffectUtility
│ │ │ ├── RNCEKVFocusEffectUtility.h
│ │ │ └── RNCEKVFocusEffectUtility.mm
│ │ └── RNCEKVKeyboardKeyPressHandler
│ │ │ ├── RNCEKVKeyboardKeyPressHandler.h
│ │ │ └── RNCEKVKeyboardKeyPressHandler.mm
│ ├── RNCEKVExternalKeyboardView.h
│ ├── RNCEKVExternalKeyboardView.mm
│ ├── RNCEKVExternalKeyboardViewManager.h
│ └── RNCEKVExternalKeyboardViewManager.mm
│ ├── RNCEKVKeyboardFocusGroupView
│ ├── RNCEKVKeyboardFocusGroup.h
│ ├── RNCEKVKeyboardFocusGroup.mm
│ ├── RNCEKVKeyboardFocusGroupManager.h
│ └── RNCEKVKeyboardFocusGroupManager.mm
│ └── RNCEKVTextInputFocusWrapper
│ ├── RNCEKVTextInputFocusWrapper.h
│ ├── RNCEKVTextInputFocusWrapper.mm
│ ├── RNCEKVTextInputFocusWrapperManager.h
│ └── RNCEKVTextInputFocusWrapperManager.mm
├── lefthook.yml
├── package-lock.json
├── package.json
├── react-native-external-keyboard.podspec
├── react-native.config.js
├── scripts
└── bootstrap.js
├── src
├── __tests__
│ └── index.test.tsx
├── components
│ ├── BaseKeyboardView
│ │ ├── BaseKeyboardView.hooks.ts
│ │ └── BaseKeyboardView.tsx
│ ├── KeyboardExtendedInput
│ │ └── KeyboardExtendedInput.tsx
│ ├── KeyboardFocusGroup
│ │ ├── KeyboardFocusGroup.ios.tsx
│ │ └── KeyboardFocusGroup.tsx
│ ├── KeyboardFocusView
│ │ ├── KeyboardFocusView.tsx
│ │ ├── hooks
│ │ │ ├── index.ts
│ │ │ └── useFocusStyle
│ │ │ │ ├── index.ts
│ │ │ │ ├── useFocusStyle.ts
│ │ │ │ └── useTintStyle.ts
│ │ └── index.ts
│ ├── RenderPropComponent
│ │ └── RenderPropComponent.tsx
│ ├── Touchable
│ │ └── Pressable.tsx
│ └── index.ts
├── context
│ ├── BubbledKeyPressContext.ts
│ ├── GroupIdentifierContext.ts
│ └── IsViewFocusedContext.ts
├── index.tsx
├── modules
│ ├── Keyboard.android.ts
│ └── Keyboard.ts
├── nativeSpec
│ ├── ExternalKeyboardViewNativeComponent.ts
│ ├── KeyboardFocusGroupNativeComponent.ts
│ ├── NativeExternalKeyboardModule.ts
│ ├── TextInputFocusWrapperNativeComponent.ts
│ └── index.ts
├── types
│ ├── BaseKeyboardView.ts
│ ├── FocusStyle.ts
│ ├── KeyboardFocusView.types.ts
│ ├── WithKeyboardFocus.ts
│ └── index.ts
└── utils
│ ├── focusEventMapper.tsx
│ ├── useFocusStyle.tsx
│ ├── useKeyboardPress
│ ├── useKeyboardPress.android.ts
│ ├── useKeyboardPress.ios.ts
│ ├── useKeyboardPress.ts
│ └── useKeyboardPress.types.ts
│ ├── useOnFocusChange.ts
│ └── withKeyboardFocus.tsx
├── tsconfig.build.json
├── tsconfig.json
├── turbo.json
└── types
└── index.d.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | indent_style = space
10 | indent_size = 2
11 |
12 | end_of_line = lf
13 | charset = utf-8
14 | trim_trailing_whitespace = true
15 | insert_final_newline = true
16 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 | # specific for windows script files
3 | *.bat text eol=crlf
--------------------------------------------------------------------------------
/.github/actions/setup/action.yml:
--------------------------------------------------------------------------------
1 | name: Setup
2 | description: Setup Node.js and install dependencies
3 |
4 | runs:
5 | using: composite
6 | steps:
7 | - name: Setup Node.js
8 | uses: actions/setup-node@v3
9 | with:
10 | node-version-file: .nvmrc
11 |
12 | - name: Cache dependencies
13 | id: yarn-cache
14 | uses: actions/cache@v3
15 | with:
16 | path: |
17 | **/node_modules
18 | .yarn/install-state.gz
19 | key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }}
20 | restore-keys: |
21 | ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
22 | ${{ runner.os }}-yarn-
23 |
24 | - name: Install dependencies
25 | if: steps.yarn-cache.outputs.cache-hit != 'true'
26 | run: yarn install --immutable
27 | shell: bash
28 |
--------------------------------------------------------------------------------
/.github/images/rnek-android-example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/.github/images/rnek-android-example.gif
--------------------------------------------------------------------------------
/.github/images/rnek-ios-example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/.github/images/rnek-ios-example.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # XDE
6 | .expo/
7 |
8 | # VSCode
9 | .vscode/
10 | jsconfig.json
11 |
12 | # Xcode
13 | #
14 | build/
15 | *.pbxuser
16 | !default.pbxuser
17 | *.mode1v3
18 | !default.mode1v3
19 | *.mode2v3
20 | !default.mode2v3
21 | *.perspectivev3
22 | !default.perspectivev3
23 | xcuserdata
24 | *.xccheckout
25 | *.moved-aside
26 | DerivedData
27 | *.hmap
28 | *.ipa
29 | *.xcuserstate
30 | project.xcworkspace
31 | .xcode.env.local
32 |
33 | # Android/IJ
34 | #
35 | .classpath
36 | .cxx
37 | .gradle
38 | .idea
39 | .project
40 | .settings
41 | local.properties
42 | android.iml
43 |
44 | # Cocoapods
45 | #
46 | example/ios/Pods
47 |
48 | # Ruby
49 | example/vendor/
50 |
51 | # node.js
52 | #
53 | node_modules/
54 | npm-debug.log
55 | yarn-debug.log
56 | yarn-error.log
57 | # Yarn
58 | .yarn/*
59 | !.yarn/patches
60 | !.yarn/plugins
61 | !.yarn/releases
62 | !.yarn/sdks
63 | !.yarn/versions
64 |
65 | example/.yarn
66 |
67 | # BUCK
68 | buck-out/
69 | \.buckd/
70 | android/app/libs
71 | android/keystores/debug.keystore
72 |
73 | # Expo
74 | .expo/
75 |
76 | # Turborepo
77 | .turbo/
78 |
79 | # generated by bob
80 | lib/
81 |
82 | # React Native Codegen
83 | ios/generated
84 | android/generated
85 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v18
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.yarn/patches/react-native-builder-bob-npm-0.35.2-4e84215963.patch:
--------------------------------------------------------------------------------
1 | diff --git a/lib/utils/patchCodegenAndroidPackage.js b/lib/utils/patchCodegenAndroidPackage.js
2 | index 2c5165815c37daa008ff6a7c246a44d51ba5f963..9ac208c458f517274d1e4f3d5c2cb9f034b2afa5 100644
3 | --- a/lib/utils/patchCodegenAndroidPackage.js
4 | +++ b/lib/utils/patchCodegenAndroidPackage.js
5 | @@ -57,8 +57,22 @@ packageJson, report) {
6 | const newFilePath = _path.default.resolve(newPackagePath, file);
7 | await _fsExtra.default.rename(filePath, newFilePath);
8 | }));
9 | - await _fsExtra.default.rm(_path.default.resolve(codegenAndroidPath, 'java/com/facebook'), {
10 | - recursive: true
11 | - });
12 | + if (
13 | + await _fsExtra.default.pathExists(
14 | + _path.default.resolve(codegenAndroidPath, 'java/com/facebook/react/viewmanagers')
15 | + )
16 | + ) {
17 | + // Keep the view managers
18 | + await _fsExtra.default.rm(
19 | + _path.default.resolve(codegenAndroidPath, 'java/com/facebook/fbreact'),
20 | + { recursive: true }
21 | + );
22 | + } else {
23 | + // Delete the entire facebook namespace
24 | + await _fsExtra.default.rm(
25 | + _path.default.resolve(codegenAndroidPath, 'java/com/facebook'),
26 | + { recursive: true }
27 | + );
28 | + }
29 | }
30 | //# sourceMappingURL=patchCodegenAndroidPackage.js.map
31 | \ No newline at end of file
32 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Artur Kalach
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | // Buildscript is evaluated before everything else so we can't use getExtOrDefault
3 | def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["ExternalKeyboard_kotlinVersion"]
4 |
5 | repositories {
6 | google()
7 | mavenCentral()
8 | }
9 |
10 | dependencies {
11 | classpath "com.android.tools.build:gradle:7.2.1"
12 | // noinspection DifferentKotlinGradleVersion
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 | }
15 | }
16 |
17 | def reactNativeArchitectures() {
18 | def value = rootProject.getProperties().get("reactNativeArchitectures")
19 | return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
20 | }
21 |
22 | def isNewArchitectureEnabled() {
23 | return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
24 | }
25 |
26 | apply plugin: "com.android.library"
27 | apply plugin: "kotlin-android"
28 |
29 | if (isNewArchitectureEnabled()) {
30 | apply plugin: "com.facebook.react"
31 | }
32 |
33 | def getExtOrDefault(name) {
34 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["ExternalKeyboard_" + name]
35 | }
36 |
37 | def getExtOrIntegerDefault(name) {
38 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ExternalKeyboard_" + name]).toInteger()
39 | }
40 |
41 | def supportsNamespace() {
42 | def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
43 | def major = parsed[0].toInteger()
44 | def minor = parsed[1].toInteger()
45 |
46 | // Namespace support was added in 7.3.0
47 | return (major == 7 && minor >= 3) || major >= 8
48 | }
49 |
50 | android {
51 | if (supportsNamespace()) {
52 | namespace "com.externalkeyboard"
53 |
54 | sourceSets {
55 | main {
56 | manifest.srcFile "src/main/AndroidManifestNew.xml"
57 | }
58 | }
59 | }
60 |
61 | compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
62 |
63 | defaultConfig {
64 | minSdkVersion getExtOrIntegerDefault("minSdkVersion")
65 | targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
66 | buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
67 |
68 | }
69 |
70 | buildFeatures {
71 | buildConfig true
72 | }
73 |
74 | buildTypes {
75 | release {
76 | minifyEnabled false
77 | }
78 | }
79 |
80 | lintOptions {
81 | disable "GradleCompatible"
82 | }
83 |
84 | compileOptions {
85 | sourceCompatibility JavaVersion.VERSION_1_8
86 | targetCompatibility JavaVersion.VERSION_1_8
87 | }
88 |
89 | sourceSets {
90 | main {
91 | if (isNewArchitectureEnabled()) {
92 | java.srcDirs += [
93 | "src/newarch",
94 | "generated/java",
95 | "generated/jni"
96 | ]
97 | } else {
98 | java.srcDirs += ["src/oldarch"]
99 | }
100 | }
101 | }
102 | }
103 |
104 | repositories {
105 | mavenCentral()
106 | google()
107 | }
108 |
109 | def kotlin_version = getExtOrDefault("kotlinVersion")
110 |
111 | dependencies {
112 | // For < 0.71, this will be from the local maven repo
113 | // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
114 | //noinspection GradleDynamicVersion
115 | implementation "com.facebook.react:react-native:+"
116 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
117 | }
118 |
119 | if (isNewArchitectureEnabled()) {
120 | react {
121 | jsRootDir = file("../src/")
122 | libraryName = "ExternalKeyboardView"
123 | codegenJavaPackageName = "com.externalkeyboard"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | ExternalKeyboard_kotlinVersion=1.7.0
2 | ExternalKeyboard_minSdkVersion=21
3 | ExternalKeyboard_targetSdkVersion=31
4 | ExternalKeyboard_compileSdkVersion=31
5 | ExternalKeyboard_ndkversion=21.4.7075529
6 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifestNew.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/ExternalKeyboardViewPackage.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard;
2 |
3 |
4 | import com.externalkeyboard.modules.ExternalKeyboardModule;
5 | import com.externalkeyboard.views.ExternalKeyboardView.ExternalKeyboardViewManager;
6 | import com.externalkeyboard.views.KeyboardFocusGroup.KeyboardFocusGroupManager;
7 | import com.externalkeyboard.views.TextInputFocusWrapper.TextInputFocusWrapperManager;
8 | import com.facebook.react.TurboReactPackage;
9 | import com.facebook.react.bridge.NativeModule;
10 | import com.facebook.react.bridge.ReactApplicationContext;
11 | import com.facebook.react.module.model.ReactModuleInfo;
12 | import com.facebook.react.module.model.ReactModuleInfoProvider;
13 | import com.facebook.react.uimanager.ViewManager;
14 |
15 | import java.util.ArrayList;
16 | import java.util.List;
17 | import java.util.HashMap;
18 | import java.util.Map;
19 |
20 | public class ExternalKeyboardViewPackage extends TurboReactPackage {
21 | @Override
22 | public NativeModule getModule(String name, ReactApplicationContext reactContext) {
23 | if (name.equals(ExternalKeyboardModule.NAME)) {
24 | return new ExternalKeyboardModule(reactContext);
25 | } else {
26 | return null;
27 | }
28 | }
29 |
30 | @Override
31 | public ReactModuleInfoProvider getReactModuleInfoProvider() {
32 | return () -> {
33 | Map map = new HashMap<>();
34 | boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
35 | map.put(ExternalKeyboardModule.NAME, new ReactModuleInfo(ExternalKeyboardModule.NAME, // name
36 | ExternalKeyboardModule.NAME, // className
37 | false, // canOverrideExistingModule
38 | false, // needsEagerInit
39 | true, // hasConstants
40 | false, // isCxxModule
41 | isTurboModule // isTurboModule
42 | ));
43 | return map;
44 | };
45 | }
46 |
47 | @Override
48 | public List createViewManagers(ReactApplicationContext reactContext) {
49 | List viewManagers = new ArrayList<>();
50 | viewManagers.add(new ExternalKeyboardViewManager());
51 | viewManagers.add(new TextInputFocusWrapperManager());
52 | viewManagers.add(new KeyboardFocusGroupManager());
53 |
54 | return viewManagers;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/events/EventHelper.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard.events;
2 |
3 | import android.view.KeyEvent;
4 |
5 | import com.facebook.react.bridge.ReactContext;
6 | import com.facebook.react.uimanager.UIManagerHelper;
7 | import com.facebook.react.uimanager.events.EventDispatcher;
8 |
9 | public class EventHelper {
10 | public static void focusChanged(ReactContext context, int id, boolean hasFocus) {
11 | int surfaceId = UIManagerHelper.getSurfaceId(context);
12 | FocusChangeEvent event = new FocusChangeEvent(surfaceId, id, hasFocus);
13 | EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id);
14 | if (eventDispatcher != null) {
15 | eventDispatcher.dispatchEvent(event);
16 | }
17 | }
18 |
19 | public static void pressDown(ReactContext context, int id, int keyCode, KeyEvent keyEvent) {
20 | int surfaceId = UIManagerHelper.getSurfaceId(context);
21 | KeyPressDownEvent keyPressDownEvent = new KeyPressDownEvent(surfaceId, id, keyCode, keyEvent);
22 | EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id);
23 | if (eventDispatcher != null) {
24 | eventDispatcher.dispatchEvent(keyPressDownEvent);
25 | }
26 | }
27 |
28 | public static void pressUp(ReactContext context, int id, int keyCode, KeyEvent keyEvent, boolean isLongPress) {
29 | int surfaceId = UIManagerHelper.getSurfaceId(context);
30 | KeyPressUpEvent keyPressUpEvent = new KeyPressUpEvent(surfaceId, id, keyCode, keyEvent, isLongPress);
31 | EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id);
32 | if (eventDispatcher != null) {
33 | eventDispatcher.dispatchEvent(keyPressUpEvent);
34 | }
35 | }
36 |
37 | public static void multiplyTextSubmit(ReactContext context, int id, String text) {
38 | int surfaceId = UIManagerHelper.getSurfaceId(context);
39 | MultiplyTextSubmit event = new MultiplyTextSubmit(surfaceId, id, text);
40 | EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id);
41 | if (eventDispatcher != null) {
42 | eventDispatcher.dispatchEvent(event);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/events/FocusChangeEvent.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard.events;
2 |
3 | import com.facebook.react.bridge.Arguments;
4 | import com.facebook.react.bridge.WritableMap;
5 | import com.facebook.react.uimanager.events.Event;
6 |
7 | public class FocusChangeEvent extends Event {
8 | public static String EVENT_NAME = "topFocusChange";
9 | public WritableMap payload;
10 |
11 | public FocusChangeEvent(int surfaceId, int id, Boolean hasFocus) {
12 | super(surfaceId, id);
13 | payload = Arguments.createMap();
14 | payload.putBoolean("isFocused", hasFocus);
15 | }
16 |
17 | @Override
18 | public String getEventName() {
19 | return EVENT_NAME;
20 | }
21 |
22 | @Override
23 | public WritableMap getEventData() {
24 | return payload;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/events/KeyPressDownEvent.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard.events;
2 |
3 | import android.view.KeyEvent;
4 |
5 | import com.facebook.react.bridge.Arguments;
6 | import com.facebook.react.bridge.WritableMap;
7 | import com.facebook.react.uimanager.events.Event;
8 |
9 | public class KeyPressDownEvent extends Event {
10 | public static String EVENT_NAME = "topKeyDownPress";
11 | public WritableMap payload;
12 |
13 | public KeyPressDownEvent(int surfaceId, int id, int keyCode, KeyEvent keyEvent) {
14 | super(surfaceId, id);
15 |
16 | int unicode = keyEvent.getUnicodeChar();
17 | payload = Arguments.createMap();
18 | payload.putInt("keyCode", keyCode);
19 | payload.putInt("unicode", unicode);
20 | payload.putString("unicodeChar", String.valueOf((char) unicode));
21 | payload.putBoolean("isLongPress", keyEvent.isLongPress());
22 | payload.putBoolean("isAltPressed", keyEvent.isAltPressed());
23 | payload.putBoolean("isShiftPressed", keyEvent.isShiftPressed());
24 | payload.putBoolean("isCtrlPressed", keyEvent.isCtrlPressed());
25 | payload.putBoolean("isCapsLockOn", keyEvent.isCapsLockOn());
26 | payload.putBoolean("hasNoModifiers", keyEvent.hasNoModifiers());
27 | }
28 |
29 | @Override
30 | public String getEventName() {
31 | return EVENT_NAME;
32 | }
33 |
34 | @Override
35 | public WritableMap getEventData() {
36 | return payload;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/events/KeyPressUpEvent.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard.events;
2 |
3 | import android.view.KeyEvent;
4 |
5 | import com.facebook.react.bridge.Arguments;
6 | import com.facebook.react.bridge.WritableMap;
7 | import com.facebook.react.uimanager.events.Event;
8 |
9 | public class KeyPressUpEvent extends Event {
10 | public static String EVENT_NAME = "topKeyUpPress";
11 | public WritableMap payload;
12 |
13 | public KeyPressUpEvent(int surfaceId, int id, int keyCode, KeyEvent keyEvent, boolean isLongPress) {
14 | super(surfaceId, id);
15 |
16 | int unicode = keyEvent.getUnicodeChar();
17 | payload = Arguments.createMap();
18 | payload.putInt("keyCode", keyCode);
19 | payload.putInt("unicode", unicode);
20 | payload.putString("unicodeChar", String.valueOf((char) unicode));
21 | payload.putBoolean("isLongPress", isLongPress);
22 | payload.putBoolean("isAltPressed", keyEvent.isAltPressed());
23 | payload.putBoolean("isShiftPressed", keyEvent.isShiftPressed());
24 | payload.putBoolean("isCtrlPressed", keyEvent.isCtrlPressed());
25 | payload.putBoolean("isCapsLockOn", keyEvent.isCapsLockOn());
26 | payload.putBoolean("hasNoModifiers", keyEvent.hasNoModifiers());
27 | }
28 |
29 | @Override
30 | public String getEventName() {
31 | return EVENT_NAME;
32 | }
33 |
34 | @Override
35 | public WritableMap getEventData() {
36 | return payload;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/events/MultiplyTextSubmit.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard.events;
2 |
3 | import com.facebook.react.bridge.Arguments;
4 | import com.facebook.react.bridge.WritableMap;
5 | import com.facebook.react.uimanager.events.Event;
6 |
7 | public class MultiplyTextSubmit extends Event {
8 | public static String EVENT_NAME = "topMultiplyTextSubmit";
9 | public WritableMap payload;
10 |
11 | public MultiplyTextSubmit(int surfaceId, int id, String text) {
12 | super(surfaceId, id);
13 | payload = Arguments.createMap();
14 | payload.putString("text", text);
15 | }
16 |
17 | @Override
18 | public String getEventName() {
19 | return EVENT_NAME;
20 | }
21 |
22 | @Override
23 | public WritableMap getEventData() {
24 | return payload;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/helper/FocusHelper.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard.helper;
2 |
3 | import android.view.View;
4 | import android.view.ViewGroup;
5 |
6 | public class FocusHelper {
7 | public static View getFocusableView(ViewGroup viewGroup) {
8 | if (viewGroup.getChildCount() > 0) {
9 | View subView = viewGroup.getChildAt(0);
10 | if (subView.isFocusable()) {
11 | return subView;
12 | } else if (subView instanceof ViewGroup) {
13 | return getFocusableView((ViewGroup) subView);
14 | }
15 | }
16 |
17 | return null;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/modules/ExternalKeyboardModule.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard.modules;
2 |
3 | import android.content.Context;
4 | import android.view.View;
5 | import android.view.inputmethod.InputMethodManager;
6 |
7 | import com.externalkeyboard.ExternalKeyboardModuleSpec;
8 | import com.facebook.react.bridge.Promise;
9 | import com.facebook.react.bridge.ReactApplicationContext;
10 | import com.facebook.react.bridge.ReactMethod;
11 |
12 | public class ExternalKeyboardModule extends ExternalKeyboardModuleSpec {
13 | public static final String NAME = "ExternalKeyboardModule";
14 | private static View focusedView = null;
15 | private final ReactApplicationContext context;
16 |
17 |
18 | private boolean dismiss() {
19 | if(focusedView == null) return false;
20 | try {
21 | InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
22 | if (imm != null) {
23 | imm.hideSoftInputFromWindow(focusedView.getWindowToken(), 0);
24 | return true;
25 | }
26 | } catch (Exception ignored){}
27 |
28 | return false;
29 | }
30 |
31 |
32 | public static void setFocusedTextInput (View _focusedView) {
33 | focusedView = _focusedView;
34 | }
35 |
36 | public ExternalKeyboardModule(ReactApplicationContext reactContext) {
37 | super(reactContext);
38 | context = reactContext;
39 | }
40 |
41 | @Override
42 | public String getName() {
43 | return NAME;
44 | }
45 |
46 | @Override
47 | @ReactMethod
48 | public void dismissKeyboard(Promise promise) {
49 | boolean result = dismiss();
50 | promise.resolve(result);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/services/KeyboardKeyPressHandler.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard.services;
2 |
3 | import android.os.Build;
4 | import android.view.KeyEvent;
5 |
6 | import java.util.HashMap;
7 |
8 | public class KeyboardKeyPressHandler {
9 | private final HashMap pressedKeys = new HashMap();
10 | private final HashMap longPress = new HashMap();
11 |
12 | private boolean actionDownHandler(int keyCode, KeyEvent keyEvent) {
13 | boolean wasAlreadyPressed = false;
14 | if (pressedKeys.containsKey(keyCode)) {
15 | wasAlreadyPressed = pressedKeys.get(keyCode);
16 | }
17 | pressedKeys.put(keyCode, true);
18 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR && keyEvent.isLongPress()) {
19 | longPress.put(keyCode, keyEvent.isLongPress());
20 | }
21 |
22 | return !wasAlreadyPressed;
23 | }
24 |
25 | private boolean actionUpHandler(int keyCode, KeyEvent keyEvent) {
26 | pressedKeys.put(keyCode, false);
27 |
28 | boolean isLongPress = false;
29 | if (longPress.containsKey(keyCode)) {
30 | isLongPress = longPress.get(keyCode);
31 | }
32 | longPress.put(keyCode, false);
33 |
34 | return isLongPress;
35 | }
36 |
37 | public PressInfo getEventsFromKeyPress(int keyCode, KeyEvent keyEvent) {
38 | PressInfo pressInfo = new PressInfo();
39 |
40 | if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
41 | pressInfo.firePressDownEvent = actionDownHandler(keyCode, keyEvent);
42 | }
43 | if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
44 | pressInfo.firePressUpEvent = true;
45 | pressInfo.isLongPress = actionUpHandler(keyCode, keyEvent);
46 | }
47 |
48 | return pressInfo;
49 | }
50 |
51 | public class PressInfo {
52 | public boolean firePressDownEvent = false;
53 | public boolean firePressUpEvent = false;
54 | public boolean isLongPress = false;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/services/KeyboardService.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard.services;
2 |
3 |
4 | import static com.facebook.react.uimanager.common.UIManagerType.FABRIC;
5 |
6 | import android.app.Activity;
7 | import android.util.Log;
8 | import android.view.View;
9 |
10 | import com.facebook.react.bridge.ReactApplicationContext;
11 | import com.facebook.react.bridge.UIManager;
12 | import com.facebook.react.uimanager.IllegalViewOperationException;
13 | import com.facebook.react.uimanager.UIManagerHelper;
14 | import com.facebook.react.uimanager.UIManagerModule;
15 | import com.facebook.react.uimanager.common.ViewUtil;
16 |
17 | public class KeyboardService {
18 | private final ReactApplicationContext context;
19 |
20 | public KeyboardService(ReactApplicationContext context) {
21 | this.context = context;
22 | }
23 |
24 | public void setKeyboardFocus(int tag) {
25 | final Activity activity = context.getCurrentActivity();
26 |
27 | if (activity == null) {
28 | return;
29 | }
30 |
31 | activity.runOnUiThread(() -> {
32 | try {
33 | int uiManagerType = ViewUtil.getUIManagerType(tag);
34 | if (uiManagerType == FABRIC) {
35 | UIManager fabricUIManager = UIManagerHelper.getUIManager(context, uiManagerType);
36 | if (fabricUIManager != null) {
37 | View view = fabricUIManager.resolveView(tag);
38 | view.requestFocus();
39 | }
40 | } else {
41 | UIManager uiManager = context.getNativeModule(UIManagerModule.class);
42 | View view = uiManager.resolveView(tag);
43 | view.requestFocus();
44 | }
45 | } catch (IllegalViewOperationException error) {
46 | Log.e("KEYBOARD_FOCUS_ERROR", error.getMessage());
47 | }
48 | });
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/views/KeyboardFocusGroup/KeyboardFocusGroup.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard.views.KeyboardFocusGroup;
2 |
3 | import android.content.Context;
4 |
5 | import com.facebook.react.views.view.ReactViewGroup;
6 |
7 | public class KeyboardFocusGroup extends ReactViewGroup {
8 | public KeyboardFocusGroup(Context context) {
9 | super(context);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/views/KeyboardFocusGroup/KeyboardFocusGroupManager.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard.views.KeyboardFocusGroup;
2 |
3 | import androidx.annotation.Nullable;
4 |
5 | import com.facebook.react.module.annotations.ReactModule;
6 | import com.facebook.react.uimanager.ThemedReactContext;
7 | import com.facebook.react.uimanager.annotations.ReactProp;
8 |
9 | @ReactModule(name = KeyboardFocusGroupManager.NAME)
10 | public class KeyboardFocusGroupManager extends com.externalkeyboard.KeyboardFocusGroupManagerSpec {
11 | public static final String NAME = "KeyboardFocusGroup";
12 |
13 | @Override
14 | public String getName() {
15 | return NAME;
16 | }
17 |
18 | @Override
19 | public KeyboardFocusGroup createViewInstance(ThemedReactContext context) {
20 | return new KeyboardFocusGroup(context);
21 | }
22 |
23 |
24 | @Override
25 | @ReactProp(name = "tintColor")
26 | public void setTintColor(KeyboardFocusGroup view, @Nullable Integer value) {
27 | //stub
28 | }
29 |
30 | @Override
31 | @ReactProp(name = "groupIdentifier")
32 | public void setGroupIdentifier(KeyboardFocusGroup wrapper, String groupIdentifier) {
33 | //stub
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/android/src/main/java/com/externalkeyboard/views/TextInputFocusWrapper/TextInputFocusWrapperManager.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard.views.TextInputFocusWrapper;
2 |
3 | import android.view.View;
4 | import android.view.ViewGroup;
5 |
6 | import androidx.annotation.NonNull;
7 | import androidx.annotation.Nullable;
8 |
9 | import com.externalkeyboard.events.FocusChangeEvent;
10 | import com.externalkeyboard.events.MultiplyTextSubmit;
11 | import com.externalkeyboard.views.ExternalKeyboardView.ExternalKeyboardView;
12 | import com.facebook.react.bridge.ReadableArray;
13 | import com.facebook.react.common.MapBuilder;
14 | import com.facebook.react.module.annotations.ReactModule;
15 | import com.facebook.react.uimanager.ThemedReactContext;
16 | import com.facebook.react.uimanager.annotations.ReactProp;
17 | import com.facebook.react.views.textinput.ReactEditText;
18 | import com.facebook.react.views.view.ReactViewGroup;
19 |
20 | import java.util.HashMap;
21 | import java.util.Map;
22 |
23 |
24 | @ReactModule(name = TextInputFocusWrapperManager.NAME)
25 | public class TextInputFocusWrapperManager extends com.externalkeyboard.TextInputFocusWrapperManagerSpec {
26 | public static final String NAME = "TextInputFocusWrapper";
27 |
28 | @Override
29 | public String getName() {
30 | return NAME;
31 | }
32 |
33 | @Override
34 | public TextInputFocusWrapper createViewInstance(ThemedReactContext context) {
35 | return subscribeOnHierarchy(new TextInputFocusWrapper(context));
36 | }
37 |
38 | @Override
39 | protected void addEventEmitters(final ThemedReactContext reactContext, TextInputFocusWrapper viewGroup) {
40 | viewGroup.subscribeOnFocus();
41 | }
42 |
43 | protected TextInputFocusWrapper subscribeOnHierarchy(TextInputFocusWrapper viewGroup) {
44 | viewGroup.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
45 | @Override
46 | public void onChildViewAdded(View parent, View child) {
47 | if (child instanceof ReactEditText) {
48 | viewGroup.setEditText((ReactEditText) child);
49 | }
50 | }
51 |
52 | @Override
53 | public void onChildViewRemoved(View parent, View child) {
54 | if (child instanceof ReactEditText) {
55 | viewGroup.setEditText(null);
56 | }
57 | }
58 | });
59 |
60 | return viewGroup;
61 | }
62 |
63 | @Override
64 | @ReactProp(name = "focusType")
65 | public void setFocusType(TextInputFocusWrapper view, int value) {
66 | view.setFocusType(value);
67 | }
68 |
69 | @Override
70 | @ReactProp(name = "blurType")
71 | public void setBlurType(TextInputFocusWrapper view, int value) {
72 | view.setBlurType(value);
73 | }
74 |
75 | @Override
76 | @ReactProp(name = "blurOnSubmit", defaultBoolean = true)
77 | public void setBlurOnSubmit(TextInputFocusWrapper view, boolean value) {
78 | view.setBlurOnSubmit(value);
79 | }
80 |
81 | @Override
82 | @ReactProp(name = "multiline")
83 | public void setMultiline(TextInputFocusWrapper view, boolean value) {
84 | view.setMultiline(value);
85 | }
86 |
87 | @Override
88 | public void setGroupIdentifier(TextInputFocusWrapper view, @Nullable String value) {
89 | //stub
90 | }
91 |
92 |
93 | @Override
94 | @ReactProp(name = "canBeFocused", defaultBoolean = true)
95 | public void setCanBeFocused(TextInputFocusWrapper view, boolean value) {
96 | view.setKeyboardFocusable(value);
97 | }
98 |
99 | @Override
100 | public void setHaloEffect(TextInputFocusWrapper view, boolean value) {
101 | //stub
102 | }
103 |
104 | @Override
105 | public void setTintColor(TextInputFocusWrapper view, @Nullable Integer value) {
106 | //stub
107 | }
108 |
109 | @Override
110 | public void onDropViewInstance(@NonNull TextInputFocusWrapper viewGroup) {
111 | viewGroup.setEditText(null);
112 | viewGroup.setOnFocusChangeListener(null);
113 | super.onDropViewInstance(viewGroup);
114 | }
115 |
116 | private Map createEventMap(String registrationName) {
117 | Map eventMap = new HashMap<>();
118 | eventMap.put("registrationName", registrationName);
119 | return eventMap;
120 | }
121 |
122 | @Nullable
123 | @Override
124 | public Map getExportedCustomDirectEventTypeConstants() {
125 | Map export = new HashMap<>();
126 |
127 | export.put(FocusChangeEvent.EVENT_NAME, createEventMap("onFocusChange"));
128 | export.put(MultiplyTextSubmit.EVENT_NAME, createEventMap("onMultiplyTextSubmit"));
129 |
130 | return export;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/android/src/newarch/ExternalKeyboardModuleSpec.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard;
2 |
3 | import com.facebook.react.bridge.ReactApplicationContext;
4 |
5 | public abstract class ExternalKeyboardModuleSpec extends NativeExternalKeyboardModuleSpec {
6 | protected ExternalKeyboardModuleSpec(ReactApplicationContext context) {
7 | super(context);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/android/src/newarch/ExternalKeyboardViewManagerSpec.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard;
2 |
3 | import com.facebook.react.viewmanagers.ExternalKeyboardViewManagerInterface;
4 | import com.facebook.react.views.view.ReactViewGroup;
5 | import com.facebook.react.views.view.ReactViewManager;
6 |
7 | public abstract class ExternalKeyboardViewManagerSpec extends ReactViewManager implements ExternalKeyboardViewManagerInterface {
8 | }
9 |
--------------------------------------------------------------------------------
/android/src/newarch/KeyboardFocusGroupManagerSpec.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard;
2 |
3 | import com.facebook.react.viewmanagers.KeyboardFocusGroupManagerInterface;
4 | import com.facebook.react.views.view.ReactViewGroup;
5 | import com.facebook.react.views.view.ReactViewManager;
6 |
7 | public abstract class KeyboardFocusGroupManagerSpec extends ReactViewManager implements KeyboardFocusGroupManagerInterface {
8 | }
9 |
--------------------------------------------------------------------------------
/android/src/newarch/TextInputFocusWrapperManagerSpec.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard;
2 |
3 | import android.view.ViewGroup;
4 |
5 | import androidx.annotation.Nullable;
6 |
7 | import com.facebook.react.uimanager.ViewGroupManager;
8 | import com.facebook.react.uimanager.ViewManagerDelegate;
9 | import com.facebook.react.viewmanagers.TextInputFocusWrapperManagerDelegate;
10 | import com.facebook.react.viewmanagers.TextInputFocusWrapperManagerInterface;
11 |
12 | public abstract class TextInputFocusWrapperManagerSpec extends ViewGroupManager implements TextInputFocusWrapperManagerInterface {
13 | private final ViewManagerDelegate mDelegate;
14 |
15 | public TextInputFocusWrapperManagerSpec() {
16 | mDelegate = new TextInputFocusWrapperManagerDelegate(this);
17 | }
18 |
19 | @Nullable
20 | @Override
21 | protected ViewManagerDelegate getDelegate() {
22 | return mDelegate;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/android/src/oldarch/ExternalKeyboardModuleSpec.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard;
2 |
3 | import com.facebook.react.bridge.Promise;
4 | import com.facebook.react.bridge.ReactApplicationContext;
5 | import com.facebook.react.bridge.ReactContextBaseJavaModule;
6 |
7 | public abstract class ExternalKeyboardModuleSpec extends ReactContextBaseJavaModule {
8 | protected ExternalKeyboardModuleSpec(ReactApplicationContext context) {
9 | super(context);
10 | }
11 |
12 | public abstract void dismissKeyboard(Promise promise);
13 | }
14 |
--------------------------------------------------------------------------------
/android/src/oldarch/ExternalKeyboardViewManagerSpec.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard;
2 |
3 | import android.view.ViewGroup;
4 |
5 | import androidx.annotation.Nullable;
6 |
7 | import com.externalkeyboard.views.ExternalKeyboardView.ExternalKeyboardView;
8 | import com.facebook.react.views.view.ReactViewManager;
9 |
10 | public abstract class ExternalKeyboardViewManagerSpec extends ReactViewManager {
11 | public abstract void setCanBeFocused(T wrapper, boolean canBeFocused);
12 |
13 | public abstract void setHasKeyDownPress(T view, boolean value);
14 |
15 | public abstract void setHasKeyUpPress(T view, boolean value);
16 |
17 | public abstract void focus(ExternalKeyboardView view);
18 |
19 | public abstract void setAutoFocus(ExternalKeyboardView view, @Nullable boolean value);
20 |
21 | public abstract void setTintColor(ExternalKeyboardView view, @Nullable Integer value);
22 |
23 | public abstract void setHasOnFocusChanged(ExternalKeyboardView view, boolean value);
24 |
25 | public abstract void setHaloEffect(ExternalKeyboardView view, boolean value);
26 |
27 | public abstract void setGroup(ExternalKeyboardView view, boolean value);
28 |
29 | public abstract void setHaloCornerRadius(ExternalKeyboardView view, float value);
30 |
31 | public abstract void setHaloExpendX(ExternalKeyboardView view, float value);
32 |
33 | public abstract void setHaloExpendY(ExternalKeyboardView view, float value);
34 |
35 | public abstract void setGroupIdentifier(ExternalKeyboardView view, @Nullable String value);
36 |
37 | public abstract void setEnableA11yFocus(ExternalKeyboardView wrapper, boolean enableA11yFocus);
38 |
39 | public abstract void setScreenAutoA11yFocus(ExternalKeyboardView wrapper, boolean enableA11yFocus);
40 |
41 | public abstract void setScreenAutoA11yFocusDelay(ExternalKeyboardView wrapper, int value);
42 | }
43 |
--------------------------------------------------------------------------------
/android/src/oldarch/KeyboardFocusGroupManagerSpec.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard;
2 |
3 | import android.view.ViewGroup;
4 |
5 | import com.facebook.react.uimanager.ViewGroupManager;
6 |
7 | public abstract class KeyboardFocusGroupManagerSpec extends ViewGroupManager {
8 | public abstract void setTintColor(T wrapper, Integer value);
9 | public abstract void setGroupIdentifier(T wrapper, String groupIdentifier);
10 | }
11 |
--------------------------------------------------------------------------------
/android/src/oldarch/TextInputFocusWrapperManagerSpec.java:
--------------------------------------------------------------------------------
1 | package com.externalkeyboard;
2 |
3 | import android.view.ViewGroup;
4 |
5 | import androidx.annotation.Nullable;
6 |
7 | import com.externalkeyboard.views.TextInputFocusWrapper.TextInputFocusWrapper;
8 | import com.facebook.react.uimanager.ViewGroupManager;
9 |
10 | public abstract class TextInputFocusWrapperManagerSpec extends ViewGroupManager {
11 | public abstract void setCanBeFocused(T wrapper, boolean canBeFocused);
12 |
13 | public abstract void setFocusType(T wrapper, int focusType);
14 |
15 | public abstract void setBlurType(T wrapper, int blurType);
16 |
17 | public abstract void setHaloEffect(TextInputFocusWrapper view, boolean value);
18 |
19 | public abstract void setTintColor(TextInputFocusWrapper view, Integer value);
20 |
21 | public abstract void setBlurOnSubmit(TextInputFocusWrapper view, boolean value);
22 |
23 | public abstract void setMultiline(TextInputFocusWrapper view, boolean value);
24 |
25 | public abstract void setGroupIdentifier(TextInputFocusWrapper view, @Nullable String value);
26 | }
27 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:@react-native/babel-preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/example/.bundle/config:
--------------------------------------------------------------------------------
1 | BUNDLE_PATH: "vendor/bundle"
2 | BUNDLE_FORCE_RUBY_PLATFORM: 1
3 |
--------------------------------------------------------------------------------
/example/.node-version:
--------------------------------------------------------------------------------
1 | 18
2 |
--------------------------------------------------------------------------------
/example/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/example/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/example/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
4 | ruby ">= 2.6.10"
5 |
6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures.
7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
9 | gem 'xcodeproj', '< 1.26.0'
10 |
--------------------------------------------------------------------------------
/example/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.6)
5 | rexml
6 | activesupport (6.1.7.6)
7 | concurrent-ruby (~> 1.0, >= 1.0.2)
8 | i18n (>= 1.6, < 2)
9 | minitest (>= 5.1)
10 | tzinfo (~> 2.0)
11 | zeitwerk (~> 2.3)
12 | addressable (2.8.6)
13 | public_suffix (>= 2.0.2, < 6.0)
14 | algoliasearch (1.27.5)
15 | httpclient (~> 2.8, >= 2.8.3)
16 | json (>= 1.5.1)
17 | atomos (0.1.3)
18 | claide (1.1.0)
19 | cocoapods (1.15.2)
20 | addressable (~> 2.8)
21 | claide (>= 1.0.2, < 2.0)
22 | cocoapods-core (= 1.15.2)
23 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
24 | cocoapods-downloader (>= 2.1, < 3.0)
25 | cocoapods-plugins (>= 1.0.0, < 2.0)
26 | cocoapods-search (>= 1.0.0, < 2.0)
27 | cocoapods-trunk (>= 1.6.0, < 2.0)
28 | cocoapods-try (>= 1.1.0, < 2.0)
29 | colored2 (~> 3.1)
30 | escape (~> 0.0.4)
31 | fourflusher (>= 2.3.0, < 3.0)
32 | gh_inspector (~> 1.0)
33 | molinillo (~> 0.8.0)
34 | nap (~> 1.0)
35 | ruby-macho (>= 2.3.0, < 3.0)
36 | xcodeproj (>= 1.23.0, < 2.0)
37 | cocoapods-core (1.15.2)
38 | activesupport (>= 5.0, < 8)
39 | addressable (~> 2.8)
40 | algoliasearch (~> 1.0)
41 | concurrent-ruby (~> 1.1)
42 | fuzzy_match (~> 2.0.4)
43 | nap (~> 1.0)
44 | netrc (~> 0.11)
45 | public_suffix (~> 4.0)
46 | typhoeus (~> 1.0)
47 | cocoapods-deintegrate (1.0.5)
48 | cocoapods-downloader (2.1)
49 | cocoapods-plugins (1.0.0)
50 | nap
51 | cocoapods-search (1.0.1)
52 | cocoapods-trunk (1.6.0)
53 | nap (>= 0.8, < 2.0)
54 | netrc (~> 0.11)
55 | cocoapods-try (1.2.0)
56 | colored2 (3.1.2)
57 | concurrent-ruby (1.2.3)
58 | escape (0.0.4)
59 | ethon (0.16.0)
60 | ffi (>= 1.15.0)
61 | ffi (1.16.3)
62 | fourflusher (2.3.1)
63 | fuzzy_match (2.0.4)
64 | gh_inspector (1.1.3)
65 | httpclient (2.8.3)
66 | i18n (1.14.1)
67 | concurrent-ruby (~> 1.0)
68 | json (2.7.1)
69 | minitest (5.22.2)
70 | molinillo (0.8.0)
71 | nanaimo (0.3.0)
72 | nap (1.1.0)
73 | netrc (0.11.0)
74 | public_suffix (4.0.7)
75 | rexml (3.2.6)
76 | ruby-macho (2.5.1)
77 | typhoeus (1.4.1)
78 | ethon (>= 0.9.0)
79 | tzinfo (2.0.6)
80 | concurrent-ruby (~> 1.0)
81 | xcodeproj (1.24.0)
82 | CFPropertyList (>= 2.3.3, < 4.0)
83 | atomos (~> 0.1.3)
84 | claide (>= 1.0.2, < 2.0)
85 | colored2 (~> 3.1)
86 | nanaimo (~> 0.3.0)
87 | rexml (~> 3.2.4)
88 | zeitwerk (2.6.13)
89 |
90 | PLATFORMS
91 | ruby
92 |
93 | DEPENDENCIES
94 | cocoapods (>= 1.11.3)
95 |
96 | RUBY VERSION
97 | ruby 2.6.10p210
98 |
99 | BUNDLED WITH
100 | 1.17.2
101 |
--------------------------------------------------------------------------------
/example/android/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/android/app/debug.keystore
--------------------------------------------------------------------------------
/example/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
--------------------------------------------------------------------------------
/example/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
--------------------------------------------------------------------------------
/example/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/externalkeyboard/example/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package externalkeyboard.example
2 |
3 | import com.facebook.react.ReactActivity
4 | import com.facebook.react.ReactActivityDelegate
5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
6 | import com.facebook.react.defaults.DefaultReactActivityDelegate
7 |
8 | class MainActivity : ReactActivity() {
9 |
10 | /**
11 | * Returns the name of the main component registered from JavaScript. This is used to schedule
12 | * rendering of the component.
13 | */
14 | override fun getMainComponentName(): String = "ExternalKeyboardExample"
15 |
16 | /**
17 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
18 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
19 | */
20 | override fun createReactActivityDelegate(): ReactActivityDelegate =
21 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
22 | }
23 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/externalkeyboard/example/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package externalkeyboard.example
2 |
3 | import android.app.Application
4 | import com.facebook.react.PackageList
5 | import com.facebook.react.ReactApplication
6 | import com.facebook.react.ReactHost
7 | import com.facebook.react.ReactNativeHost
8 | import com.facebook.react.ReactPackage
9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
10 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
11 | import com.facebook.react.defaults.DefaultReactNativeHost
12 | import com.facebook.react.soloader.OpenSourceMergedSoMapping
13 | import com.facebook.soloader.SoLoader
14 |
15 | class MainApplication : Application(), ReactApplication {
16 |
17 | override val reactNativeHost: ReactNativeHost =
18 | object : DefaultReactNativeHost(this) {
19 | override fun getPackages(): List =
20 | PackageList(this).packages.apply {
21 | // Packages that cannot be autolinked yet can be added manually here, for example:
22 | // add(MyReactNativePackage())
23 | }
24 |
25 | override fun getJSMainModuleName(): String = "index"
26 |
27 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
28 |
29 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
30 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
31 | }
32 |
33 | override val reactHost: ReactHost
34 | get() = getDefaultReactHost(applicationContext, reactNativeHost)
35 |
36 | override fun onCreate() {
37 | super.onCreate()
38 | SoLoader.init(this, OpenSourceMergedSoMapping)
39 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
40 | // If you opted-in for the New Architecture, we load the native entry point for this app.
41 | load()
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
22 |
23 |
24 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ExternalKeyboardExample
3 |
4 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/example/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | buildToolsVersion = "35.0.0"
4 | minSdkVersion = 24
5 | compileSdkVersion = 35
6 | targetSdkVersion = 34
7 | ndkVersion = "26.1.10909125"
8 | kotlinVersion = "1.9.24"
9 | }
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | dependencies {
15 | classpath("com.android.tools.build:gradle")
16 | classpath("com.facebook.react:react-native-gradle-plugin")
17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
18 | }
19 | }
20 |
21 | apply plugin: "com.facebook.react.rootproject"
22 |
--------------------------------------------------------------------------------
/example/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 |
25 | # Use this property to specify which architecture you want to build.
26 | # You can also override it from the CLI using
27 | # ./gradlew -PreactNativeArchitectures=x86_64
28 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
29 |
30 | # Use this property to enable support to the new architecture.
31 | # This will allow you to use TurboModules and the Fabric render in
32 | # your application. You should enable this flag either if you want
33 | # to write custom TurboModules/Fabric components OR use libraries that
34 | # are providing them.
35 | newArchEnabled=true
36 |
37 | # Use this property to enable or disable the Hermes JS engine.
38 | # If set to false, you will be using JSC instead.
39 | hermesEnabled=true
40 |
--------------------------------------------------------------------------------
/example/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/example/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/example/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/example/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
2 | plugins { id("com.facebook.react.settings") }
3 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
4 | rootProject.name = 'externalkeyboard.example'
5 | include ':app'
6 | includeBuild('../node_modules/@react-native/gradle-plugin')
7 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ExternalKeyboardExample",
3 | "displayName": "ExternalKeyboardExample"
4 | }
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { getConfig } = require('react-native-builder-bob/babel-config');
3 | const pkg = require('../package.json');
4 |
5 | const root = path.resolve(__dirname, '..');
6 |
7 | module.exports = getConfig(
8 | {
9 | presets: ['module:@react-native/babel-preset'],
10 | },
11 | { root, pkg }
12 | );
13 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | import { AppRegistry } from 'react-native';
2 | import { App } from './src/App';
3 | import { name as appName } from './app.json';
4 |
5 | AppRegistry.registerComponent(appName, () => App);
6 |
--------------------------------------------------------------------------------
/example/ios/.xcode.env:
--------------------------------------------------------------------------------
1 | # This `.xcode.env` file is versioned and is used to source the environment
2 | # used when running script phases inside Xcode.
3 | # To customize your local environment, you can create an `.xcode.env.local`
4 | # file that is not versioned.
5 |
6 | # NODE_BINARY variable contains the PATH to the node executable.
7 | #
8 | # Customize the NODE_BINARY variable here.
9 | # For example, to use nvm with brew, add the following line
10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use
11 | export NODE_BINARY=$(command -v node)
12 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExample-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExample.xcodeproj/xcshareddata/xcschemes/ExternalKeyboardExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExample.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExample/AppDelegate.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface AppDelegate : RCTAppDelegate
5 |
6 | @end
7 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExample/AppDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 |
3 | #import
4 |
5 | @implementation AppDelegate
6 |
7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
8 | {
9 | self.moduleName = @"ExternalKeyboardExample";
10 | // You can add your custom initial props in the dictionary below.
11 | // They will be passed down to the ViewController used by React Native.
12 | self.initialProps = @{};
13 |
14 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
15 | }
16 |
17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
18 | {
19 | return [self bundleURL];
20 | }
21 |
22 | - (NSURL *)bundleURL
23 | {
24 | #if DEBUG
25 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
26 | #else
27 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
28 | #endif
29 | }
30 |
31 | @end
32 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExample/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "scale" : "1x",
46 | "size" : "1024x1024"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExample/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | ExternalKeyboardExample
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 |
30 | NSAllowsArbitraryLoads
31 |
32 | NSAllowsLocalNetworking
33 |
34 |
35 | NSLocationWhenInUseUsageDescription
36 |
37 | UILaunchStoryboardName
38 | LaunchScreen
39 | UIRequiredDeviceCapabilities
40 |
41 | arm64
42 |
43 | UISupportedInterfaceOrientations
44 |
45 | UIInterfaceOrientationPortrait
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 | UIViewControllerBasedStatusBarAppearance
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExample/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExample/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPIType
9 | NSPrivacyAccessedAPICategoryFileTimestamp
10 | NSPrivacyAccessedAPITypeReasons
11 |
12 | C617.1
13 |
14 |
15 |
16 | NSPrivacyAccessedAPIType
17 | NSPrivacyAccessedAPICategoryUserDefaults
18 | NSPrivacyAccessedAPITypeReasons
19 |
20 | CA92.1
21 |
22 |
23 |
24 | NSPrivacyAccessedAPIType
25 | NSPrivacyAccessedAPICategorySystemBootTime
26 | NSPrivacyAccessedAPITypeReasons
27 |
28 | 35F9.1
29 |
30 |
31 |
32 | NSPrivacyCollectedDataTypes
33 |
34 | NSPrivacyTracking
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExample/main.m:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | #import "AppDelegate.h"
4 |
5 | int main(int argc, char *argv[])
6 | {
7 | @autoreleasepool {
8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExampleTests/ExternalKeyboardExampleTests.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | #import
5 | #import
6 |
7 | #define TIMEOUT_SECONDS 600
8 | #define TEXT_TO_LOOK_FOR @"Welcome to React"
9 |
10 | @interface ExternalKeyboardExampleTests : XCTestCase
11 |
12 | @end
13 |
14 | @implementation ExternalKeyboardExampleTests
15 |
16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test
17 | {
18 | if (test(view)) {
19 | return YES;
20 | }
21 | for (UIView *subview in [view subviews]) {
22 | if ([self findSubviewInView:subview matching:test]) {
23 | return YES;
24 | }
25 | }
26 | return NO;
27 | }
28 |
29 | - (void)testRendersWelcomeScreen
30 | {
31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];
32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
33 | BOOL foundElement = NO;
34 |
35 | __block NSString *redboxError = nil;
36 | #ifdef DEBUG
37 | RCTSetLogFunction(
38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
39 | if (level >= RCTLogLevelError) {
40 | redboxError = message;
41 | }
42 | });
43 | #endif
44 |
45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
48 |
49 | foundElement = [self findSubviewInView:vc.view
50 | matching:^BOOL(UIView *view) {
51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
52 | return YES;
53 | }
54 | return NO;
55 | }];
56 | }
57 |
58 | #ifdef DEBUG
59 | RCTSetLogFunction(RCTDefaultLogFunction);
60 | #endif
61 |
62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
64 | }
65 |
66 | @end
67 |
--------------------------------------------------------------------------------
/example/ios/ExternalKeyboardExampleTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/example/ios/File.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | // ExternalKeyboardExample
4 | //
5 |
6 | import Foundation
7 |
--------------------------------------------------------------------------------
/example/ios/Podfile:
--------------------------------------------------------------------------------
1 | ENV['RCT_NEW_ARCH_ENABLED'] = '1'
2 |
3 | # Resolve react_native_pods.rb with node to allow for hoisting
4 | require Pod::Executable.execute_command('node', ['-p',
5 | 'require.resolve(
6 | "react-native/scripts/react_native_pods.rb",
7 | {paths: [process.argv[1]]},
8 | )', __dir__]).strip
9 |
10 | platform :ios, min_ios_version_supported
11 | prepare_react_native_project!
12 |
13 | linkage = ENV['USE_FRAMEWORKS']
14 | if linkage != nil
15 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
16 | use_frameworks! :linkage => linkage.to_sym
17 | end
18 |
19 | target 'ExternalKeyboardExample' do
20 | config = use_native_modules!
21 |
22 | use_react_native!(
23 | :path => config[:reactNativePath],
24 | # An absolute path to your application root.
25 | :app_path => "#{Pod::Config.instance.installation_root}/.."
26 | )
27 |
28 | target 'ExternalKeyboardExampleTests' do
29 | inherit! :complete
30 | # Pods for testing
31 | end
32 |
33 |
34 | pre_install do |installer|
35 | system("cd ../../ && npx bob build")
36 | end
37 |
38 | post_install do |installer|
39 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
40 | react_native_post_install(
41 | installer,
42 | config[:reactNativePath],
43 | :mac_catalyst_enabled => false,
44 | # :ccache_enabled => true
45 | )
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { getDefaultConfig } = require('@react-native/metro-config');
3 | const { getConfig } = require('react-native-builder-bob/metro-config');
4 | const pkg = require('../package.json');
5 |
6 | const root = path.resolve(__dirname, '..');
7 |
8 | /**
9 | * Metro configuration
10 | * https://facebook.github.io/metro/docs/configuration
11 | *
12 | * @type {import('metro-config').MetroConfig}
13 | */
14 | module.exports = getConfig(getDefaultConfig(__dirname), {
15 | root,
16 | pkg,
17 | project: __dirname,
18 | });
19 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "external-keyboard-example",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "ios": "react-native run-ios",
8 | "start": "react-native start",
9 | "pods": "pod-install --quiet"
10 | },
11 | "dependencies": {
12 | "@react-navigation/bottom-tabs": "^7.2.0",
13 | "@react-navigation/native": "^7.0.14",
14 | "@react-navigation/native-stack": "^7.2.0",
15 | "react": "18.3.1",
16 | "react-native": "0.76.5",
17 | "react-native-gesture-handler": "^2.22.1",
18 | "react-native-safe-area-context": "^5.1.0",
19 | "react-native-screens": "^4.5.0"
20 | },
21 | "devDependencies": {
22 | "@babel/core": "^7.25.2",
23 | "@babel/preset-env": "^7.25.3",
24 | "@babel/runtime": "^7.25.0",
25 | "@react-native-community/cli": "15.0.1",
26 | "@react-native-community/cli-platform-android": "15.0.1",
27 | "@react-native-community/cli-platform-ios": "15.0.1",
28 | "@react-native/babel-preset": "0.76.5",
29 | "@react-native/metro-config": "0.76.5",
30 | "@react-native/typescript-config": "0.76.5",
31 | "react-native-builder-bob": "^0.35.2"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/example/react-native.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const pkg = require('../package.json');
3 |
4 | module.exports = {
5 | project: {
6 | ios: {
7 | automaticPodsInstallation: true,
8 | },
9 | },
10 | dependencies: {
11 | [pkg.name]: {
12 | root: path.join(__dirname, '..'),
13 | platforms: {
14 | // Codegen script incorrectly fails without this
15 | // So we explicitly specify the platforms with empty object
16 | ios: {},
17 | android: {},
18 | },
19 | },
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { GestureHandlerRootView } from 'react-native-gesture-handler';
2 |
3 | import { Home } from './screens/Home/Home';
4 | import { Button, SafeAreaView, StyleSheet } from 'react-native';
5 | import { View, Text } from 'react-native';
6 | import { createNativeStackNavigator } from '@react-navigation/native-stack';
7 | import {
8 | NavigationContainer,
9 | type NavigationProp,
10 | } from '@react-navigation/native';
11 |
12 | export function DetailsScreen() {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | const Stack = createNativeStackNavigator();
23 |
24 | function HomeScreen({ navigation }: { navigation: NavigationProp }) {
25 | return (
26 |
27 | Home Screen
28 |
33 | );
34 | }
35 |
36 | export function App() {
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | const styles = StyleSheet.create({
50 | flex: {
51 | flex: 1,
52 | },
53 | container: {
54 | flex: 1,
55 | backgroundColor: '#f4f4f4',
56 | },
57 | home: { flex: 1, alignItems: 'center', justifyContent: 'center' },
58 | });
59 |
--------------------------------------------------------------------------------
/example/src/BaseExample.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import {
4 | StyleSheet,
5 | Text,
6 | View,
7 | TouchableOpacity as RNTouchableOpacity,
8 | } from 'react-native';
9 | import {
10 | KeyboardExtendedBaseView,
11 | type KeyPress,
12 | KeyboardExtendedInput,
13 | KeyboardExtendedView,
14 | KeyboardExtendedPressable,
15 | type KeyboardExtendedViewType,
16 | type OnKeyPress,
17 | withKeyboardFocus,
18 | } from 'react-native-external-keyboard';
19 |
20 | const TouchableOpacity = withKeyboardFocus(RNTouchableOpacity);
21 |
22 | export const BaseExample = () => {
23 | const ref = React.useRef(null);
24 | const [isKeyDown, setIsKeyDown] = React.useState(true);
25 | const [status, setStatus] = React.useState('Not pressed');
26 | const [textInput, setTextInput] = React.useState('Text input here!');
27 | const [keyInfo, setKeyInfo] = React.useState(undefined);
28 |
29 | const onKeyUpHandler = (e: OnKeyPress) => {
30 | setIsKeyDown(false);
31 | setKeyInfo(e.nativeEvent);
32 | };
33 | const onKeyDownHandler = (e: OnKeyPress) => {
34 | setIsKeyDown(true);
35 | setKeyInfo(e.nativeEvent);
36 | };
37 |
38 | const [showAutoFocus, setShowAutoFocus] = React.useState(false);
39 | return (
40 |
41 | {
45 | ref.current?.focus();
46 | }}
47 | >
48 | Jump
49 |
50 | {
52 | setShowAutoFocus(true);
53 | }}
54 | >
55 | Show auto focus component
56 |
57 | {showAutoFocus && (
58 |
59 | AutoFocus
60 |
61 | )}
62 | setStatus('onPress')}
67 | onPressIn={() => setStatus('onPressIn')}
68 | onPressOut={() => setStatus('onPressOut')}
69 | onLongPress={() => setStatus('onLongPress')}
70 | >
71 | On Press Check: {status}
72 |
73 |
74 | Catch
75 |
76 |
81 | {isKeyDown ? 'Press begin:' : 'Press ended:'}
82 | {Object.keys(keyInfo ?? {}).map((key) => (
83 |
84 | {
85 | {`${key}: ${
86 | (keyInfo as Record)[key] ??
87 | ''
88 | }`}
89 | }
90 |
91 | ))}
92 |
93 |
94 |
95 | Parent component
96 |
97 | Child component 1
98 |
99 |
100 | Child component 2
101 |
102 |
103 |
108 |
109 | Border here
110 |
111 |
112 | );
113 | };
114 |
115 | const styles = StyleSheet.create({
116 | container: {
117 | flex: 1,
118 | alignItems: 'center',
119 | justifyContent: 'center',
120 | },
121 | box: {
122 | width: 60,
123 | height: 60,
124 | marginVertical: 20,
125 | },
126 | divider: { height: 10 },
127 | pressFocusStyle: { backgroundColor: '#b2c6b7' },
128 | borderExample: {
129 | marginVertical: 10,
130 | borderColor: 'black',
131 | borderRadius: 15,
132 | borderWidth: 2,
133 | padding: 10,
134 | },
135 | });
136 |
--------------------------------------------------------------------------------
/example/src/ModalExample.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Button, Modal, Pressable, StyleSheet, Text, View } from 'react-native';
4 | import { withKeyboardFocus } from 'react-native-external-keyboard';
5 |
6 | const KeyboardedPressable = withKeyboardFocus(Pressable);
7 |
8 | export const ModalExample = () => {
9 | const [showModal, setShowModal] = React.useState(false);
10 |
11 | return (
12 |
13 | {
15 | setShowModal(true);
16 | }}
17 | >
18 | Jump
19 |
20 |
21 |
22 |
23 | Modal here
24 |
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | const styles = StyleSheet.create({
45 | container: {
46 | flex: 1,
47 | alignItems: 'center',
48 | justifyContent: 'center',
49 | },
50 | box: {
51 | width: 60,
52 | height: 60,
53 | marginVertical: 20,
54 | },
55 | modalRootView: {
56 | flex: 1,
57 | justifyContent: 'center',
58 | alignItems: 'center',
59 | },
60 | divider: { height: 10 },
61 | pressFocusStyle: { backgroundColor: '#b2c6b7' },
62 | borderExample: {
63 | marginVertical: 10,
64 | borderColor: 'black',
65 | borderRadius: 15,
66 | borderWidth: 2,
67 | padding: 10,
68 | },
69 | });
70 |
--------------------------------------------------------------------------------
/example/src/components/Cats/Cats.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef, useImperativeHandle, useRef } from 'react';
2 | import { ScrollView, StyleSheet } from 'react-native';
3 | import type { KeyboardFocus } from 'react-native-external-keyboard';
4 | import { FocusableImage } from './FocusableImage';
5 |
6 | export const Cats = forwardRef((_, ref) => {
7 | const firstRef = useRef(null);
8 |
9 | useImperativeHandle(ref, () => ({
10 | focus: () => firstRef?.current?.focus(),
11 | }));
12 |
13 | return (
14 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | });
39 |
40 | const styles = StyleSheet.create({
41 | scroll: { flexDirection: 'row', flexWrap: 'wrap' },
42 | container: { height: 150, padding: 1 },
43 | image: { width: '100%', height: 150 },
44 | });
45 |
--------------------------------------------------------------------------------
/example/src/components/Cats/FocusableImage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Image,
4 | type ImageSourcePropType,
5 | type DimensionValue,
6 | Platform,
7 | StyleSheet,
8 | } from 'react-native';
9 | import {
10 | Pressable,
11 | type KeyboardFocus,
12 | type TintType,
13 | } from 'react-native-external-keyboard';
14 |
15 | const tinyType: TintType | undefined = Platform.select({
16 | android: 'hover',
17 | default: undefined,
18 | });
19 |
20 | export const FocusableImage = React.forwardRef<
21 | KeyboardFocus,
22 | { source: ImageSourcePropType; width: string }
23 | >(({ width, source }, ref) => {
24 | return (
25 |
34 |
35 |
36 | );
37 | });
38 |
39 | const styles = StyleSheet.create({
40 | container: { height: 150, padding: 1 },
41 | image: { width: '100%', height: 150 },
42 | });
43 |
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/01.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/02.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/03.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/06.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/07.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/08.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/09.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/10.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/11.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/12.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/13.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/14.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/15.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/16.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/17.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/18.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/19.png
--------------------------------------------------------------------------------
/example/src/components/Cats/assets/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArturKalach/react-native-external-keyboard/6d4ae711800c15f095f088fadfb03ec2ceb177f8/example/src/components/Cats/assets/20.png
--------------------------------------------------------------------------------
/example/src/components/ContrastColors/Color/Color.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, Text, View, type ViewStyle } from 'react-native';
2 |
3 | type ColorProps = {
4 | color: string;
5 | colorTag: string;
6 | backround: string;
7 | contrast: string;
8 | style?: ViewStyle;
9 | };
10 |
11 | export const Color = ({
12 | color,
13 | colorTag,
14 | backround,
15 | contrast,
16 | style,
17 | }: ColorProps) => {
18 | return (
19 |
28 |
36 | {backround}
37 |
38 |
39 |
47 | Contrast
48 |
49 |
50 | {colorTag}
51 | {contrast}
52 |
53 |
54 | );
55 | };
56 |
57 | const styles = StyleSheet.create({
58 | container: {
59 | borderRadius: 10,
60 | padding: 10,
61 | },
62 | color: {
63 | width: '100%',
64 | textAlign: 'center',
65 | marginBottom: 5,
66 | fontWeight: 'bold',
67 | padding: 10,
68 | },
69 | divider: {
70 | backgroundColor: 'white',
71 | height: 1,
72 | width: '100%',
73 | marginBottom: 5,
74 | },
75 | contrast: {
76 | width: '100%',
77 | textAlign: 'center',
78 | marginBottom: 5,
79 | fontSize: 10,
80 | },
81 | colorInfo: {
82 | justifyContent: 'space-between',
83 | flexDirection: 'row',
84 | },
85 | bold: {
86 | fontWeight: 'bold',
87 | },
88 | });
89 |
--------------------------------------------------------------------------------
/example/src/components/ContrastColors/ContrastColors.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from 'react';
2 | import { FlatList, StyleSheet, View } from 'react-native';
3 | import { Color } from './Color/Color';
4 | import { type KeyboardFocus, Pressable } from 'react-native-external-keyboard';
5 |
6 | const colors: {
7 | background: string;
8 | color: string;
9 | colorTag: string;
10 | contrast: string;
11 | }[] = [
12 | {
13 | background: '#000000',
14 | color: '#E5C804',
15 | colorTag: 'Orange Text',
16 | contrast: '12.59:1',
17 | },
18 | {
19 | background: '#5e9753',
20 | color: '#fff',
21 | colorTag: 'White Text',
22 | contrast: '3.48:1',
23 | },
24 | {
25 | background: '#8034ec',
26 | color: '#fff',
27 | colorTag: 'White Text',
28 | contrast: '5.8:1',
29 | },
30 | {
31 | background: '#59152c',
32 | color: '#fff',
33 | colorTag: 'White Text',
34 | contrast: '13.38:1',
35 | },
36 | {
37 | background: '#71b4a3',
38 | color: '#000',
39 | colorTag: 'Black Text',
40 | contrast: '8.75:1',
41 | },
42 | {
43 | background: '#00bf7d',
44 | color: '#000',
45 | colorTag: 'Black Text',
46 | contrast: '8.75:1',
47 | },
48 | {
49 | background: '#00b4c5',
50 | color: '#000',
51 | colorTag: 'Black Text',
52 | contrast: '8.33:1',
53 | },
54 | {
55 | background: '#c44601',
56 | color: '#fff',
57 | colorTag: 'White Text',
58 | contrast: '4.97:1',
59 | },
60 | {
61 | background: '#b51963',
62 | color: '#fff',
63 | colorTag: 'White Text',
64 | contrast: '6.39:1',
65 | },
66 | {
67 | background: '#89ce00',
68 | color: '#000',
69 | colorTag: 'Black Text',
70 | contrast: '10.89:1',
71 | },
72 | ];
73 |
74 | const Separator = () => ;
75 |
76 | export const ContrastColors = forwardRef((_, ref) => {
77 | return (
78 | (
82 |
88 |
94 |
95 | )}
96 | ItemSeparatorComponent={Separator}
97 | columnWrapperStyle={styles.column}
98 | keyExtractor={(item) => `${item.background}_${item.color}`}
99 | numColumns={2}
100 | />
101 | );
102 | });
103 |
104 | const styles = StyleSheet.create({
105 | container: { padding: 10 },
106 | column: { justifyContent: 'space-between' },
107 | item: { width: '48%' },
108 | separator: { width: '100%', height: 13 },
109 | });
110 |
--------------------------------------------------------------------------------
/example/src/components/LineButton/LineButton.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, Text, type ViewStyle } from 'react-native';
2 | import { Pressable } from 'react-native-external-keyboard';
3 |
4 | export type LineButtonProps = {
5 | title: string;
6 | onPress?: () => void;
7 | onLongPress?: () => void;
8 | style?: ViewStyle;
9 | onFocus?: () => void;
10 | onBlur?: () => void;
11 | autoFocus?: boolean;
12 | };
13 |
14 | export const LineButton = ({
15 | title,
16 | onPress,
17 | onLongPress,
18 | style,
19 | onFocus,
20 | onBlur,
21 | autoFocus,
22 | }: LineButtonProps) => {
23 | return (
24 |
35 | {title}
36 |
37 | );
38 | };
39 |
40 | const styles = StyleSheet.create({
41 | container: {
42 | borderRadius: 15,
43 | },
44 | content: {
45 | height: 45,
46 | paddingHorizontal: 10,
47 | paddingVertical: 5,
48 | justifyContent: 'center',
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/example/src/screens/Home/Home.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { LineButton } from '../../components/LineButton/LineButton';
4 | import type { KeyboardFocus } from 'react-native-external-keyboard';
5 | import { Cats } from '../../components/Cats/Cats';
6 | import { ComponentsExample } from '../../components/ComponentsExample/ComponentsExample';
7 | import { FocusGroupExample } from '../../components/ContrastColors/FocusGroupExample';
8 |
9 | const Divider = () => (
10 |
11 |
12 |
13 | );
14 |
15 | const Menu = ({
16 | onPress,
17 | onLongPress,
18 | }: {
19 | onLongPress: (value: string) => void;
20 | onPress: (value: string) => void;
21 | }) => {
22 | return (
23 |
24 | onLongPress('Components')}
26 | onPress={() => onPress('Components')}
27 | title="Components"
28 | />
29 |
30 | onLongPress('Colors')}
33 | onPress={() => onPress('Colors')}
34 | title="Colors"
35 | />
36 |
37 | onLongPress('Cats')}
39 | onPress={() => onPress('Cats')}
40 | title="Cats"
41 | />
42 |
43 | );
44 | };
45 |
46 | const ContentDivider = () => ;
47 |
48 | export const Home = () => {
49 | const [selected, setSelected] = useState('Components');
50 | const componentsExampleRef = useRef(null);
51 | const contrastColorsRef = useRef(null);
52 | const catsRef = useRef(null);
53 | const onLongPressHandle = (value: string) => {
54 | if (value === 'Components') {
55 | componentsExampleRef?.current?.focus();
56 | }
57 | if (value === 'Colors') {
58 | contrastColorsRef?.current?.focus();
59 | }
60 |
61 | if (value === 'Cats') {
62 | catsRef?.current?.focus();
63 | }
64 | };
65 |
66 | return (
67 |
68 |
69 |
70 |
71 | {selected}:
72 |
73 | {selected === 'Components' && (
74 |
75 | )}
76 | {selected === 'Colors' && }
77 | {selected === 'Cats' && }
78 |
79 | );
80 | };
81 |
82 | const styles = StyleSheet.create({
83 | container: { flex: 1 },
84 | menu: { padding: 10 },
85 | content: {
86 | flex: 1,
87 | backgroundColor: '#ffffff',
88 | borderRadius: 15,
89 | padding: 10,
90 | },
91 | divider: { height: 20, width: '100%' },
92 | menuDivider: {
93 | width: '100%',
94 | backgroundColor: '#ffffff',
95 | paddingHorizontal: 10,
96 | },
97 | menuDividerLine: {
98 | backgroundColor: '#aaa',
99 | width: '100%',
100 | height: 1,
101 | },
102 | menuContainer: { borderRadius: 15, backgroundColor: '#ffffff' },
103 | });
104 |
--------------------------------------------------------------------------------
/ios/Delegates/RNCEKVFocusDelegate/RNCEKVFocusDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVFocusDelegate.h
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 24/01/2025.
6 | //
7 |
8 | #ifndef RNCEKVFocusDelegate_h
9 | #define RNCEKVFocusDelegate_h
10 |
11 | #import
12 | #import "RNCEKVFocusProtocol.h"
13 |
14 | @interface RNCEKVFocusDelegate : NSObject
15 |
16 | - (instancetype _Nonnull )initWithView:(UIView *_Nonnull)view;
17 |
18 | - (UIView*_Nonnull)getFocusingView;
19 | - (BOOL)canBecomeFocused;
20 | - (nullable NSNumber*)isFocusChanged:(UIFocusUpdateContext *)context;
21 |
22 | @end
23 |
24 |
25 | #endif /* RNCEKVFocusDelegate_h */
26 |
--------------------------------------------------------------------------------
/ios/Delegates/RNCEKVFocusDelegate/RNCEKVFocusDelegate.mm:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVFocusDelegate.mm
3 | // react-native-external-keyboard
4 | //
5 | // Created by Artur Kalach on 24/01/2025.
6 | //
7 |
8 | #import
9 |
10 |
11 | #import "RNCEKVFocusDelegate.h"
12 | #import "RNCEKVFocusProtocol.h"
13 | #import "RNCEKVFocusEffectUtility.h"
14 |
15 | @implementation RNCEKVFocusDelegate{
16 | UIView* _delegate;
17 | }
18 |
19 | - (instancetype _Nonnull )initWithView:(UIView *_Nonnull)delegate{
20 | self = [super init];
21 | if (self) {
22 | _delegate = delegate;
23 | }
24 | return self;
25 | }
26 |
27 | // ToDo RNCEKV-2, Double check condition, count more then 1 means that a view can be a ViewGroup, bun when we use hover component it can be considered as ViewGroup instead of touchable component, it can be improved by flag, or by removing hover component from js implementation
28 | - (UIView*)getFocusingView {
29 | if(_delegate.isGroup) {
30 | return _delegate;
31 | }
32 |
33 | if(_delegate.subviews.count > 0 && _delegate.subviews[0].canBecomeFocused) {
34 | return _delegate.subviews[0];
35 | }
36 |
37 | return _delegate;
38 | }
39 |
40 | - (BOOL)canBecomeFocused {
41 | if(!_delegate.canBeFocused) {
42 | return false;
43 | }
44 | return [self getFocusingView] == _delegate;
45 | }
46 |
47 | - (NSNumber*)isFocusChanged:(UIFocusUpdateContext *)context {
48 | UIView* view = [self getFocusingView];
49 | if(context.nextFocusedView == view) {
50 | return @YES;
51 | } else if (context.previouslyFocusedView == view) {
52 | return @NO;
53 | }
54 |
55 | return nil;
56 | }
57 |
58 |
59 | @end
60 |
--------------------------------------------------------------------------------
/ios/Delegates/RNCEKVFocusDelegate/RNCEKVFocusProtocol.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVFocusProtocol.h
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 24/01/2025.
6 | //
7 |
8 | #ifndef RNCEKVFocusProtocol_h
9 | #define RNCEKVFocusProtocol_h
10 |
11 | #import
12 |
13 | @protocol RNCEKVFocusProtocol
14 | - (BOOL)canBeFocused;
15 | - (BOOL)isGroup;
16 | @end
17 |
18 | #endif /* RNCEKVFocusProtocol_h */
19 |
--------------------------------------------------------------------------------
/ios/Delegates/RNCEKVGroupIdentifierDelegate/RNCEKVGroupIdentifierDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVGroupIdentifierDelegate.h
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 24/01/2025.
6 | //
7 |
8 | #ifndef RNCEKVGroupIdentifierDelegate_h
9 | #define RNCEKVGroupIdentifierDelegate_h
10 |
11 | #import
12 | #import "RNCEKVGroupIdentifierProtocol.h"
13 |
14 | @interface RNCEKVGroupIdentifierDelegate : NSObject
15 |
16 | - (instancetype _Nonnull )initWithView:(UIView *_Nonnull)view;
17 |
18 | - (NSString*_Nonnull) getFocusGroupIdentifier;
19 | - (void)updateGroupIdentifier;
20 | - (void)clear;
21 | @end
22 |
23 |
24 | #endif /* RNCEKVGroupIdentifierDelegate_h */
25 |
--------------------------------------------------------------------------------
/ios/Delegates/RNCEKVGroupIdentifierDelegate/RNCEKVGroupIdentifierDelegate.mm:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVGroupIdentifierDelegate.mm
3 | // react-native-external-keyboard
4 | //
5 | // Created by Artur Kalach on 24/01/2025.
6 | //
7 |
8 | #import
9 |
10 | #import "RNCEKVGroupIdentifierDelegate.h"
11 | #import "RNCEKVFocusEffectUtility.h"
12 |
13 | @implementation RNCEKVGroupIdentifierDelegate {
14 | UIView* _delegate;
15 | NSString* _tagId;
16 | }
17 |
18 | - (instancetype _Nonnull )initWithView:(UIView *_Nonnull)delegate{
19 | self = [super init];
20 | if (self) {
21 | _delegate = delegate;
22 | }
23 | return self;
24 | }
25 |
26 |
27 | - (NSString*) getFocusGroupIdentifier {
28 | if(_delegate.customGroupId) {
29 | return _delegate.customGroupId;
30 | }
31 |
32 | if(_tagId) {
33 | return _tagId;
34 | }
35 |
36 | NSUUID *uuid = [NSUUID UUID];
37 | NSString *uniqueID = [uuid UUIDString];
38 | _tagId = [NSString stringWithFormat:@"app.group.%@", uniqueID];
39 |
40 | return _tagId;
41 | }
42 |
43 |
44 | - (void) updateGroupIdentifier {
45 | if (@available(iOS 14.0, *)) {
46 | UIView* focusView = [_delegate getFocusTargetView];
47 | if(focusView) {
48 | focusView.focusGroupIdentifier = [self getFocusGroupIdentifier];
49 | }
50 | }
51 | }
52 |
53 | - (void) clear {
54 | _tagId = nil;
55 | if (@available(iOS 14.0, *)) {
56 | UIView* focusView = [_delegate getFocusTargetView];
57 | if(focusView) {
58 | focusView.focusGroupIdentifier = nil;
59 | }
60 | }
61 | }
62 | @end
63 |
--------------------------------------------------------------------------------
/ios/Delegates/RNCEKVGroupIdentifierDelegate/RNCEKVGroupIdentifierProtocol.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVGroupIdentifierProtocol.h
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 24/01/2025.
6 | //
7 |
8 | #ifndef RNCEKVGroupIdentifierProtocol_h
9 | #define RNCEKVGroupIdentifierProtocol_h
10 |
11 | #import
12 |
13 | @protocol RNCEKVGroupIdentifierProtocol
14 |
15 | - (NSString*) customGroupId;
16 | - (UIView*) getFocusTargetView;
17 |
18 | @end
19 |
20 | #endif /* RNCEKVGroupIdentifierProtocol_h */
21 |
--------------------------------------------------------------------------------
/ios/Delegates/RNCEKVHaloDelegate/RNCEKVHaloDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVHaloDelegate.h
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 24/01/2025.
6 | //
7 |
8 | #ifndef RNCEKVHaloDelegate_h
9 | #define RNCEKVHaloDelegate_h
10 |
11 | #import
12 | #import "RNCEKVHaloProtocol.h"
13 |
14 | @interface RNCEKVHaloDelegate : NSObject
15 |
16 | - (instancetype _Nonnull )initWithView:(UIView *_Nonnull)view;
17 |
18 | - (void)displayHalo:(BOOL)force;
19 | - (void)displayHalo;
20 | - (void)updateHalo;
21 | - (void)clear;
22 |
23 | @end
24 |
25 | #endif /* RNCEKVHaloDelegate_h */
26 |
--------------------------------------------------------------------------------
/ios/Delegates/RNCEKVHaloDelegate/RNCEKVHaloDelegate.mm:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVHaloDelegate.mm
3 | // react-native-external-keyboard
4 | //
5 | // Created by Artur Kalach on 24/01/2025.
6 | //
7 |
8 | #import
9 |
10 | #import "RNCEKVFocusEffectUtility.h"
11 | #import "RNCEKVHaloDelegate.h"
12 |
13 | @implementation RNCEKVHaloDelegate {
14 | UIView *_delegate;
15 | UIFocusEffect *_focusEffect;
16 | CGFloat _prevHaloExpendX;
17 | CGFloat _prevHaloExpendY;
18 | CGFloat _prevHaloCornerRadius;
19 | CGRect _prevBounds;
20 | BOOL _recycled;
21 | }
22 |
23 | - (instancetype _Nonnull)initWithView:
24 | (UIView *_Nonnull)delegate {
25 | self = [super init];
26 | if (self) {
27 | _delegate = delegate;
28 | _focusEffect = nil;
29 | _prevBounds = CGRect();
30 | _prevHaloExpendX = 0;
31 | _prevHaloExpendY = 0;
32 | _prevHaloCornerRadius = 0;
33 | _recycled = true;
34 | }
35 | return self;
36 | }
37 |
38 | - (BOOL)isHaloHidden {
39 | NSNumber *isHaloActive = [_delegate isHaloActive];
40 | return [isHaloActive isEqual:@NO];
41 | }
42 |
43 | - (void)displayHalo:(BOOL)force {
44 | _focusEffect = nil;
45 | _recycled = true;
46 |
47 | [self displayHalo];
48 | }
49 |
50 | - (void)displayHalo {
51 | if (@available(iOS 15.0, *)) {
52 | UIView *focusingView = [_delegate getFocusTargetView];
53 | UIFocusEffect *prevEffect = _focusEffect;
54 |
55 | BOOL isHidden = [self isHaloHidden];
56 |
57 | if (isHidden) {
58 | _focusEffect = [RNCEKVFocusEffectUtility emptyFocusEffect];
59 | }
60 |
61 | BOOL hasHaloSettings = _delegate.haloExpendX || _delegate.haloExpendY ||
62 | _delegate.haloCornerRadius;
63 | BOOL isDifferentBounds =
64 | !CGRectEqualToRect(_prevBounds, focusingView.bounds);
65 | BOOL isDifferent = _prevHaloExpendX != _delegate.haloExpendX ||
66 | _prevHaloExpendY != _delegate.haloExpendY ||
67 | _prevHaloCornerRadius != _delegate.haloCornerRadius ||
68 | isDifferentBounds;
69 |
70 | // ToDo refactor for better halo setup RNCEKV-7, RNCEKV-8
71 | if (!isHidden && hasHaloSettings && isDifferent) {
72 | _prevHaloExpendX = _delegate.haloExpendX;
73 | _prevHaloExpendY = _delegate.haloExpendY;
74 | _prevHaloCornerRadius = _delegate.haloCornerRadius;
75 | _prevBounds = focusingView.bounds;
76 |
77 | _focusEffect =
78 | [RNCEKVFocusEffectUtility getFocusEffect:focusingView
79 | withExpandedX:_delegate.haloExpendX
80 | withExpandedY:_delegate.haloExpendY
81 | withCornerRadius:_delegate.haloCornerRadius];
82 | }
83 |
84 | if ((_focusEffect == nil && _recycled) || (_focusEffect != nil && prevEffect != _focusEffect &&
85 | focusingView.focusEffect != _focusEffect)) {
86 | _recycled = false;
87 | focusingView.focusEffect = _focusEffect;
88 | }
89 | }
90 | }
91 |
92 | - (void)updateHalo {
93 | if ([self isHaloHidden])
94 | return;
95 | if (@available(iOS 15.0, *)) {
96 | BOOL shouldUpdate = _delegate.haloExpendX || _delegate.haloExpendY ||
97 | _delegate.haloCornerRadius;
98 | if (!shouldUpdate)
99 | return;
100 |
101 | UIView *focusingView = [_delegate getFocusTargetView];
102 | UIFocusEffect *focusEffect =
103 | [RNCEKVFocusEffectUtility getFocusEffect:focusingView
104 | withExpandedX:_delegate.haloExpendX
105 | withExpandedY:_delegate.haloExpendY
106 | withCornerRadius:_delegate.haloCornerRadius];
107 |
108 | focusingView.focusEffect = focusEffect;
109 | }
110 | }
111 |
112 | - (void) clear {
113 | UIView *focusingView = [_delegate getFocusTargetView];
114 | focusingView.focusEffect = nil;
115 | _focusEffect = nil;
116 | _recycled = true;
117 | _prevBounds = CGRect();
118 | _prevHaloExpendX = 0;
119 | _prevHaloExpendY = 0;
120 | _prevHaloCornerRadius = 0;
121 | }
122 |
123 | @end
124 |
--------------------------------------------------------------------------------
/ios/Delegates/RNCEKVHaloDelegate/RNCEKVHaloProtocol.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVHaloProtocol.h
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 24/01/2025.
6 | //
7 |
8 | #ifndef RNCEKVHaloProtocol_h
9 | #define RNCEKVHaloProtocol_h
10 |
11 | @protocol RNCEKVHaloProtocol
12 |
13 | - (NSNumber *)isHaloActive;
14 | - (CGFloat) haloCornerRadius;
15 | - (CGFloat) haloExpendX;
16 | - (CGFloat) haloExpendY;
17 | - (UIView*) getFocusTargetView;
18 |
19 | @end
20 |
21 | #endif /* RNCEKVHaloProtocol_h */
22 |
--------------------------------------------------------------------------------
/ios/Extensions/RCTTextInputComponentView+RNCEKVExternalKeyboard.h:
--------------------------------------------------------------------------------
1 | //
2 | // RCTTextInputComponentView+RNCEKVExternalKeyboard.h
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 14/11/2024.
6 | //
7 |
8 | #ifndef RCTTextInputComponentView_RNCEKVExternalKeyboard_h
9 | #define RCTTextInputComponentView_RNCEKVExternalKeyboard_h
10 | #import
11 | #import
12 |
13 | @interface RCTTextInputComponentView (RNCEKVExternalKeyboard)
14 |
15 | @property (nonatomic, readonly) UIView *backedTextInputView;
16 |
17 | @end
18 |
19 | #endif /* RCTTextInputComponentView_RNCEKVExternalKeyboard_h */
20 |
--------------------------------------------------------------------------------
/ios/Extensions/RCTTextInputComponentView+RNCEKVExternalKeyboard.mm:
--------------------------------------------------------------------------------
1 | //
2 | // RCTTextInputComponentView+RNCEKVExternalKeyboard.mm
3 | // CocoaAsyncSocket
4 | //
5 | // Created by Artur Kalach on 14/11/2024.
6 | //
7 | #ifdef RCT_NEW_ARCH_ENABLED
8 | #import "RCTTextInputComponentView+RNCEKVExternalKeyboard.h"
9 | #import
10 | #import
11 |
12 | @implementation RCTTextInputComponentView (RNCEKVExternalKeyboard)
13 |
14 | - (UIView *)backedTextInputView {
15 | Ivar ivar = class_getInstanceVariable([self class], "_backedTextInputView");
16 | return object_getIvar(self, ivar);
17 | }
18 |
19 | @end
20 |
21 | #endif
22 |
23 |
--------------------------------------------------------------------------------
/ios/Extensions/UIViewController+RNCEKVExternalKeyboard.h:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+RNCEKVExternalKeyboard.h
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 06/10/2024.
6 | //
7 |
8 | #ifndef UIViewController_RNCEKVExternalKeyboard_h
9 | #define UIViewController_RNCEKVExternalKeyboard_h
10 |
11 | #import
12 |
13 | @interface UIViewController (RNCEKVExternalKeyboard)
14 | @property (nonatomic, strong) UIView *customFocusView;
15 | @end
16 |
17 | #endif /* UIViewController_RNCEKVExternalKeyboard_h */
18 |
--------------------------------------------------------------------------------
/ios/Extensions/UIViewController+RNCEKVExternalKeyboard.mm:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+RNCEKVExternalKeyboard.m
3 | // CocoaAsyncSocket
4 | //
5 | // Created by Artur Kalach on 06/10/2024.
6 | //
7 |
8 | #import
9 |
10 | #import "UIViewController+RNCEKVExternalKeyboard.h"
11 | #import
12 |
13 |
14 |
15 | void SwizzleInstanceMethod(Class swizzleClass, SEL originalSelector, SEL swizzledSelector) {
16 | Method originalMethod = class_getInstanceMethod(swizzleClass, originalSelector);
17 | Method swizzledMethod = class_getInstanceMethod(swizzleClass, swizzledSelector);
18 | BOOL didAddMethod = class_addMethod(swizzleClass,
19 | originalSelector,
20 | method_getImplementation(swizzledMethod),
21 | method_getTypeEncoding(swizzledMethod));
22 |
23 | if (didAddMethod) {
24 | class_replaceMethod(swizzleClass,
25 | swizzledSelector,
26 | method_getImplementation(originalMethod),
27 | method_getTypeEncoding(originalMethod));
28 | } else {
29 | method_exchangeImplementations(originalMethod, swizzledMethod);
30 | }
31 | }
32 |
33 | static char kCustomFocusViewKey;
34 |
35 | @implementation UIViewController (RNCEKVExternalKeyboard)
36 |
37 | - (UIView *)customFocusView {
38 | return objc_getAssociatedObject(self, &kCustomFocusViewKey);
39 | }
40 |
41 | - (void)setCustomFocusView:(UIView *)customFocusView {
42 | objc_setAssociatedObject(self, &kCustomFocusViewKey, customFocusView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
43 | }
44 |
45 | + (void)load
46 | {
47 | static dispatch_once_t once_token;
48 |
49 | dispatch_once(&once_token, ^{
50 | SwizzleInstanceMethod([self class], @selector(viewDidAppear:), @selector(keyboardedViewDidAppear:));
51 |
52 | method_exchangeImplementations(
53 | class_getInstanceMethod(self, @selector(preferredFocusEnvironments)),
54 | class_getInstanceMethod(self, @selector(keyboardedPreferredFocusEnvironments))
55 | );
56 | });
57 | }
58 |
59 | - (void)keyboardedViewDidAppear:(BOOL)animated {
60 | [self keyboardedViewDidAppear:animated];
61 | [[NSNotificationCenter defaultCenter] postNotificationName:@"ViewControllerChangedNotification" object:self];
62 | }
63 |
64 | - (NSArray> *)keyboardedPreferredFocusEnvironments {
65 | NSArray> *originalEnvironments = [self keyboardedPreferredFocusEnvironments];
66 |
67 | NSMutableArray *focusEnvironments = [originalEnvironments mutableCopy];
68 |
69 | UIView *customFocusView = self.customFocusView;
70 | if (customFocusView) {
71 | [focusEnvironments insertObject:customFocusView atIndex:0];
72 | }
73 |
74 | return focusEnvironments;
75 | }
76 |
77 |
78 | @end
79 |
--------------------------------------------------------------------------------
/ios/Modules/RNCEKVExternalKeyboardModule.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVExternalKeyboardModule.h
3 | //
4 | // Copyright © 2022 Facebook. All rights reserved.
5 | //
6 |
7 | #ifndef RNCEKVExternalKeyboardModule_h
8 | #define RNCEKVExternalKeyboardModule_h
9 |
10 | #import
11 |
12 |
13 | @interface RNCEKVExternalKeyboardModule : NSObject
14 | @end
15 |
16 |
17 | #endif /* RNCEKVExternalKeyboardModule_h */
18 |
--------------------------------------------------------------------------------
/ios/Modules/RNCEKVExternalKeyboardModule.mm:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVExternalKeyboardModule.m
3 | // react-native-external-keyboard
4 | //
5 | // Created by Artur Kalach on 16/03/2025.
6 | //
7 |
8 | #import
9 | #import "RNCEKVExternalKeyboardModule.h"
10 |
11 | #ifdef RCT_NEW_ARCH_ENABLED
12 | #import "RNExternalKeyboardViewSpec/RNExternalKeyboardViewSpec.h"
13 | using namespace facebook::react;
14 |
15 | #endif
16 |
17 | @implementation RNCEKVExternalKeyboardModule
18 |
19 |
20 | + (BOOL)requiresMainQueueSetup
21 | {
22 | return YES;
23 | }
24 |
25 | RCT_EXPORT_MODULE(ExternalKeyboardModule);
26 |
27 |
28 | RCT_EXPORT_METHOD(dismissKeyboard) {
29 |
30 | }
31 |
32 | #ifdef RCT_NEW_ARCH_ENABLED
33 | - (std::shared_ptr)getTurboModule:
34 | (const facebook::react::ObjCTurboModule::InitParams &)params
35 | {
36 | return std::make_shared(params);
37 | }
38 | #endif
39 | @end
40 |
41 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVExternalKeyboardView/Helpers/RNCEKVFabricEventHelper/RNCEKVFabricEventHelper.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVFabricEventHelper.h
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 22/08/2024.
6 | //
7 |
8 | #ifdef RCT_NEW_ARCH_ENABLED
9 | #ifndef RNCEKVFabricEventHelper_h
10 | #define RNCEKVFabricEventHelper_h
11 | #import
12 |
13 | using namespace facebook::react;
14 |
15 | @interface RNCEKVFabricEventHelper: NSObject
16 |
17 | + (void)onKeyDownPressEventEmmiter:(NSDictionary*) dictionary withEmitter:(facebook::react::SharedViewEventEmitter) emitter;
18 |
19 | + (void)onKeyUpPressEventEmmiter:(NSDictionary*) dictionary withEmitter:(facebook::react::SharedViewEventEmitter) emitter;
20 |
21 | + (void)onBubbledKeyDownPressEventEmmiter:(NSDictionary*) dictionary withEmitter:(facebook::react::SharedViewEventEmitter) emitter;
22 |
23 | + (void)onBubbledKeyUpPressEventEmmiter:(NSDictionary*) dictionary withEmitter:(facebook::react::SharedViewEventEmitter) emitter;
24 |
25 | + (void)onFocusChangeEventEmmiter:(BOOL)isFocused withEmitter:(facebook::react::SharedViewEventEmitter) emitter;
26 |
27 | + (void)onContextMenuPressEventEmmiter:(facebook::react::SharedViewEventEmitter) emitter;
28 |
29 | + (void)onBubbledContextMenuPressEventEmmiter:(facebook::react::SharedViewEventEmitter) emitter;
30 |
31 | @end
32 |
33 |
34 | #endif /* RNCEKVFabricEventHelper_h */
35 | #endif
36 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVExternalKeyboardView/Helpers/RNCEKVFabricEventHelper/RNCEKVFabricEventHelper.mm:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVFabricEventHelper.m
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 22/08/2024.
6 | //
7 | #ifdef RCT_NEW_ARCH_ENABLED
8 | #import
9 |
10 | #import "RNCEKVFabricEventHelper.h"
11 |
12 | #import
13 | #import
14 | #import
15 | #import
16 |
17 | using namespace facebook::react;
18 |
19 | @implementation RNCEKVFabricEventHelper
20 |
21 | + (void)onKeyDownPressEventEmmiter:(NSDictionary*) dictionary withEmitter:(facebook::react::SharedViewEventEmitter) _eventEmitter {
22 | if (_eventEmitter) {
23 | auto viewEventEmitter = std::static_pointer_cast(_eventEmitter);
24 |
25 | facebook::react::ExternalKeyboardViewEventEmitter::OnKeyDownPress data = {
26 | .keyCode = [[dictionary valueForKey:@"keyCode"] intValue],
27 | .isLongPress = [[dictionary valueForKey:@"isLongPress"] boolValue],
28 | .isAltPressed = [[dictionary valueForKey:@"isAltPressed"] boolValue],
29 | .isShiftPressed = [[dictionary valueForKey:@"isShiftPressed"] boolValue],
30 | .isCtrlPressed = [[dictionary valueForKey:@"isCtrlPressed"] boolValue],
31 | .isCapsLockOn = [[dictionary valueForKey:@"isCapsLockOn"] boolValue],
32 | .hasNoModifiers = [[dictionary valueForKey:@"hasNoModifiers"] boolValue],
33 | .unicode = [[dictionary valueForKey:@"unicode"] intValue],
34 | .unicodeChar = [[[dictionary valueForKey:@"unicodeChar"] stringValue] UTF8String],
35 | };
36 | viewEventEmitter->onKeyDownPress(data);
37 | };
38 | }
39 |
40 | + (void)onKeyUpPressEventEmmiter:(NSDictionary*) dictionary withEmitter:(facebook::react::SharedViewEventEmitter) _eventEmitter {
41 | if (_eventEmitter) {
42 | auto viewEventEmitter = std::static_pointer_cast(_eventEmitter);
43 |
44 | facebook::react::ExternalKeyboardViewEventEmitter::OnKeyUpPress data = {
45 | .keyCode = [[dictionary valueForKey:@"keyCode"] intValue],
46 | .isLongPress = [[dictionary valueForKey:@"isLongPress"] boolValue],
47 | .isAltPressed = [[dictionary valueForKey:@"isAltPressed"] boolValue],
48 | .isShiftPressed = [[dictionary valueForKey:@"isShiftPressed"] boolValue],
49 | .isCtrlPressed = [[dictionary valueForKey:@"isCtrlPressed"] boolValue],
50 | .isCapsLockOn = [[dictionary valueForKey:@"isCapsLockOn"] boolValue],
51 | .hasNoModifiers = [[dictionary valueForKey:@"hasNoModifiers"] boolValue],
52 | .unicode = [[dictionary valueForKey:@"unicode"] intValue],
53 | .unicodeChar = [[[dictionary valueForKey:@"unicodeChar"] stringValue] UTF8String],
54 | };
55 | viewEventEmitter->onKeyUpPress(data);
56 | };
57 | }
58 |
59 | + (void)onFocusChangeEventEmmiter:(BOOL)isFocused withEmitter:(facebook::react::SharedViewEventEmitter) _eventEmitter {
60 | if (_eventEmitter) {
61 | auto viewEventEmitter = std::static_pointer_cast(_eventEmitter);
62 | facebook::react::ExternalKeyboardViewEventEmitter::OnFocusChange data = {
63 | .isFocused = isFocused,
64 | };
65 | viewEventEmitter->onFocusChange(data);
66 | };
67 | }
68 |
69 | + (void)onContextMenuPressEventEmmiter:(facebook::react::SharedViewEventEmitter) _eventEmitter {
70 | if (_eventEmitter) {
71 | auto viewEventEmitter = std::static_pointer_cast(_eventEmitter);
72 | facebook::react::ExternalKeyboardViewEventEmitter::OnContextMenuPress data = {};
73 | viewEventEmitter->onContextMenuPress(data);
74 | };
75 | }
76 |
77 | + (void)onBubbledContextMenuPressEventEmmiter:(facebook::react::SharedViewEventEmitter) _eventEmitter {
78 | if (_eventEmitter) {
79 | auto viewEventEmitter = std::static_pointer_cast(_eventEmitter);
80 | facebook::react::ExternalKeyboardViewEventEmitter::OnBubbledContextMenuPress data = {};
81 | viewEventEmitter->onBubbledContextMenuPress(data);
82 | };
83 | }
84 |
85 |
86 | @end
87 | #endif
88 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVExternalKeyboardView/Helpers/RNCEKVFocusEffectUtility/RNCEKVFocusEffectUtility.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVFocusEffectUtility.h
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 26/08/2024.
6 | //
7 |
8 | #ifndef RNCEKVFocusEffectUtility_h
9 | #define RNCEKVFocusEffectUtility_h
10 |
11 | #import
12 |
13 | @interface RNCEKVFocusEffectUtility : NSObject
14 |
15 | + (UIFocusEffect *)emptyFocusEffect API_AVAILABLE(ios(15.0));
16 | + (UIFocusEffect *)getFocusEffect:(UIView *)effectView
17 | withExpandedX:(CGFloat)expandedX
18 | withExpandedY:(CGFloat)expandedY
19 | withCornerRadius:(CGFloat)cornerRadius API_AVAILABLE(ios(15.0));
20 |
21 | @end
22 |
23 | #endif /* RNCEKVFocusEffectUtility_h */
24 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVExternalKeyboardView/Helpers/RNCEKVFocusEffectUtility/RNCEKVFocusEffectUtility.mm:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVFocusEffectUtility.m
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 26/08/2024.
6 | //
7 |
8 | #import
9 | #import
10 | #import "RNCEKVFocusEffectUtility.h"
11 |
12 | @implementation RNCEKVFocusEffectUtility
13 |
14 | + (UIFocusEffect *)emptyFocusEffect {
15 | static UIFocusEffect *emptyFocusEffect = nil;
16 | static dispatch_once_t onceToken;
17 | dispatch_once(&onceToken, ^{
18 | emptyFocusEffect = [UIFocusHaloEffect effectWithPath: [UIBezierPath bezierPath]];
19 | });
20 | return emptyFocusEffect;
21 | }
22 |
23 | + (UIFocusEffect *)getFocusEffect:(UIView *)effectView
24 | withExpandedX:(CGFloat)expandedX
25 | withExpandedY:(CGFloat)expandedY
26 | withCornerRadius:(CGFloat)cornerRadius
27 | {
28 | CGRect haloRect = effectView.bounds;
29 | if(expandedX || expandedY) {
30 | CGFloat dx = expandedX ? expandedX : 0;
31 | CGFloat dy = expandedY ? expandedY : 0;
32 | haloRect = CGRectInset(haloRect, -dx, -dy);
33 | }
34 | CALayerCornerCurve cornerCurve = kCACornerCurveContinuous;
35 |
36 | UIFocusEffect *focusEffect = [UIFocusHaloEffect effectWithRoundedRect:haloRect cornerRadius:cornerRadius curve:cornerCurve];
37 |
38 | return focusEffect;
39 | }
40 |
41 | @end
42 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVExternalKeyboardView/Helpers/RNCEKVKeyboardKeyPressHandler/RNCEKVKeyboardKeyPressHandler.h:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardKeyPressHandler.h
3 | // ExternalKeyboard
4 | //
5 | // Created by Artur Kalach on 21.06.2023.
6 | // Copyright © 2023 Facebook. All rights reserved.
7 | //
8 |
9 | #ifndef RNCEKVKeyboardKeyPressHandler_h
10 | #define RNCEKVKeyboardKeyPressHandler_h
11 |
12 | @interface RNCEKVKeyboardKeyPressHandler:NSObject {
13 | NSMutableDictionary* _keyPressedTimestamps;
14 | }
15 |
16 | -(NSDictionary*) getKeyPressEventInfo:(NSSet *)presses
17 | withEvent:(UIPressesEvent *)event;
18 |
19 | -(NSDictionary*) actionDownHandler:(NSSet *)presses
20 | withEvent:(UIPressesEvent *)event;
21 |
22 | -(NSDictionary*) actionUpHandler:(NSSet *)presses
23 | withEvent:(UIPressesEvent *)event;
24 |
25 | @end
26 |
27 | #endif /* KeyboardKeyPressHandler_h */
28 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVExternalKeyboardView/Helpers/RNCEKVKeyboardKeyPressHandler/RNCEKVKeyboardKeyPressHandler.mm:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardKeyPressHandler.m
3 | // ExternalKeyboard
4 | //
5 | // Created by Artur Kalach on 21.06.2023.
6 | // Copyright © 2023 Facebook. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "UIKit/UIResponder.h"
11 | #import "RNCEKVKeyboardKeyPressHandler.h"
12 | #import
13 |
14 | @implementation RNCEKVKeyboardKeyPressHandler
15 |
16 | static const float LONG_PRESS_DURATION = 0.5;
17 |
18 | - (instancetype)init {
19 | self = [super init];
20 | if (self) {
21 | _keyPressedTimestamps = [[NSMutableDictionary alloc] init];
22 | }
23 | return self;
24 | }
25 |
26 | -(NSDictionary*) getKeyPressEventInfo:(NSSet *)presses
27 | withEvent:(UIPressesEvent *)event {
28 | UIKey *key = presses.allObjects[0].key;
29 |
30 | NSNumber *keyCode = @(key.keyCode);
31 | unichar unicode = 0;
32 | NSString *unicodeChar = @"";
33 | if ([key.characters length] != 0) {
34 | unicode = [key.characters characterAtIndex:0];
35 | unicodeChar = [[NSString alloc] initWithCharacters:&unicode length:1];
36 | }
37 | NSNumber *isAltPressed = @((key.modifierFlags & UIKeyModifierAlternate) > 0);
38 | NSNumber *isShiftPressed = @((key.modifierFlags & UIKeyModifierShift) > 0);
39 | NSNumber *isCtrlPressed = @((key.modifierFlags & UIKeyModifierControl) > 0);
40 | NSNumber *isCapsLockOn = @((key.modifierFlags & UIKeyModifierAlphaShift) > 0);
41 | NSNumber *hasNoModifiers = @(key.modifierFlags == 0);
42 |
43 | return @{
44 | @"keyCode": keyCode,
45 | @"isLongPress": @NO,
46 | @"unicode": @(unicode),
47 | @"unicodeChar": unicodeChar,
48 | @"isAltPressed": isAltPressed,
49 | @"isShiftPressed": isShiftPressed,
50 | @"isCtrlPressed": isCtrlPressed,
51 | @"isCapsLockOn": isCapsLockOn,
52 | @"hasNoModifiers": hasNoModifiers,
53 | };
54 | }
55 |
56 | -(NSDictionary*) actionDownHandler:(NSSet *)presses
57 | withEvent:(UIPressesEvent *)event{
58 | UIKey *key = presses.allObjects[0].key;
59 | NSNumber *keyCode = @(key.keyCode);
60 | [_keyPressedTimestamps setObject: @(event.timestamp) forKey: keyCode];
61 | NSDictionary *info = [self getKeyPressEventInfo:presses withEvent:event];
62 | return info;
63 | }
64 |
65 | -(NSDictionary*) actionUpHandler:(NSSet *)presses
66 | withEvent:(UIPressesEvent *)event {
67 | UIKey *key = presses.allObjects[0].key;
68 | NSNumber *keyCode = @(key.keyCode);
69 | NSNumber *begunPressTimestamp = [_keyPressedTimestamps objectForKey:keyCode];
70 | NSNumber *pressDuration = @([@(event.timestamp) doubleValue] - [begunPressTimestamp doubleValue]);
71 | NSNumber *isLognPress = @([pressDuration doubleValue] >= LONG_PRESS_DURATION);
72 |
73 | NSDictionary *info = [self getKeyPressEventInfo:presses withEvent:event];
74 | NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:info];
75 | [result addEntriesFromDictionary: @{
76 | @"isLongPress": isLognPress,
77 | }];
78 |
79 | return result;
80 | }
81 |
82 | @end
83 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVExternalKeyboardView/RNCEKVExternalKeyboardView.h:
--------------------------------------------------------------------------------
1 | #ifndef RNCEKVExternalKeyboardViewNativeComponent_h
2 | #define RNCEKVExternalKeyboardViewNativeComponent_h
3 | #import "RNCEKVKeyboardKeyPressHandler.h"
4 | #import
5 | #import "RNCEKVFocusProtocol.h"
6 | #import "RNCEKVHaloProtocol.h"
7 | #import "RNCEKVGroupIdentifierProtocol.h"
8 |
9 | #ifdef RCT_NEW_ARCH_ENABLED
10 | #import
11 |
12 |
13 | NS_ASSUME_NONNULL_BEGIN
14 |
15 | @interface RNCEKVExternalKeyboardView : RCTViewComponentView
16 | @property (nonatomic, strong, nullable) NSNumber *isHaloActive;
17 | @property BOOL canBeFocused;
18 | @property BOOL hasOnPressUp;
19 | @property BOOL hasOnPressDown;
20 | @property BOOL hasOnFocusChanged;
21 | @property BOOL isGroup;
22 | @property BOOL enableA11yFocus;
23 | @property (nonatomic, assign) CGFloat haloCornerRadius;
24 | @property (nonatomic, assign) CGFloat haloExpendX;
25 | @property (nonatomic, assign) CGFloat haloExpendY;
26 | @property (nullable, nonatomic, strong) UIView* myPreferredFocusedView;
27 | @property (nonatomic, strong, nullable) NSString *customGroupId;
28 | @property BOOL autoFocus;
29 |
30 | - (UIView*)getFocusTargetView;
31 |
32 | - (void)focus;
33 |
34 | @end
35 |
36 | NS_ASSUME_NONNULL_END
37 |
38 |
39 | #else /* RCT_NEW_ARCH_ENABLED */
40 |
41 |
42 | #import
43 | @interface RNCEKVExternalKeyboardView : RCTView
44 |
45 | @property BOOL autoFocus;
46 | @property BOOL canBeFocused;
47 | @property BOOL hasOnPressUp;
48 | @property BOOL hasOnPressDown;
49 | @property BOOL hasOnFocusChanged;
50 | @property BOOL isGroup;
51 | @property BOOL enableA11yFocus;
52 | @property UIView* myPreferredFocusedView;
53 | @property (nonatomic, assign) CGFloat haloCornerRadius;
54 | @property (nonatomic, assign) CGFloat haloExpendX;
55 | @property (nonatomic, assign) CGFloat haloExpendY;
56 | @property (nonatomic, copy) RCTDirectEventBlock onFocusChange;
57 | @property (nonatomic, copy) RCTDirectEventBlock onContextMenuPress;
58 | @property (nonatomic, copy) RCTDirectEventBlock onKeyUpPress;
59 | @property (nonatomic, copy) RCTDirectEventBlock onKeyDownPress;
60 | @property (nonatomic, copy) RCTBubblingEventBlock onBubbledContextMenuPress;
61 | @property (nonatomic, strong, nullable) NSString *customGroupId;
62 |
63 | - (UIView*)getFocusTargetView;
64 |
65 | @property (nonatomic, strong, nullable) NSNumber *isHaloActive;
66 | - (void)focus;
67 | @end
68 |
69 |
70 | #endif /* RCT_NEW_ARCH_ENABLED */
71 | #endif /* ExternalKeyboardViewNativeComponent_h */
72 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVExternalKeyboardView/RNCEKVExternalKeyboardViewManager.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVExternalKeyboardViewManager.h
3 | // RNCEKVExternalKeyboard
4 | //
5 | // Created by Artur Kalach on 17.07.2023.
6 | // Copyright © 2023 Facebook. All rights reserved.
7 | //
8 |
9 | #ifndef RNCEKVExternalKeyboardViewManager_h
10 | #define RNCEKVExternalKeyboardViewManager_h
11 |
12 | #import
13 | @interface RNCEKVExternalKeyboardViewManager : RCTViewManager
14 | @end
15 |
16 | #endif /* RNCEKVExternalKeyboardViewManager_h */
17 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVExternalKeyboardView/RNCEKVExternalKeyboardViewManager.mm:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 | #import "RNCEKVExternalKeyboardViewManager.h"
4 | #import "RNCEKVExternalKeyboardView.h"
5 | #import "RCTBridge.h"
6 |
7 | @implementation RNCEKVExternalKeyboardViewManager
8 |
9 | RCT_EXPORT_MODULE(ExternalKeyboardView)
10 |
11 | - (UIView *)view
12 | {
13 | return [[RNCEKVExternalKeyboardView alloc] init];
14 | }
15 |
16 | RCT_EXPORT_VIEW_PROPERTY(onFocusChange, RCTDirectEventBlock)
17 | RCT_EXPORT_VIEW_PROPERTY(onContextMenuPress, RCTDirectEventBlock)
18 | RCT_EXPORT_VIEW_PROPERTY(onBubbledContextMenuPress, RCTBubblingEventBlock)
19 | RCT_EXPORT_VIEW_PROPERTY(onKeyUpPress, RCTDirectEventBlock)
20 | RCT_EXPORT_VIEW_PROPERTY(onKeyDownPress, RCTDirectEventBlock)
21 | RCT_EXPORT_VIEW_PROPERTY(myPreferredFocusedView, UIView)
22 |
23 | RCT_CUSTOM_VIEW_PROPERTY(canBeFocused, BOOL, RNCEKVExternalKeyboardView)
24 | {
25 | BOOL value = json ? [RCTConvert BOOL:json] : YES;
26 | [view setCanBeFocused: value];
27 | }
28 |
29 | RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RNCEKVExternalKeyboardView)
30 | {
31 | if (json) {
32 | UIColor *tintColor = [RCTConvert UIColor:json];
33 | [view setTintColor: tintColor];
34 | }
35 | }
36 |
37 | RCT_CUSTOM_VIEW_PROPERTY(hasOnFocusChanged, BOOL, RNCEKVExternalKeyboardView)
38 | {
39 | BOOL value = json ? [RCTConvert BOOL:json] : NO;
40 | [view setHasOnFocusChanged: value];
41 | }
42 |
43 | RCT_CUSTOM_VIEW_PROPERTY(hasKeyDownPress, BOOL, RNCEKVExternalKeyboardView)
44 | {
45 | BOOL value = json ? [RCTConvert BOOL:json] : NO;
46 | [view setHasOnPressDown: value];
47 | }
48 |
49 | RCT_CUSTOM_VIEW_PROPERTY(hasKeyUpPress, BOOL, RNCEKVExternalKeyboardView)
50 | {
51 | BOOL value = json ? [RCTConvert BOOL:json] : NO;
52 | [view setHasOnPressUp: value];
53 | }
54 |
55 | RCT_CUSTOM_VIEW_PROPERTY(autoFocus, BOOL, RNCEKVExternalKeyboardView)
56 | {
57 | if (json) {
58 | BOOL value = [RCTConvert BOOL:json];
59 | [view setAutoFocus: value];
60 | }
61 | }
62 |
63 | RCT_CUSTOM_VIEW_PROPERTY(haloEffect, BOOL, RNCEKVExternalKeyboardView)
64 | {
65 | if(json) {
66 | BOOL value = [RCTConvert BOOL:json];
67 | if(view.isHaloActive == nil && !value) {
68 | [view setIsHaloActive: @0];
69 | }
70 | if(view.isHaloActive != nil) {
71 | [view setIsHaloActive: @(value)];
72 | }
73 | }
74 | }
75 |
76 | RCT_CUSTOM_VIEW_PROPERTY(enableA11yFocus, BOOL, RNCEKVExternalKeyboardView)
77 | {
78 | if(json) {
79 | BOOL value = json ? [RCTConvert BOOL:json] : NO;
80 | [view setEnableA11yFocus: value];
81 | }
82 | }
83 |
84 | RCT_CUSTOM_VIEW_PROPERTY(screenAutoA11yFocus, BOOL, RNCEKVExternalKeyboardView)
85 | {
86 | //stub
87 | }
88 |
89 | RCT_CUSTOM_VIEW_PROPERTY(screenAutoA11yFocusDelay, int, RNCEKVExternalKeyboardView)
90 | {
91 | //stub
92 | }
93 |
94 | RCT_CUSTOM_VIEW_PROPERTY(haloCornerRadius, float, RNCEKVExternalKeyboardView)
95 | {
96 | if(json) {
97 | CGFloat value = [RCTConvert CGFloat:json];
98 | [view setHaloCornerRadius: value];
99 | }
100 | }
101 |
102 | RCT_CUSTOM_VIEW_PROPERTY(haloExpendX, float, RNCEKVExternalKeyboardView)
103 | {
104 | if(json) {
105 | CGFloat value = [RCTConvert CGFloat:json];
106 | [view setHaloExpendX: value];
107 | }
108 | }
109 |
110 | RCT_CUSTOM_VIEW_PROPERTY(haloExpendY, float, RNCEKVExternalKeyboardView)
111 | {
112 | if(json) {
113 | CGFloat value = [RCTConvert CGFloat:json];
114 | [view setHaloExpendY: value];
115 | }
116 | }
117 |
118 |
119 | RCT_CUSTOM_VIEW_PROPERTY(group, BOOL, RNCEKVExternalKeyboardView)
120 | {
121 | BOOL value = json ? [RCTConvert BOOL:json] : NO;
122 | [view setIsGroup: value];
123 | }
124 |
125 | RCT_CUSTOM_VIEW_PROPERTY(groupIdentifier, NSString, RNCEKVExternalKeyboardView)
126 | {
127 | NSString* value = json ? [RCTConvert NSString:json] : nil;
128 | [view setCustomGroupId: value];
129 | }
130 |
131 |
132 |
133 | RCT_EXPORT_METHOD(focus:(nonnull NSNumber *)reactTag)
134 | {
135 | [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) {
136 | UIView *view = viewRegistry[reactTag];
137 | if (!view || ![view isKindOfClass:[RNCEKVExternalKeyboardView class]]) {
138 | return;
139 | }
140 | RNCEKVExternalKeyboardView *keyboardView = (RNCEKVExternalKeyboardView*)view;
141 | [keyboardView focus];
142 | }];
143 | }
144 |
145 | @end
146 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVKeyboardFocusGroupView/RNCEKVKeyboardFocusGroup.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVKeyboardFocusGroup.h
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 24/12/2024.
6 | //
7 |
8 | #ifndef RNCEKVKeyboardFocusGroup_h
9 | #define RNCEKVKeyboardFocusGroup_h
10 |
11 | #import
12 |
13 | #ifdef RCT_NEW_ARCH_ENABLED
14 | #import
15 |
16 |
17 | NS_ASSUME_NONNULL_BEGIN
18 |
19 | @interface RNCEKVKeyboardFocusGroup : RCTViewComponentView
20 | @property (nonatomic, strong, nullable) NSString *customGroupId;
21 | @property BOOL isGroupFocused;
22 | @end
23 |
24 | NS_ASSUME_NONNULL_END
25 |
26 |
27 | #else /* RCT_NEW_ARCH_ENABLED */
28 |
29 |
30 | #import
31 | @interface RNCEKVKeyboardFocusGroup : RCTView
32 | @property (nonatomic, strong, nullable) NSString *customGroupId;
33 | @property BOOL isGroupFocused;
34 | @property (nonatomic, copy) RCTDirectEventBlock onGroupFocusChange;
35 | @end
36 |
37 |
38 | #endif /* RCT_NEW_ARCH_ENABLED */
39 | #endif /* RNCEKVKeyboardFocusGroup_h */
40 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVKeyboardFocusGroupView/RNCEKVKeyboardFocusGroup.mm:
--------------------------------------------------------------------------------
1 | //
2 | // TintColorView.m
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 24/12/2024.
6 | //
7 |
8 | #import "RNCEKVKeyboardFocusGroup.h"
9 | #import
10 | #import
11 |
12 | #ifdef RCT_NEW_ARCH_ENABLED
13 | #include
14 | #import
15 | #import
16 | #import
17 | #import
18 |
19 | #import "RCTFabricComponentsPlugins.h"
20 | #import "RNCEKVFabricEventHelper.h"
21 | #import
22 |
23 | using namespace facebook::react;
24 |
25 | @interface RNCEKVKeyboardFocusGroup ()
26 |
27 | @end
28 |
29 | #endif
30 |
31 | @implementation RNCEKVKeyboardFocusGroup
32 |
33 | - (instancetype)initWithFrame:(CGRect)frame
34 | {
35 | if (self = [super initWithFrame:frame]) {
36 | _isGroupFocused = false;
37 | #ifdef RCT_NEW_ARCH_ENABLED
38 | static const auto defaultProps = std::make_shared();
39 | _props = defaultProps;
40 | #endif
41 | }
42 |
43 | return self;
44 | }
45 |
46 | - (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context
47 | withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator {
48 |
49 | [super didUpdateFocusInContext:context withAnimationCoordinator:coordinator];
50 | NSString* nextFocusGroup = context.nextFocusedView.focusGroupIdentifier;
51 | BOOL isFocused = [nextFocusGroup isEqual: _customGroupId];
52 | if(_isGroupFocused != isFocused){
53 | _isGroupFocused = isFocused;
54 | [self onFocusChangeHandler: isFocused];
55 | }
56 | }
57 |
58 | - (void)layoutSubviews {
59 | [super layoutSubviews];
60 |
61 | if (@available(iOS 14.0, *)) {
62 | if(_customGroupId) {
63 | self.focusGroupIdentifier = _customGroupId;
64 | }
65 | }
66 | }
67 |
68 | #ifdef RCT_NEW_ARCH_ENABLED
69 |
70 | - (void)onFocusChangeHandler:(BOOL) isFocused {
71 | if (_eventEmitter) {
72 | auto viewEventEmitter = std::static_pointer_cast(_eventEmitter);
73 | facebook::react::KeyboardFocusGroupEventEmitter::OnGroupFocusChange data = {
74 | .isFocused = isFocused,
75 | };
76 | viewEventEmitter->onGroupFocusChange(data);
77 | };
78 | }
79 |
80 | #else
81 | - (void)onFocusChangeHandler:(BOOL) isFocused {
82 | if(self.onGroupFocusChange) {
83 | self.onGroupFocusChange(@{ @"isFocused": @(isFocused) });
84 | }
85 | }
86 | #endif
87 |
88 |
89 | #ifdef RCT_NEW_ARCH_ENABLED
90 | + (ComponentDescriptorProvider)componentDescriptorProvider
91 | {
92 | return concreteComponentDescriptorProvider();
93 | }
94 |
95 | - (void)prepareForRecycle
96 | {
97 | [super prepareForRecycle];
98 | _customGroupId = nil;
99 | self.tintColor = nil;
100 | }
101 |
102 | - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
103 | {
104 | const auto &oldViewProps = *std::static_pointer_cast(_props);
105 | const auto &newViewProps = *std::static_pointer_cast(props);
106 | [super updateProps
107 | :props oldProps:oldProps];
108 |
109 |
110 | UIColor* newColor = RCTUIColorFromSharedColor(newViewProps.tintColor);
111 | BOOL isDifferentColor = ![newColor isEqual: self.tintColor];
112 | BOOL renewColor = newColor != nil && self.tintColor == nil;
113 | BOOL isColorChanged = oldViewProps.tintColor != newViewProps.tintColor;
114 | if(isColorChanged || renewColor || isDifferentColor) {
115 | self.tintColor = newColor;
116 | }
117 |
118 | if(oldViewProps.groupIdentifier != newViewProps.groupIdentifier || !self.customGroupId) {
119 | if(newViewProps.groupIdentifier.empty()) {
120 | [self setCustomGroupId:nil];
121 | } else {
122 | NSString *newGroupId = [NSString stringWithUTF8String:newViewProps.groupIdentifier.c_str()];
123 | [self setCustomGroupId:newGroupId];
124 | }
125 | }
126 |
127 | }
128 |
129 |
130 | Class KeyboardFocusGroupCls(void)
131 | {
132 | return RNCEKVKeyboardFocusGroup.class;
133 | }
134 |
135 |
136 | #endif
137 |
138 |
139 |
140 | @end
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVKeyboardFocusGroupView/RNCEKVKeyboardFocusGroupManager.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVKeyboardFocusGroupManager.h
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 24/12/2024.
6 | //
7 |
8 | #ifndef RNCEKVTintColorViewManager_h
9 | #define RNCEKVTintColorViewManager_h
10 |
11 | #import
12 | @interface RNCEKVKeyboardFocusGroupManager : RCTViewManager
13 | @end
14 |
15 |
16 | #endif /* RNCEKVKeyboardFocusGroupManager_h */
17 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVKeyboardFocusGroupView/RNCEKVKeyboardFocusGroupManager.mm:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVKeyboardFocusGroupManager.m
3 | // Pods
4 | //
5 | // Created by Artur Kalach on 24/12/2024.
6 | //
7 |
8 | #import
9 | #import
10 | #import "RNCEKVKeyboardFocusGroupManager.h"
11 | #import "RNCEKVKeyboardFocusGroup.h"
12 | #import "RCTBridge.h"
13 |
14 | @implementation RNCEKVKeyboardFocusGroupManager
15 |
16 | RCT_EXPORT_VIEW_PROPERTY(onGroupFocusChange, RCTDirectEventBlock)
17 |
18 | RCT_EXPORT_MODULE(KeyboardFocusGroup)
19 |
20 | - (UIView *)view
21 | {
22 | return [[RNCEKVKeyboardFocusGroup alloc] init];
23 | }
24 |
25 | RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RNCEKVKeyboardFocusGroup)
26 | {
27 | if (json) {
28 | UIColor *tintColor = [RCTConvert UIColor:json];
29 | [view setTintColor: tintColor];
30 | }
31 | }
32 |
33 | RCT_CUSTOM_VIEW_PROPERTY(groupIdentifier, NSString, RNCEKVKeyboardFocusGroup)
34 | {
35 | NSString* value = json ? [RCTConvert NSString:json] : nil;
36 | [view setCustomGroupId: value];
37 | }
38 |
39 |
40 |
41 | @end
42 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVTextInputFocusWrapper/RNCEKVTextInputFocusWrapper.h:
--------------------------------------------------------------------------------
1 | #ifndef RNCEKVTextInputFocusWrapper_h
2 | #define RNCEKVTextInputFocusWrapper_h
3 | #import
4 | #import
5 | #import "RNCEKVGroupIdentifierProtocol.h"
6 | #import
7 |
8 | #ifdef RCT_NEW_ARCH_ENABLED
9 | #import
10 |
11 |
12 | NS_ASSUME_NONNULL_BEGIN
13 |
14 | @interface RNCEKVTextInputFocusWrapper : RCTViewComponentView {
15 | RCTUITextField* _textField;
16 | RCTUITextView* _textView;
17 | }
18 |
19 | @property (nonatomic, strong, nullable) NSNumber *isHaloActive;
20 | @property BOOL canBeFocused;
21 | @property BOOL blurOnSubmit;
22 | @property int focusType;
23 | @property int blurType;
24 | @property BOOL multiline;
25 | @property (nonatomic, strong, nullable) NSString *customGroupId;
26 | - (UIView*)getFocusTargetView;
27 |
28 | - (void)onFocusChange:(BOOL)isFocused;
29 | - (void)onMultiplyTextSubmitHandler;
30 |
31 | @end
32 |
33 | NS_ASSUME_NONNULL_END
34 |
35 |
36 | #else /* RCT_NEW_ARCH_ENABLED */
37 |
38 |
39 | #import
40 | @interface RNCEKVTextInputFocusWrapper : RCTView {
41 | RCTUITextField* _textField;
42 | RCTUITextView* _textView;
43 | }
44 |
45 | @property (nonatomic, strong, nullable) NSNumber *isHaloActive;
46 | @property BOOL canBeFocused;
47 | @property int focusType;
48 | @property int blurType;
49 | @property BOOL blurOnSubmit;
50 | @property BOOL multiline;
51 | @property (nonatomic, copy) RCTDirectEventBlock onFocusChange;
52 | @property (nonatomic, copy) RCTDirectEventBlock onMultiplyTextSubmit;
53 | @property NSString* customGroupId;
54 | - (UIView*)getFocusTargetView;
55 |
56 | - (void)onFocusChange:(BOOL)isFocused;
57 | - (void)onMultiplyTextSubmitHandler;
58 | @end
59 |
60 |
61 | #endif /* RCT_NEW_ARCH_ENABLED */
62 | #endif /* RNCEKVTextInputFocusWrapper_h */
63 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVTextInputFocusWrapper/RNCEKVTextInputFocusWrapperManager.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNCEKVTextInputFocusWrapperManager.h
3 | // ExternalKeyboard
4 | //
5 | // Created by Artur Kalach on 18/05/2024.
6 | // Copyright © 2024 Facebook. All rights reserved.
7 | //
8 |
9 | #ifndef RNCEKVTextInputFocusWrapperManager_h
10 | #define RNCEKVTextInputFocusWrapperManager_h
11 |
12 | #import
13 | @interface RNCEKVTextInputFocusWrapperManager : RCTViewManager
14 | @end
15 |
16 | #endif /* RNCEKVTextInputFocusWrapperManager_h */
17 |
--------------------------------------------------------------------------------
/ios/Views/RNCEKVTextInputFocusWrapper/RNCEKVTextInputFocusWrapperManager.mm:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 | #import "RNCEKVTextInputFocusWrapperManager.h"
4 | #import "RNCEKVTextInputFocusWrapper.h"
5 | #import "RCTBridge.h"
6 |
7 | @implementation RNCEKVTextInputFocusWrapperManager
8 |
9 | RCT_EXPORT_MODULE(TextInputFocusWrapper)
10 |
11 | - (UIView *)view
12 | {
13 | return [[RNCEKVTextInputFocusWrapper alloc] init];
14 | }
15 |
16 | RCT_EXPORT_VIEW_PROPERTY(onFocusChange, RCTDirectEventBlock)
17 | RCT_EXPORT_VIEW_PROPERTY(onMultiplyTextSubmit, RCTDirectEventBlock)
18 |
19 | RCT_CUSTOM_VIEW_PROPERTY(canBeFocused, BOOL, RNCEKVTextInputFocusWrapper)
20 | {
21 | BOOL value = json ? [RCTConvert BOOL:json] : YES;
22 | [view setCanBeFocused: value];
23 | }
24 |
25 | RCT_CUSTOM_VIEW_PROPERTY(groupIdentifier, NSString, RNCEKVTextInputFocusWrapper)
26 | {
27 | NSString* value = json ? [RCTConvert NSString:json] : nil;
28 | [view setCustomGroupId: value];
29 | }
30 |
31 | RCT_CUSTOM_VIEW_PROPERTY(blurOnSubmit, BOOL, RNCEKVTextInputFocusWrapper)
32 | {
33 | BOOL value = json ? [RCTConvert BOOL:json] : YES;
34 | [view setBlurOnSubmit: value];
35 | }
36 |
37 | RCT_CUSTOM_VIEW_PROPERTY(multiline, BOOL, RNCEKVTextInputFocusWrapper)
38 | {
39 | BOOL value = json ? [RCTConvert BOOL:json] : NO;
40 | [view setMultiline: value];
41 | }
42 |
43 |
44 | RCT_CUSTOM_VIEW_PROPERTY(focusType, int, RNCEKVTextInputFocusWrapper)
45 | {
46 | int value = json ? [RCTConvert int:json] : 0;
47 | [view setFocusType: value];
48 | }
49 |
50 | RCT_CUSTOM_VIEW_PROPERTY(blurType, int, RNCEKVTextInputFocusWrapper)
51 | {
52 | int value = json ? [RCTConvert int:json] : 0;
53 | [view setBlurType: value];
54 | }
55 |
56 | RCT_CUSTOM_VIEW_PROPERTY(haloEffect, BOOL, RNCEKVTextInputFocusWrapper)
57 | {
58 | if(json) {
59 | BOOL value = [RCTConvert BOOL:json];
60 | if(view.isHaloActive == nil && !value) {
61 | [view setIsHaloActive: @0];
62 | }
63 | if(view.isHaloActive != nil) {
64 | [view setIsHaloActive: @(value)];
65 | }
66 | }
67 | }
68 |
69 | RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RNCEKVTextInputFocusWrapper)
70 | {
71 | if (json) {
72 | UIColor *tintColor = [RCTConvert UIColor:json];
73 | [view setTintColor: tintColor];
74 | }
75 | }
76 |
77 |
78 | @end
79 |
--------------------------------------------------------------------------------
/lefthook.yml:
--------------------------------------------------------------------------------
1 | pre-commit:
2 | parallel: true
3 | commands:
4 | lint:
5 | glob: "*.{js,ts,jsx,tsx}"
6 | run: npx eslint {staged_files}
7 | types:
8 | glob: "*.{js,ts, jsx, tsx}"
9 | run: npx tsc
10 | commit-msg:
11 | parallel: true
12 | commands:
13 | commitlint:
14 | run: npx commitlint --edit
15 |
--------------------------------------------------------------------------------
/react-native-external-keyboard.podspec:
--------------------------------------------------------------------------------
1 | require "json"
2 |
3 | package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4 | folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
5 |
6 | Pod::Spec.new do |s|
7 | s.name = "react-native-external-keyboard"
8 | s.version = package["version"]
9 | s.summary = package["description"]
10 | s.homepage = package["homepage"]
11 | s.license = package["license"]
12 | s.authors = package["author"]
13 |
14 | s.platforms = { :ios => min_ios_version_supported }
15 | s.source = { :git => "https://github.com/ArturKalach/react-native-external-keyboard.git", :tag => "#{s.version}" }
16 |
17 | s.source_files = "ios/**/*.{h,m,mm,cpp}"
18 | s.private_header_files = "ios/generated/**/*.h"
19 |
20 | # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
21 | # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
22 | if respond_to?(:install_modules_dependencies, true)
23 | install_modules_dependencies(s)
24 | else
25 | s.dependency "React-Core"
26 |
27 | # Don't install the dependencies when we run `pod install` in the old architecture.
28 | if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
29 | s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
30 | s.pod_target_xcconfig = {
31 | "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost $(PROJECT_DIR)/ios/generated\"",
32 | "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
33 | "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
34 | }
35 | s.dependency "React-Codegen"
36 | s.dependency "RCT-Folly"
37 | s.dependency "RCTRequired"
38 | s.dependency "RCTTypeSafety"
39 | s.dependency "ReactCommon/turbomodule/core"
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/react-native.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('@react-native-community/cli-types').UserDependencyConfig}
3 | */
4 | module.exports = {
5 | dependency: {
6 | platforms: {
7 | android: {
8 | cmakeListsPath: 'build/generated/source/codegen/jni/CMakeLists.txt',
9 | },
10 | },
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/scripts/bootstrap.js:
--------------------------------------------------------------------------------
1 | const os = require('os');
2 | const path = require('path');
3 | const child_process = require('child_process');
4 |
5 | const root = path.resolve(__dirname, '..');
6 | const args = process.argv.slice(2);
7 | const options = {
8 | cwd: process.cwd(),
9 | env: process.env,
10 | stdio: 'inherit',
11 | encoding: 'utf-8',
12 | };
13 |
14 | if (os.type() === 'Windows_NT') {
15 | options.shell = true;
16 | }
17 |
18 | let result;
19 |
20 | if (process.cwd() !== root || args.length) {
21 | // We're not in the root of the project, or additional arguments were passed
22 | // In this case, forward the command to `yarn`
23 | result = child_process.spawnSync('yarn', args, options);
24 | } else {
25 | // If `yarn` is run without arguments, perform bootstrap
26 | result = child_process.spawnSync('yarn', ['bootstrap'], options);
27 | }
28 |
29 | process.exitCode = result.status;
30 |
--------------------------------------------------------------------------------
/src/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | it.todo('write a test');
2 |
--------------------------------------------------------------------------------
/src/components/BaseKeyboardView/BaseKeyboardView.hooks.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { useKeyPressContext } from '../../context/BubbledKeyPressContext';
3 |
4 | const bubbleStub = () => {};
5 |
6 | export const useBubbledInfo = (onBubbledContextMenuPress?: () => void) => {
7 | const keyPressContext = useKeyPressContext();
8 |
9 | const context = useMemo(
10 | () => ({
11 | bubbledMenu:
12 | Boolean(onBubbledContextMenuPress) || keyPressContext.bubbledMenu,
13 | }),
14 | [keyPressContext.bubbledMenu, onBubbledContextMenuPress]
15 | );
16 |
17 | const contextMenu = context.bubbledMenu
18 | ? (onBubbledContextMenuPress ?? bubbleStub)
19 | : undefined;
20 |
21 | return {
22 | contextMenu,
23 | context,
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/BaseKeyboardView/BaseKeyboardView.tsx:
--------------------------------------------------------------------------------
1 | import React, { type ComponentType, useImperativeHandle, useRef } from 'react';
2 | import { Platform } from 'react-native';
3 | import { ExternalKeyboardViewNative } from '../../nativeSpec';
4 | import { Commands } from '../../nativeSpec/ExternalKeyboardViewNativeComponent';
5 | import type {
6 | BaseKeyboardViewProps,
7 | BaseKeyboardViewType,
8 | } from '../../types/BaseKeyboardView';
9 | import type { View } from 'react-native';
10 | import { KeyPressContext } from '../../context/BubbledKeyPressContext';
11 | import { useBubbledInfo } from './BaseKeyboardView.hooks';
12 | import { useGroupIdentifierContext } from '../../context/GroupIdentifierContext';
13 | import { useOnFocusChange } from '../../utils/useOnFocusChange';
14 |
15 | type NativeRef = React.ElementRef;
16 | const isIOS = Platform.OS === 'ios';
17 |
18 | const DEFAULT_EXPOSE_METHODS = [
19 | 'blur',
20 | 'measure',
21 | 'measureInWindow',
22 | 'measureLayout',
23 | 'setNativeProps',
24 | ];
25 |
26 | export const BaseKeyboardView = React.memo(
27 | React.forwardRef(
28 | (
29 | {
30 | onFocusChange,
31 | onKeyUpPress,
32 | onKeyDownPress,
33 | onBubbledContextMenuPress,
34 | haloEffect,
35 | autoFocus,
36 | canBeFocused = true,
37 | focusable = true,
38 | group = false,
39 | onFocus,
40 | onBlur,
41 | viewRef,
42 | groupIdentifier,
43 | tintColor,
44 | ignoreGroupFocusHint,
45 | exposeMethods = DEFAULT_EXPOSE_METHODS,
46 | enableA11yFocus = false,
47 | screenAutoA11yFocusDelay = 500,
48 | ...props
49 | },
50 | ref
51 | ) => {
52 | const localRef = useRef();
53 | const targetRef = viewRef ?? localRef;
54 |
55 | const contextIdentifier = useGroupIdentifierContext();
56 |
57 | useImperativeHandle(ref, () => {
58 | const actions: Record = {};
59 |
60 | exposeMethods.forEach((method) => {
61 | actions[method] = (...args: any[]) => {
62 | const componentActions = targetRef?.current as unknown as Record<
63 | string,
64 | Function
65 | >;
66 | return componentActions?.[method]?.(...args);
67 | };
68 | });
69 |
70 | actions.focus = () => {
71 | if (targetRef?.current) {
72 | Commands.focus(targetRef.current as NativeRef);
73 | }
74 | };
75 |
76 | return actions as unknown as BaseKeyboardViewType;
77 | }, [exposeMethods, targetRef]);
78 |
79 | const bubbled = useBubbledInfo(onBubbledContextMenuPress);
80 |
81 | const onFocusChangeHandler = useOnFocusChange({
82 | onFocusChange,
83 | onFocus,
84 | onBlur,
85 | });
86 |
87 | const hasOnFocusChanged = onFocusChange || onFocus || onBlur;
88 | const ignoreFocusHint = Platform.OS !== 'ios' || !ignoreGroupFocusHint;
89 |
90 | return (
91 |
92 |
113 |
114 | );
115 | }
116 | )
117 | );
118 |
--------------------------------------------------------------------------------
/src/components/KeyboardExtendedInput/KeyboardExtendedInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import {
3 | View,
4 | TextInput,
5 | Platform,
6 | type TextInputProps,
7 | type StyleProp,
8 | type ViewStyle,
9 | StyleSheet,
10 | type ColorValue,
11 | } from 'react-native';
12 |
13 | import { TextInputFocusWrapperNative } from '../../nativeSpec';
14 | import type { FocusStyle } from '../../types/FocusStyle';
15 | import { useFocusStyle } from '../../utils/useFocusStyle';
16 | import { focusEventMapper } from '../../utils/focusEventMapper';
17 | import type { TintType } from '../../types/WithKeyboardFocus';
18 | import {
19 | type RenderProp,
20 | RenderPropComponent,
21 | } from '../RenderPropComponent/RenderPropComponent';
22 | import { useGroupIdentifierContext } from '../../context/GroupIdentifierContext';
23 |
24 | const isIOS = Platform.OS === 'ios';
25 |
26 | const focusMap = {
27 | default: 0,
28 | press: 1,
29 | auto: 2,
30 | };
31 |
32 | const blurMap = {
33 | default: 0,
34 | disable: 1,
35 | auto: 2,
36 | };
37 |
38 | export type KeyboardFocusViewProps = TextInputProps & {
39 | focusType?: keyof typeof focusMap;
40 | blurType?: keyof typeof blurMap;
41 | containerStyle?: StyleProp;
42 | onFocusChange?: (isFocused: boolean) => void;
43 | focusStyle?: FocusStyle;
44 | haloEffect?: boolean;
45 | canBeFocusable?: boolean;
46 | focusable?: boolean;
47 | tintColor?: ColorValue;
48 | tintType?: TintType;
49 | containerFocusStyle?: FocusStyle;
50 | FocusHoverComponent?: RenderProp;
51 | submitBehavior?: string;
52 | groupIdentifier?: string;
53 | };
54 |
55 | export const KeyboardExtendedInput = React.forwardRef<
56 | TextInput,
57 | KeyboardFocusViewProps
58 | >(
59 | (
60 | {
61 | focusType = 'default',
62 | blurType = 'default',
63 | containerStyle,
64 | onFocusChange,
65 | focusStyle,
66 | style,
67 | haloEffect = true,
68 | canBeFocusable = true,
69 | focusable = true,
70 | containerFocusStyle,
71 | tintColor,
72 | tintType = 'default',
73 | FocusHoverComponent,
74 | onSubmitEditing,
75 | submitBehavior,
76 | groupIdentifier,
77 | ...props
78 | },
79 | ref
80 | ) => {
81 | const {
82 | focused,
83 | containerFocusedStyle,
84 | componentFocusedStyle,
85 | onFocusChangeHandler,
86 | hoverColor,
87 | } = useFocusStyle({
88 | onFocusChange,
89 | tintColor,
90 | focusStyle,
91 | containerFocusStyle,
92 | tintType,
93 | });
94 |
95 | const contextIdentifier = useGroupIdentifierContext();
96 |
97 | const withHaloEffect = tintType === 'default' && haloEffect;
98 |
99 | const nativeFocusHandler = useMemo(
100 | () => focusEventMapper(onFocusChangeHandler),
101 | [onFocusChangeHandler]
102 | );
103 |
104 | const HoverComonent = useMemo(() => {
105 | if (FocusHoverComponent) return FocusHoverComponent;
106 | if (tintType === 'hover')
107 | return ;
108 |
109 | return undefined;
110 | }, [FocusHoverComponent, hoverColor, tintType]);
111 |
112 | const blurOnSubmit = submitBehavior
113 | ? submitBehavior === 'blurAndSubmit'
114 | : (props.blurOnSubmit ?? true);
115 |
116 | return (
117 |
130 |
138 | {focused && HoverComonent && (
139 |
140 | )}
141 |
142 | );
143 | }
144 | );
145 |
146 | const styles = StyleSheet.create({
147 | absolute: {
148 | position: 'absolute',
149 | top: 0,
150 | left: 0,
151 | right: 0,
152 | bottom: 0,
153 | },
154 | opacity: {
155 | opacity: 0.3,
156 | },
157 | });
158 |
--------------------------------------------------------------------------------
/src/components/KeyboardFocusGroup/KeyboardFocusGroup.ios.tsx:
--------------------------------------------------------------------------------
1 | import React, { type PropsWithChildren } from 'react';
2 | import { type ColorValue, type ViewProps } from 'react-native';
3 | import { KeyboardFocusGroupNative } from '../../nativeSpec';
4 | import { GroupIdentifierContext } from '../../context/GroupIdentifierContext';
5 | import { useOnFocusChange } from '../../utils/useOnFocusChange';
6 | import { useFocusStyle } from '../../utils/useFocusStyle';
7 | import type { FocusStyle } from '../../types';
8 |
9 | export type KeyboardFocusGroupProps = PropsWithChildren<{
10 | groupIdentifier?: string;
11 | tintColor?: ColorValue;
12 | onFocus?: () => void;
13 | onBlur?: () => void;
14 | onFocusChange?: (isFocused: boolean) => void;
15 | focusStyle?: FocusStyle;
16 | }>;
17 |
18 | export const KeyboardFocusGroup = React.memo<
19 | ViewProps & KeyboardFocusGroupProps
20 | >((props) => {
21 | const { groupIdentifier } = props;
22 |
23 | const { containerFocusedStyle: focusStyle, onFocusChangeHandler } =
24 | useFocusStyle({
25 | onFocusChange: props.onFocusChange,
26 | containerFocusStyle: props.focusStyle,
27 | });
28 |
29 | const onGroupFocusChangeHandler = useOnFocusChange({
30 | ...props,
31 | onFocusChange: onFocusChangeHandler,
32 | });
33 |
34 | if (!groupIdentifier)
35 | return (
36 |
41 | );
42 |
43 | return (
44 |
45 |
50 |
51 | );
52 | });
53 |
--------------------------------------------------------------------------------
/src/components/KeyboardFocusGroup/KeyboardFocusGroup.tsx:
--------------------------------------------------------------------------------
1 | import { type PropsWithChildren } from 'react';
2 | import { type ColorValue, View, type ViewProps } from 'react-native';
3 | import type { FocusStyle } from '../../types';
4 |
5 | export type KeyboardFocusGroupProps = PropsWithChildren<
6 | ViewProps & {
7 | groupIdentifier?: string;
8 | tintColor?: ColorValue;
9 | onFocus?: () => void;
10 | onBlur?: () => void;
11 | onFocusChange?: (isFocused: boolean) => void;
12 | focusStyle?: FocusStyle;
13 | }
14 | >;
15 |
16 | export const KeyboardFocusGroup =
17 | View as unknown as React.FC;
18 |
--------------------------------------------------------------------------------
/src/components/KeyboardFocusView/KeyboardFocusView.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { View, StyleSheet, Platform } from 'react-native';
3 | import type { KeyboardFocusViewProps } from '../../types/KeyboardFocusView.types';
4 | import { BaseKeyboardView } from '../BaseKeyboardView/BaseKeyboardView';
5 | import type {
6 | BaseKeyboardViewType,
7 | KeyboardFocus,
8 | } from '../../types/BaseKeyboardView';
9 | import type { TintType } from '../../types/WithKeyboardFocus';
10 | import {
11 | type RenderProp,
12 | RenderPropComponent,
13 | } from '../RenderPropComponent/RenderPropComponent';
14 | import { useFocusStyle } from '../../utils/useFocusStyle';
15 | import { useKeyboardPress } from '../../utils/useKeyboardPress/useKeyboardPress';
16 | import { IsViewFocusedContext } from '../../context/IsViewFocusedContext';
17 |
18 | export const KeyboardFocusView = React.forwardRef<
19 | BaseKeyboardViewType | KeyboardFocus,
20 | KeyboardFocusViewProps & {
21 | tintType?: TintType;
22 | FocusHoverComponent?: RenderProp;
23 | withView?: boolean;
24 | }
25 | >(
26 | (
27 | {
28 | tintType = 'default',
29 | autoFocus,
30 | focusStyle,
31 | style,
32 | onFocusChange,
33 | onPress,
34 | onLongPress,
35 | onKeyUpPress,
36 | onKeyDownPress,
37 | group = false,
38 | haloEffect = true,
39 | canBeFocused = true,
40 | focusable = true,
41 | withView = true, //ToDo RNCEKV-9 update and rename Discussion #63
42 | tintColor,
43 | onFocus,
44 | onBlur,
45 | FocusHoverComponent,
46 | children,
47 | accessible,
48 | triggerCodes,
49 | ...props
50 | },
51 | ref
52 | ) => {
53 | const { focused, containerFocusedStyle, onFocusChangeHandler, hoverColor } =
54 | useFocusStyle({
55 | onFocusChange,
56 | tintColor,
57 | containerFocusStyle: focusStyle,
58 | tintType,
59 | });
60 |
61 | const withHaloEffect = tintType === 'default' && haloEffect;
62 |
63 | const { onKeyUpPressHandler, onKeyDownPressHandler } = useKeyboardPress({
64 | onKeyUpPress,
65 | onKeyDownPress,
66 | onPress,
67 | onLongPress,
68 | triggerCodes,
69 | });
70 |
71 | const HoverComonent = useMemo(() => {
72 | if (FocusHoverComponent) return FocusHoverComponent;
73 | if (tintType === 'hover')
74 | return ;
75 |
76 | return undefined;
77 | }, [FocusHoverComponent, hoverColor, tintType]);
78 |
79 | const a11y = useMemo(() => {
80 | return (
81 | (Platform.OS === 'android' && withView && accessible !== false) ||
82 | accessible
83 | );
84 | }, [accessible, withView]);
85 |
86 | return (
87 |
88 |
106 | {children}
107 | {focused && HoverComonent && (
108 |
109 | )}
110 |
111 |
112 | );
113 | }
114 | );
115 |
116 | const styles = StyleSheet.create({
117 | absolute: {
118 | position: 'absolute',
119 | top: 0,
120 | left: 0,
121 | right: 0,
122 | bottom: 0,
123 | },
124 | opacity: {
125 | opacity: 0.3,
126 | },
127 | });
128 |
--------------------------------------------------------------------------------
/src/components/KeyboardFocusView/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export { useFocusStyle } from './useFocusStyle';
2 |
--------------------------------------------------------------------------------
/src/components/KeyboardFocusView/hooks/useFocusStyle/index.ts:
--------------------------------------------------------------------------------
1 | export { useFocusStyle } from './useFocusStyle';
2 |
--------------------------------------------------------------------------------
/src/components/KeyboardFocusView/hooks/useFocusStyle/useFocusStyle.ts:
--------------------------------------------------------------------------------
1 | import { useState, useMemo, useCallback } from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import type { FocusStyle } from '../../../../types';
4 |
5 | export const useFocusStyle = (
6 | focusStyle?: FocusStyle,
7 | onFocusChange?: (isFocused: boolean) => void
8 | ) => {
9 | const [focused, setFocusStatus] = useState(false);
10 |
11 | const onFocusChangeHandler = useCallback(
12 | (isFocused: boolean) => {
13 | setFocusStatus(isFocused);
14 | onFocusChange?.(isFocused);
15 | },
16 | [onFocusChange]
17 | );
18 |
19 | const fStyle = useMemo(() => {
20 | if (!focusStyle) return focused ? styles.defaultHighlight : undefined;
21 | const specificStyle =
22 | typeof focusStyle === 'function' ? focusStyle({ focused }) : focusStyle;
23 | return focused ? specificStyle : undefined;
24 | }, [focused, focusStyle]);
25 |
26 | return {
27 | onFocusChangeHandler,
28 | fStyle,
29 | };
30 | };
31 |
32 | const styles = StyleSheet.create({
33 | defaultHighlight: { backgroundColor: '#dce3f9' },
34 | });
35 |
--------------------------------------------------------------------------------
/src/components/KeyboardFocusView/hooks/useFocusStyle/useTintStyle.ts:
--------------------------------------------------------------------------------
1 | import { useState, useMemo, useCallback } from 'react';
2 | import type { FocusStyle } from '../../../../types';
3 |
4 | export const useTintStyle = ({
5 | focusStyle,
6 | haloEffect,
7 | onFocusChange,
8 | tintBackground = '#dce3f9',
9 | }: {
10 | haloEffect?: boolean;
11 | focusStyle?: FocusStyle;
12 | tintBackground?: string;
13 | onFocusChange?: (isFocused: boolean) => void;
14 | }) => {
15 | const [focused, setFocusStatus] = useState(false);
16 |
17 | const onFocusChangeHandler = useCallback(
18 | (isFocused: boolean) => {
19 | setFocusStatus(isFocused);
20 | onFocusChange?.(isFocused);
21 | },
22 | [onFocusChange]
23 | );
24 |
25 | const fStyle = useMemo(() => {
26 | if (!focusStyle) return undefined;
27 | const specificStyle =
28 | typeof focusStyle === 'function' ? focusStyle({ focused }) : focusStyle;
29 | return focused ? specificStyle : undefined;
30 | }, [focused, focusStyle]);
31 |
32 | const tintStyle = useMemo(() => {
33 | if (haloEffect) return;
34 | return focused ? { backgroundColor: tintBackground } : undefined;
35 | }, [haloEffect, focused, tintBackground]);
36 | return {
37 | onFocusChangeHandler,
38 | tintStyle,
39 | fStyle,
40 | };
41 | };
42 |
--------------------------------------------------------------------------------
/src/components/KeyboardFocusView/index.ts:
--------------------------------------------------------------------------------
1 | export { KeyboardFocusView } from './KeyboardFocusView';
2 |
--------------------------------------------------------------------------------
/src/components/RenderPropComponent/RenderPropComponent.tsx:
--------------------------------------------------------------------------------
1 | import React, { type FunctionComponent, type ReactElement } from 'react';
2 |
3 | export type RenderProp =
4 | | ReactElement
5 | | FunctionComponent
6 | | (() => ReactElement);
7 |
8 | export const RenderPropComponent = ({ render }: { render: RenderProp }) => {
9 | if (React.isValidElement(render)) {
10 | return render;
11 | } else if (typeof render === 'function') {
12 | const Component = render;
13 | return ;
14 | } else {
15 | return null;
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/Touchable/Pressable.tsx:
--------------------------------------------------------------------------------
1 | import { Pressable as RNPressable } from 'react-native';
2 |
3 | import { withKeyboardFocus } from '../../utils/withKeyboardFocus';
4 |
5 | export const Pressable = withKeyboardFocus(RNPressable);
6 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | BaseKeyboardView as ExternalKeyboardView,
3 | BaseKeyboardView as KeyboardExtendedBaseView,
4 | BaseKeyboardView,
5 | } from './BaseKeyboardView/BaseKeyboardView';
6 | export {
7 | KeyboardFocusView,
8 | KeyboardFocusView as KeyboardExtendedView,
9 | } from './KeyboardFocusView';
10 |
--------------------------------------------------------------------------------
/src/context/BubbledKeyPressContext.ts:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 |
3 | export const KeyPressContext = React.createContext<{
4 | bubbledMenu: boolean;
5 | }>({
6 | bubbledMenu: false,
7 | });
8 |
9 | export const useKeyPressContext = () => useContext(KeyPressContext);
10 |
--------------------------------------------------------------------------------
/src/context/GroupIdentifierContext.ts:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 |
3 | export const GroupIdentifierContext = React.createContext(
4 | undefined
5 | );
6 |
7 | export const useGroupIdentifierContext = () =>
8 | useContext(GroupIdentifierContext);
9 |
--------------------------------------------------------------------------------
/src/context/IsViewFocusedContext.ts:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 |
3 | export const IsViewFocusedContext = React.createContext(false);
4 |
5 | export const useIsViewFocused = () => useContext(IsViewFocusedContext);
6 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | export {
2 | ExternalKeyboardViewNative,
3 | TextInputFocusWrapperNative,
4 | type KeyPress,
5 | } from './nativeSpec';
6 |
7 | export type {
8 | OnKeyPress,
9 | KeyboardFocus,
10 | BaseKeyboardViewType as KeyboardExtendedViewType,
11 | } from './types/BaseKeyboardView';
12 |
13 | export type { TintType } from './types/WithKeyboardFocus';
14 |
15 | export {
16 | BaseKeyboardView,
17 | KeyboardFocusView,
18 | ExternalKeyboardView,
19 | KeyboardExtendedView,
20 | KeyboardExtendedBaseView,
21 | } from './components';
22 | export {
23 | Pressable,
24 | Pressable as KeyboardExtendedPressable,
25 | } from './components/Touchable/Pressable';
26 | export {
27 | KeyboardExtendedInput,
28 | KeyboardExtendedInput as TextInput,
29 | } from './components/KeyboardExtendedInput/KeyboardExtendedInput';
30 | export { KeyboardFocusGroup } from './components/KeyboardFocusGroup/KeyboardFocusGroup';
31 | export { withKeyboardFocus } from './utils/withKeyboardFocus';
32 | export { useIsViewFocused } from './context/IsViewFocusedContext';
33 | import * as Keyboard from './modules/Keyboard';
34 | export { Keyboard };
35 |
--------------------------------------------------------------------------------
/src/modules/Keyboard.android.ts:
--------------------------------------------------------------------------------
1 | import { NativeModules, Platform, Keyboard } from 'react-native';
2 |
3 | const LINKING_ERROR =
4 | `The package 'react-native-external-keyboard' doesn't seem to be linked. Make sure: \n\n${Platform.select(
5 | { ios: "- You have run 'pod install'\n", default: '' }
6 | )}- You rebuilt the app after installing the package\n` +
7 | `- You are not using Expo Go\n`;
8 |
9 | // @ts-expect-error
10 | const isTurboModuleEnabled = global.__turboModuleProxy != null;
11 | const ExternalKeyboardModule = isTurboModuleEnabled
12 | ? require('../nativeSpec/NativeExternalKeyboardModule').default
13 | : NativeModules.ExternalKeyboardModule;
14 |
15 | export const ExternalKeyboard =
16 | ExternalKeyboardModule ||
17 | new Proxy(
18 | {},
19 | {
20 | get() {
21 | throw new Error(LINKING_ERROR);
22 | },
23 | }
24 | );
25 |
26 | export function dismiss() {
27 | Keyboard.dismiss();
28 | ExternalKeyboard.dismissKeyboard();
29 | }
30 |
--------------------------------------------------------------------------------
/src/modules/Keyboard.ts:
--------------------------------------------------------------------------------
1 | import { Keyboard } from 'react-native';
2 |
3 | export function dismiss() {
4 | Keyboard.dismiss();
5 | }
6 |
--------------------------------------------------------------------------------
/src/nativeSpec/ExternalKeyboardViewNativeComponent.ts:
--------------------------------------------------------------------------------
1 | import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
2 | import type { ColorValue, ViewProps } from 'react-native';
3 | import type {
4 | BubblingEventHandler,
5 | DirectEventHandler,
6 | Float,
7 | Int32,
8 | } from 'react-native/Libraries/Types/CodegenTypes';
9 | import type { ComponentType } from 'react';
10 | import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';
11 |
12 | export type FocusChange = Readonly<{
13 | isFocused: boolean;
14 | }>;
15 |
16 | export type EnterPress = Readonly<{
17 | isShiftPressed: boolean;
18 | isAltPressed: boolean;
19 | isEnterPress: boolean;
20 | }>;
21 |
22 | export type KeyPress = Readonly<{
23 | keyCode: Int32;
24 | unicode: Int32;
25 | unicodeChar: string;
26 | isLongPress: boolean;
27 | isAltPressed: boolean;
28 | isShiftPressed: boolean;
29 | isCtrlPressed: boolean;
30 | isCapsLockOn: boolean;
31 | hasNoModifiers: boolean;
32 | }>;
33 |
34 | export interface ExternalKeyboardNativeProps extends ViewProps {
35 | onFocusChange?: DirectEventHandler;
36 | onKeyUpPress?: DirectEventHandler;
37 | onKeyDownPress?: DirectEventHandler;
38 | onContextMenuPress?: DirectEventHandler<{}>;
39 | onBubbledContextMenuPress?: BubblingEventHandler<{}>;
40 | canBeFocused?: boolean;
41 | hasKeyDownPress?: boolean;
42 | hasKeyUpPress?: boolean;
43 | hasOnFocusChanged?: boolean;
44 | autoFocus?: boolean;
45 | haloEffect?: boolean;
46 | haloCornerRadius?: Float;
47 | haloExpendX?: Float;
48 | haloExpendY?: Float;
49 | tintColor?: ColorValue;
50 | group?: boolean;
51 | groupIdentifier?: string;
52 | enableA11yFocus?: boolean;
53 | screenAutoA11yFocus?: boolean;
54 | screenAutoA11yFocusDelay?: Int32;
55 | }
56 |
57 | export interface NativeCommands {
58 | focus: (viewRef: React.ElementRef) => void;
59 | }
60 |
61 | export const Commands: NativeCommands = codegenNativeCommands({
62 | supportedCommands: ['focus'],
63 | });
64 |
65 | export default codegenNativeComponent(
66 | 'ExternalKeyboardView'
67 | );
68 |
--------------------------------------------------------------------------------
/src/nativeSpec/KeyboardFocusGroupNativeComponent.ts:
--------------------------------------------------------------------------------
1 | import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
2 | import type { ViewProps, ColorValue } from 'react-native';
3 | import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
4 |
5 | export type FocusChange = Readonly<{
6 | isFocused: boolean;
7 | }>;
8 |
9 | export interface KeyboardFocusGroupNativeComponentProps extends ViewProps {
10 | onGroupFocusChange?: DirectEventHandler;
11 | tintColor?: ColorValue;
12 | groupIdentifier?: string;
13 | }
14 |
15 | export default codegenNativeComponent(
16 | 'KeyboardFocusGroup'
17 | );
18 |
--------------------------------------------------------------------------------
/src/nativeSpec/NativeExternalKeyboardModule.ts:
--------------------------------------------------------------------------------
1 | import type { TurboModule } from 'react-native';
2 | import { TurboModuleRegistry } from 'react-native';
3 |
4 | export interface Spec extends TurboModule {
5 | dismissKeyboard: () => Promise;
6 | }
7 |
8 | export default TurboModuleRegistry.get('ExternalKeyboardModule');
9 |
--------------------------------------------------------------------------------
/src/nativeSpec/TextInputFocusWrapperNativeComponent.ts:
--------------------------------------------------------------------------------
1 | import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
2 | import type { ViewProps, ColorValue } from 'react-native';
3 | import type {
4 | DirectEventHandler,
5 | Int32,
6 | } from 'react-native/Libraries/Types/CodegenTypes';
7 |
8 | export type FocusChange = Readonly<{
9 | isFocused: boolean;
10 | }>;
11 |
12 | export type MultiplyTextSubmit = Readonly<{
13 | text: string;
14 | }>;
15 |
16 | export interface TextInputFocusWrapperNativeComponent extends ViewProps {
17 | onFocusChange?: DirectEventHandler;
18 | onMultiplyTextSubmit?: DirectEventHandler;
19 | focusType?: Int32;
20 | blurType?: Int32;
21 | canBeFocused?: boolean;
22 | haloEffect?: boolean;
23 | tintColor?: ColorValue;
24 | blurOnSubmit?: boolean;
25 | multiline?: boolean;
26 | groupIdentifier?: string;
27 | }
28 |
29 | export default codegenNativeComponent(
30 | 'TextInputFocusWrapper'
31 | );
32 |
--------------------------------------------------------------------------------
/src/nativeSpec/index.ts:
--------------------------------------------------------------------------------
1 | import ExternalKeyboardViewNative from './ExternalKeyboardViewNativeComponent';
2 | import TextInputFocusWrapperNative from './TextInputFocusWrapperNativeComponent';
3 | import KeyboardFocusGroupNative from './KeyboardFocusGroupNativeComponent';
4 |
5 | import type { KeyPress } from './ExternalKeyboardViewNativeComponent';
6 |
7 | export {
8 | TextInputFocusWrapperNative,
9 | ExternalKeyboardViewNative,
10 | KeyboardFocusGroupNative,
11 | type KeyPress,
12 | };
13 |
--------------------------------------------------------------------------------
/src/types/BaseKeyboardView.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | View,
3 | ViewProps,
4 | NativeSyntheticEvent,
5 | ColorValue,
6 | } from 'react-native';
7 | import type { KeyPress } from '../nativeSpec/ExternalKeyboardViewNativeComponent';
8 | import type { RefObject } from 'react';
9 |
10 | export type OnKeyPress = NativeSyntheticEvent & {
11 | nativeEvent?: { target?: number };
12 | currentTarget?: { _nativeTag?: number };
13 | };
14 |
15 | export type OnKeyPressFn = (e: OnKeyPress) => void;
16 | export type KeyboardFocus = { focus: () => void };
17 | export type BaseKeyboardViewType = Partial & KeyboardFocus;
18 |
19 | export type BaseFocusViewProps = {
20 | viewRef?: RefObject;
21 | group?: boolean;
22 | onFocusChange?: (isFocused: boolean, tag?: number) => void;
23 | onKeyUpPress?: OnKeyPressFn;
24 | onKeyDownPress?: OnKeyPressFn;
25 | onContextMenuPress?: () => void;
26 | onBubbledContextMenuPress?: () => void;
27 | haloEffect?: boolean;
28 | autoFocus?: boolean;
29 | canBeFocused?: boolean;
30 | focusable?: boolean;
31 | onFocus?: () => void;
32 | onBlur?: () => void;
33 | tintColor?: ColorValue;
34 | haloCornerRadius?: number;
35 | haloExpendX?: number;
36 | haloExpendY?: number;
37 | groupIdentifier?: string;
38 | ignoreGroupFocusHint?: boolean;
39 | exposeMethods?: string[];
40 | enableA11yFocus?: boolean;
41 | screenAutoA11yFocus?: boolean;
42 | screenAutoA11yFocusDelay?: number;
43 | };
44 |
45 | export type BaseKeyboardViewProps = ViewProps & BaseFocusViewProps;
46 |
--------------------------------------------------------------------------------
/src/types/FocusStyle.ts:
--------------------------------------------------------------------------------
1 | import type { StyleProp, ViewStyle } from 'react-native';
2 |
3 | export type FocusStateCallbackType = {
4 | readonly focused: boolean;
5 | };
6 |
7 | export type FocusStyle =
8 | | StyleProp
9 | | ((state: FocusStateCallbackType) => StyleProp)
10 | | undefined;
11 |
--------------------------------------------------------------------------------
/src/types/KeyboardFocusView.types.ts:
--------------------------------------------------------------------------------
1 | import type { NativeSyntheticEvent, GestureResponderEvent } from 'react-native';
2 | import type { FocusStyle } from './FocusStyle';
3 | import type {
4 | BaseFocusViewProps,
5 | BaseKeyboardViewProps,
6 | OnKeyPress,
7 | } from './BaseKeyboardView';
8 |
9 | export type KeyboardFocusEvent = NativeSyntheticEvent<{
10 | isFocused: boolean;
11 | }>;
12 |
13 | export type OnFocusChangeFn = (e: KeyboardFocusEvent) => void;
14 |
15 | export type FocusStateCallbackType = {
16 | readonly focused: boolean;
17 | };
18 |
19 | export type FocusViewProps = {
20 | exposeMethods?: string[];
21 | triggerCodes?: number[];
22 | focusStyle?: FocusStyle;
23 | onPress?: (e: GestureResponderEvent | OnKeyPress) => void;
24 | onLongPress?: (e?: GestureResponderEvent | OnKeyPress) => void;
25 | onFocus?: () => void;
26 | onBlur?: () => void;
27 | } & BaseFocusViewProps;
28 |
29 | export type KeyboardFocusViewProps = BaseKeyboardViewProps & FocusViewProps;
30 |
--------------------------------------------------------------------------------
/src/types/WithKeyboardFocus.ts:
--------------------------------------------------------------------------------
1 | import type { StyleProp, ViewStyle } from 'react-native';
2 | import type { FocusStyle } from './FocusStyle';
3 | import type { KeyboardFocusViewProps } from './KeyboardFocusView.types';
4 |
5 | export type PressType = ((e?: E) => void) | (() => void) | undefined;
6 |
7 | export type WithKeyboardFocus = KeyboardFocusViewProps & {
8 | tintBackground?: string;
9 | containerStyle?: StyleProp;
10 | containerFocusStyle?: FocusStyle;
11 | };
12 |
13 | export type TintType = 'default' | 'hover' | 'background' | 'none';
14 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export type {
2 | KeyboardFocusViewProps,
3 | OnFocusChangeFn,
4 | } from './KeyboardFocusView.types';
5 | export type { FocusStateCallbackType, FocusStyle } from './FocusStyle';
6 |
--------------------------------------------------------------------------------
/src/utils/focusEventMapper.tsx:
--------------------------------------------------------------------------------
1 | import type { NativeSyntheticEvent } from 'react-native';
2 |
3 | export const focusEventMapper =
4 | (fn?: (isFocused: boolean) => void) =>
5 | (
6 | e: NativeSyntheticEvent<{
7 | isFocused: boolean;
8 | }>
9 | ) =>
10 | fn?.(e.nativeEvent.isFocused);
11 |
--------------------------------------------------------------------------------
/src/utils/useFocusStyle.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useMemo, useCallback } from 'react';
2 | import {
3 | Platform,
4 | type ColorValue,
5 | type PressableProps,
6 | Pressable,
7 | } from 'react-native';
8 | import type { FocusStyle } from '../types';
9 | import type { TintType } from '../types/WithKeyboardFocus';
10 |
11 | const backgroundTintMap = Platform.select>>({
12 | ios: {
13 | background: true,
14 | },
15 | default: {
16 | background: true,
17 | default: true,
18 | },
19 | });
20 |
21 | const DEFAULT_BACKGROUND_TINT = '#dce3f9';
22 |
23 | type UseFocusStyleProps = {
24 | focusStyle?: FocusStyle;
25 | containerFocusStyle?: FocusStyle;
26 | onFocusChange?: (isFocused: boolean) => void;
27 | tintColor?: ColorValue;
28 | tintType?: TintType;
29 | style?: PressableProps['style'];
30 | Component?: React.ComponentType;
31 | withPressedStyle?: boolean;
32 | };
33 |
34 | export const useFocusStyle = ({
35 | focusStyle,
36 | onFocusChange,
37 | containerFocusStyle,
38 | tintColor,
39 | tintType = 'default',
40 | style,
41 | Component,
42 | withPressedStyle = false,
43 | }: UseFocusStyleProps) => {
44 | const [focused, setFocusStatus] = useState(false);
45 |
46 | const onFocusChangeHandler = useCallback(
47 | (isFocused: boolean) => {
48 | setFocusStatus(isFocused);
49 | onFocusChange?.(isFocused);
50 | },
51 | [onFocusChange]
52 | );
53 |
54 | const componentFocusedStyle = useMemo(() => {
55 | const specificStyle =
56 | typeof focusStyle === 'function' ? focusStyle({ focused }) : focusStyle;
57 | return focused ? specificStyle : undefined;
58 | }, [focusStyle, focused]);
59 |
60 | const hoverColor = useMemo(
61 | () => ({
62 | backgroundColor: tintColor,
63 | }),
64 | [tintColor]
65 | );
66 |
67 | const containerFocusedStyle = useMemo(() => {
68 | if (backgroundTintMap[tintType] && !containerFocusStyle) {
69 | return focused
70 | ? { backgroundColor: tintColor ?? DEFAULT_BACKGROUND_TINT }
71 | : undefined;
72 | }
73 | if (!containerFocusStyle) return undefined;
74 |
75 | const specificStyle =
76 | typeof containerFocusStyle === 'function'
77 | ? containerFocusStyle({ focused })
78 | : containerFocusStyle;
79 |
80 | return focused ? specificStyle : undefined;
81 | }, [containerFocusStyle, focused, tintColor, tintType]);
82 |
83 | const dafaultComponentStyle = useMemo(
84 | () => [style, componentFocusedStyle],
85 | [style, componentFocusedStyle]
86 | );
87 | const styleHandlerPressable = useCallback(
88 | ({ pressed }: { pressed: boolean }) => {
89 | if (typeof style === 'function') {
90 | return [style({ pressed }), componentFocusedStyle];
91 | } else {
92 | return [style, componentFocusedStyle];
93 | }
94 | },
95 | [componentFocusedStyle, style]
96 | );
97 |
98 | const componentStyleViewStyle =
99 | Component === Pressable || withPressedStyle
100 | ? styleHandlerPressable
101 | : dafaultComponentStyle;
102 |
103 | return {
104 | componentStyleViewStyle,
105 | componentFocusedStyle,
106 | containerFocusedStyle,
107 | onFocusChangeHandler,
108 | hoverColor,
109 | focused,
110 | };
111 | };
112 |
--------------------------------------------------------------------------------
/src/utils/useKeyboardPress/useKeyboardPress.android.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo, useRef } from 'react';
2 | import type { GestureResponderEvent } from 'react-native';
3 | import type { UseKeyboardPressProps } from './useKeyboardPress.types';
4 | import type { OnKeyPress, OnKeyPressFn } from '../../types/BaseKeyboardView';
5 |
6 | export const ANDROID_SPACE_KEY_CODE = 62;
7 | export const ANDROID_DPAD_CENTER_CODE = 23;
8 | export const ANDROID_ENTER_CODE = 66;
9 |
10 | export const ANDROID_TRIGGER_CODES = [
11 | ANDROID_SPACE_KEY_CODE,
12 | ANDROID_DPAD_CENTER_CODE,
13 | ANDROID_ENTER_CODE,
14 | ];
15 |
16 | const MILLISECOND_THRESHOLD = 20;
17 |
18 | export const useKeyboardPress = <
19 | T extends (event?: any) => void,
20 | K extends (event?: any) => void,
21 | >({
22 | onKeyUpPress,
23 | onKeyDownPress,
24 | onPressIn,
25 | onPressOut,
26 | onPress,
27 | onLongPress,
28 | triggerCodes = ANDROID_TRIGGER_CODES,
29 | }: UseKeyboardPressProps) => {
30 | const thresholdTime = useRef(0);
31 | const onKeyUpPressHandler = useCallback(
32 | (e) => {
33 | const {
34 | nativeEvent: { keyCode, isLongPress },
35 | } = e;
36 |
37 | onPressOut?.(e as unknown as GestureResponderEvent);
38 | onKeyUpPress?.(e);
39 |
40 | if (triggerCodes.includes(keyCode)) {
41 | if (isLongPress) {
42 | thresholdTime.current = e?.timeStamp;
43 | onLongPress?.({} as GestureResponderEvent);
44 | }
45 | }
46 | },
47 | [onPressOut, onKeyUpPress, triggerCodes, onLongPress]
48 | );
49 |
50 | const onKeyDownPressHandler = useMemo(() => {
51 | if (!onPressIn) return onKeyDownPress;
52 | return (e: OnKeyPress) => {
53 | onKeyDownPress?.(e);
54 | if (triggerCodes.includes(e.nativeEvent.keyCode)) {
55 | onPressIn?.(e as unknown as GestureResponderEvent);
56 | }
57 | };
58 | }, [onKeyDownPress, onPressIn, triggerCodes]);
59 |
60 | const onPressHandler = useCallback(
61 | (event: GestureResponderEvent) => {
62 | const pressThreshold = (event?.timeStamp ?? 0) - thresholdTime.current;
63 | if (pressThreshold > MILLISECOND_THRESHOLD) {
64 | onPress?.(event);
65 | }
66 | },
67 | [onPress]
68 | );
69 |
70 | const hasHandler = onPressOut || onKeyUpPress || onLongPress || onPress;
71 | return {
72 | onKeyUpPressHandler: hasHandler && onKeyUpPressHandler,
73 | onKeyDownPressHandler,
74 | onPressHandler: onPress && onPressHandler,
75 | };
76 | };
77 |
--------------------------------------------------------------------------------
/src/utils/useKeyboardPress/useKeyboardPress.ios.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import type { UseKeyboardPressProps } from './useKeyboardPress.types';
3 | import type { OnKeyPress } from '../../types/BaseKeyboardView';
4 | import type { GestureResponderEvent } from 'react-native';
5 |
6 | const IOS_SPACE_KEY = 44;
7 | const IOS_RETURN_OR_ENTER = 40;
8 |
9 | const IOS_TRIGGER_CODES = [IOS_SPACE_KEY, IOS_RETURN_OR_ENTER];
10 |
11 | export const useKeyboardPress = <
12 | T extends (event?: any) => void,
13 | K extends (event?: any) => void,
14 | >({
15 | onKeyUpPress,
16 | onKeyDownPress,
17 | onPress,
18 | onPressIn,
19 | onPressOut,
20 | onLongPress,
21 | triggerCodes = IOS_TRIGGER_CODES,
22 | }: UseKeyboardPressProps) => {
23 | const onKeyUpPressHandler = useMemo(() => {
24 | return (e: OnKeyPress) => {
25 | onKeyUpPress?.(e);
26 |
27 | if (triggerCodes.includes(e.nativeEvent.keyCode)) {
28 | onPressOut?.(e);
29 | if (e.nativeEvent.isLongPress) {
30 | onLongPress?.({} as GestureResponderEvent);
31 | } else {
32 | onPress?.({} as GestureResponderEvent);
33 | }
34 | }
35 | };
36 | }, [onKeyUpPress, onLongPress, onPress, onPressOut, triggerCodes]);
37 |
38 | const onKeyDownPressHandler = useMemo(() => {
39 | if (!onPressIn) return onKeyDownPress;
40 | return (e: OnKeyPress) => {
41 | onKeyDownPress?.(e);
42 | if (triggerCodes.includes(e.nativeEvent.keyCode)) {
43 | onPressIn?.(e);
44 | }
45 | };
46 | }, [onKeyDownPress, onPressIn, triggerCodes]);
47 |
48 | return {
49 | onKeyUpPressHandler,
50 | onKeyDownPressHandler,
51 | onPressHandler: onPress,
52 | };
53 | };
54 |
--------------------------------------------------------------------------------
/src/utils/useKeyboardPress/useKeyboardPress.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import type { UseKeyboardPressProps } from './useKeyboardPress.types';
3 | import type { OnKeyPress } from '../../types/BaseKeyboardView';
4 |
5 | const IOS_SPACE_KEY = 44;
6 | const IOS_RETURN_OR_ENTER = 40;
7 |
8 | const IOS_TRIGGER_CODES = [IOS_SPACE_KEY, IOS_RETURN_OR_ENTER];
9 |
10 | export const useKeyboardPress = <
11 | T extends (event?: any) => void,
12 | K extends (event?: any) => void,
13 | >({
14 | onKeyUpPress,
15 | onKeyDownPress,
16 | onPress,
17 | onPressIn,
18 | onPressOut,
19 | triggerCodes = IOS_TRIGGER_CODES,
20 | }: UseKeyboardPressProps) => {
21 | const onKeyUpPressHandler = useMemo(() => {
22 | if (!onPressOut) return onKeyUpPress;
23 | return (e: OnKeyPress) => {
24 | onKeyUpPress?.(e);
25 | if (triggerCodes.includes(e.nativeEvent.keyCode)) {
26 | onPressOut?.(e);
27 | }
28 | };
29 | }, [onKeyUpPress, onPressOut, triggerCodes]);
30 |
31 | const onKeyDownPressHandler = useMemo(() => {
32 | if (!onPressIn) return onKeyDownPress;
33 | return (e: OnKeyPress) => {
34 | onKeyDownPress?.(e);
35 | if (triggerCodes.includes(e.nativeEvent.keyCode)) {
36 | onPressIn?.(e);
37 | }
38 | };
39 | }, [onKeyDownPress, onPressIn, triggerCodes]);
40 |
41 | return {
42 | onKeyUpPressHandler,
43 | onKeyDownPressHandler,
44 | onPressHandler: onPress,
45 | };
46 | };
47 |
--------------------------------------------------------------------------------
/src/utils/useKeyboardPress/useKeyboardPress.types.ts:
--------------------------------------------------------------------------------
1 | import type { OnKeyPressFn } from '../../types/BaseKeyboardView';
2 |
3 | export type UseKeyboardPressProps = {
4 | triggerCodes?: number[];
5 | onKeyUpPress?: OnKeyPressFn;
6 | onKeyDownPress?: OnKeyPressFn;
7 | onLongPress?: T;
8 | onPress?: T;
9 | onPressIn?: K;
10 | onPressOut?: K;
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/useOnFocusChange.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import type { OnFocusChangeFn } from '../types';
3 |
4 | type UseFocusChange = {
5 | onFocusChange?: (f: boolean, tag?: number) => void;
6 | onFocus?: () => void;
7 | onBlur?: () => void;
8 | };
9 |
10 | export const useOnFocusChange = ({
11 | onFocusChange,
12 | onFocus,
13 | onBlur,
14 | }: UseFocusChange) =>
15 | useCallback(
16 | (e) => {
17 | onFocusChange?.(
18 | e.nativeEvent.isFocused,
19 | (e?.nativeEvent as unknown as { target?: number })?.target
20 | );
21 | if (e.nativeEvent.isFocused) {
22 | onFocus?.();
23 | } else {
24 | onBlur?.();
25 | }
26 | },
27 | [onBlur, onFocus, onFocusChange]
28 | );
29 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "exclude": ["example", "lib"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "rootDir": ".",
4 | "paths": {
5 | "react-native-external-keyboard": ["./src/index"]
6 | },
7 | "allowUnreachableCode": false,
8 | "allowUnusedLabels": false,
9 | "esModuleInterop": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "jsx": "react-jsx",
12 | "lib": ["ESNext"],
13 | "module": "ESNext",
14 | "moduleResolution": "Bundler",
15 | "noFallthroughCasesInSwitch": true,
16 | "noImplicitReturns": true,
17 | "noImplicitUseStrict": false,
18 | "noStrictGenericChecks": false,
19 | "noUncheckedIndexedAccess": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "resolveJsonModule": true,
23 | "resolvePackageJsonImports": false,
24 | "skipLibCheck": true,
25 | "strict": true,
26 | "target": "ESNext",
27 | "verbatimModuleSyntax": true
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "pipeline": {
4 | "build:android": {
5 | "env": ["ORG_GRADLE_PROJECT_newArchEnabled"],
6 | "inputs": [
7 | "package.json",
8 | "android",
9 | "!android/build",
10 | "src/*.ts",
11 | "src/*.tsx",
12 | "example/package.json",
13 | "example/android",
14 | "!example/android/.gradle",
15 | "!example/android/build",
16 | "!example/android/app/build"
17 | ],
18 | "outputs": []
19 | },
20 | "build:ios": {
21 | "env": ["RCT_NEW_ARCH_ENABLED"],
22 | "inputs": [
23 | "package.json",
24 | "*.podspec",
25 | "ios",
26 | "src/*.ts",
27 | "src/*.tsx",
28 | "example/package.json",
29 | "example/ios",
30 | "!example/ios/build",
31 | "!example/ios/Pods"
32 | ],
33 | "outputs": []
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | declare module 'react' {
4 | function useId(): string;
5 | declare const useId: useId | undefined;
6 | }
7 |
8 | declare module 'react-native' {
9 | interface TextInputProps {
10 | submitBehavior?: string;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------