expo-tvos-search 1.2.3 → 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 +325 -17
- 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/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/expo-tvos-search)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://github.com/keiver/expo-tvos-search/actions)
|
|
6
|
+
[](https://bundlephobia.com/package/expo-tvos-search)
|
|
6
7
|
|
|
7
8
|
A native tvOS search component for Expo and React Native using SwiftUI's `.searchable` modifier. Handles focus, keyboard navigation, and accessibility out of the box.
|
|
8
9
|
|
|
@@ -27,7 +28,6 @@ A native tvOS search component for Expo and React Native using SwiftUI's `.searc
|
|
|
27
28
|
</tr>
|
|
28
29
|
</table>
|
|
29
30
|
|
|
30
|
-
|
|
31
31
|
## Installation
|
|
32
32
|
|
|
33
33
|
```bash
|
|
@@ -40,30 +40,114 @@ Or install from GitHub:
|
|
|
40
40
|
npx expo install github:keiver/expo-tvos-search
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
Then rebuild your native project
|
|
43
|
+
Then follow the **tvOS prerequisites** below and rebuild your native project.
|
|
44
|
+
|
|
45
|
+
## Prerequisites for tvOS Builds (Expo)
|
|
46
|
+
|
|
47
|
+
Your project must be configured for React Native tvOS to build and run this module.
|
|
48
|
+
|
|
49
|
+
**Quick Checklist:**
|
|
50
|
+
|
|
51
|
+
- ✅ `react-native-tvos` in use
|
|
52
|
+
- ✅ `@react-native-tvos/config-tv` installed + added to Expo plugins
|
|
53
|
+
- ✅ Run prebuild with `EXPO_TV=1`
|
|
54
|
+
|
|
55
|
+
### 1. Swap to react-native-tvos
|
|
56
|
+
|
|
57
|
+
Replace `react-native` with the [tvOS fork](https://github.com/react-native-tvos/react-native-tvos):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm remove react-native && npm install react-native-tvos@latest
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. Install the tvOS config plugin
|
|
64
|
+
|
|
65
|
+
Install:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx expo install @react-native-tvos/config-tv
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then add the plugin in `app.json` / `app.config.js`:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"expo": {
|
|
76
|
+
"plugins": ["@react-native-tvos/config-tv"]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 3. Generate native projects with tvOS enabled
|
|
44
82
|
|
|
45
83
|
```bash
|
|
46
84
|
EXPO_TV=1 npx expo prebuild --clean
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Then run:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
47
90
|
npx expo run:ios
|
|
48
91
|
```
|
|
49
92
|
|
|
93
|
+
### 4. Common gotchas
|
|
94
|
+
|
|
95
|
+
- **Prebuild must be re-run** if you add/remove tvOS dependencies or change the tvOS plugin configuration.
|
|
96
|
+
- **If you see App Transport Security errors** for images, ensure your `imageUrl` uses `https://` (recommended) or add the appropriate ATS exceptions.
|
|
97
|
+
- **If the tvOS keyboard/search UI doesn't appear**, confirm you're actually running a tvOS target/simulator, not an iOS target.
|
|
98
|
+
|
|
50
99
|
## Usage
|
|
51
100
|
|
|
52
101
|
```tsx
|
|
53
|
-
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
102
|
+
import React, { useState } from "react";
|
|
103
|
+
import { Alert } from "react-native";
|
|
104
|
+
import {
|
|
105
|
+
TvosSearchView,
|
|
106
|
+
isNativeSearchAvailable,
|
|
107
|
+
type SearchResult,
|
|
108
|
+
} from "expo-tvos-search";
|
|
109
|
+
|
|
110
|
+
const PLANETS: SearchResult[] = [
|
|
111
|
+
{ id: "mercury", title: "Mercury", subtitle: "Smallest planet", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Mercury_in_true_color.jpg/400px-Mercury_in_true_color.jpg" },
|
|
112
|
+
{ id: "venus", title: "Venus", subtitle: "Hottest planet", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e5/Venus-real_color.jpg/400px-Venus-real_color.jpg" },
|
|
113
|
+
{ id: "earth", title: "Earth", subtitle: "Our home", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/The_Earth_seen_from_Apollo_17.jpg/400px-The_Earth_seen_from_Apollo_17.jpg" },
|
|
114
|
+
{ id: "mars", title: "Mars", subtitle: "The red planet", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/02/OSIRIS_Mars_true_color.jpg/400px-OSIRIS_Mars_true_color.jpg" },
|
|
115
|
+
{ id: "jupiter", title: "Jupiter", subtitle: "Largest planet", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Jupiter_New_Horizons.jpg/400px-Jupiter_New_Horizons.jpg" },
|
|
116
|
+
{ id: "saturn", title: "Saturn", subtitle: "Ringed giant", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c7/Saturn_during_Equinox.jpg/400px-Saturn_during_Equinox.jpg" },
|
|
117
|
+
{ id: "uranus", title: "Uranus", subtitle: "Ice giant", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/Uranus2.jpg/400px-Uranus2.jpg" },
|
|
118
|
+
{ id: "neptune", title: "Neptune", subtitle: "Windiest planet", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/63/Neptune_-_Voyager_2_%2829347980845%29_flatten_crop.jpg/400px-Neptune_-_Voyager_2_%2829347980845%29_flatten_crop.jpg" },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
export function SearchScreen() {
|
|
122
|
+
const [results, setResults] = useState<SearchResult[]>([]);
|
|
57
123
|
const [isLoading, setIsLoading] = useState(false);
|
|
58
124
|
|
|
59
|
-
const handleSearch = (event) => {
|
|
60
|
-
const query = event.nativeEvent
|
|
61
|
-
|
|
125
|
+
const handleSearch = (event: { nativeEvent: { query: string } }) => {
|
|
126
|
+
const { query } = event.nativeEvent;
|
|
127
|
+
|
|
128
|
+
if (!query.trim()) {
|
|
129
|
+
setResults([]);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setIsLoading(true);
|
|
134
|
+
|
|
135
|
+
// Simulate async search
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
const filtered = PLANETS.filter((planet) =>
|
|
138
|
+
planet.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
139
|
+
planet.subtitle?.toLowerCase().includes(query.toLowerCase())
|
|
140
|
+
);
|
|
141
|
+
setResults(filtered);
|
|
142
|
+
setIsLoading(false);
|
|
143
|
+
}, 300);
|
|
62
144
|
};
|
|
63
145
|
|
|
64
|
-
const handleSelect = (event) => {
|
|
65
|
-
const
|
|
66
|
-
|
|
146
|
+
const handleSelect = (event: { nativeEvent: { id: string } }) => {
|
|
147
|
+
const planet = PLANETS.find((p) => p.id === event.nativeEvent.id);
|
|
148
|
+
if (planet) {
|
|
149
|
+
Alert.alert(planet.title, planet.subtitle);
|
|
150
|
+
}
|
|
67
151
|
};
|
|
68
152
|
|
|
69
153
|
if (!isNativeSearchAvailable()) {
|
|
@@ -74,7 +158,7 @@ function SearchScreen() {
|
|
|
74
158
|
<TvosSearchView
|
|
75
159
|
results={results}
|
|
76
160
|
columns={5}
|
|
77
|
-
placeholder="Search..."
|
|
161
|
+
placeholder="Search planets..."
|
|
78
162
|
isLoading={isLoading}
|
|
79
163
|
topInset={140}
|
|
80
164
|
onSearch={handleSearch}
|
|
@@ -85,13 +169,146 @@ function SearchScreen() {
|
|
|
85
169
|
}
|
|
86
170
|
```
|
|
87
171
|
|
|
172
|
+
> **⚠️ IMPORTANT**: Always debounce the `onSearch` callback to prevent excessive API calls or expensive operations on every keystroke. In production apps, use a debounce library like lodash or implement your own debounce function:
|
|
173
|
+
>
|
|
174
|
+
> ```tsx
|
|
175
|
+
> import { useMemo } from 'react';
|
|
176
|
+
> import { debounce } from 'lodash'; // or your preferred debounce implementation
|
|
177
|
+
>
|
|
178
|
+
> function SearchScreen() {
|
|
179
|
+
> const [results, setResults] = useState([]);
|
|
180
|
+
>
|
|
181
|
+
> const debouncedSearch = useMemo(
|
|
182
|
+
> () => debounce((query: string) => {
|
|
183
|
+
> // Your actual search logic (API call, etc.)
|
|
184
|
+
> fetchSearchResults(query).then(setResults);
|
|
185
|
+
> }, 300), // 300ms delay
|
|
186
|
+
> []
|
|
187
|
+
> );
|
|
188
|
+
>
|
|
189
|
+
> return (
|
|
190
|
+
> <TvosSearchView
|
|
191
|
+
> results={results}
|
|
192
|
+
> onSearch={(e) => debouncedSearch(e.nativeEvent.query)}
|
|
193
|
+
> onSelectItem={handleSelect}
|
|
194
|
+
> />
|
|
195
|
+
> );
|
|
196
|
+
> }
|
|
197
|
+
> ```
|
|
198
|
+
|
|
199
|
+
### Error Handling and Monitoring
|
|
200
|
+
|
|
201
|
+
The library provides error and validation warning callbacks for production monitoring:
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
<TvosSearchView
|
|
205
|
+
results={results}
|
|
206
|
+
onSearch={handleSearch}
|
|
207
|
+
onSelectItem={handleSelect}
|
|
208
|
+
onError={(e) => {
|
|
209
|
+
const { category, message, context } = e.nativeEvent;
|
|
210
|
+
// Log to your error monitoring service
|
|
211
|
+
console.error(`[Search Error] ${category}: ${message}`, context);
|
|
212
|
+
// Examples: 'module_unavailable', 'validation_failed', 'image_load_failed', 'unknown'
|
|
213
|
+
}}
|
|
214
|
+
onValidationWarning={(e) => {
|
|
215
|
+
const { type, message, context } = e.nativeEvent;
|
|
216
|
+
// Log non-fatal issues for monitoring
|
|
217
|
+
console.warn(`[Validation] ${type}: ${message}`, context);
|
|
218
|
+
// Examples: 'field_truncated', 'value_clamped', 'url_invalid', 'validation_failed'
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
These callbacks help you:
|
|
224
|
+
- Monitor data quality issues (truncated fields, invalid URLs)
|
|
225
|
+
- Track when props are clamped to safe ranges
|
|
226
|
+
- Detect when results exceed the 500-item limit
|
|
227
|
+
- Log errors for debugging in production
|
|
228
|
+
|
|
229
|
+
### Customizing Colors and Card Dimensions
|
|
230
|
+
|
|
231
|
+
You can customize the appearance of the search interface with color and dimension props:
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
<TvosSearchView
|
|
235
|
+
results={results}
|
|
236
|
+
onSearch={handleSearch}
|
|
237
|
+
onSelectItem={handleSelect}
|
|
238
|
+
// Custom colors
|
|
239
|
+
textColor="#E5E5E5" // Light gray text on dark background
|
|
240
|
+
accentColor="#E50914" // Red focused borders (House Flix style 😂)
|
|
241
|
+
// Custom card dimensions
|
|
242
|
+
cardWidth={420} // Landscape cards
|
|
243
|
+
cardHeight={240} // 16:9 aspect ratio
|
|
244
|
+
style={{ flex: 1 }}
|
|
245
|
+
/>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Color props:**
|
|
249
|
+
- `textColor` - Affects subtitle text, empty state text, and placeholder icons
|
|
250
|
+
- `accentColor` - Affects focused card borders and highlights
|
|
251
|
+
|
|
252
|
+
**Dimension props:**
|
|
253
|
+
- Default: 280x420 (portrait, 2:3 aspect ratio)
|
|
254
|
+
- Landscape example: 420x240 (16:9)
|
|
255
|
+
- Square example: 300x300 (1:1)
|
|
256
|
+
|
|
257
|
+
### Layout and Spacing Customization
|
|
258
|
+
|
|
259
|
+
Control image display, card spacing, and overlay padding for different layouts:
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
<TvosSearchView
|
|
263
|
+
results={results}
|
|
264
|
+
onSearch={handleSearch}
|
|
265
|
+
onSelectItem={handleSelect}
|
|
266
|
+
// Image display mode
|
|
267
|
+
imageContentMode="fit" // 'fill' (default, crop), 'fit'/'contain' (letterbox)
|
|
268
|
+
// Card spacing
|
|
269
|
+
cardMargin={60} // Space between cards (default: 40)
|
|
270
|
+
cardPadding={20} // Padding inside overlay (default: 16)
|
|
271
|
+
style={{ flex: 1 }}
|
|
272
|
+
/>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Layout props:**
|
|
276
|
+
- `imageContentMode` - How images fill cards:
|
|
277
|
+
- `'fill'` (default) - Crops image to fill entire card
|
|
278
|
+
- `'fit'` or `'contain'` - Shows entire image, may add letterboxing
|
|
279
|
+
- `cardMargin` - Controls spacing between cards in the grid (both horizontal and vertical)
|
|
280
|
+
- `cardPadding` - Controls padding inside the card's title overlay for better text spacing
|
|
281
|
+
|
|
282
|
+
**Common use cases:**
|
|
283
|
+
- **Portrait posters** (movie/TV): `imageContentMode="fill"` + `cardMargin={40}`
|
|
284
|
+
- **Landscape thumbnails** (episodes): `imageContentMode="fit"` + `cardMargin={60}`
|
|
285
|
+
- **Compact grid** (music): `cardMargin={20}` + `cardPadding={12}`
|
|
286
|
+
- **Spacious layout** (photos): `cardMargin={60}` + `cardPadding={20}`
|
|
287
|
+
|
|
288
|
+
## Example App
|
|
289
|
+
|
|
290
|
+
**[Tomo TV](https://github.com/keiver/tomotv)** is a tvOS application that uses `expo-tvos-search` in a real-world Jellyfin client. It demonstrates the search component integrated with a complete media browsing experience, including:
|
|
291
|
+
|
|
292
|
+
- Search interaction with tvOS remote
|
|
293
|
+
- Focus navigation through results
|
|
294
|
+
- Integration with a live media library
|
|
295
|
+
- Complete setup instructions and screenshots
|
|
296
|
+
|
|
297
|
+
Check out Tomo TV to see `expo-tvos-search` in action and reference its implementation for your own projects.
|
|
298
|
+
|
|
299
|
+
## See it in action:
|
|
300
|
+
|
|
301
|
+
<p align="center">
|
|
302
|
+
<img src="screenshots/expo-tvos-search.gif" width="700" alt="expo-tvos-search screen in action" loading="lazy" />
|
|
303
|
+
</p>
|
|
304
|
+
|
|
88
305
|
## Props
|
|
89
306
|
|
|
90
307
|
| Prop | Type | Default | Description |
|
|
91
308
|
|------|------|---------|-------------|
|
|
92
309
|
| `results` | `SearchResult[]` | `[]` | Array of search results |
|
|
93
310
|
| `columns` | `number` | `5` | Number of columns in the grid |
|
|
94
|
-
| `placeholder` | `string` | `"Search..."` | Search field placeholder |
|
|
311
|
+
| `placeholder` | `string` | `"Search movies and videos..."` | Search field placeholder |
|
|
95
312
|
| `isLoading` | `boolean` | `false` | Shows loading indicator |
|
|
96
313
|
| `showTitle` | `boolean` | `false` | Show title below each result |
|
|
97
314
|
| `showSubtitle` | `boolean` | `false` | Show subtitle below title |
|
|
@@ -104,8 +321,19 @@ function SearchScreen() {
|
|
|
104
321
|
| `searchingText` | `string` | `"Searching..."` | Text shown during search |
|
|
105
322
|
| `noResultsText` | `string` | `"No results found"` | Text shown when no results found |
|
|
106
323
|
| `noResultsHintText` | `string` | `"Try a different search term"` | Hint text below no results message |
|
|
324
|
+
| `textColor` | `string` | system default | Color for text and UI elements (hex format, e.g., "#FFFFFF") |
|
|
325
|
+
| `accentColor` | `string` | `"#FFC312"` | Accent color for focused elements (hex format, e.g., "#FFC312") |
|
|
326
|
+
| `cardWidth` | `number` | `280` | Width of each result card in points |
|
|
327
|
+
| `cardHeight` | `number` | `420` | Height of each result card in points |
|
|
328
|
+
| `imageContentMode` | `'fill' \| 'fit' \| 'contain'` | `'fill'` | How images fill the card: `fill` (crop to fill), `fit`/`contain` (letterbox) |
|
|
329
|
+
| `cardMargin` | `number` | `40` | Spacing between cards in the grid (horizontal and vertical) |
|
|
330
|
+
| `cardPadding` | `number` | `16` | Padding inside the card for overlay content (title/subtitle) |
|
|
331
|
+
| `overlayTitleSize` | `number` | `20` | Font size for title text in the blur overlay (when showTitleOverlay is true) |
|
|
107
332
|
| `onSearch` | `function` | required | Called when search text changes |
|
|
108
333
|
| `onSelectItem` | `function` | required | Called when result is selected |
|
|
334
|
+
| `onError` | `function` | optional | Called when errors occur (image loading failures, validation errors) |
|
|
335
|
+
| `onValidationWarning` | `function` | optional | Called for non-fatal warnings (truncated fields, clamped values, invalid URLs) |
|
|
336
|
+
| `style` | `ViewStyle` | optional | Style object for the view container |
|
|
109
337
|
|
|
110
338
|
## SearchResult Type
|
|
111
339
|
|
|
@@ -127,12 +355,77 @@ The native implementation applies the following validation and constraints:
|
|
|
127
355
|
- **Image URL schemes**: Only HTTP and HTTPS URLs are accepted for `imageUrl`. Other URL schemes (e.g., `file://`, `data:`) are rejected.
|
|
128
356
|
- **HTTPS recommended**: HTTP URLs may be blocked by App Transport Security on tvOS unless explicitly allowed in Info.plist.
|
|
129
357
|
|
|
358
|
+
## Focus Handling - Do's and Don'ts
|
|
359
|
+
|
|
360
|
+
The native `.searchable` modifier manages focus automatically. Here's what to do and what to avoid:
|
|
361
|
+
|
|
362
|
+
### ✅ Do: Render directly in your screen
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
function SearchScreen() {
|
|
366
|
+
return (
|
|
367
|
+
<TvosSearchView
|
|
368
|
+
results={results}
|
|
369
|
+
onSearch={handleSearch}
|
|
370
|
+
onSelectItem={handleSelect}
|
|
371
|
+
style={{ flex: 1 }}
|
|
372
|
+
/>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### ❌ Don't: Wrap in focusable containers
|
|
378
|
+
|
|
379
|
+
```tsx
|
|
380
|
+
// ❌ WRONG - breaks focus navigation
|
|
381
|
+
function SearchScreen() {
|
|
382
|
+
return (
|
|
383
|
+
<Pressable> {/* Don't wrap in Pressable */}
|
|
384
|
+
<TvosSearchView ... />
|
|
385
|
+
</Pressable>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ❌ WRONG - interferes with native focus
|
|
390
|
+
function SearchScreen() {
|
|
391
|
+
return (
|
|
392
|
+
<TouchableOpacity> {/* Don't wrap in TouchableOpacity */}
|
|
393
|
+
<TvosSearchView ... />
|
|
394
|
+
</TouchableOpacity>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Why this breaks**: Focusable wrappers steal focus from the native SwiftUI search container, which breaks directional navigation.
|
|
400
|
+
|
|
401
|
+
### ✅ Do: Use non-interactive containers
|
|
402
|
+
|
|
403
|
+
```tsx
|
|
404
|
+
// ✅ CORRECT - View is not focusable
|
|
405
|
+
function SearchScreen() {
|
|
406
|
+
return (
|
|
407
|
+
<View style={{ flex: 1, backgroundColor: '#000' }}>
|
|
408
|
+
<TvosSearchView ... />
|
|
409
|
+
</View>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ✅ CORRECT - SafeAreaView is not focusable
|
|
414
|
+
function SearchScreen() {
|
|
415
|
+
return (
|
|
416
|
+
<SafeAreaView style={{ flex: 1 }}>
|
|
417
|
+
<TvosSearchView ... />
|
|
418
|
+
</SafeAreaView>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
130
423
|
## Requirements
|
|
131
424
|
|
|
132
|
-
- Node.js 18
|
|
425
|
+
- Node.js 18+
|
|
133
426
|
- Expo SDK 51+
|
|
134
|
-
- tvOS 15
|
|
135
|
-
-
|
|
427
|
+
- tvOS 15+
|
|
428
|
+
- Project configured for tvOS (`react-native-tvos` + `@react-native-tvos/config-tv`)
|
|
136
429
|
|
|
137
430
|
## Troubleshooting
|
|
138
431
|
|
|
@@ -146,6 +439,8 @@ EXPO_TV=1 npx expo prebuild --clean
|
|
|
146
439
|
npx expo run:ios
|
|
147
440
|
```
|
|
148
441
|
|
|
442
|
+
**Note:** Expo Go doesn't support this. Build a dev client or native build instead.
|
|
443
|
+
|
|
149
444
|
### Images not loading
|
|
150
445
|
|
|
151
446
|
1. Verify your image URLs are HTTPS (HTTP may be blocked by App Transport Security)
|
|
@@ -183,6 +478,19 @@ Tests cover:
|
|
|
183
478
|
- Component rendering when native module is unavailable
|
|
184
479
|
- Event structure validation
|
|
185
480
|
|
|
481
|
+
## Contributing
|
|
482
|
+
|
|
483
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on:
|
|
484
|
+
- Code of conduct
|
|
485
|
+
- Development setup
|
|
486
|
+
- Testing requirements
|
|
487
|
+
- Commit message conventions
|
|
488
|
+
- Pull request process
|
|
489
|
+
|
|
490
|
+
### Adding New Props
|
|
491
|
+
|
|
492
|
+
If you're adding new props to the library, follow the comprehensive checklist in [.claude/CLAUDE-adding-new-props.md](.claude/CLAUDE-adding-new-props.md). This memory bank provides a 9-step guide ensuring props are properly wired from TypeScript through to Swift rendering.
|
|
493
|
+
|
|
186
494
|
## License
|
|
187
495
|
|
|
188
496
|
MIT
|
package/build/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import { ViewStyle } from "react-native";
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import type { ViewStyle } from "react-native";
|
|
3
3
|
/**
|
|
4
4
|
* Event payload for search text changes.
|
|
5
5
|
* Fired when the user types in the native search field.
|
|
@@ -20,6 +20,38 @@ export interface SelectItemEvent {
|
|
|
20
20
|
id: string;
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Categories of errors that can occur in the search view.
|
|
25
|
+
*/
|
|
26
|
+
export type SearchViewErrorCategory = "module_unavailable" | "validation_failed" | "image_load_failed" | "unknown";
|
|
27
|
+
/**
|
|
28
|
+
* Event payload for error callbacks.
|
|
29
|
+
* Provides details about errors that occur during search view operations.
|
|
30
|
+
*/
|
|
31
|
+
export interface SearchViewErrorEvent {
|
|
32
|
+
nativeEvent: {
|
|
33
|
+
/** Category of the error for programmatic handling */
|
|
34
|
+
category: SearchViewErrorCategory;
|
|
35
|
+
/** Human-readable error message */
|
|
36
|
+
message: string;
|
|
37
|
+
/** Optional additional context (e.g., result ID, URL) */
|
|
38
|
+
context?: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Event payload for validation warnings.
|
|
43
|
+
* Non-fatal issues like truncated fields or clamped values.
|
|
44
|
+
*/
|
|
45
|
+
export interface ValidationWarningEvent {
|
|
46
|
+
nativeEvent: {
|
|
47
|
+
/** Type of validation warning */
|
|
48
|
+
type: "field_truncated" | "value_clamped" | "url_invalid" | "validation_failed";
|
|
49
|
+
/** Human-readable warning message */
|
|
50
|
+
message: string;
|
|
51
|
+
/** Optional additional context */
|
|
52
|
+
context?: string;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
23
55
|
/**
|
|
24
56
|
* Represents a single search result displayed in the grid.
|
|
25
57
|
*/
|
|
@@ -69,7 +101,7 @@ export interface TvosSearchViewProps {
|
|
|
69
101
|
columns?: number;
|
|
70
102
|
/**
|
|
71
103
|
* Placeholder text shown in the search field when empty.
|
|
72
|
-
* @default "Search..."
|
|
104
|
+
* @default "Search movies and videos..."
|
|
73
105
|
*/
|
|
74
106
|
placeholder?: string;
|
|
75
107
|
/**
|
|
@@ -141,6 +173,61 @@ export interface TvosSearchViewProps {
|
|
|
141
173
|
* @default "Try a different search term"
|
|
142
174
|
*/
|
|
143
175
|
noResultsHintText?: string;
|
|
176
|
+
/**
|
|
177
|
+
* Color for text and UI elements in the search interface.
|
|
178
|
+
* Hex color string (e.g., "#FFFFFF", "#E5E5E5").
|
|
179
|
+
* @default Uses system default based on userInterfaceStyle
|
|
180
|
+
* @example "#E5E5E5" for light gray text on dark background
|
|
181
|
+
*/
|
|
182
|
+
textColor?: string;
|
|
183
|
+
/**
|
|
184
|
+
* Accent color for focused elements and highlights.
|
|
185
|
+
* Hex color string (e.g., "#FFC312").
|
|
186
|
+
* @default "#FFC312" (gold)
|
|
187
|
+
* @example "#E50914" for Netflix red
|
|
188
|
+
*/
|
|
189
|
+
accentColor?: string;
|
|
190
|
+
/**
|
|
191
|
+
* Width of each result card in points.
|
|
192
|
+
* Allows customization for portrait, landscape, or square layouts.
|
|
193
|
+
* @default 280
|
|
194
|
+
* @example 420 for landscape cards
|
|
195
|
+
*/
|
|
196
|
+
cardWidth?: number;
|
|
197
|
+
/**
|
|
198
|
+
* Height of each result card in points.
|
|
199
|
+
* Allows customization for portrait, landscape, or square layouts.
|
|
200
|
+
* @default 420
|
|
201
|
+
* @example 240 for landscape cards (16:9 ratio with width=420)
|
|
202
|
+
*/
|
|
203
|
+
cardHeight?: number;
|
|
204
|
+
/**
|
|
205
|
+
* How the image fills the card area.
|
|
206
|
+
* - 'fill': Image fills entire card, may crop (default)
|
|
207
|
+
* - 'fit': Image fits within card, may show letterboxing
|
|
208
|
+
* - 'contain': Same as fit (alias for consistency)
|
|
209
|
+
* @default "fill"
|
|
210
|
+
*/
|
|
211
|
+
imageContentMode?: 'fill' | 'fit' | 'contain';
|
|
212
|
+
/**
|
|
213
|
+
* Spacing between cards in the grid layout (both horizontal and vertical).
|
|
214
|
+
* @default 40
|
|
215
|
+
* @example 60 for spacious layouts, 20 for compact grids
|
|
216
|
+
*/
|
|
217
|
+
cardMargin?: number;
|
|
218
|
+
/**
|
|
219
|
+
* Padding inside the card for overlay content (title, subtitle).
|
|
220
|
+
* @default 16
|
|
221
|
+
* @example 20 for more breathing room, 12 for compact cards
|
|
222
|
+
*/
|
|
223
|
+
cardPadding?: number;
|
|
224
|
+
/**
|
|
225
|
+
* Font size for title in the blur overlay (when showTitleOverlay is true).
|
|
226
|
+
* Allows customization of overlay text size for different card layouts.
|
|
227
|
+
* @default 20
|
|
228
|
+
* @example 18 for smaller cards, 24 for larger cards
|
|
229
|
+
*/
|
|
230
|
+
overlayTitleSize?: number;
|
|
144
231
|
/**
|
|
145
232
|
* Callback fired when the search text changes.
|
|
146
233
|
* Debounce this handler to avoid excessive API calls.
|
|
@@ -151,6 +238,30 @@ export interface TvosSearchViewProps {
|
|
|
151
238
|
* Use the `id` from the event to identify which result was selected.
|
|
152
239
|
*/
|
|
153
240
|
onSelectItem: (event: SelectItemEvent) => void;
|
|
241
|
+
/**
|
|
242
|
+
* Optional callback fired when errors occur.
|
|
243
|
+
* Use this to monitor and log issues in production.
|
|
244
|
+
* @example
|
|
245
|
+
* ```tsx
|
|
246
|
+
* onError={(e) => {
|
|
247
|
+
* const { category, message, context } = e.nativeEvent;
|
|
248
|
+
* logger.error(`Search error [${category}]: ${message}`, { context });
|
|
249
|
+
* }}
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
onError?: (event: SearchViewErrorEvent) => void;
|
|
253
|
+
/**
|
|
254
|
+
* Optional callback fired for non-fatal validation warnings.
|
|
255
|
+
* Examples: truncated fields, clamped values, invalid URLs.
|
|
256
|
+
* @example
|
|
257
|
+
* ```tsx
|
|
258
|
+
* onValidationWarning={(e) => {
|
|
259
|
+
* const { type, message } = e.nativeEvent;
|
|
260
|
+
* console.warn(`Validation warning [${type}]: ${message}`);
|
|
261
|
+
* }}
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
onValidationWarning?: (event: ValidationWarningEvent) => void;
|
|
154
265
|
/**
|
|
155
266
|
* Optional style for the view container.
|
|
156
267
|
*/
|
|
@@ -189,7 +300,7 @@ export interface TvosSearchViewProps {
|
|
|
189
300
|
* @param props - Component props
|
|
190
301
|
* @returns The native search view on tvOS, or `null` if unavailable
|
|
191
302
|
*/
|
|
192
|
-
export declare function TvosSearchView(props: TvosSearchViewProps):
|
|
303
|
+
export declare function TvosSearchView(props: TvosSearchViewProps): JSX.Element | null;
|
|
193
304
|
/**
|
|
194
305
|
* Checks if the native tvOS search component is available.
|
|
195
306
|
*
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}
|
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 });
|