react-native-keyboard-controller 1.16.1 → 1.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -70,6 +70,12 @@ android {
70
70
  } else {
71
71
  java.srcDirs += ['src/paper']
72
72
  }
73
+
74
+ if (project.ext.shouldUseBaseReactPackage()) {
75
+ java.srcDirs += ['src/base']
76
+ } else {
77
+ java.srcDirs += ['src/turbo']
78
+ }
73
79
  }
74
80
  }
75
81
 
@@ -39,4 +39,8 @@ project.ext.resolveReactNativeDirectory = { ->
39
39
 
40
40
  project.ext.shouldConsumeReactNativeFromMavenCentral = { ->
41
41
  return REACT_NATIVE_MINOR_VERSION >= 71
42
- }
42
+ }
43
+
44
+ project.ext.shouldUseBaseReactPackage = { ->
45
+ return REACT_NATIVE_MINOR_VERSION >= 74
46
+ }
@@ -0,0 +1,61 @@
1
+ package com.reactnativekeyboardcontroller
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import com.facebook.react.uimanager.ViewManager
9
+ import com.reactnativekeyboardcontroller.modules.KeyboardControllerModuleImpl
10
+ import com.reactnativekeyboardcontroller.modules.StatusBarManagerCompatModuleImpl
11
+
12
+ class KeyboardControllerPackage : BaseReactPackage() {
13
+ override fun getModule(
14
+ name: String,
15
+ reactContext: ReactApplicationContext,
16
+ ): NativeModule? =
17
+ when (name) {
18
+ KeyboardControllerModuleImpl.NAME -> {
19
+ KeyboardControllerModule(reactContext)
20
+ }
21
+ StatusBarManagerCompatModuleImpl.NAME -> {
22
+ StatusBarManagerCompatModule(reactContext)
23
+ }
24
+ else -> {
25
+ null
26
+ }
27
+ }
28
+
29
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
30
+ ReactModuleInfoProvider {
31
+ val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
32
+ val isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
33
+
34
+ moduleInfos[KeyboardControllerModuleImpl.NAME] =
35
+ ReactModuleInfo(
36
+ KeyboardControllerModuleImpl.NAME,
37
+ KeyboardControllerModuleImpl.NAME,
38
+ false, // canOverrideExistingModule
39
+ false, // needsEagerInit
40
+ false, // isCxxModule
41
+ isTurboModule, // isTurboModule
42
+ )
43
+ moduleInfos[StatusBarManagerCompatModuleImpl.NAME] =
44
+ ReactModuleInfo(
45
+ StatusBarManagerCompatModuleImpl.NAME,
46
+ StatusBarManagerCompatModuleImpl.NAME,
47
+ false, // canOverrideExistingModule
48
+ false, // needsEagerInit
49
+ false, // isCxxModule
50
+ isTurboModule, // isTurboModule
51
+ )
52
+ moduleInfos
53
+ }
54
+
55
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
56
+ listOf(
57
+ KeyboardControllerViewManager(reactContext),
58
+ KeyboardGestureAreaViewManager(reactContext),
59
+ OverKeyboardViewManager(reactContext),
60
+ )
61
+ }
@@ -24,6 +24,11 @@ class KeyboardControllerViewManager(
24
24
 
25
25
  override fun createViewInstance(context: ThemedReactContext): ReactViewGroup = manager.createViewInstance(context)
26
26
 
27
+ override fun onAfterUpdateTransaction(view: ReactViewGroup) {
28
+ super.onAfterUpdateTransaction(view)
29
+ manager.setEdgeToEdge(view as EdgeToEdgeReactViewGroup)
30
+ }
31
+
27
32
  @ReactProp(name = "statusBarTranslucent")
28
33
  override fun setStatusBarTranslucent(
29
34
  view: ReactViewGroup,
@@ -44,6 +44,10 @@ class KeyboardControllerViewManagerImpl(
44
44
  view.setPreserveEdgeToEdge(isPreservingEdgeToEdge)
45
45
  }
46
46
 
47
+ fun setEdgeToEdge(view: EdgeToEdgeReactViewGroup) {
48
+ view.setEdgeToEdge()
49
+ }
50
+
47
51
  fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
48
52
  val map: MutableMap<String, Any> =
49
53
  MapBuilder.of(
@@ -33,6 +33,7 @@ class EdgeToEdgeReactViewGroup(
33
33
  private var isNavigationBarTranslucent = false
34
34
  private var isPreservingEdgeToEdge = false
35
35
  private var active = false
36
+ private var isEdgeToEdge = false
36
37
 
37
38
  // internal class members
38
39
  private var eventView: ReactViewGroup? = null
@@ -124,12 +125,18 @@ class EdgeToEdgeReactViewGroup(
124
125
  }
125
126
  }
126
127
 
127
- private fun goToEdgeToEdge(edgeToEdge: Boolean) {
128
- reactContext.currentActivity?.let {
129
- WindowCompat.setDecorFitsSystemWindows(
130
- it.window,
131
- !edgeToEdge,
132
- )
128
+ fun setEdgeToEdge() {
129
+ val nextValue = active || isPreservingEdgeToEdge
130
+
131
+ if (isEdgeToEdge != nextValue) {
132
+ isEdgeToEdge = nextValue
133
+
134
+ reactContext.currentActivity?.let {
135
+ WindowCompat.setDecorFitsSystemWindows(
136
+ it.window,
137
+ !isEdgeToEdge,
138
+ )
139
+ }
133
140
  }
134
141
  }
135
142
 
@@ -182,17 +189,12 @@ class EdgeToEdgeReactViewGroup(
182
189
 
183
190
  // region State managers
184
191
  private fun enable() {
185
- this.goToEdgeToEdge(true)
186
192
  this.setupWindowInsets()
187
193
  this.setupKeyboardCallbacks()
188
194
  modalAttachedWatcher.enable()
189
195
  }
190
196
 
191
197
  private fun disable() {
192
- if (!isPreservingEdgeToEdge) {
193
- this.goToEdgeToEdge(false)
194
- }
195
-
196
198
  this.setupWindowInsets()
197
199
  this.removeKeyboardCallbacks()
198
200
  modalAttachedWatcher.disable()
@@ -3,6 +3,7 @@ package com.reactnativekeyboardcontroller
3
3
  import com.facebook.react.bridge.ReactApplicationContext
4
4
  import com.facebook.react.uimanager.ThemedReactContext
5
5
  import com.facebook.react.uimanager.annotations.ReactProp
6
+ import com.facebook.react.views.view.ReactViewGroup
6
7
  import com.facebook.react.views.view.ReactViewManager
7
8
  import com.reactnativekeyboardcontroller.managers.KeyboardControllerViewManagerImpl
8
9
  import com.reactnativekeyboardcontroller.views.EdgeToEdgeReactViewGroup
@@ -17,6 +18,11 @@ class KeyboardControllerViewManager(
17
18
  override fun createViewInstance(reactContext: ThemedReactContext): EdgeToEdgeReactViewGroup =
18
19
  manager.createViewInstance(reactContext)
19
20
 
21
+ override fun onAfterUpdateTransaction(view: ReactViewGroup) {
22
+ super.onAfterUpdateTransaction(view)
23
+ manager.setEdgeToEdge(view as EdgeToEdgeReactViewGroup)
24
+ }
25
+
20
26
  @ReactProp(name = "enabled")
21
27
  fun setEnabled(
22
28
  view: EdgeToEdgeReactViewGroup,
@@ -14,9 +14,10 @@ public class KeyboardControllerModuleImpl: NSObject {
14
14
 
15
15
  @objc
16
16
  public static func dismiss(_ keepFocus: Bool) {
17
- guard let input = UIResponder.current as? TextInput else { return }
17
+ let responder = UIResponder.current
18
18
 
19
19
  if keepFocus {
20
+ guard let input = responder as? TextInput else { return }
20
21
  let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onTextInputTapped(_:)))
21
22
  tapGesture.name = keyboardRevealGestureName
22
23
  input.addGestureRecognizer(tapGesture)
@@ -31,7 +32,7 @@ public class KeyboardControllerModuleImpl: NSObject {
31
32
  object: input
32
33
  )
33
34
  } else {
34
- input.resignFirstResponder()
35
+ responder?.resignFirstResponder()
35
36
  }
36
37
  }
37
38
 
@@ -11,28 +11,9 @@ import UIKit
11
11
 
12
12
  @objc
13
13
  public extension UIResponder {
14
- private weak static var _currentFirstResponder: UIResponder?
15
-
16
14
  static var current: UIResponder? {
17
- UIResponder._currentFirstResponder = nil
18
- UIApplication.shared.sendAction(#selector(findFirstResponder(sender:)), to: nil, from: nil, for: nil)
19
- return UIResponder._currentFirstResponder
20
- }
21
-
22
- internal func findFirstResponder(sender _: AnyObject) {
23
- let type = String(describing: type(of: self))
24
- // handle `contextMenuHidden` prop - in this case the parent is considered as a first responder
25
- // (but actually its children is an actual input), so we apply correction here and point out
26
- // to the actual first responder (first children)
27
- let isChildrenActuallyFirstResponder =
28
- type == "RCTMultilineTextInputView" ||
29
- type == "RCTSinglelineTextInputView" ||
30
- type == "RCTTextInputComponentView"
31
- if isChildrenActuallyFirstResponder {
32
- UIResponder._currentFirstResponder = (self as? UIView)?.subviews[0]
33
- } else {
34
- UIResponder._currentFirstResponder = self
35
- }
15
+ guard let window = UIApplication.shared.activeWindow else { return nil }
16
+ return window.findFirstResponder()
36
17
  }
37
18
  }
38
19
 
@@ -34,6 +34,18 @@ public extension UIView {
34
34
  return false
35
35
  }
36
36
  }
37
+
38
+ func findFirstResponder() -> UIView? {
39
+ if isFirstResponder {
40
+ return self
41
+ }
42
+ for subview in subviews {
43
+ if let responder = subview.findFirstResponder() {
44
+ return responder
45
+ }
46
+ }
47
+ return nil
48
+ }
37
49
  }
38
50
 
39
51
  public extension Optional where Wrapped == UIView {
@@ -80,12 +80,6 @@ public class FocusedInputObserver: NSObject {
80
80
  name: UIResponder.keyboardWillShowNotification,
81
81
  object: nil
82
82
  )
83
- NotificationCenter.default.addObserver(
84
- self,
85
- selector: #selector(keyboardWillHide),
86
- name: UIResponder.keyboardWillHideNotification,
87
- object: nil
88
- )
89
83
  NotificationCenter.default.addObserver(
90
84
  self,
91
85
  selector: #selector(didReceiveFocus),
@@ -118,10 +112,6 @@ public class FocusedInputObserver: NSObject {
118
112
  NotificationCenter.default.removeObserver(self)
119
113
  }
120
114
 
121
- @objc func keyboardWillHide(_: Notification) {
122
- onBlur()
123
- }
124
-
125
115
  @objc func didReceiveFocus(_: Notification) {
126
116
  if UIResponder.current == currentResponder {
127
117
  // focus was already handled by keyboard event
@@ -131,22 +121,13 @@ public class FocusedInputObserver: NSObject {
131
121
  onFocus()
132
122
  }
133
123
 
134
- /**
135
- * We handle blur events in `keyboardWillHide` handler
136
- * And this additional handler is needed only to have
137
- * a consistent state when keyboard is not shown
138
- */
139
124
  @objc func didReceiveBlur(_: Notification) {
140
- if currentResponder == nil {
141
- // blur was already handled by keyboard event
142
- return
143
- }
144
125
  // blur gets triggered on endEditing, so we check if no upcoming
145
126
  // didReceiveFocus events are coming to exclude `noFocusedInput`
146
127
  // event when user switches between inputs
147
128
  DispatchQueue.main.async {
148
129
  // check that it wasn't a switch between inputs
149
- if self.currentResponder == nil {
130
+ if !(UIResponder.current is TextInput) {
150
131
  self.onBlur()
151
132
  }
152
133
  }
@@ -173,10 +154,6 @@ public class FocusedInputObserver: NSObject {
173
154
  }
174
155
 
175
156
  func onBlur() {
176
- // when we switch to next input, but `showSoftInput={false}`
177
- if UIResponder.current is TextInput {
178
- return
179
- }
180
157
  removeObservers(newResponder: nil)
181
158
  currentInput = nil
182
159
  currentResponder = nil
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-keyboard-controller",
3
- "version": "1.16.1",
3
+ "version": "1.16.3",
4
4
  "description": "Keyboard manager which works in identical way on both iOS and Android",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -17,6 +17,7 @@
17
17
  "jest",
18
18
  "!lib/typescript/example",
19
19
  "!android/build",
20
+ "!android/src/test",
20
21
  "!android/.gradle",
21
22
  "!android/gradle",
22
23
  "!android/gradlew",
@@ -79,11 +80,11 @@
79
80
  },
80
81
  "devDependencies": {
81
82
  "@commitlint/config-conventional": "^11.0.0",
82
- "@react-native/babel-preset": "0.76.2",
83
- "@react-native/eslint-config": "0.76.2",
83
+ "@react-native/babel-preset": "0.77.0",
84
+ "@react-native/eslint-config": "0.77.0",
84
85
  "@release-it/conventional-changelog": "^2.0.0",
85
86
  "@testing-library/react-hooks": "^8.0.1",
86
- "@types/jest": "^29.2.1",
87
+ "@types/jest": "^29.5.13",
87
88
  "@types/react": "^18.2.6",
88
89
  "@typescript-eslint/eslint-plugin": "^6.7.4",
89
90
  "@typescript-eslint/parser": "^6.7.4",
@@ -104,9 +105,9 @@
104
105
  "pod-install": "^0.1.0",
105
106
  "prettier": "^2.8.8",
106
107
  "react": "18.3.1",
107
- "react-native": "0.76.2",
108
+ "react-native": "0.77.0",
108
109
  "react-native-builder-bob": "^0.18.0",
109
- "react-native-reanimated": "3.16.1",
110
+ "react-native-reanimated": "3.16.7",
110
111
  "react-test-renderer": "18.2.0",
111
112
  "release-it": "^14.2.2",
112
113
  "typescript": "5.6.3"
@@ -169,6 +170,13 @@
169
170
  "jsSrcsDir": "./src/specs",
170
171
  "android": {
171
172
  "javaPackageName": "com.reactnativekeyboardcontroller"
173
+ },
174
+ "ios": {
175
+ "componentProvider": {
176
+ "KeyboardControllerView": "KeyboardControllerView",
177
+ "KeyboardGestureArea": "KeyboardGestureArea",
178
+ "OverKeyboardView": "OverKeyboardView"
179
+ }
172
180
  }
173
181
  }
174
182
  }
@@ -1,45 +0,0 @@
1
- package com.reactnativekeyboardcontroller.traversal
2
-
3
- import android.content.Context
4
- import android.widget.EditText
5
- import androidx.test.core.app.ApplicationProvider
6
- import org.junit.Assert.assertEquals
7
- import org.junit.Assert.assertFalse
8
- import org.junit.Assert.assertNull
9
- import org.junit.Assert.assertTrue
10
- import org.junit.Test
11
- import org.junit.runner.RunWith
12
- import org.robolectric.RobolectricTestRunner
13
-
14
- @RunWith(RobolectricTestRunner::class)
15
- class FocusedInputHolderTest {
16
- @Test
17
- fun `FocusedInputHolder should hold a weak reference`() {
18
- val context = ApplicationProvider.getApplicationContext<Context>()
19
- var input: EditText? = EditText(context)
20
-
21
- FocusedInputHolder.set(input as EditText)
22
-
23
- assertEquals(FocusedInputHolder.get(), input)
24
-
25
- input = null
26
-
27
- @Suppress("detekt:ExplicitGarbageCollectionCall")
28
- System.gc()
29
-
30
- assertNull(FocusedInputHolder.get())
31
- }
32
-
33
- @Test
34
- fun `focus() should request focus on expected field`() {
35
- val context = ApplicationProvider.getApplicationContext<Context>()
36
- val input = EditText(context)
37
-
38
- assertFalse(input.hasFocus())
39
-
40
- FocusedInputHolder.set(input)
41
- FocusedInputHolder.focus()
42
-
43
- assertTrue(input.hasFocus())
44
- }
45
- }
@@ -1,198 +0,0 @@
1
- package com.reactnativekeyboardcontroller.traversal
2
-
3
- import android.content.Context
4
- import android.widget.EditText
5
- import android.widget.LinearLayout
6
- import androidx.test.core.app.ApplicationProvider
7
- import com.reactnativekeyboardcontroller.extensions.focus
8
- import org.junit.Assert.assertTrue
9
- import org.junit.Before
10
- import org.junit.Test
11
- import org.junit.runner.RunWith
12
- import org.robolectric.RobolectricTestRunner
13
- import org.robolectric.shadows.ShadowLooper
14
-
15
- @RunWith(RobolectricTestRunner::class)
16
- class ViewHierarchyNavigatorTest {
17
- private lateinit var layout: LinearLayout
18
- private lateinit var editText1: EditText
19
- private lateinit var editText2: EditText
20
- private lateinit var editText3: EditText
21
- private lateinit var editText4: EditText
22
- private lateinit var editText5: EditText
23
- private lateinit var editText6: EditText
24
- private lateinit var editText7: EditText
25
- private lateinit var editText8: EditText
26
- private lateinit var editText9: EditText
27
- private lateinit var editText10: EditText
28
- private lateinit var editText11: EditText
29
- private lateinit var editText12: EditText
30
- private lateinit var editText13: EditText
31
-
32
- @Suppress("detekt:CyclomaticComplexMethod")
33
- @Before
34
- fun setUp() {
35
- val context = ApplicationProvider.getApplicationContext<Context>()
36
-
37
- editText1 = EditText(context).apply { id = 1 }
38
- editText2 = EditText(context).apply { id = 2 }
39
- editText3 =
40
- EditText(context).apply {
41
- id = 3
42
- isEnabled = false
43
- }
44
- editText4 =
45
- EditText(context).apply {
46
- id = 4
47
- isEnabled = false
48
- }
49
- editText5 = EditText(context).apply { id = 5 }
50
- editText6 = EditText(context).apply { id = 6 }
51
- editText7 = EditText(context).apply { id = 7 }
52
- editText8 = EditText(context).apply { id = 8 }
53
- editText9 = EditText(context).apply { id = 9 }
54
- editText10 = EditText(context).apply { id = 10 }
55
- editText11 = EditText(context).apply { id = 11 }
56
- editText12 = EditText(context).apply { id = 12 }
57
- editText13 = EditText(context).apply { id = 13 }
58
-
59
- layout =
60
- LinearLayout(context).apply {
61
- addView(editText1)
62
- addView(editText2)
63
- addView(editText3)
64
- addView(editText4)
65
- addView(
66
- LinearLayout(context).apply {
67
- addView(editText5)
68
- addView(editText6)
69
- addView(editText7)
70
- },
71
- )
72
- addView(editText8)
73
- addView(editText9)
74
- addView(editText10)
75
- addView(editText11)
76
- addView(editText12)
77
- addView(editText13)
78
- }
79
- }
80
-
81
- @Test
82
- fun `getAllInputFields returns all EditTexts in ViewGroup`() {
83
- val editTexts = ViewHierarchyNavigator.getAllInputFields(layout)
84
-
85
- // 13 (all) - 2 (disabled)
86
- assertTrue(editTexts.size == 11)
87
- }
88
-
89
- @Test
90
- fun `setFocusTo to 'next' should set focus to next field`() {
91
- editText1.focus()
92
-
93
- ViewHierarchyNavigator.setFocusTo("next", editText1)
94
-
95
- ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
96
-
97
- assertTrue(editText2.hasFocus())
98
- }
99
-
100
- @Test
101
- fun `setFocusTo to 'prev' should set focus to previous field`() {
102
- editText2.focus()
103
-
104
- ViewHierarchyNavigator.setFocusTo("prev", editText2)
105
-
106
- ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
107
-
108
- assertTrue(editText1.hasFocus())
109
- }
110
-
111
- @Test
112
- fun `setFocusTo to 'next' should skip non-editable fields`() {
113
- editText2.focus()
114
-
115
- ViewHierarchyNavigator.setFocusTo("next", editText2)
116
-
117
- ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
118
-
119
- assertTrue(editText5.hasFocus())
120
- }
121
-
122
- @Test
123
- fun `setFocusTo to 'prev' should skip non-editable fields`() {
124
- editText5.focus()
125
-
126
- ViewHierarchyNavigator.setFocusTo("prev", editText5)
127
-
128
- ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
129
-
130
- assertTrue(editText2.hasFocus())
131
- }
132
-
133
- @Test
134
- fun `setFocusTo to 'next' should set focus relatively to current group`() {
135
- editText5.focus()
136
-
137
- ViewHierarchyNavigator.setFocusTo("next", editText5)
138
-
139
- ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
140
-
141
- assertTrue(editText6.hasFocus())
142
- }
143
-
144
- @Test
145
- fun `setFocusTo to 'prev' should set focus relatively to current group`() {
146
- editText7.focus()
147
-
148
- ViewHierarchyNavigator.setFocusTo("prev", editText7)
149
-
150
- ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
151
-
152
- assertTrue(editText6.hasFocus())
153
- }
154
-
155
- @Test
156
- fun `setFocusTo to 'next' should correctly exit from current group`() {
157
- editText7.focus()
158
-
159
- ViewHierarchyNavigator.setFocusTo("next", editText7)
160
-
161
- ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
162
-
163
- assertTrue(editText8.hasFocus())
164
- }
165
-
166
- @Test
167
- fun `setFocusTo to 'prev' should set focus to last element in group`() {
168
- editText8.focus()
169
-
170
- ViewHierarchyNavigator.setFocusTo("prev", editText8)
171
-
172
- ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
173
-
174
- assertTrue(editText7.hasFocus())
175
- }
176
-
177
- @Test
178
- fun `setFocusTo to 'next' should do nothing if it's last element`() {
179
- editText13.focus()
180
-
181
- ViewHierarchyNavigator.setFocusTo("next", editText13)
182
-
183
- ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
184
-
185
- assertTrue(editText13.hasFocus())
186
- }
187
-
188
- @Test
189
- fun `setFocusTo to 'prev' should do nothing if it's first element`() {
190
- editText1.focus()
191
-
192
- ViewHierarchyNavigator.setFocusTo("prev", editText1)
193
-
194
- ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
195
-
196
- assertTrue(editText1.hasFocus())
197
- }
198
- }