@rn-tools/core 3.0.1
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/CHANGELOG.md +7 -0
- package/README.md +3 -0
- package/android/build.gradle +47 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/rntoolscore/RNToolsCoreModule.kt +118 -0
- package/android/src/main/java/expo/modules/rntoolscore/SafeAreaUtils.kt +87 -0
- package/expo-module.config.json +17 -0
- package/ios/RNToolsCore.podspec +30 -0
- package/ios/RNToolsCoreModule.swift +110 -0
- package/mocks/react-native.mock.ts +21 -0
- package/mocks/render-node-probe.tsx +50 -0
- package/mocks/setup.ts +6 -0
- package/package.json +37 -0
- package/src/index.ts +4 -0
- package/src/keyboard.ts +43 -0
- package/src/render-tree.test.tsx +483 -0
- package/src/render-tree.tsx +580 -0
- package/src/safe-area.ts +83 -0
- package/src/store.tsx +82 -0
- package/tsconfig.json +10 -0
- package/vitest.config.mts +21 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
|
|
3
|
+
group = 'expo.modules.rntoolscore'
|
|
4
|
+
version = '0.1.0'
|
|
5
|
+
|
|
6
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
+
apply from: expoModulesCorePlugin
|
|
8
|
+
applyKotlinExpoModulesCorePlugin()
|
|
9
|
+
useCoreDependencies()
|
|
10
|
+
useExpoPublishing()
|
|
11
|
+
|
|
12
|
+
def useManagedAndroidSdkVersions = false
|
|
13
|
+
if (useManagedAndroidSdkVersions) {
|
|
14
|
+
useDefaultAndroidSdkVersions()
|
|
15
|
+
} else {
|
|
16
|
+
buildscript {
|
|
17
|
+
ext.safeExtGet = { prop, fallback ->
|
|
18
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
project.android {
|
|
22
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
23
|
+
defaultConfig {
|
|
24
|
+
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
25
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
android {
|
|
31
|
+
namespace "expo.modules.rntoolscore"
|
|
32
|
+
defaultConfig {
|
|
33
|
+
versionCode 1
|
|
34
|
+
versionName "0.1.0"
|
|
35
|
+
}
|
|
36
|
+
lintOptions {
|
|
37
|
+
abortOnError false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
repositories {
|
|
41
|
+
mavenCentral()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
dependencies {
|
|
45
|
+
implementation 'com.facebook.react:react-native:+'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
package expo.modules.rntoolscore
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import android.view.ViewGroup
|
|
5
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
6
|
+
import expo.modules.kotlin.modules.Module
|
|
7
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
8
|
+
import java.util.concurrent.CountDownLatch
|
|
9
|
+
import java.util.concurrent.TimeUnit
|
|
10
|
+
|
|
11
|
+
class RNToolsCoreModule : Module() {
|
|
12
|
+
private var lastInsets: EdgeInsets? = null
|
|
13
|
+
private var attachedView: View? = null
|
|
14
|
+
private var insetsListener: View.OnApplyWindowInsetsListener? = null
|
|
15
|
+
|
|
16
|
+
override fun definition() = ModuleDefinition {
|
|
17
|
+
Name("RNToolsCore")
|
|
18
|
+
|
|
19
|
+
Events("onSafeAreaInsetsChange")
|
|
20
|
+
|
|
21
|
+
Function("getSafeAreaInsets") {
|
|
22
|
+
val fallback = mapOf(
|
|
23
|
+
"top" to 0f,
|
|
24
|
+
"right" to 0f,
|
|
25
|
+
"bottom" to 0f,
|
|
26
|
+
"left" to 0f
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
val activity = appContext.currentActivity ?: return@Function fallback
|
|
30
|
+
val decorView = activity.window?.decorView as? ViewGroup ?: return@Function fallback
|
|
31
|
+
|
|
32
|
+
if (UiThreadUtil.isOnUiThread()) {
|
|
33
|
+
val insets = getSafeAreaInsets(decorView) ?: return@Function fallback
|
|
34
|
+
return@Function insetsToMap(insets)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
val latch = CountDownLatch(1)
|
|
38
|
+
var result: Map<String, Float> = fallback
|
|
39
|
+
|
|
40
|
+
UiThreadUtil.runOnUiThread {
|
|
41
|
+
val insets = getSafeAreaInsets(decorView)
|
|
42
|
+
result = if (insets == null) fallback else insetsToMap(insets)
|
|
43
|
+
latch.countDown()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
latch.await(200, TimeUnit.MILLISECONDS)
|
|
47
|
+
return@Function result
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
OnCreate {
|
|
51
|
+
attachInsetsListener()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
OnDestroy {
|
|
55
|
+
detachInsetsListener()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
OnActivityEntersForeground {
|
|
59
|
+
attachInsetsListener()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private fun attachInsetsListener() {
|
|
64
|
+
UiThreadUtil.runOnUiThread {
|
|
65
|
+
val activity = appContext.currentActivity ?: return@runOnUiThread
|
|
66
|
+
val decorView = activity.window?.decorView as? ViewGroup ?: return@runOnUiThread
|
|
67
|
+
|
|
68
|
+
attachedView = decorView
|
|
69
|
+
insetsListener = View.OnApplyWindowInsetsListener { view, insets ->
|
|
70
|
+
emitSafeAreaInsetsIfChanged(view)
|
|
71
|
+
view.onApplyWindowInsets(insets)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
decorView.setOnApplyWindowInsetsListener(insetsListener)
|
|
75
|
+
decorView.requestApplyInsets()
|
|
76
|
+
emitSafeAreaInsetsIfChanged(decorView)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private fun detachInsetsListener() {
|
|
81
|
+
UiThreadUtil.runOnUiThread {
|
|
82
|
+
attachedView?.setOnApplyWindowInsetsListener(null)
|
|
83
|
+
attachedView = null
|
|
84
|
+
insetsListener = null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private fun emitSafeAreaInsetsIfChanged(view: View?) {
|
|
89
|
+
val target = view ?: return
|
|
90
|
+
val insets = getSafeAreaInsets(target) ?: return
|
|
91
|
+
|
|
92
|
+
if (lastInsets != null && areInsetsEqual(lastInsets!!, insets)) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
lastInsets = insets
|
|
97
|
+
sendEvent(
|
|
98
|
+
"onSafeAreaInsetsChange",
|
|
99
|
+
mapOf("insets" to insetsToMap(insets))
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fun areInsetsEqual(lhs: EdgeInsets, rhs: EdgeInsets): Boolean {
|
|
104
|
+
return lhs.top == rhs.top &&
|
|
105
|
+
lhs.right == rhs.right &&
|
|
106
|
+
lhs.bottom == rhs.bottom &&
|
|
107
|
+
lhs.left == rhs.left
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private fun insetsToMap(insets: EdgeInsets): Map<String, Float> {
|
|
111
|
+
return mapOf(
|
|
112
|
+
"top" to insets.top,
|
|
113
|
+
"right" to insets.right,
|
|
114
|
+
"bottom" to insets.bottom,
|
|
115
|
+
"left" to insets.left
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
package expo.modules.rntoolscore
|
|
2
|
+
|
|
3
|
+
import android.graphics.Rect
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import android.view.View
|
|
6
|
+
import android.view.WindowInsets
|
|
7
|
+
import androidx.annotation.RequiresApi
|
|
8
|
+
import kotlin.math.max
|
|
9
|
+
import kotlin.math.min
|
|
10
|
+
|
|
11
|
+
data class EdgeInsets(
|
|
12
|
+
val top: Float,
|
|
13
|
+
val right: Float,
|
|
14
|
+
val bottom: Float,
|
|
15
|
+
val left: Float
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
@RequiresApi(Build.VERSION_CODES.R)
|
|
19
|
+
private fun getRootWindowInsetsCompatR(rootView: View): EdgeInsets? {
|
|
20
|
+
val insets =
|
|
21
|
+
rootView.rootWindowInsets?.getInsets(
|
|
22
|
+
WindowInsets.Type.statusBars() or
|
|
23
|
+
WindowInsets.Type.displayCutout() or
|
|
24
|
+
WindowInsets.Type.navigationBars() or
|
|
25
|
+
WindowInsets.Type.captionBar()
|
|
26
|
+
) ?: return null
|
|
27
|
+
|
|
28
|
+
return EdgeInsets(
|
|
29
|
+
top = insets.top.toFloat(),
|
|
30
|
+
right = insets.right.toFloat(),
|
|
31
|
+
bottom = insets.bottom.toFloat(),
|
|
32
|
+
left = insets.left.toFloat()
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@RequiresApi(Build.VERSION_CODES.M)
|
|
37
|
+
@Suppress("DEPRECATION")
|
|
38
|
+
private fun getRootWindowInsetsCompatM(rootView: View): EdgeInsets? {
|
|
39
|
+
val insets = rootView.rootWindowInsets ?: return null
|
|
40
|
+
return EdgeInsets(
|
|
41
|
+
top = insets.systemWindowInsetTop.toFloat(),
|
|
42
|
+
right = insets.systemWindowInsetRight.toFloat(),
|
|
43
|
+
// Use the min to avoid including the keyboard while still honoring nav bars.
|
|
44
|
+
bottom = min(insets.systemWindowInsetBottom, insets.stableInsetBottom).toFloat(),
|
|
45
|
+
left = insets.systemWindowInsetLeft.toFloat()
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private fun getRootWindowInsetsCompatBase(rootView: View): EdgeInsets? {
|
|
50
|
+
val visibleRect = Rect()
|
|
51
|
+
rootView.getWindowVisibleDisplayFrame(visibleRect)
|
|
52
|
+
return EdgeInsets(
|
|
53
|
+
top = visibleRect.top.toFloat(),
|
|
54
|
+
right = (rootView.width - visibleRect.right).toFloat(),
|
|
55
|
+
bottom = (rootView.height - visibleRect.bottom).toFloat(),
|
|
56
|
+
left = visibleRect.left.toFloat()
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private fun getRootWindowInsetsCompat(rootView: View): EdgeInsets? {
|
|
61
|
+
return when {
|
|
62
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> getRootWindowInsetsCompatR(rootView)
|
|
63
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> getRootWindowInsetsCompatM(rootView)
|
|
64
|
+
else -> getRootWindowInsetsCompatBase(rootView)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fun getSafeAreaInsets(view: View): EdgeInsets? {
|
|
69
|
+
if (view.height == 0) {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
val rootView = view.rootView
|
|
74
|
+
val windowInsets = getRootWindowInsetsCompat(rootView) ?: return null
|
|
75
|
+
|
|
76
|
+
val windowWidth = rootView.width.toFloat()
|
|
77
|
+
val windowHeight = rootView.height.toFloat()
|
|
78
|
+
val visibleRect = Rect()
|
|
79
|
+
view.getGlobalVisibleRect(visibleRect)
|
|
80
|
+
|
|
81
|
+
return EdgeInsets(
|
|
82
|
+
top = max(windowInsets.top - visibleRect.top, 0f),
|
|
83
|
+
right = max(min(visibleRect.left + view.width - windowWidth, 0f) + windowInsets.right, 0f),
|
|
84
|
+
bottom = max(min(visibleRect.top + view.height - windowHeight, 0f) + windowInsets.bottom, 0f),
|
|
85
|
+
left = max(windowInsets.left - visibleRect.left, 0f)
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
ENV['RCT_NEW_ARCH_ENABLED'] ||= '1'
|
|
4
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
5
|
+
new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
|
|
6
|
+
|
|
7
|
+
Pod::Spec.new do |s|
|
|
8
|
+
s.name = 'RNToolsCore'
|
|
9
|
+
s.version = package['version']
|
|
10
|
+
s.summary = package['description']
|
|
11
|
+
s.description = package['description']
|
|
12
|
+
s.license = package['license']
|
|
13
|
+
s.author = package['author']
|
|
14
|
+
s.homepage = package['homepage']
|
|
15
|
+
s.platforms = {
|
|
16
|
+
:ios => '16.0',
|
|
17
|
+
:tvos => '16.0'
|
|
18
|
+
}
|
|
19
|
+
s.swift_version = '5.4'
|
|
20
|
+
s.source = { git: 'https://github.com/ajsmth/rn-tools' }
|
|
21
|
+
s.static_framework = true
|
|
22
|
+
|
|
23
|
+
s.dependency 'ExpoModulesCore'
|
|
24
|
+
s.pod_target_xcconfig = {
|
|
25
|
+
'DEFINES_MODULE' => 'YES',
|
|
26
|
+
'OTHER_SWIFT_FLAGS' => '$(inherited)'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
30
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
public class RNToolsCoreModule: Module {
|
|
5
|
+
private var lastInsets: UIEdgeInsets?
|
|
6
|
+
private var observers: [NSObjectProtocol] = []
|
|
7
|
+
|
|
8
|
+
public func definition() -> ModuleDefinition {
|
|
9
|
+
Name("RNToolsCore")
|
|
10
|
+
|
|
11
|
+
Events("onSafeAreaInsetsChange")
|
|
12
|
+
|
|
13
|
+
Function("getSafeAreaInsets") { () -> [String: CGFloat] in
|
|
14
|
+
if Thread.isMainThread {
|
|
15
|
+
return self.safeAreaInsetsDictionary()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var result = self.safeAreaInsetsDictionary()
|
|
19
|
+
DispatchQueue.main.sync {
|
|
20
|
+
result = self.safeAreaInsetsDictionary()
|
|
21
|
+
}
|
|
22
|
+
return result
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
OnCreate {
|
|
26
|
+
self.startObservingSafeArea()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
OnDestroy {
|
|
30
|
+
self.stopObservingSafeArea()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
OnAppBecomesActive {
|
|
34
|
+
self.emitSafeAreaInsetsIfChanged()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private func startObservingSafeArea() {
|
|
39
|
+
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
|
40
|
+
let center = NotificationCenter.default
|
|
41
|
+
let names: [NSNotification.Name] = [
|
|
42
|
+
UIApplication.didBecomeActiveNotification,
|
|
43
|
+
UIApplication.willEnterForegroundNotification,
|
|
44
|
+
UIDevice.orientationDidChangeNotification,
|
|
45
|
+
UIApplication.didChangeStatusBarFrameNotification,
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
observers = names.map { name in
|
|
49
|
+
center.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
|
|
50
|
+
self?.emitSafeAreaInsetsIfChanged()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
emitSafeAreaInsetsIfChanged()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private func stopObservingSafeArea() {
|
|
58
|
+
let center = NotificationCenter.default
|
|
59
|
+
observers.forEach { center.removeObserver($0) }
|
|
60
|
+
observers.removeAll()
|
|
61
|
+
UIDevice.current.endGeneratingDeviceOrientationNotifications()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func emitSafeAreaInsetsIfChanged() {
|
|
65
|
+
let send = { [weak self] in
|
|
66
|
+
guard let self else { return }
|
|
67
|
+
let windowInsets = self.keyWindow()?.safeAreaInsets ?? .zero
|
|
68
|
+
if let last = self.lastInsets, self.areInsetsEqual(last, windowInsets) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
self.lastInsets = windowInsets
|
|
72
|
+
self.sendEvent("onSafeAreaInsetsChange", ["insets": self.safeAreaInsetsDictionary(windowInsets)])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if Thread.isMainThread {
|
|
76
|
+
send()
|
|
77
|
+
} else {
|
|
78
|
+
DispatchQueue.main.async { send() }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func safeAreaInsetsDictionary(_ insets: UIEdgeInsets? = nil) -> [String: CGFloat] {
|
|
83
|
+
let resolved = insets ?? keyWindow()?.safeAreaInsets ?? .zero
|
|
84
|
+
return [
|
|
85
|
+
"top": resolved.top,
|
|
86
|
+
"right": resolved.right,
|
|
87
|
+
"bottom": resolved.bottom,
|
|
88
|
+
"left": resolved.left,
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private func areInsetsEqual(_ lhs: UIEdgeInsets, _ rhs: UIEdgeInsets) -> Bool {
|
|
93
|
+
return lhs.top == rhs.top &&
|
|
94
|
+
lhs.right == rhs.right &&
|
|
95
|
+
lhs.bottom == rhs.bottom &&
|
|
96
|
+
lhs.left == rhs.left
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private func keyWindow() -> UIWindow? {
|
|
100
|
+
let scenes = UIApplication.shared.connectedScenes
|
|
101
|
+
.compactMap { $0 as? UIWindowScene }
|
|
102
|
+
.flatMap { $0.windows }
|
|
103
|
+
|
|
104
|
+
if let window = scenes.first(where: { $0.isKeyWindow }) {
|
|
105
|
+
return window
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return UIApplication.shared.windows.first(where: { $0.isKeyWindow })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const Platform = {
|
|
2
|
+
OS: "ios",
|
|
3
|
+
select: (options: Record<string, unknown>) => options.ios,
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export const Keyboard = {
|
|
7
|
+
addListener: () => ({ remove: () => {} }),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const StyleSheet = {
|
|
11
|
+
absoluteFill: {},
|
|
12
|
+
create: (styles: Record<string, unknown>) => styles,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function View(props: { children?: unknown }) {
|
|
16
|
+
return props.children;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Text(props: { children?: unknown }) {
|
|
20
|
+
return props.children;
|
|
21
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
type RenderNode,
|
|
4
|
+
getRenderNode,
|
|
5
|
+
getRenderNodeActive,
|
|
6
|
+
getRenderNodeDepth,
|
|
7
|
+
getRenderNodeChildren,
|
|
8
|
+
getRenderNodeParent,
|
|
9
|
+
useRenderTreeSelector,
|
|
10
|
+
} from "../src/render-tree";
|
|
11
|
+
|
|
12
|
+
export type RenderNodeProbeData = {
|
|
13
|
+
node: RenderNode;
|
|
14
|
+
type: RenderNode["type"];
|
|
15
|
+
active: boolean;
|
|
16
|
+
depth: number;
|
|
17
|
+
parent: RenderNode | null;
|
|
18
|
+
children: RenderNode[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function RenderNodeProbe(props: {
|
|
22
|
+
render: (data: RenderNodeProbeData) => React.ReactNode;
|
|
23
|
+
}) {
|
|
24
|
+
const data = useRenderTreeSelector((chart, id) => {
|
|
25
|
+
const node = getRenderNode(chart, id);
|
|
26
|
+
if (!node) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const parent = getRenderNodeParent(chart, id);
|
|
31
|
+
const children = getRenderNodeChildren(chart, id);
|
|
32
|
+
const depth = getRenderNodeDepth(chart, id);
|
|
33
|
+
const active = getRenderNodeActive(chart, id);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
node,
|
|
37
|
+
type: node.type,
|
|
38
|
+
active,
|
|
39
|
+
depth,
|
|
40
|
+
parent,
|
|
41
|
+
children,
|
|
42
|
+
} satisfies RenderNodeProbeData;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!data) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return <>{props.render(data)}</>;
|
|
50
|
+
}
|
package/mocks/setup.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rn-tools/core",
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "Core utilities for rn-tools",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"react-native",
|
|
11
|
+
"expo",
|
|
12
|
+
"core",
|
|
13
|
+
"RNToolsCore"
|
|
14
|
+
],
|
|
15
|
+
"repository": "https://github.com/ajsmth/rn-tools/tree/main/packages/core",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/ajsmth/rn-tools/issues"
|
|
18
|
+
},
|
|
19
|
+
"author": "andy <andydevs123@gmail.com> (ajsmth)",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"homepage": "https://github.com/ajsmth/rn-tools#readme",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@testing-library/react": "^14.2.1",
|
|
24
|
+
"@types/react": "18.3.12",
|
|
25
|
+
"@types/react-test-renderer": "^18.0.0",
|
|
26
|
+
"react-test-renderer": "^18.2.0",
|
|
27
|
+
"vitest": "^1.6.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"use-sync-external-store": "^1.2.2"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"expo": "*",
|
|
34
|
+
"react": "*",
|
|
35
|
+
"react-native": "*"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/index.ts
ADDED
package/src/keyboard.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Keyboard, KeyboardEvent } from "react-native";
|
|
2
|
+
import { createStore, useStore } from "./store";
|
|
3
|
+
|
|
4
|
+
export type KeyboardState = {
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const fallbackHeight = 0;
|
|
9
|
+
|
|
10
|
+
export function getKeyboardHeight(): number {
|
|
11
|
+
return fallbackHeight;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const keyboardHeightStore = createStore<KeyboardState>({
|
|
15
|
+
height: getKeyboardHeight(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const updateHeight = (height: number) => {
|
|
19
|
+
keyboardHeightStore.setState((state) => {
|
|
20
|
+
if (state.height === height) {
|
|
21
|
+
return state;
|
|
22
|
+
}
|
|
23
|
+
return { ...state, height };
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const handleShow = (event: KeyboardEvent) => {
|
|
28
|
+
const nextHeight = event.endCoordinates?.height ?? 0;
|
|
29
|
+
updateHeight(nextHeight);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleHide = () => {
|
|
33
|
+
updateHeight(0);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
Keyboard.addListener("keyboardWillShow", handleShow);
|
|
37
|
+
Keyboard.addListener("keyboardWillHide", handleHide);
|
|
38
|
+
Keyboard.addListener("keyboardDidShow", handleShow);
|
|
39
|
+
Keyboard.addListener("keyboardDidHide", handleHide);
|
|
40
|
+
|
|
41
|
+
export const useKeyboardHeight = (): number => {
|
|
42
|
+
return useStore(keyboardHeightStore, (state) => state.height);
|
|
43
|
+
};
|