expo-tvos-search 1.2.2 → 1.3.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/README.md +338 -50
- package/build/index.d.ts +115 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +54 -6
- package/ios/ExpoTvosSearchModule.swift +67 -4
- package/ios/ExpoTvosSearchView.swift +325 -61
- package/ios/MarqueeText.swift +2 -2
- package/package.json +9 -1
- package/src/__tests__/index.test.tsx +456 -1
- package/src/index.tsx +197 -9
package/build/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Platform } from "react-native";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Native view component loaded at module initialization.
|
|
5
|
+
* Returns null on non-tvOS platforms or when the native module is unavailable.
|
|
6
|
+
*/
|
|
7
7
|
let NativeView = null;
|
|
8
8
|
if (Platform.OS === "ios" && Platform.isTV) {
|
|
9
9
|
try {
|
|
@@ -11,9 +11,38 @@ if (Platform.OS === "ios" && Platform.isTV) {
|
|
|
11
11
|
if (typeof requireNativeViewManager === "function") {
|
|
12
12
|
NativeView = requireNativeViewManager("ExpoTvosSearch");
|
|
13
13
|
}
|
|
14
|
+
else {
|
|
15
|
+
console.warn("[expo-tvos-search] requireNativeViewManager is not a function. " +
|
|
16
|
+
"This usually indicates an incompatible expo-modules-core version. " +
|
|
17
|
+
"Try reinstalling expo-modules-core or updating to a compatible version.");
|
|
18
|
+
}
|
|
14
19
|
}
|
|
15
|
-
catch {
|
|
16
|
-
//
|
|
20
|
+
catch (error) {
|
|
21
|
+
// Categorize the error to help with debugging
|
|
22
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
23
|
+
if (errorMessage.includes("expo-modules-core")) {
|
|
24
|
+
console.warn("[expo-tvos-search] Failed to load expo-modules-core. " +
|
|
25
|
+
"Make sure expo-modules-core is installed: npm install expo-modules-core\n" +
|
|
26
|
+
`Error: ${errorMessage}`);
|
|
27
|
+
}
|
|
28
|
+
else if (errorMessage.includes("ExpoTvosSearch")) {
|
|
29
|
+
console.warn("[expo-tvos-search] Native module ExpoTvosSearch not found. " +
|
|
30
|
+
"This usually means:\n" +
|
|
31
|
+
"1. You haven't run 'expo prebuild' yet, or\n" +
|
|
32
|
+
"2. The native project needs to be rebuilt (try 'expo prebuild --clean')\n" +
|
|
33
|
+
"3. You're not running on a tvOS simulator/device\n" +
|
|
34
|
+
`Error: ${errorMessage}`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Unexpected error - log full details for debugging
|
|
38
|
+
console.warn("[expo-tvos-search] Unexpected error loading native module.\n" +
|
|
39
|
+
`Error: ${errorMessage}\n` +
|
|
40
|
+
"Please report this issue at: https://github.com/keiver/expo-tvos-search/issues");
|
|
41
|
+
// In development, log the full error for debugging
|
|
42
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
43
|
+
console.error("[expo-tvos-search] Full error details:", error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
17
46
|
}
|
|
18
47
|
}
|
|
19
48
|
/**
|
|
@@ -51,6 +80,25 @@ if (Platform.OS === "ios" && Platform.isTV) {
|
|
|
51
80
|
*/
|
|
52
81
|
export function TvosSearchView(props) {
|
|
53
82
|
if (!NativeView) {
|
|
83
|
+
// Warn in development when native module is unavailable
|
|
84
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
85
|
+
const isRunningOnTvOS = Platform.OS === "ios" && Platform.isTV;
|
|
86
|
+
if (isRunningOnTvOS) {
|
|
87
|
+
// On tvOS but module failed to load - this is unexpected
|
|
88
|
+
console.warn("[expo-tvos-search] TvosSearchView is rendering null on tvOS. " +
|
|
89
|
+
"This usually means:\n" +
|
|
90
|
+
"1. The native module wasn't built properly (try 'expo prebuild --clean')\n" +
|
|
91
|
+
"2. expo-modules-core is missing or incompatible\n" +
|
|
92
|
+
"3. The app needs to be restarted after installing the module\n\n" +
|
|
93
|
+
"Check the earlier console logs for specific error details.");
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Not on tvOS - expected behavior, but developer might want to know
|
|
97
|
+
console.info("[expo-tvos-search] TvosSearchView is not available on " +
|
|
98
|
+
`${Platform.OS}${Platform.isTV ? " (TV)" : ""}. ` +
|
|
99
|
+
"Use isNativeSearchAvailable() to check before rendering this component.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
54
102
|
return null;
|
|
55
103
|
}
|
|
56
104
|
return React.createElement(NativeView, { ...props });
|
|
@@ -12,17 +12,32 @@ public class ExpoTvosSearchModule: Module {
|
|
|
12
12
|
Name("ExpoTvosSearch")
|
|
13
13
|
|
|
14
14
|
View(ExpoTvosSearchView.self) {
|
|
15
|
-
Events("onSearch", "onSelectItem")
|
|
15
|
+
Events("onSearch", "onSelectItem", "onError", "onValidationWarning")
|
|
16
16
|
|
|
17
17
|
Prop("results") { (view: ExpoTvosSearchView, results: [[String: Any]]) in
|
|
18
18
|
// Limit results array size to prevent memory issues
|
|
19
19
|
let limitedResults = Array(results.prefix(Self.maxResults))
|
|
20
|
+
if results.count > Self.maxResults {
|
|
21
|
+
view.onValidationWarning([
|
|
22
|
+
"type": "value_clamped",
|
|
23
|
+
"message": "Results array truncated from \(results.count) to \(Self.maxResults) items",
|
|
24
|
+
"context": "maxResults=\(Self.maxResults)"
|
|
25
|
+
])
|
|
26
|
+
}
|
|
20
27
|
view.updateResults(limitedResults)
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
Prop("columns") { (view: ExpoTvosSearchView, columns: Int) in
|
|
24
31
|
// Clamp columns between min and max for safe grid layout
|
|
25
|
-
|
|
32
|
+
let clampedValue = min(max(Self.minColumns, columns), Self.maxColumns)
|
|
33
|
+
if clampedValue != columns {
|
|
34
|
+
view.onValidationWarning([
|
|
35
|
+
"type": "value_clamped",
|
|
36
|
+
"message": "columns value \(columns) was clamped to range [\(Self.minColumns), \(Self.maxColumns)]",
|
|
37
|
+
"context": "columns=\(clampedValue)"
|
|
38
|
+
])
|
|
39
|
+
}
|
|
40
|
+
view.columns = clampedValue
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
Prop("placeholder") { (view: ExpoTvosSearchView, placeholder: String) in
|
|
@@ -48,7 +63,15 @@ public class ExpoTvosSearchModule: Module {
|
|
|
48
63
|
|
|
49
64
|
Prop("topInset") { (view: ExpoTvosSearchView, topInset: Double) in
|
|
50
65
|
// Clamp to non-negative values (max 500 points reasonable for any screen)
|
|
51
|
-
|
|
66
|
+
let clampedValue = min(max(0, topInset), 500)
|
|
67
|
+
if clampedValue != topInset {
|
|
68
|
+
view.onValidationWarning([
|
|
69
|
+
"type": "value_clamped",
|
|
70
|
+
"message": "topInset value \(topInset) was clamped to range [0, 500]",
|
|
71
|
+
"context": "topInset=\(clampedValue)"
|
|
72
|
+
])
|
|
73
|
+
}
|
|
74
|
+
view.topInset = CGFloat(clampedValue)
|
|
52
75
|
}
|
|
53
76
|
|
|
54
77
|
Prop("showTitleOverlay") { (view: ExpoTvosSearchView, show: Bool) in
|
|
@@ -61,7 +84,15 @@ public class ExpoTvosSearchModule: Module {
|
|
|
61
84
|
|
|
62
85
|
Prop("marqueeDelay") { (view: ExpoTvosSearchView, delay: Double) in
|
|
63
86
|
// Clamp between 0 and maxMarqueeDelay seconds
|
|
64
|
-
|
|
87
|
+
let clampedValue = min(max(0, delay), Self.maxMarqueeDelay)
|
|
88
|
+
if clampedValue != delay {
|
|
89
|
+
view.onValidationWarning([
|
|
90
|
+
"type": "value_clamped",
|
|
91
|
+
"message": "marqueeDelay value \(delay) was clamped to range [0, \(Self.maxMarqueeDelay)]",
|
|
92
|
+
"context": "marqueeDelay=\(clampedValue)"
|
|
93
|
+
])
|
|
94
|
+
}
|
|
95
|
+
view.marqueeDelay = clampedValue
|
|
65
96
|
}
|
|
66
97
|
|
|
67
98
|
Prop("emptyStateText") { (view: ExpoTvosSearchView, text: String) in
|
|
@@ -79,6 +110,38 @@ public class ExpoTvosSearchModule: Module {
|
|
|
79
110
|
Prop("noResultsHintText") { (view: ExpoTvosSearchView, text: String) in
|
|
80
111
|
view.noResultsHintText = String(text.prefix(Self.maxStringLength))
|
|
81
112
|
}
|
|
113
|
+
|
|
114
|
+
Prop("textColor") { (view: ExpoTvosSearchView, colorHex: String?) in
|
|
115
|
+
view.textColor = colorHex
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
Prop("accentColor") { (view: ExpoTvosSearchView, colorHex: String) in
|
|
119
|
+
view.accentColor = colorHex
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
Prop("cardWidth") { (view: ExpoTvosSearchView, width: Double) in
|
|
123
|
+
view.cardWidth = CGFloat(max(50, min(1000, width))) // Clamp to reasonable range
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Prop("cardHeight") { (view: ExpoTvosSearchView, height: Double) in
|
|
127
|
+
view.cardHeight = CGFloat(max(50, min(1000, height))) // Clamp to reasonable range
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Prop("imageContentMode") { (view: ExpoTvosSearchView, mode: String) in
|
|
131
|
+
view.imageContentMode = mode
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Prop("cardMargin") { (view: ExpoTvosSearchView, margin: Double) in
|
|
135
|
+
view.cardMargin = CGFloat(max(0, min(200, margin))) // Clamp to reasonable range
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Prop("cardPadding") { (view: ExpoTvosSearchView, padding: Double) in
|
|
139
|
+
view.cardPadding = CGFloat(max(0, min(100, padding))) // Clamp to reasonable range
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Prop("overlayTitleSize") { (view: ExpoTvosSearchView, size: Double) in
|
|
143
|
+
view.overlayTitleSize = CGFloat(max(8, min(72, size))) // Clamp to reasonable font size range
|
|
144
|
+
}
|
|
82
145
|
}
|
|
83
146
|
}
|
|
84
147
|
}
|