react-native-controlled-input 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ControlledInput.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +37 -0
- package/android/build.gradle +92 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/controlledinput/ControlledInputPackage.kt +19 -0
- package/android/src/main/java/com/controlledinput/ControlledInputView.kt +141 -0
- package/android/src/main/java/com/controlledinput/ControlledInputViewManager.kt +79 -0
- package/android/src/main/java/com/controlledinput/JetpackComposeView.kt +184 -0
- package/ios/ControlledInputView.h +14 -0
- package/ios/ControlledInputView.mm +108 -0
- package/ios/RNControlledInput.swift +48 -0
- package/lib/module/ControlledInputViewNativeComponent.ts +55 -0
- package/lib/module/index.js +35 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/react-native-codegen.d.js +2 -0
- package/lib/module/react-native-codegen.d.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/ControlledInputViewNativeComponent.d.ts +36 -0
- package/lib/typescript/src/ControlledInputViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +9 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +174 -0
- package/src/ControlledInputViewNativeComponent.ts +55 -0
- package/src/index.tsx +59 -0
- package/src/react-native-codegen.d.ts +21 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "ControlledInput"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/veliseev93/react-native-controlled-input.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
|
|
19
|
+
install_modules_dependencies(s)
|
|
20
|
+
end
|
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 veliseev
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# react-native-controlled-input
|
|
2
|
+
|
|
3
|
+
React Native Controlled Inputative Controlled Input
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install react-native-controlled-input
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { ControlledInputView } from "react-native-controlled-input";
|
|
18
|
+
|
|
19
|
+
// ...
|
|
20
|
+
|
|
21
|
+
<ControlledInputView color="tomato" />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Contributing
|
|
26
|
+
|
|
27
|
+
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
28
|
+
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
29
|
+
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.getExtOrDefault = {name ->
|
|
3
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ControlledInput__' + name]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
repositories {
|
|
7
|
+
google()
|
|
8
|
+
mavenCentral()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
dependencies {
|
|
12
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
13
|
+
// noinspection DifferentKotlinGradleVersion
|
|
14
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
15
|
+
classpath "org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:${getExtOrDefault('kotlinVersion')}"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
apply plugin: "com.android.library"
|
|
21
|
+
apply plugin: "kotlin-android"
|
|
22
|
+
apply plugin: "org.jetbrains.kotlin.plugin.compose"
|
|
23
|
+
|
|
24
|
+
apply plugin: "com.facebook.react"
|
|
25
|
+
|
|
26
|
+
def getExtOrIntegerDefault(name) {
|
|
27
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ControlledInput__" + name]).toInteger()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
android {
|
|
31
|
+
namespace "com.controlledinput"
|
|
32
|
+
|
|
33
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
34
|
+
|
|
35
|
+
defaultConfig {
|
|
36
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
37
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
buildFeatures {
|
|
41
|
+
buildConfig true
|
|
42
|
+
compose true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildTypes {
|
|
46
|
+
release {
|
|
47
|
+
minifyEnabled false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
lintOptions {
|
|
52
|
+
disable "GradleCompatible"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
compileOptions {
|
|
56
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
57
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
sourceSets {
|
|
61
|
+
main {
|
|
62
|
+
java.srcDirs += [
|
|
63
|
+
"generated/java",
|
|
64
|
+
"generated/jni"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
repositories {
|
|
71
|
+
mavenCentral()
|
|
72
|
+
google()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def kotlin_version = getExtOrDefault("kotlinVersion")
|
|
76
|
+
|
|
77
|
+
dependencies {
|
|
78
|
+
implementation "com.facebook.react:react-android"
|
|
79
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
80
|
+
|
|
81
|
+
// Material Components (for BottomSheetDialog)
|
|
82
|
+
implementation 'com.google.android.material:material:1.13.0'
|
|
83
|
+
|
|
84
|
+
// Jetpack Compose dependencies
|
|
85
|
+
def composeBom = platform('androidx.compose:compose-bom:2025.12.01')
|
|
86
|
+
implementation composeBom
|
|
87
|
+
implementation 'androidx.compose.ui:ui'
|
|
88
|
+
implementation 'androidx.compose.ui:ui-tooling-preview'
|
|
89
|
+
implementation 'androidx.compose.material3:material3'
|
|
90
|
+
implementation 'androidx.activity:activity-compose:1.12.2'
|
|
91
|
+
debugImplementation 'androidx.compose.ui:ui-tooling'
|
|
92
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package com.controlledinput;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
import java.util.ArrayList
|
|
8
|
+
|
|
9
|
+
class ControlledInputViewPackage : ReactPackage {
|
|
10
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
11
|
+
val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
|
|
12
|
+
viewManagers.add(ControlledInputViewManager())
|
|
13
|
+
return viewManagers
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
17
|
+
return emptyList()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
package com.controlledinput
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.util.AttributeSet
|
|
6
|
+
import android.view.inputmethod.InputMethodManager
|
|
7
|
+
import android.widget.LinearLayout
|
|
8
|
+
import androidx.compose.material3.Text
|
|
9
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
10
|
+
import androidx.compose.runtime.collectAsState
|
|
11
|
+
import androidx.compose.runtime.getValue
|
|
12
|
+
import androidx.compose.runtime.remember
|
|
13
|
+
import androidx.compose.ui.focus.FocusRequester
|
|
14
|
+
import androidx.compose.ui.platform.ComposeView
|
|
15
|
+
import androidx.compose.ui.platform.LocalFocusManager
|
|
16
|
+
import com.facebook.react.bridge.ReactContext
|
|
17
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
18
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
19
|
+
|
|
20
|
+
class ControlledInputView : LinearLayout {
|
|
21
|
+
constructor(context: Context) : super(context) {
|
|
22
|
+
configureComponent(context)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
|
26
|
+
configureComponent(context)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
|
30
|
+
context,
|
|
31
|
+
attrs,
|
|
32
|
+
defStyleAttr
|
|
33
|
+
) {
|
|
34
|
+
configureComponent(context)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
internal lateinit var viewModel: JetpackComposeViewModel
|
|
38
|
+
private val blurSignal = MutableStateFlow(0)
|
|
39
|
+
private val focusSignal = MutableStateFlow(0)
|
|
40
|
+
|
|
41
|
+
fun blur() {
|
|
42
|
+
// триггерим compose снять фокус
|
|
43
|
+
blurSignal.value = blurSignal.value + 1
|
|
44
|
+
|
|
45
|
+
// на всякий случай прячем клавиатуру на уровне View
|
|
46
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
|
47
|
+
imm.hideSoftInputFromWindow(windowToken, 0)
|
|
48
|
+
|
|
49
|
+
// и снимаем фокус у самого android view (не всегда достаточно, но не мешает)
|
|
50
|
+
clearFocus()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fun focus() {
|
|
54
|
+
// триггерим compose запросить фокус
|
|
55
|
+
focusSignal.value = focusSignal.value + 1
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private fun configureComponent(context: Context) {
|
|
59
|
+
|
|
60
|
+
layoutParams = LayoutParams(
|
|
61
|
+
LayoutParams.WRAP_CONTENT,
|
|
62
|
+
LayoutParams.WRAP_CONTENT
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
ComposeView(context).also {
|
|
66
|
+
it.layoutParams = LayoutParams(
|
|
67
|
+
LayoutParams.WRAP_CONTENT,
|
|
68
|
+
LayoutParams.WRAP_CONTENT
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
viewModel = JetpackComposeViewModel()
|
|
72
|
+
|
|
73
|
+
it.setContent {
|
|
74
|
+
val value = viewModel.value.collectAsState().value
|
|
75
|
+
val blurTick by blurSignal.collectAsState()
|
|
76
|
+
val focusTick by focusSignal.collectAsState()
|
|
77
|
+
val focusManager = LocalFocusManager.current
|
|
78
|
+
val focusRequester = remember { FocusRequester() }
|
|
79
|
+
|
|
80
|
+
// при каждом blurTick снимаем фокус в compose
|
|
81
|
+
LaunchedEffect(blurTick) {
|
|
82
|
+
if (blurTick > 0) {
|
|
83
|
+
focusManager.clearFocus(force = true)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// при каждом focusTick запрашиваем фокус в compose
|
|
88
|
+
LaunchedEffect(focusTick) {
|
|
89
|
+
if (focusTick > 0) {
|
|
90
|
+
focusRequester.requestFocus()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
JetpackComposeView(
|
|
95
|
+
value = value,
|
|
96
|
+
inputStyle = viewModel.inputStyle,
|
|
97
|
+
onTextChange = { value ->
|
|
98
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
99
|
+
val viewId = this.id
|
|
100
|
+
UIManagerHelper
|
|
101
|
+
.getEventDispatcherForReactTag(context as ReactContext, viewId)
|
|
102
|
+
?.dispatchEvent(
|
|
103
|
+
TextChangeEvent(
|
|
104
|
+
surfaceId,
|
|
105
|
+
viewId,
|
|
106
|
+
value
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
},
|
|
110
|
+
onFocus = {
|
|
111
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
112
|
+
val viewId = this.id
|
|
113
|
+
UIManagerHelper
|
|
114
|
+
.getEventDispatcherForReactTag(context as ReactContext, viewId)
|
|
115
|
+
?.dispatchEvent(
|
|
116
|
+
FocusEvent(
|
|
117
|
+
surfaceId,
|
|
118
|
+
viewId
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
},
|
|
122
|
+
onBlur = {
|
|
123
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
124
|
+
val viewId = this.id
|
|
125
|
+
UIManagerHelper
|
|
126
|
+
.getEventDispatcherForReactTag(context as ReactContext, viewId)
|
|
127
|
+
?.dispatchEvent(
|
|
128
|
+
BlurEvent(
|
|
129
|
+
surfaceId,
|
|
130
|
+
viewId
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
},
|
|
134
|
+
focusRequester = focusRequester
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
addView(it)
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package com.controlledinput
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableMap
|
|
4
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
6
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
8
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
9
|
+
import com.facebook.react.viewmanagers.ControlledInputViewManagerInterface
|
|
10
|
+
import com.facebook.react.viewmanagers.ControlledInputViewManagerDelegate
|
|
11
|
+
import com.facebook.react.common.MapBuilder
|
|
12
|
+
|
|
13
|
+
@ReactModule(name = ControlledInputViewManager.NAME)
|
|
14
|
+
class ControlledInputViewManager : SimpleViewManager<ControlledInputView>(),
|
|
15
|
+
ControlledInputViewManagerInterface<ControlledInputView> {
|
|
16
|
+
private val mDelegate: ViewManagerDelegate<ControlledInputView>
|
|
17
|
+
|
|
18
|
+
init {
|
|
19
|
+
mDelegate = ControlledInputViewManagerDelegate(this)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override fun getDelegate(): ViewManagerDelegate<ControlledInputView>? {
|
|
23
|
+
return mDelegate
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
override fun getName(): String {
|
|
27
|
+
return NAME
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public override fun createViewInstance(context: ThemedReactContext): ControlledInputView {
|
|
31
|
+
return ControlledInputView(context)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@ReactProp(name = "value")
|
|
35
|
+
override fun setValue(view: ControlledInputView, value: String?) {
|
|
36
|
+
value?.let {
|
|
37
|
+
view.viewModel.setValue(value)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@ReactProp(name = "inputStyle")
|
|
42
|
+
override fun setInputStyle(view: ControlledInputView, inputStyle: ReadableMap?) {
|
|
43
|
+
val style = if (inputStyle == null) {
|
|
44
|
+
null
|
|
45
|
+
} else {
|
|
46
|
+
InputStyle(
|
|
47
|
+
color = if (inputStyle.hasKey("color")) inputStyle.getString("color") else null,
|
|
48
|
+
fontSize = if (inputStyle.hasKey("fontSize")) inputStyle.getDouble("fontSize") else null,
|
|
49
|
+
height = if (inputStyle.hasKey("height")) inputStyle.getDouble("height") else null,
|
|
50
|
+
paddingTop = if (inputStyle.hasKey("paddingTop")) inputStyle.getDouble("paddingTop") else null,
|
|
51
|
+
paddingBottom = if (inputStyle.hasKey("paddingBottom")) inputStyle.getDouble("paddingBottom") else null,
|
|
52
|
+
paddingLeft = if (inputStyle.hasKey("paddingLeft")) inputStyle.getDouble("paddingLeft") else null,
|
|
53
|
+
paddingRight = if (inputStyle.hasKey("paddingRight")) inputStyle.getDouble("paddingRight") else null,
|
|
54
|
+
borderWidth = if (inputStyle.hasKey("borderWidth")) inputStyle.getDouble("borderWidth") else null,
|
|
55
|
+
borderRadius = if (inputStyle.hasKey("borderRadius")) inputStyle.getDouble("borderRadius") else null,
|
|
56
|
+
borderColor = if (inputStyle.hasKey("borderColor")) inputStyle.getString("borderColor") else null,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
view.viewModel.setInputStyle(style)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
companion object {
|
|
63
|
+
const val NAME = "ControlledInputView"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> = mutableMapOf(
|
|
67
|
+
TextChangeEvent.EVENT_NAME to MapBuilder.of("registrationName", "onTextChange"),
|
|
68
|
+
FocusEvent.EVENT_NAME to MapBuilder.of("registrationName", "onFocus"),
|
|
69
|
+
BlurEvent.EVENT_NAME to MapBuilder.of("registrationName", "onBlur")
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
override fun focus(view: ControlledInputView?) {
|
|
73
|
+
view?.focus()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override fun blur(view: ControlledInputView?) {
|
|
77
|
+
view?.blur()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
package com.controlledinput
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.border
|
|
4
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
5
|
+
import androidx.compose.foundation.layout.Box
|
|
6
|
+
import androidx.compose.ui.draw.clip
|
|
7
|
+
import androidx.compose.foundation.layout.PaddingValues
|
|
8
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
9
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
10
|
+
import androidx.compose.foundation.layout.height
|
|
11
|
+
import androidx.compose.foundation.layout.padding
|
|
12
|
+
import androidx.compose.foundation.text.BasicTextField
|
|
13
|
+
import androidx.compose.foundation.text.input.InputTransformation
|
|
14
|
+
import androidx.compose.foundation.text.input.TextFieldState
|
|
15
|
+
import androidx.compose.foundation.text.input.byValue
|
|
16
|
+
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
|
17
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
18
|
+
import androidx.compose.foundation.interaction.FocusInteraction
|
|
19
|
+
import androidx.compose.runtime.Composable
|
|
20
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
21
|
+
import androidx.compose.runtime.collectAsState
|
|
22
|
+
import androidx.compose.runtime.getValue
|
|
23
|
+
import androidx.compose.runtime.remember
|
|
24
|
+
import androidx.compose.ui.Modifier
|
|
25
|
+
import androidx.compose.ui.focus.FocusRequester
|
|
26
|
+
import androidx.compose.ui.focus.focusRequester
|
|
27
|
+
import androidx.compose.ui.graphics.Color
|
|
28
|
+
import androidx.compose.ui.text.TextStyle
|
|
29
|
+
import androidx.compose.ui.unit.dp
|
|
30
|
+
import androidx.compose.ui.unit.sp
|
|
31
|
+
import androidx.lifecycle.ViewModel
|
|
32
|
+
import com.facebook.react.bridge.Arguments
|
|
33
|
+
import com.facebook.react.bridge.WritableMap
|
|
34
|
+
import com.facebook.react.uimanager.events.Event
|
|
35
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
36
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
37
|
+
|
|
38
|
+
data class InputStyle(
|
|
39
|
+
val color: String? = null,
|
|
40
|
+
val fontSize: Double? = null,
|
|
41
|
+
val height: Double? = null,
|
|
42
|
+
val paddingTop: Double? = null,
|
|
43
|
+
val paddingBottom: Double? = null,
|
|
44
|
+
val paddingLeft: Double? = null,
|
|
45
|
+
val paddingRight: Double? = null,
|
|
46
|
+
val borderWidth: Double? = null,
|
|
47
|
+
val borderRadius: Double? = null,
|
|
48
|
+
val borderColor: String? = null,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@Composable
|
|
52
|
+
fun JetpackComposeView(
|
|
53
|
+
value: String,
|
|
54
|
+
inputStyle: StateFlow<InputStyle?>,
|
|
55
|
+
onTextChange: (value: String) -> Unit,
|
|
56
|
+
onFocus: (() -> Unit)? = null,
|
|
57
|
+
onBlur: (() -> Unit)? = null,
|
|
58
|
+
focusRequester: FocusRequester,
|
|
59
|
+
) {
|
|
60
|
+
val state = remember { TextFieldState(value) }
|
|
61
|
+
val style by inputStyle.collectAsState()
|
|
62
|
+
val interactionSource = remember { MutableInteractionSource() }
|
|
63
|
+
|
|
64
|
+
if (state.text.toString() != value) {
|
|
65
|
+
state.setTextAndPlaceCursorAtEnd(value)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
LaunchedEffect(interactionSource) {
|
|
69
|
+
interactionSource.interactions.collect { interaction ->
|
|
70
|
+
when (interaction) {
|
|
71
|
+
is FocusInteraction.Focus -> {
|
|
72
|
+
onFocus?.invoke()
|
|
73
|
+
}
|
|
74
|
+
is FocusInteraction.Unfocus -> {
|
|
75
|
+
onBlur?.invoke()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
val textColor = style?.color?.let { Color(android.graphics.Color.parseColor(it)) } ?: Color.White
|
|
82
|
+
val fontSize = style?.fontSize?.let { it.sp } ?: 24.sp
|
|
83
|
+
val height = style?.height?.let { it.dp }
|
|
84
|
+
val paddingValues = PaddingValues(
|
|
85
|
+
start = style?.paddingLeft?.dp ?: 0.dp,
|
|
86
|
+
top = style?.paddingTop?.dp ?: 0.dp,
|
|
87
|
+
end = style?.paddingRight?.dp ?: 0.dp,
|
|
88
|
+
bottom = style?.paddingBottom?.dp ?: 0.dp,
|
|
89
|
+
)
|
|
90
|
+
val borderWidth = style?.borderWidth?.dp ?: 0.dp
|
|
91
|
+
val borderRadius = style?.borderRadius?.dp ?: 0.dp
|
|
92
|
+
val borderColor = style?.borderColor?.let { Color(android.graphics.Color.parseColor(it)) } ?: Color.Transparent
|
|
93
|
+
|
|
94
|
+
Box(
|
|
95
|
+
modifier = Modifier
|
|
96
|
+
.fillMaxWidth()
|
|
97
|
+
.then(height?.let { Modifier.height(it) } ?: Modifier)
|
|
98
|
+
.clip(RoundedCornerShape(borderRadius))
|
|
99
|
+
.border(borderWidth, borderColor, RoundedCornerShape(borderRadius))
|
|
100
|
+
) {
|
|
101
|
+
BasicTextField(
|
|
102
|
+
state,
|
|
103
|
+
inputTransformation = InputTransformation.byValue { _, proposed ->
|
|
104
|
+
onTextChange(proposed.toString())
|
|
105
|
+
proposed
|
|
106
|
+
},
|
|
107
|
+
modifier = Modifier
|
|
108
|
+
.fillMaxWidth()
|
|
109
|
+
.padding(paddingValues)
|
|
110
|
+
.focusRequester(focusRequester),
|
|
111
|
+
textStyle = TextStyle(
|
|
112
|
+
color = textColor,
|
|
113
|
+
fontSize = fontSize,
|
|
114
|
+
),
|
|
115
|
+
interactionSource = interactionSource,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class TextChangeEvent(
|
|
121
|
+
surfaceId: Int,
|
|
122
|
+
viewId: Int,
|
|
123
|
+
val value: String,
|
|
124
|
+
) : Event<TextChangeEvent>(surfaceId, viewId) {
|
|
125
|
+
override fun getEventName() = EVENT_NAME
|
|
126
|
+
|
|
127
|
+
override fun getCoalescingKey(): Short = 0
|
|
128
|
+
|
|
129
|
+
override fun getEventData(): WritableMap? = Arguments.createMap().also {
|
|
130
|
+
|
|
131
|
+
it.putString("value", value)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
companion object {
|
|
135
|
+
const val EVENT_NAME = "onTextChange"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class FocusEvent(
|
|
140
|
+
surfaceId: Int,
|
|
141
|
+
viewId: Int,
|
|
142
|
+
) : Event<FocusEvent>(surfaceId, viewId) {
|
|
143
|
+
override fun getEventName() = EVENT_NAME
|
|
144
|
+
|
|
145
|
+
override fun getCoalescingKey(): Short = 0
|
|
146
|
+
|
|
147
|
+
override fun getEventData(): WritableMap? = Arguments.createMap()
|
|
148
|
+
|
|
149
|
+
companion object {
|
|
150
|
+
const val EVENT_NAME = "onFocus"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class BlurEvent(
|
|
155
|
+
surfaceId: Int,
|
|
156
|
+
viewId: Int,
|
|
157
|
+
) : Event<BlurEvent>(surfaceId, viewId) {
|
|
158
|
+
override fun getEventName() = EVENT_NAME
|
|
159
|
+
|
|
160
|
+
override fun getCoalescingKey(): Short = 0
|
|
161
|
+
|
|
162
|
+
override fun getEventData(): WritableMap? = Arguments.createMap()
|
|
163
|
+
|
|
164
|
+
companion object {
|
|
165
|
+
const val EVENT_NAME = "onBlur"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class JetpackComposeViewModel : ViewModel() {
|
|
171
|
+
private val _value = MutableStateFlow("")
|
|
172
|
+
private val _inputStyle = MutableStateFlow<InputStyle?>(null)
|
|
173
|
+
|
|
174
|
+
val value: StateFlow<String> get() = _value
|
|
175
|
+
val inputStyle: StateFlow<InputStyle?> get() = _inputStyle
|
|
176
|
+
|
|
177
|
+
fun setValue(newValue: String) {
|
|
178
|
+
_value.value = newValue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fun setInputStyle(style: InputStyle?) {
|
|
182
|
+
_inputStyle.value = style
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#import <React/RCTViewComponentView.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
#ifndef ControlledInputViewNativeComponent_h
|
|
5
|
+
#define ControlledInputViewNativeComponent_h
|
|
6
|
+
|
|
7
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
8
|
+
|
|
9
|
+
@interface ControlledInputView : RCTViewComponentView
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
NS_ASSUME_NONNULL_END
|
|
13
|
+
|
|
14
|
+
#endif /* ControlledInputViewNativeComponent_h */
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#import "ControlledInputView.h"
|
|
2
|
+
|
|
3
|
+
#if __has_include("ControlledInput-Swift.h")
|
|
4
|
+
#import "ControlledInput-Swift.h"
|
|
5
|
+
#else
|
|
6
|
+
#import <ControlledInput/ControlledInput-Swift.h>
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
#import <React/RCTConversions.h>
|
|
10
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
11
|
+
|
|
12
|
+
#import <react/renderer/components/ControlledInputViewSpec/ComponentDescriptors.h>
|
|
13
|
+
#import <react/renderer/components/ControlledInputViewSpec/Props.h>
|
|
14
|
+
#import <react/renderer/components/ControlledInputViewSpec/RCTComponentViewHelpers.h>
|
|
15
|
+
|
|
16
|
+
using namespace facebook::react;
|
|
17
|
+
|
|
18
|
+
@interface ControlledInputView () <RCTControlledInputViewViewProtocol>
|
|
19
|
+
@end
|
|
20
|
+
|
|
21
|
+
@implementation ControlledInputView {
|
|
22
|
+
RNControlledInput * _inputView;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
26
|
+
{
|
|
27
|
+
return concreteComponentDescriptorProvider<ControlledInputViewComponentDescriptor>();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
31
|
+
{
|
|
32
|
+
if (self = [super initWithFrame:frame]) {
|
|
33
|
+
static const auto defaultProps = std::make_shared<const ControlledInputViewProps>();
|
|
34
|
+
_props = defaultProps;
|
|
35
|
+
|
|
36
|
+
_inputView = [[RNControlledInput alloc] initWithFrame:self.bounds];
|
|
37
|
+
|
|
38
|
+
self.contentView = _inputView;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return self;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
45
|
+
{
|
|
46
|
+
const auto &oldViewProps = *std::static_pointer_cast<ControlledInputViewProps const>(_props);
|
|
47
|
+
const auto &newViewProps = *std::static_pointer_cast<ControlledInputViewProps const>(props);
|
|
48
|
+
|
|
49
|
+
if (oldViewProps.value != newViewProps.value) {
|
|
50
|
+
_inputView.value = [NSString stringWithUTF8String:newViewProps.value.c_str()];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Update inputStyle props
|
|
54
|
+
const auto &style = newViewProps.inputStyle;
|
|
55
|
+
const auto &oldStyle = oldViewProps.inputStyle;
|
|
56
|
+
|
|
57
|
+
if (oldStyle.color != style.color) {
|
|
58
|
+
_inputView.textColor = RCTUIColorFromSharedColor(style.color);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (oldStyle.fontSize != style.fontSize) {
|
|
62
|
+
_inputView.fontSize = style.fontSize;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (oldStyle.height != style.height) {
|
|
66
|
+
_inputView.inputHeight = style.height;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (oldStyle.paddingTop != style.paddingTop ||
|
|
70
|
+
oldStyle.paddingBottom != style.paddingBottom ||
|
|
71
|
+
oldStyle.paddingLeft != style.paddingLeft ||
|
|
72
|
+
oldStyle.paddingRight != style.paddingRight) {
|
|
73
|
+
_inputView.padding = UIEdgeInsetsMake(style.paddingTop, style.paddingLeft, style.paddingBottom, style.paddingRight);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (oldStyle.borderWidth != style.borderWidth) {
|
|
77
|
+
_inputView.borderWidth = style.borderWidth;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (oldStyle.borderRadius != style.borderRadius) {
|
|
81
|
+
_inputView.borderRadius = style.borderRadius;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (oldStyle.borderColor != style.borderColor) {
|
|
85
|
+
_inputView.borderColor = RCTUIColorFromSharedColor(style.borderColor);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
[super updateProps:props oldProps:oldProps];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
|
|
92
|
+
{
|
|
93
|
+
if ([commandName isEqualToString:@"focus"]) {
|
|
94
|
+
NSLog(@"[ControlledInputView] handleCommand focus");
|
|
95
|
+
[_inputView focus];
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if ([commandName isEqualToString:@"blur"]) {
|
|
100
|
+
NSLog(@"[ControlledInputView] handleCommand blur");
|
|
101
|
+
[_inputView blur];
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
[super handleCommand:commandName args:args];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
@objc(RNControlledInput)
|
|
4
|
+
public class RNControlledInput: UIView {
|
|
5
|
+
|
|
6
|
+
@objc public var value: String? {
|
|
7
|
+
didSet {
|
|
8
|
+
// Update UI
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@objc public var textColor: UIColor?
|
|
13
|
+
@objc public var fontSize: CGFloat = 16
|
|
14
|
+
@objc public var inputHeight: CGFloat = 0
|
|
15
|
+
@objc public var padding: UIEdgeInsets = .zero
|
|
16
|
+
@objc public var borderWidth: CGFloat = 0
|
|
17
|
+
@objc public var borderRadius: CGFloat = 0
|
|
18
|
+
@objc public var borderColor: UIColor?
|
|
19
|
+
|
|
20
|
+
public override var canBecomeFirstResponder: Bool {
|
|
21
|
+
true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@objc public func focus() {
|
|
25
|
+
print("[ControlledInputView] RNControlledInput.focus()")
|
|
26
|
+
_ = becomeFirstResponder()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@objc public func blur() {
|
|
30
|
+
print("[ControlledInputView] RNControlledInput.blur()")
|
|
31
|
+
_ = resignFirstResponder()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@objc public override init(frame: CGRect) {
|
|
35
|
+
super.init(frame: frame)
|
|
36
|
+
self.backgroundColor = .red
|
|
37
|
+
self.translatesAutoresizingMaskIntoConstraints = false
|
|
38
|
+
|
|
39
|
+
NSLayoutConstraint.activate([
|
|
40
|
+
self.widthAnchor.constraint(equalToConstant: 100),
|
|
41
|
+
self.heightAnchor.constraint(equalToConstant: 100)
|
|
42
|
+
])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
required init?(coder: NSCoder) {
|
|
46
|
+
fatalError("init(coder:) has not been implemented")
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
codegenNativeComponent,
|
|
3
|
+
type ViewProps,
|
|
4
|
+
type ColorValue,
|
|
5
|
+
type HostComponent,
|
|
6
|
+
} from 'react-native';
|
|
7
|
+
import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';
|
|
8
|
+
import type {
|
|
9
|
+
BubblingEventHandler,
|
|
10
|
+
Double,
|
|
11
|
+
} from 'react-native/Libraries/Types/CodegenTypes';
|
|
12
|
+
|
|
13
|
+
interface TextChangeEvent {
|
|
14
|
+
value: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface FocusEvent {
|
|
18
|
+
// Empty event
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface BlurEvent {
|
|
22
|
+
// Empty event
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface InputStyle {
|
|
26
|
+
color?: ColorValue;
|
|
27
|
+
fontSize?: Double;
|
|
28
|
+
height?: Double;
|
|
29
|
+
paddingTop?: Double;
|
|
30
|
+
paddingBottom?: Double;
|
|
31
|
+
paddingLeft?: Double;
|
|
32
|
+
paddingRight?: Double;
|
|
33
|
+
borderWidth?: Double;
|
|
34
|
+
borderRadius?: Double;
|
|
35
|
+
borderColor?: ColorValue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface NativeProps extends ViewProps {
|
|
39
|
+
value?: string;
|
|
40
|
+
inputStyle?: InputStyle;
|
|
41
|
+
onTextChange?: BubblingEventHandler<Readonly<TextChangeEvent>>;
|
|
42
|
+
onFocus?: BubblingEventHandler<Readonly<FocusEvent>>;
|
|
43
|
+
onBlur?: BubblingEventHandler<Readonly<BlurEvent>>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface NativeCommands {
|
|
47
|
+
focus: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
|
|
48
|
+
blur: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
|
|
52
|
+
supportedCommands: ['focus', 'blur'],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export default codegenNativeComponent<NativeProps>('ControlledInputView');
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
|
4
|
+
import { Platform } from 'react-native';
|
|
5
|
+
import ControlledInputViewNativeComponent, { Commands } from './ControlledInputViewNativeComponent';
|
|
6
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
7
|
+
export const ControlledInputView = /*#__PURE__*/forwardRef((props, ref) => {
|
|
8
|
+
const nativeRef = useRef(null);
|
|
9
|
+
useImperativeHandle(ref, () => ({
|
|
10
|
+
blur: () => {
|
|
11
|
+
if (!nativeRef.current) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
15
|
+
console.log(`[ControlledInputView] ${Platform.OS} blur command -> native`);
|
|
16
|
+
Commands.blur(nativeRef.current);
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
focus: () => {
|
|
20
|
+
if (!nativeRef.current) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
24
|
+
console.log(`[ControlledInputView] ${Platform.OS} focus command -> native`);
|
|
25
|
+
Commands.focus(nativeRef.current);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}));
|
|
29
|
+
return /*#__PURE__*/_jsx(ControlledInputViewNativeComponent, {
|
|
30
|
+
...props,
|
|
31
|
+
ref: nativeRef
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
export * from './ControlledInputViewNativeComponent';
|
|
35
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["forwardRef","useImperativeHandle","useRef","Platform","ControlledInputViewNativeComponent","Commands","jsx","_jsx","ControlledInputView","props","ref","nativeRef","blur","current","OS","console","log","focus"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SACEA,UAAU,EACVC,mBAAmB,EACnBC,MAAM,QAGD,OAAO;AACd,SAASC,QAAQ,QAAQ,cAAc;AACvC,OAAOC,kCAAkC,IACvCC,QAAQ,QACH,sCAAsC;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAS9C,OAAO,MAAMC,mBAAmB,gBAAGR,UAAU,CAG3C,CAACS,KAAK,EAAEC,GAAG,KAAK;EAChB,MAAMC,SAAS,GACbT,MAAM,CAAwD,IAAI,CAAC;EAErED,mBAAmB,CAACS,GAAG,EAAE,OAAO;IAC9BE,IAAI,EAAEA,CAAA,KAAM;MACV,IAAI,CAACD,SAAS,CAACE,OAAO,EAAE;QACtB;MACF;MAEA,IAAIV,QAAQ,CAACW,EAAE,KAAK,KAAK,IAAIX,QAAQ,CAACW,EAAE,KAAK,SAAS,EAAE;QACtDC,OAAO,CAACC,GAAG,CACT,yBAAyBb,QAAQ,CAACW,EAAE,yBACtC,CAAC;QACDT,QAAQ,CAACO,IAAI,CAACD,SAAS,CAACE,OAAO,CAAC;MAClC;IACF,CAAC;IACDI,KAAK,EAAEA,CAAA,KAAM;MACX,IAAI,CAACN,SAAS,CAACE,OAAO,EAAE;QACtB;MACF;MAEA,IAAIV,QAAQ,CAACW,EAAE,KAAK,KAAK,IAAIX,QAAQ,CAACW,EAAE,KAAK,SAAS,EAAE;QACtDC,OAAO,CAACC,GAAG,CACT,yBAAyBb,QAAQ,CAACW,EAAE,0BACtC,CAAC;QACDT,QAAQ,CAACY,KAAK,CAACN,SAAS,CAACE,OAAO,CAAC;MACnC;IACF;EACF,CAAC,CAAC,CAAC;EAEH,oBACEN,IAAA,CAACH,kCAAkC;IAAA,GAAKK,KAAK;IAAEC,GAAG,EAAEC;EAAiB,CAAE,CAAC;AAE5E,CAAC,CAAC;AAEF,cAAc,sCAAsC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["react-native-codegen.d.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ViewProps, type ColorValue, type HostComponent } from 'react-native';
|
|
2
|
+
import type { BubblingEventHandler, Double } from 'react-native/Libraries/Types/CodegenTypes';
|
|
3
|
+
interface TextChangeEvent {
|
|
4
|
+
value: string;
|
|
5
|
+
}
|
|
6
|
+
interface FocusEvent {
|
|
7
|
+
}
|
|
8
|
+
interface BlurEvent {
|
|
9
|
+
}
|
|
10
|
+
interface InputStyle {
|
|
11
|
+
color?: ColorValue;
|
|
12
|
+
fontSize?: Double;
|
|
13
|
+
height?: Double;
|
|
14
|
+
paddingTop?: Double;
|
|
15
|
+
paddingBottom?: Double;
|
|
16
|
+
paddingLeft?: Double;
|
|
17
|
+
paddingRight?: Double;
|
|
18
|
+
borderWidth?: Double;
|
|
19
|
+
borderRadius?: Double;
|
|
20
|
+
borderColor?: ColorValue;
|
|
21
|
+
}
|
|
22
|
+
export interface NativeProps extends ViewProps {
|
|
23
|
+
value?: string;
|
|
24
|
+
inputStyle?: InputStyle;
|
|
25
|
+
onTextChange?: BubblingEventHandler<Readonly<TextChangeEvent>>;
|
|
26
|
+
onFocus?: BubblingEventHandler<Readonly<FocusEvent>>;
|
|
27
|
+
onBlur?: BubblingEventHandler<Readonly<BlurEvent>>;
|
|
28
|
+
}
|
|
29
|
+
export interface NativeCommands {
|
|
30
|
+
focus: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
|
|
31
|
+
blur: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
|
|
32
|
+
}
|
|
33
|
+
export declare const Commands: NativeCommands;
|
|
34
|
+
declare const _default: import("react-native/types_generated/Libraries/Utilities/codegenNativeComponent").NativeComponentType<NativeProps>;
|
|
35
|
+
export default _default;
|
|
36
|
+
//# sourceMappingURL=ControlledInputViewNativeComponent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ControlledInputViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/ControlledInputViewNativeComponent.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EACV,oBAAoB,EACpB,MAAM,EACP,MAAM,2CAA2C,CAAC;AAEnD,UAAU,eAAe;IACvB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,UAAU;CAEnB;AAED,UAAU,SAAS;CAElB;AAED,UAAU,UAAU;IAClB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,UAAU,CAAC;CAC1B;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,oBAAoB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC;IAC/D,OAAO,CAAC,EAAE,oBAAoB,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;IACrD,MAAM,CAAC,EAAE,oBAAoB,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,KAAK,IAAI,CAAC;IACvE,IAAI,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,KAAK,IAAI,CAAC;CACvE;AAED,eAAO,MAAM,QAAQ,EAAE,cAErB,CAAC;;AAEH,wBAA0E"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ControlledInputViewRef {
|
|
2
|
+
blur: () => void;
|
|
3
|
+
focus: () => void;
|
|
4
|
+
}
|
|
5
|
+
export declare const ControlledInputView: import("react").ForwardRefExoticComponent<Omit<Omit<import("./ControlledInputViewNativeComponent").NativeProps, "ref"> & {
|
|
6
|
+
ref?: React.Ref<import("react-native").HostInstance>;
|
|
7
|
+
}, "ref"> & import("react").RefAttributes<ControlledInputViewRef>>;
|
|
8
|
+
export * from './ControlledInputViewNativeComponent';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,eAAO,MAAM,mBAAmB;;kEAqC9B,CAAC;AAEH,cAAc,sCAAsC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-controlled-input",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "React Native Controlled Inputative Controlled Input",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"ios",
|
|
20
|
+
"cpp",
|
|
21
|
+
"*.podspec",
|
|
22
|
+
"react-native.config.js",
|
|
23
|
+
"!ios/build",
|
|
24
|
+
"!android/build",
|
|
25
|
+
"!android/gradle",
|
|
26
|
+
"!android/gradlew",
|
|
27
|
+
"!android/gradlew.bat",
|
|
28
|
+
"!android/local.properties",
|
|
29
|
+
"!**/__tests__",
|
|
30
|
+
"!**/__fixtures__",
|
|
31
|
+
"!**/__mocks__",
|
|
32
|
+
"!**/.*"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"example": "yarn workspace react-native-controlled-input-example",
|
|
36
|
+
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
37
|
+
"prepare": "bob build",
|
|
38
|
+
"typecheck": "tsc",
|
|
39
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
40
|
+
"test": "jest",
|
|
41
|
+
"release": "release-it --only-version"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react-native",
|
|
45
|
+
"ios",
|
|
46
|
+
"android"
|
|
47
|
+
],
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/veliseev93/react-native-controlled-input.git"
|
|
51
|
+
},
|
|
52
|
+
"author": "veliseev <veliseev@ronasit.com> (https://github.com/veliseev93)",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/veliseev93/react-native-controlled-input/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/veliseev93/react-native-controlled-input#readme",
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"registry": "https://registry.npmjs.org/"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
63
|
+
"@eslint/compat": "^1.3.2",
|
|
64
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
65
|
+
"@eslint/js": "^9.35.0",
|
|
66
|
+
"@react-native/babel-preset": "0.83.0",
|
|
67
|
+
"@react-native/eslint-config": "0.83.0",
|
|
68
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
69
|
+
"@types/jest": "^29.5.14",
|
|
70
|
+
"@types/react": "^19.2.0",
|
|
71
|
+
"commitlint": "^19.8.1",
|
|
72
|
+
"del-cli": "^6.0.0",
|
|
73
|
+
"eslint": "^9.35.0",
|
|
74
|
+
"eslint-config-prettier": "^10.1.8",
|
|
75
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
76
|
+
"jest": "^29.7.0",
|
|
77
|
+
"lefthook": "^2.0.3",
|
|
78
|
+
"prettier": "^2.8.8",
|
|
79
|
+
"react": "19.2.0",
|
|
80
|
+
"react-native": "0.83.0",
|
|
81
|
+
"react-native-builder-bob": "^0.40.17",
|
|
82
|
+
"release-it": "^19.0.4",
|
|
83
|
+
"turbo": "^2.5.6",
|
|
84
|
+
"typescript": "^5.9.2"
|
|
85
|
+
},
|
|
86
|
+
"peerDependencies": {
|
|
87
|
+
"react": "*",
|
|
88
|
+
"react-native": "*"
|
|
89
|
+
},
|
|
90
|
+
"workspaces": [
|
|
91
|
+
"example"
|
|
92
|
+
],
|
|
93
|
+
"packageManager": "yarn@4.11.0",
|
|
94
|
+
"react-native-builder-bob": {
|
|
95
|
+
"source": "src",
|
|
96
|
+
"output": "lib",
|
|
97
|
+
"targets": [
|
|
98
|
+
[
|
|
99
|
+
"module",
|
|
100
|
+
{
|
|
101
|
+
"esm": true
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
"typescript",
|
|
106
|
+
{
|
|
107
|
+
"project": "tsconfig.build.json"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
"codegenConfig": {
|
|
113
|
+
"name": "ControlledInputViewSpec",
|
|
114
|
+
"type": "all",
|
|
115
|
+
"jsSrcsDir": "src",
|
|
116
|
+
"android": {
|
|
117
|
+
"javaPackageName": "com.controlledinput"
|
|
118
|
+
},
|
|
119
|
+
"ios": {
|
|
120
|
+
"componentProvider": {
|
|
121
|
+
"ControlledInputView": "ControlledInputView"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"prettier": {
|
|
126
|
+
"quoteProps": "consistent",
|
|
127
|
+
"singleQuote": true,
|
|
128
|
+
"tabWidth": 2,
|
|
129
|
+
"trailingComma": "es5",
|
|
130
|
+
"useTabs": false
|
|
131
|
+
},
|
|
132
|
+
"jest": {
|
|
133
|
+
"preset": "react-native",
|
|
134
|
+
"modulePathIgnorePatterns": [
|
|
135
|
+
"<rootDir>/example/node_modules",
|
|
136
|
+
"<rootDir>/lib/"
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
"commitlint": {
|
|
140
|
+
"extends": [
|
|
141
|
+
"@commitlint/config-conventional"
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
"release-it": {
|
|
145
|
+
"git": {
|
|
146
|
+
"commitMessage": "chore: release ${version}",
|
|
147
|
+
"tagName": "v${version}"
|
|
148
|
+
},
|
|
149
|
+
"npm": {
|
|
150
|
+
"publish": true
|
|
151
|
+
},
|
|
152
|
+
"github": {
|
|
153
|
+
"release": true
|
|
154
|
+
},
|
|
155
|
+
"plugins": {
|
|
156
|
+
"@release-it/conventional-changelog": {
|
|
157
|
+
"preset": {
|
|
158
|
+
"name": "angular"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
"create-react-native-library": {
|
|
164
|
+
"type": "fabric-view",
|
|
165
|
+
"languages": "kotlin-objc",
|
|
166
|
+
"tools": [
|
|
167
|
+
"eslint",
|
|
168
|
+
"jest",
|
|
169
|
+
"lefthook",
|
|
170
|
+
"release-it"
|
|
171
|
+
],
|
|
172
|
+
"version": "0.56.1"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
codegenNativeComponent,
|
|
3
|
+
type ViewProps,
|
|
4
|
+
type ColorValue,
|
|
5
|
+
type HostComponent,
|
|
6
|
+
} from 'react-native';
|
|
7
|
+
import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';
|
|
8
|
+
import type {
|
|
9
|
+
BubblingEventHandler,
|
|
10
|
+
Double,
|
|
11
|
+
} from 'react-native/Libraries/Types/CodegenTypes';
|
|
12
|
+
|
|
13
|
+
interface TextChangeEvent {
|
|
14
|
+
value: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface FocusEvent {
|
|
18
|
+
// Empty event
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface BlurEvent {
|
|
22
|
+
// Empty event
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface InputStyle {
|
|
26
|
+
color?: ColorValue;
|
|
27
|
+
fontSize?: Double;
|
|
28
|
+
height?: Double;
|
|
29
|
+
paddingTop?: Double;
|
|
30
|
+
paddingBottom?: Double;
|
|
31
|
+
paddingLeft?: Double;
|
|
32
|
+
paddingRight?: Double;
|
|
33
|
+
borderWidth?: Double;
|
|
34
|
+
borderRadius?: Double;
|
|
35
|
+
borderColor?: ColorValue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface NativeProps extends ViewProps {
|
|
39
|
+
value?: string;
|
|
40
|
+
inputStyle?: InputStyle;
|
|
41
|
+
onTextChange?: BubblingEventHandler<Readonly<TextChangeEvent>>;
|
|
42
|
+
onFocus?: BubblingEventHandler<Readonly<FocusEvent>>;
|
|
43
|
+
onBlur?: BubblingEventHandler<Readonly<BlurEvent>>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface NativeCommands {
|
|
47
|
+
focus: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
|
|
48
|
+
blur: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
|
|
52
|
+
supportedCommands: ['focus', 'blur'],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export default codegenNativeComponent<NativeProps>('ControlledInputView');
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useImperativeHandle,
|
|
4
|
+
useRef,
|
|
5
|
+
type ComponentProps,
|
|
6
|
+
type ElementRef,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { Platform } from 'react-native';
|
|
9
|
+
import ControlledInputViewNativeComponent, {
|
|
10
|
+
Commands,
|
|
11
|
+
} from './ControlledInputViewNativeComponent';
|
|
12
|
+
|
|
13
|
+
type NativeProps = ComponentProps<typeof ControlledInputViewNativeComponent>;
|
|
14
|
+
|
|
15
|
+
export interface ControlledInputViewRef {
|
|
16
|
+
blur: () => void;
|
|
17
|
+
focus: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ControlledInputView = forwardRef<
|
|
21
|
+
ControlledInputViewRef,
|
|
22
|
+
NativeProps
|
|
23
|
+
>((props, ref) => {
|
|
24
|
+
const nativeRef =
|
|
25
|
+
useRef<ElementRef<typeof ControlledInputViewNativeComponent>>(null);
|
|
26
|
+
|
|
27
|
+
useImperativeHandle(ref, () => ({
|
|
28
|
+
blur: () => {
|
|
29
|
+
if (!nativeRef.current) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
34
|
+
console.log(
|
|
35
|
+
`[ControlledInputView] ${Platform.OS} blur command -> native`
|
|
36
|
+
);
|
|
37
|
+
Commands.blur(nativeRef.current);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
focus: () => {
|
|
41
|
+
if (!nativeRef.current) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
46
|
+
console.log(
|
|
47
|
+
`[ControlledInputView] ${Platform.OS} focus command -> native`
|
|
48
|
+
);
|
|
49
|
+
Commands.focus(nativeRef.current);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<ControlledInputViewNativeComponent {...props} ref={nativeRef as any} />
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export * from './ControlledInputViewNativeComponent';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare module 'react-native/Libraries/Types/CodegenTypes' {
|
|
2
|
+
export type Double = number;
|
|
3
|
+
export type Float = number;
|
|
4
|
+
export type Int32 = number;
|
|
5
|
+
export type UnsafeObject = object;
|
|
6
|
+
|
|
7
|
+
export interface BubblingEventHandler<T> {
|
|
8
|
+
(event: { nativeEvent: T }): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DirectEventHandler<T> {
|
|
12
|
+
(event: { nativeEvent: T }): void;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module 'react-native/Libraries/Utilities/codegenNativeComponent' {
|
|
17
|
+
import type { HostComponent } from 'react-native';
|
|
18
|
+
export default function codegenNativeComponent<T>(
|
|
19
|
+
componentName: string
|
|
20
|
+
): HostComponent<T>;
|
|
21
|
+
}
|