react-native-keyboard-controller 1.16.1 → 1.16.2

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.
@@ -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 {
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.2",
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",
@@ -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
- }