expo-tvos-search 1.3.0 → 1.4.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/CHANGELOG.md +111 -0
- package/README.md +212 -312
- package/build/index.d.ts +57 -5
- package/build/index.d.ts.map +1 -1
- package/ios/CachedAsyncImage.swift +78 -0
- package/ios/ExpoTvosSearchModule.swift +31 -7
- package/ios/ExpoTvosSearchView.swift +250 -421
- package/ios/HexColorParser.swift +55 -0
- package/ios/MarqueeText.swift +28 -51
- package/ios/SearchResultCard.swift +172 -0
- package/ios/SearchResultItem.swift +8 -0
- package/ios/Tests/HexColorParserTests.swift +183 -0
- package/ios/Tests/SearchViewModelTests.swift +1 -1
- package/ios/TvosSearchContentView.swift +125 -0
- package/package.json +2 -1
- package/src/__tests__/events.test.ts +128 -0
- package/src/__tests__/index.test.tsx +47 -1
- package/src/index.tsx +61 -5
package/README.md
CHANGED
|
@@ -3,31 +3,18 @@
|
|
|
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)
|
|
7
6
|
|
|
8
|
-
A native tvOS search component for Expo and React Native using SwiftUI's `.searchable` modifier.
|
|
7
|
+
A native tvOS search component for Expo and React Native using SwiftUI's `.searchable` modifier. Provides the native tvOS search experience with automatic focus handling, remote control support, and flexible customization for media apps.
|
|
8
|
+
|
|
9
|
+
**Platform Support:**
|
|
10
|
+
- tvOS 15.0+
|
|
11
|
+
- Expo SDK 51+
|
|
12
|
+
- React Native tvOS 0.71+
|
|
9
13
|
|
|
10
14
|
<p align="center">
|
|
11
|
-
<img src="screenshots/
|
|
15
|
+
<img src="screenshots/default.png" alt="expo-tvos-search fullscreen native tvOS search component" style="border-radius: 16px;max-width: 100%;"/>
|
|
12
16
|
</p>
|
|
13
17
|
|
|
14
|
-
<table>
|
|
15
|
-
<tr>
|
|
16
|
-
<td align="center">
|
|
17
|
-
<img src="screenshots/default.png" width="280" alt="Search"/><br/>
|
|
18
|
-
<sub>Native Search</sub>
|
|
19
|
-
</td>
|
|
20
|
-
<td align="center">
|
|
21
|
-
<img src="screenshots/results.png" width="280" alt="Results"/><br/>
|
|
22
|
-
<sub>Results</sub>
|
|
23
|
-
</td>
|
|
24
|
-
<td align="center">
|
|
25
|
-
<img src="screenshots/no-results.png" width="280" alt="No Results"/><br/>
|
|
26
|
-
<sub>Empty State</sub>
|
|
27
|
-
</td>
|
|
28
|
-
</tr>
|
|
29
|
-
</table>
|
|
30
|
-
|
|
31
18
|
## Installation
|
|
32
19
|
|
|
33
20
|
```bash
|
|
@@ -40,30 +27,18 @@ Or install from GitHub:
|
|
|
40
27
|
npx expo install github:keiver/expo-tvos-search
|
|
41
28
|
```
|
|
42
29
|
|
|
43
|
-
|
|
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:**
|
|
30
|
+
## Prerequisites for tvOS Builds
|
|
50
31
|
|
|
51
|
-
|
|
52
|
-
- ✅ `@react-native-tvos/config-tv` installed + added to Expo plugins
|
|
53
|
-
- ✅ Run prebuild with `EXPO_TV=1`
|
|
32
|
+
Your project must be configured for React Native tvOS to use this module.
|
|
54
33
|
|
|
55
|
-
### 1.
|
|
56
|
-
|
|
57
|
-
Replace `react-native` with the [tvOS fork](https://github.com/react-native-tvos/react-native-tvos):
|
|
34
|
+
### 1. Install react-native-tvos
|
|
58
35
|
|
|
59
36
|
```bash
|
|
60
|
-
npm
|
|
37
|
+
npm install react-native-tvos@latest
|
|
61
38
|
```
|
|
62
39
|
|
|
63
40
|
### 2. Install the tvOS config plugin
|
|
64
41
|
|
|
65
|
-
Install:
|
|
66
|
-
|
|
67
42
|
```bash
|
|
68
43
|
npx expo install @react-native-tvos/config-tv
|
|
69
44
|
```
|
|
@@ -78,49 +53,35 @@ Then add the plugin in `app.json` / `app.config.js`:
|
|
|
78
53
|
}
|
|
79
54
|
```
|
|
80
55
|
|
|
81
|
-
### 3. Generate native projects with tvOS enabled
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
EXPO_TV=1 npx expo prebuild --clean
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Then run:
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
npx expo run:ios
|
|
91
|
-
```
|
|
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
|
-
|
|
99
56
|
## Usage
|
|
100
57
|
|
|
58
|
+
This example from the demo's [Portrait tab](https://github.com/keiver/expo-tvos-search-demo/blob/main/app/(tabs)/portrait.tsx) shows a complete implementation:
|
|
59
|
+
|
|
101
60
|
```tsx
|
|
102
|
-
import
|
|
103
|
-
import { Alert } from
|
|
61
|
+
import { useState } from 'react';
|
|
62
|
+
import { Alert } from 'react-native';
|
|
63
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
64
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
104
65
|
import {
|
|
105
66
|
TvosSearchView,
|
|
106
67
|
isNativeSearchAvailable,
|
|
107
68
|
type SearchResult,
|
|
108
|
-
} from
|
|
69
|
+
} from 'expo-tvos-search';
|
|
109
70
|
|
|
110
71
|
const PLANETS: SearchResult[] = [
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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" },
|
|
72
|
+
{
|
|
73
|
+
id: 'earth',
|
|
74
|
+
title: 'Earth - The Blue Marble of Life',
|
|
75
|
+
subtitle: 'Our home planet, the only known world to harbor life',
|
|
76
|
+
imageUrl: require('./assets/planets/earth.webp'),
|
|
77
|
+
},
|
|
78
|
+
// ... all planets
|
|
119
79
|
];
|
|
120
80
|
|
|
121
|
-
export function SearchScreen() {
|
|
81
|
+
export default function SearchScreen() {
|
|
122
82
|
const [results, setResults] = useState<SearchResult[]>([]);
|
|
123
83
|
const [isLoading, setIsLoading] = useState(false);
|
|
84
|
+
const insets = useSafeAreaInsets();
|
|
124
85
|
|
|
125
86
|
const handleSearch = (event: { nativeEvent: { query: string } }) => {
|
|
126
87
|
const { query } = event.nativeEvent;
|
|
@@ -132,11 +93,12 @@ export function SearchScreen() {
|
|
|
132
93
|
|
|
133
94
|
setIsLoading(true);
|
|
134
95
|
|
|
135
|
-
//
|
|
96
|
+
// Debounce dummy search (300ms)
|
|
136
97
|
setTimeout(() => {
|
|
137
|
-
const filtered = PLANETS.filter(
|
|
138
|
-
planet
|
|
139
|
-
|
|
98
|
+
const filtered = PLANETS.filter(
|
|
99
|
+
planet =>
|
|
100
|
+
planet.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
101
|
+
planet.subtitle?.toLowerCase().includes(query.toLowerCase())
|
|
140
102
|
);
|
|
141
103
|
setResults(filtered);
|
|
142
104
|
setIsLoading(false);
|
|
@@ -144,207 +106,251 @@ export function SearchScreen() {
|
|
|
144
106
|
};
|
|
145
107
|
|
|
146
108
|
const handleSelect = (event: { nativeEvent: { id: string } }) => {
|
|
147
|
-
const planet = PLANETS.find(
|
|
109
|
+
const planet = PLANETS.find(p => p.id === event.nativeEvent.id);
|
|
148
110
|
if (planet) {
|
|
149
111
|
Alert.alert(planet.title, planet.subtitle);
|
|
150
112
|
}
|
|
151
113
|
};
|
|
152
114
|
|
|
153
115
|
if (!isNativeSearchAvailable()) {
|
|
154
|
-
return
|
|
116
|
+
return null; // Or show web fallback
|
|
155
117
|
}
|
|
156
118
|
|
|
157
119
|
return (
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
columns={5}
|
|
161
|
-
placeholder="Search planets..."
|
|
162
|
-
isLoading={isLoading}
|
|
163
|
-
topInset={140}
|
|
164
|
-
onSearch={handleSearch}
|
|
165
|
-
onSelectItem={handleSelect}
|
|
120
|
+
<LinearGradient
|
|
121
|
+
colors={['#0f172a', '#1e293b', '#0f172a']}
|
|
166
122
|
style={{ flex: 1 }}
|
|
167
|
-
|
|
123
|
+
>
|
|
124
|
+
<TvosSearchView
|
|
125
|
+
results={results}
|
|
126
|
+
columns={4}
|
|
127
|
+
placeholder="Search planets..."
|
|
128
|
+
isLoading={isLoading}
|
|
129
|
+
topInset={insets.top + 80}
|
|
130
|
+
onSearch={handleSearch}
|
|
131
|
+
onSelectItem={handleSelect}
|
|
132
|
+
textColor="#E5E5E5"
|
|
133
|
+
accentColor="#E50914"
|
|
134
|
+
cardWidth={280}
|
|
135
|
+
cardHeight={420}
|
|
136
|
+
overlayTitleSize={18}
|
|
137
|
+
style={{ flex: 1 }}
|
|
138
|
+
/>
|
|
139
|
+
</LinearGradient>
|
|
168
140
|
);
|
|
169
141
|
}
|
|
170
142
|
```
|
|
171
143
|
|
|
172
|
-
>
|
|
173
|
-
|
|
174
|
-
>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
144
|
+
<p align="center">
|
|
145
|
+
<img src="screenshots/no-results.png" alt="No results for search screen using expo-tvos-search" style="border-radius: 16px;max-width: 100%;"/><br/>
|
|
146
|
+
</p>
|
|
147
|
+
|
|
148
|
+
## Demo App and Common Configurations
|
|
149
|
+
|
|
150
|
+
Explore all configurations in the [demo app](https://github.com/keiver/expo-tvos-search-demo).
|
|
151
|
+
|
|
152
|
+
### Portrait Cards
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
<TvosSearchView
|
|
156
|
+
columns={4}
|
|
157
|
+
cardWidth={280}
|
|
158
|
+
cardHeight={420}
|
|
159
|
+
overlayTitleSize={18}
|
|
160
|
+
// ... other props
|
|
161
|
+
/>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Landscape Cards
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
<TvosSearchView
|
|
168
|
+
columns={3}
|
|
169
|
+
cardWidth={500}
|
|
170
|
+
cardHeight={280}
|
|
171
|
+
// ... other props
|
|
172
|
+
/>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Mini Grid
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
<TvosSearchView
|
|
179
|
+
columns={5}
|
|
180
|
+
cardWidth={240}
|
|
181
|
+
cardHeight={360}
|
|
182
|
+
cardMargin={60} // v1.3.0 - extra spacing
|
|
183
|
+
// ... other props
|
|
184
|
+
/>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### External Titles
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
<TvosSearchView
|
|
191
|
+
showTitle={true}
|
|
192
|
+
showSubtitle={true}
|
|
193
|
+
showTitleOverlay={false}
|
|
194
|
+
// ... other props
|
|
195
|
+
/>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Error Handling
|
|
202
199
|
|
|
203
200
|
```tsx
|
|
204
201
|
<TvosSearchView
|
|
205
|
-
results={results}
|
|
206
|
-
onSearch={handleSearch}
|
|
207
|
-
onSelectItem={handleSelect}
|
|
208
202
|
onError={(e) => {
|
|
209
203
|
const { category, message, context } = e.nativeEvent;
|
|
210
|
-
// Log to your error monitoring service
|
|
211
204
|
console.error(`[Search Error] ${category}: ${message}`, context);
|
|
212
|
-
// Examples: 'module_unavailable', 'validation_failed', 'image_load_failed', 'unknown'
|
|
213
205
|
}}
|
|
214
206
|
onValidationWarning={(e) => {
|
|
215
207
|
const { type, message, context } = e.nativeEvent;
|
|
216
|
-
// Log non-fatal issues for monitoring
|
|
217
208
|
console.warn(`[Validation] ${type}: ${message}`, context);
|
|
218
|
-
// Examples: 'field_truncated', 'value_clamped', 'url_invalid', 'validation_failed'
|
|
219
209
|
}}
|
|
210
|
+
// ... other props
|
|
220
211
|
/>
|
|
221
212
|
```
|
|
222
213
|
|
|
223
|
-
|
|
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
|
|
214
|
+
### Apple TV Hardware Keyboard Support (v1.3.2+)
|
|
228
215
|
|
|
229
|
-
|
|
216
|
+
On real Apple TV hardware, React Native's `RCTTVRemoteHandler` installs gesture recognizers that consume Siri Remote presses before they reach SwiftUI's `.searchable` text field, which prevents keyboard input. When the search field gains focus, this component temporarily disables touch cancellation using the official `react-native-tvos` notification API, and also disables tap/long-press recognizers from parent views (to cover cases like `react-native-gesture-handler`). Swipe and pan recognizers stay active for keyboard navigation. Everything is restored when focus leaves the field. This only applies to physical devices -- the Simulator doesn't need it.
|
|
217
|
+
|
|
218
|
+
If this interferes with gesture handling in your app, please [open an issue](https://github.com/keiver/expo-tvos-search/issues) so we can sort it out.
|
|
230
219
|
|
|
231
|
-
|
|
220
|
+
For additional control, you can use the focus callbacks with `TVEventControl`:
|
|
232
221
|
|
|
233
222
|
```tsx
|
|
223
|
+
import { TVEventControl } from 'react-native';
|
|
224
|
+
|
|
234
225
|
<TvosSearchView
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
cardWidth={420} // Landscape cards
|
|
243
|
-
cardHeight={240} // 16:9 aspect ratio
|
|
244
|
-
style={{ flex: 1 }}
|
|
226
|
+
onSearchFieldFocused={() => {
|
|
227
|
+
TVEventControl.disableGestureHandlersCancelTouches();
|
|
228
|
+
}}
|
|
229
|
+
onSearchFieldBlurred={() => {
|
|
230
|
+
TVEventControl.enableGestureHandlersCancelTouches();
|
|
231
|
+
}}
|
|
232
|
+
// ... other props
|
|
245
233
|
/>
|
|
246
234
|
```
|
|
247
235
|
|
|
248
|
-
|
|
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:
|
|
236
|
+
### Customizing Colors and Card Dimensions
|
|
260
237
|
|
|
261
238
|
```tsx
|
|
262
239
|
<TvosSearchView
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// Card spacing
|
|
269
|
-
cardMargin={60} // Space between cards (default: 40)
|
|
270
|
-
cardPadding={20} // Padding inside overlay (default: 16)
|
|
271
|
-
style={{ flex: 1 }}
|
|
240
|
+
textColor="#E5E5E5"
|
|
241
|
+
accentColor="#E50914"
|
|
242
|
+
cardWidth={420}
|
|
243
|
+
cardHeight={240}
|
|
244
|
+
// ... other props
|
|
272
245
|
/>
|
|
273
246
|
```
|
|
274
247
|
|
|
275
|
-
|
|
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}`
|
|
248
|
+
### Title Overlay Customization (v1.3.0+)
|
|
287
249
|
|
|
288
|
-
|
|
250
|
+
```tsx
|
|
251
|
+
<TvosSearchView
|
|
252
|
+
overlayTitleSize={22}
|
|
253
|
+
enableMarquee={true}
|
|
254
|
+
marqueeDelay={1.5}
|
|
255
|
+
// ... other props
|
|
256
|
+
/>
|
|
257
|
+
```
|
|
289
258
|
|
|
290
|
-
|
|
259
|
+
### Layout Spacing (v1.3.0+)
|
|
291
260
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
261
|
+
```tsx
|
|
262
|
+
<TvosSearchView
|
|
263
|
+
cardMargin={60}
|
|
264
|
+
cardPadding={25}
|
|
265
|
+
// ... other props
|
|
266
|
+
/>
|
|
267
|
+
```
|
|
296
268
|
|
|
297
|
-
|
|
269
|
+
### Image Display Mode
|
|
298
270
|
|
|
299
|
-
|
|
271
|
+
```tsx
|
|
272
|
+
<TvosSearchView
|
|
273
|
+
imageContentMode="fit" // 'fill' (crop), 'fit'/'contain' (letterbox)
|
|
274
|
+
// ... other props
|
|
275
|
+
/>
|
|
276
|
+
```
|
|
300
277
|
|
|
301
278
|
<p align="center">
|
|
302
|
-
<img src="screenshots/
|
|
279
|
+
<img src="screenshots/results.png" alt="Results for search screen using expo-tvos-search" style="border-radius: 16px;max-width: 100%;"/><br/>
|
|
303
280
|
</p>
|
|
304
281
|
|
|
305
282
|
## Props
|
|
306
283
|
|
|
284
|
+
### Core Props
|
|
285
|
+
|
|
307
286
|
| Prop | Type | Default | Description |
|
|
308
287
|
|------|------|---------|-------------|
|
|
309
288
|
| `results` | `SearchResult[]` | `[]` | Array of search results |
|
|
310
289
|
| `columns` | `number` | `5` | Number of columns in the grid |
|
|
311
|
-
| `placeholder` | `string` | `"Search
|
|
290
|
+
| `placeholder` | `string` | `"Search..."` | Search field placeholder |
|
|
291
|
+
| `searchText` | `string` | — | Programmatically set search field text (restore state, deep links) |
|
|
312
292
|
| `isLoading` | `boolean` | `false` | Shows loading indicator |
|
|
293
|
+
|
|
294
|
+
### Card Dimensions & Spacing
|
|
295
|
+
|
|
296
|
+
| Prop | Type | Default | Description |
|
|
297
|
+
|------|------|---------|-------------|
|
|
298
|
+
| `cardWidth` | `number` | `280` | Width of each result card in points |
|
|
299
|
+
| `cardHeight` | `number` | `420` | Height of each result card in points |
|
|
300
|
+
| `cardMargin` | `number` | `40` | **(v1.3.0+)** Spacing between cards in the grid (horizontal and vertical) |
|
|
301
|
+
| `cardPadding` | `number` | `16` | **(v1.3.0+)** Padding inside the card for overlay content (title/subtitle) |
|
|
302
|
+
| `topInset` | `number` | `0` | Top padding (for tab bar clearance) |
|
|
303
|
+
|
|
304
|
+
### Display Options
|
|
305
|
+
|
|
306
|
+
| Prop | Type | Default | Description |
|
|
307
|
+
|------|------|---------|-------------|
|
|
313
308
|
| `showTitle` | `boolean` | `false` | Show title below each result |
|
|
314
309
|
| `showSubtitle` | `boolean` | `false` | Show subtitle below title |
|
|
315
|
-
| `showFocusBorder` | `boolean` | `false` | Show border on focused item |
|
|
316
|
-
| `topInset` | `number` | `0` | Top padding (for tab bar clearance) |
|
|
317
310
|
| `showTitleOverlay` | `boolean` | `true` | Show title overlay with gradient at bottom of card |
|
|
311
|
+
| `showFocusBorder` | `boolean` | `false` | Show border on focused item |
|
|
312
|
+
| `imageContentMode` | `'fill' \| 'fit' \| 'contain'` | `'fill'` | How images fill the card: `fill` (crop to fill), `fit`/`contain` (letterbox) |
|
|
313
|
+
|
|
314
|
+
### Styling & Colors
|
|
315
|
+
|
|
316
|
+
| Prop | Type | Default | Description |
|
|
317
|
+
|------|------|---------|-------------|
|
|
318
|
+
| `textColor` | `string` | system default | Color for text and UI elements (hex format, e.g., "#FFFFFF") |
|
|
319
|
+
| `accentColor` | `string` | `"#FFC312"` | Accent color for focused elements (hex format, e.g., "#FFC312") |
|
|
320
|
+
| `overlayTitleSize` | `number` | `20` | **(v1.3.0+)** Font size for title text in the blur overlay (when showTitleOverlay is true) |
|
|
321
|
+
|
|
322
|
+
### Animation
|
|
323
|
+
|
|
324
|
+
| Prop | Type | Default | Description |
|
|
325
|
+
|------|------|---------|-------------|
|
|
318
326
|
| `enableMarquee` | `boolean` | `true` | Enable marquee scrolling for long titles |
|
|
319
327
|
| `marqueeDelay` | `number` | `1.5` | Delay in seconds before marquee starts |
|
|
320
|
-
|
|
328
|
+
|
|
329
|
+
### Text Customization
|
|
330
|
+
|
|
331
|
+
| Prop | Type | Default | Description |
|
|
332
|
+
|------|------|---------|-------------|
|
|
333
|
+
| `emptyStateText` | `string` | `"Search your library"` | Text shown when search field is empty |
|
|
321
334
|
| `searchingText` | `string` | `"Searching..."` | Text shown during search |
|
|
322
335
|
| `noResultsText` | `string` | `"No results found"` | Text shown when no results found |
|
|
323
336
|
| `noResultsHintText` | `string` | `"Try a different search term"` | Hint text below no results message |
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
|
328
|
-
|
|
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) |
|
|
337
|
+
|
|
338
|
+
### Event Handlers
|
|
339
|
+
|
|
340
|
+
| Prop | Type | Default | Description |
|
|
341
|
+
|------|------|---------|-------------|
|
|
332
342
|
| `onSearch` | `function` | required | Called when search text changes |
|
|
333
343
|
| `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
|
-
| `
|
|
344
|
+
| `onError` | `function` | optional | **(v1.2.0+)** Called when errors occur (image loading failures, validation errors) |
|
|
345
|
+
| `onValidationWarning` | `function` | optional | **(v1.2.0+)** Called for non-fatal warnings (truncated fields, clamped values, invalid URLs) |
|
|
346
|
+
| `onSearchFieldFocused` | `function` | optional | **(v1.3.2+)** Called when native search field gains focus. Use with `TVEventControl` for Apple TV hardware keyboard support. |
|
|
347
|
+
| `onSearchFieldBlurred` | `function` | optional | **(v1.3.2+)** Called when native search field loses focus. Use to re-enable gesture handlers. |
|
|
337
348
|
|
|
338
|
-
|
|
349
|
+
### Other
|
|
339
350
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
title: string;
|
|
344
|
-
subtitle?: string;
|
|
345
|
-
imageUrl?: string;
|
|
346
|
-
}
|
|
347
|
-
```
|
|
351
|
+
| Prop | Type | Default | Description |
|
|
352
|
+
|------|------|---------|-------------|
|
|
353
|
+
| `style` | `ViewStyle` | optional | Style object for the view container |
|
|
348
354
|
|
|
349
355
|
## Result Handling
|
|
350
356
|
|
|
@@ -352,117 +358,9 @@ The native implementation applies the following validation and constraints:
|
|
|
352
358
|
|
|
353
359
|
- **Maximum results**: The results array is capped at 500 items. Any results beyond this limit are silently ignored.
|
|
354
360
|
- **Required fields**: Results with empty `id` or `title` are automatically filtered out and not displayed.
|
|
355
|
-
- **Image URL schemes**:
|
|
361
|
+
- **Image URL schemes**: HTTP, HTTPS, and `data:` URIs are accepted for `imageUrl`. Other URL schemes (e.g., `file://`) are rejected.
|
|
356
362
|
- **HTTPS recommended**: HTTP URLs may be blocked by App Transport Security on tvOS unless explicitly allowed in Info.plist.
|
|
357
363
|
|
|
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
|
-
|
|
423
|
-
## Requirements
|
|
424
|
-
|
|
425
|
-
- Node.js 18+
|
|
426
|
-
- Expo SDK 51+
|
|
427
|
-
- tvOS 15+
|
|
428
|
-
- Project configured for tvOS (`react-native-tvos` + `@react-native-tvos/config-tv`)
|
|
429
|
-
|
|
430
|
-
## Troubleshooting
|
|
431
|
-
|
|
432
|
-
### Native module not found
|
|
433
|
-
|
|
434
|
-
If you see `requireNativeViewManager("ExpoTvosSearch") returned null`, the native module hasn't been built:
|
|
435
|
-
|
|
436
|
-
```bash
|
|
437
|
-
# Clean and rebuild with tvOS support
|
|
438
|
-
EXPO_TV=1 npx expo prebuild --clean
|
|
439
|
-
npx expo run:ios
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
**Note:** Expo Go doesn't support this. Build a dev client or native build instead.
|
|
443
|
-
|
|
444
|
-
### Images not loading
|
|
445
|
-
|
|
446
|
-
1. Verify your image URLs are HTTPS (HTTP may be blocked by App Transport Security)
|
|
447
|
-
2. Ensure required authentication parameters are included in image URLs
|
|
448
|
-
3. For local development, ensure your server is accessible from the Apple TV
|
|
449
|
-
|
|
450
|
-
### Focus issues
|
|
451
|
-
|
|
452
|
-
If focus doesn't move correctly:
|
|
453
|
-
|
|
454
|
-
1. Ensure `columns` prop matches your layout (default: 5)
|
|
455
|
-
2. Check `topInset` if the first row is hidden under the tab bar
|
|
456
|
-
3. The native `.searchable` modifier handles focus automatically - avoid wrapping in focusable containers
|
|
457
|
-
|
|
458
|
-
### Marquee not scrolling
|
|
459
|
-
|
|
460
|
-
If long titles don't scroll when focused:
|
|
461
|
-
|
|
462
|
-
1. Verify `enableMarquee={true}` (default)
|
|
463
|
-
2. Check `marqueeDelay` - scrolling starts after this delay (default: 1.5s)
|
|
464
|
-
3. Text only scrolls if it overflows the card width
|
|
465
|
-
|
|
466
364
|
## Testing
|
|
467
365
|
|
|
468
366
|
Run TypeScript tests:
|
|
@@ -474,6 +372,7 @@ npm run test:coverage # Generate coverage report
|
|
|
474
372
|
```
|
|
475
373
|
|
|
476
374
|
Tests cover:
|
|
375
|
+
|
|
477
376
|
- `isNativeSearchAvailable()` behavior on different platforms
|
|
478
377
|
- Component rendering when native module is unavailable
|
|
479
378
|
- Event structure validation
|
|
@@ -481,6 +380,7 @@ Tests cover:
|
|
|
481
380
|
## Contributing
|
|
482
381
|
|
|
483
382
|
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on:
|
|
383
|
+
|
|
484
384
|
- Code of conduct
|
|
485
385
|
- Development setup
|
|
486
386
|
- Testing requirements
|
|
@@ -489,8 +389,8 @@ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guid
|
|
|
489
389
|
|
|
490
390
|
### Adding New Props
|
|
491
391
|
|
|
492
|
-
If you're adding new props to the library, follow the comprehensive checklist in [
|
|
392
|
+
If you're adding new props to the library, follow the comprehensive checklist in [CLAUDE-adding-new-props.md](./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
393
|
|
|
494
394
|
## License
|
|
495
395
|
|
|
496
|
-
MIT
|
|
396
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|