expo-tvos-search 1.3.1 → 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 +47 -282
- 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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.3.2] - 2026-01-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Apple TV hardware keyboard support**: New `onSearchFieldFocused` and `onSearchFieldBlurred` event callbacks
|
|
12
|
+
- Enables proper Siri Remote keyboard input on physical Apple TV devices
|
|
13
|
+
- Works with `TVEventControl.disableGestureHandlersCancelTouches()` for JS-side handling
|
|
14
|
+
- Native Swift implementation automatically disables tap/press gesture recognizers when search field is focused
|
|
15
|
+
- Keeps swipe/pan recognizers enabled for keyboard navigation
|
|
16
|
+
- TypeScript type `SearchFieldFocusEvent` for the new focus events
|
|
17
|
+
- **Data URI support for images**: `imageUrl` now accepts `data:` URIs (e.g., `data:image/png;base64,...`) in addition to HTTP/HTTPS URLs
|
|
18
|
+
- Enables inline base64-encoded images without external requests
|
|
19
|
+
- Useful for cached thumbnails or placeholder images
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Siri Remote click events not reaching native SwiftUI search field on real Apple TV hardware
|
|
23
|
+
- React Native gesture handlers intercepting keyboard input before it reached SwiftUI `.searchable` modifier
|
|
24
|
+
|
|
25
|
+
## [1.3.1] - 2026-01-21
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Minor stability improvements
|
|
29
|
+
- Documentation updates
|
|
30
|
+
|
|
31
|
+
## [1.3.0] - 2026-01-20
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- `cardMargin` prop - Customize spacing between cards in the grid (default: 40)
|
|
35
|
+
- `cardPadding` prop - Customize padding inside cards for overlay content (default: 16)
|
|
36
|
+
- `overlayTitleSize` prop - Customize font size for title in blur overlay (default: 20)
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- Improved grid layout flexibility with customizable spacing
|
|
40
|
+
|
|
41
|
+
## [1.2.3] - 2026-01-20
|
|
42
|
+
- Patch release for npm publish workflow fix.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- GitHub Actions workflow step for npm publish, trigger on PR with labels: `release:patch`, `release:minor`, `release:major`
|
|
46
|
+
|
|
47
|
+
## [1.2.0] - 2026-01-17
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- `onError` callback - Receive notifications for fatal errors (image loading failures, validation errors)
|
|
51
|
+
- `onValidationWarning` callback - Receive non-fatal warnings (truncated fields, clamped values, invalid URLs)
|
|
52
|
+
- `SearchViewErrorEvent` and `ValidationWarningEvent` TypeScript types
|
|
53
|
+
- JSDoc documentation for all TypeScript exports
|
|
54
|
+
- `SearchEvent` and `SelectItemEvent` type interfaces for improved type safety
|
|
55
|
+
- Coverage thresholds (80% global) in Jest configuration
|
|
56
|
+
- Explicit tvOS modules section in expo-module.config.json
|
|
57
|
+
- Performance and Accessibility documentation sections in README
|
|
58
|
+
- Debug logging for skipped invalid results (id or title missing/empty)
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
- Input validation: `columns` prop now clamps between 1-10 (was: min 1 only)
|
|
62
|
+
- Input validation: `topInset` prop now clamps between 0-500 (was: min 0 only)
|
|
63
|
+
- Input validation: `marqueeDelay` prop now clamps between 0-60 seconds (was: min 0 only)
|
|
64
|
+
- Input validation: `placeholder` prop now limited to 500 characters
|
|
65
|
+
- Input validation: `results` array now limited to 500 items max
|
|
66
|
+
- MarqueeAnimationCalculator guards against division by zero
|
|
67
|
+
- MarqueeAnimationCalculator ensures non-negative values for spacing and distances
|
|
68
|
+
|
|
69
|
+
### Security
|
|
70
|
+
- URL scheme validation: `imageUrl` now only accepts HTTP/HTTPS schemes
|
|
71
|
+
- String length limits: `id`, `title`, `subtitle` clamped to 500 characters each
|
|
72
|
+
- Empty string rejection: Results with empty `id` or `title` are now skipped
|
|
73
|
+
- Added `@types/react-native` and build dependencies for TypeScript compilation
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
- TypeScript build now compiles successfully with `jsx: "react"` option
|
|
77
|
+
- Empty strings no longer pass validation for required fields
|
|
78
|
+
|
|
79
|
+
## [1.1.0] - 2026-01-15
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
- Marquee scrolling animation for long titles that overflow card width
|
|
83
|
+
- `enableMarquee` prop to toggle marquee behavior (default: true)
|
|
84
|
+
- `marqueeDelay` prop to control delay before scrolling starts (default: 1.5s)
|
|
85
|
+
- `showTitleOverlay` prop for gradient title overlay at bottom of cards
|
|
86
|
+
- MarqueeAnimationCalculator for testable animation logic
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
- Title display now uses overlay by default instead of below-card text
|
|
90
|
+
- Improved focus state visual feedback
|
|
91
|
+
|
|
92
|
+
## [1.0.0] - 2026-01-10
|
|
93
|
+
|
|
94
|
+
### Added
|
|
95
|
+
- Initial release
|
|
96
|
+
- Native SwiftUI search view with `.searchable` modifier
|
|
97
|
+
- Grid layout for search results with configurable columns
|
|
98
|
+
- `TvosSearchView` React Native component
|
|
99
|
+
- `isNativeSearchAvailable()` utility function
|
|
100
|
+
- Core props:
|
|
101
|
+
- `results`, `columns`, `placeholder`, `isLoading`
|
|
102
|
+
- `cardWidth`, `cardHeight` - Customizable card dimensions
|
|
103
|
+
- `imageContentMode` - Image scaling (`fill`, `fit`, `contain`)
|
|
104
|
+
- `textColor`, `accentColor` - Color customization
|
|
105
|
+
- `showTitle`, `showSubtitle`, `showFocusBorder` - Display options
|
|
106
|
+
- `topInset` - Tab bar clearance
|
|
107
|
+
- `emptyStateText`, `searchingText`, `noResultsText`, `noResultsHintText` - Text customization
|
|
108
|
+
- `onSearch` and `onSelectItem` event callbacks
|
|
109
|
+
- Automatic fallback when native module is unavailable
|
|
110
|
+
- TypeScript type definitions (`SearchResult`, `TvosSearchViewProps`, etc.)
|
|
111
|
+
- Comprehensive test suite
|
package/README.md
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
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
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.
|
|
9
8
|
|
|
@@ -13,7 +12,7 @@ A native tvOS search component for Expo and React Native using SwiftUI's `.searc
|
|
|
13
12
|
- React Native tvOS 0.71+
|
|
14
13
|
|
|
15
14
|
<p align="center">
|
|
16
|
-
<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%;"/>
|
|
17
16
|
</p>
|
|
18
17
|
|
|
19
18
|
## Installation
|
|
@@ -28,55 +27,18 @@ Or install from GitHub:
|
|
|
28
27
|
npx expo install github:keiver/expo-tvos-search
|
|
29
28
|
```
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
## Prerequisites for tvOS Builds
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
Your project must be configured for React Native tvOS to use this module.
|
|
34
33
|
|
|
35
|
-
###
|
|
34
|
+
### 1. Install react-native-tvos
|
|
36
35
|
|
|
37
|
-
**[expo-tvos-search-demo](https://github.com/keiver/expo-tvos-search-demo)** - Comprehensive showcase with 7 tabs demonstrating all library features:
|
|
38
|
-
|
|
39
|
-
- **Default** - 4-column grid with custom colors
|
|
40
|
-
- **Portrait** - Netflix-style tall cards (280×420)
|
|
41
|
-
- **Landscape** - Wide 16:9 cards (500×280)
|
|
42
|
-
- **Mini** - Compact 5-column layout (240×360)
|
|
43
|
-
- **External Title** - Titles displayed below cards
|
|
44
|
-
- **Minimal** - Bare minimum setup (5 props)
|
|
45
|
-
- **Help** - Feature overview and usage guide
|
|
46
|
-
|
|
47
|
-
Clone and run:
|
|
48
36
|
```bash
|
|
49
|
-
|
|
50
|
-
cd expo-tvos-search-demo
|
|
51
|
-
npm install
|
|
52
|
-
npm run prebuild
|
|
53
|
-
npm run tvos
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
The demo uses a planet search theme with 8 planets (Mercury to Neptune) and demonstrates all library features with real working code.
|
|
57
|
-
|
|
58
|
-
## Prerequisites for tvOS Builds (Expo)
|
|
59
|
-
|
|
60
|
-
Your project must be configured for React Native tvOS to build and run this module.
|
|
61
|
-
|
|
62
|
-
**Quick Checklist:**
|
|
63
|
-
|
|
64
|
-
- ✅ `react-native-tvos` in use
|
|
65
|
-
- ✅ `@react-native-tvos/config-tv` installed + added to Expo plugins
|
|
66
|
-
- ✅ Run prebuild with `EXPO_TV=1`
|
|
67
|
-
|
|
68
|
-
### 1. Swap to react-native-tvos
|
|
69
|
-
|
|
70
|
-
Replace `react-native` with the [tvOS fork](https://github.com/react-native-tvos/react-native-tvos):
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
npm remove react-native && npm install react-native-tvos@latest
|
|
37
|
+
npm install react-native-tvos@latest
|
|
74
38
|
```
|
|
75
39
|
|
|
76
40
|
### 2. Install the tvOS config plugin
|
|
77
41
|
|
|
78
|
-
Install:
|
|
79
|
-
|
|
80
42
|
```bash
|
|
81
43
|
npx expo install @react-native-tvos/config-tv
|
|
82
44
|
```
|
|
@@ -91,32 +53,9 @@ Then add the plugin in `app.json` / `app.config.js`:
|
|
|
91
53
|
}
|
|
92
54
|
```
|
|
93
55
|
|
|
94
|
-
### 3. Generate native projects with tvOS enabled
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
EXPO_TV=1 npx expo prebuild --clean
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Then run:
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
npx expo run:ios
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
|
|
107
56
|
## Usage
|
|
108
57
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
For the absolute minimum setup, see the [Minimal tab](https://github.com/keiver/expo-tvos-search-demo/blob/main/app/(tabs)/minimal.tsx) in the demo app.
|
|
112
|
-
|
|
113
|
-
<p align="center">
|
|
114
|
-
<img src="screenshots/demo-default.png" width="80%" alt="Minimal demo screen for expo-tvos-search" style="border-radius: 16px;max-width: 100%;"/><br/>
|
|
115
|
-
</p>
|
|
116
|
-
|
|
117
|
-
### Complete Example
|
|
118
|
-
|
|
119
|
-
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 with best practices:
|
|
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:
|
|
120
59
|
|
|
121
60
|
```tsx
|
|
122
61
|
import { useState } from 'react';
|
|
@@ -136,7 +75,7 @@ const PLANETS: SearchResult[] = [
|
|
|
136
75
|
subtitle: 'Our home planet, the only known world to harbor life',
|
|
137
76
|
imageUrl: require('./assets/planets/earth.webp'),
|
|
138
77
|
},
|
|
139
|
-
// ...
|
|
78
|
+
// ... all planets
|
|
140
79
|
];
|
|
141
80
|
|
|
142
81
|
export default function SearchScreen() {
|
|
@@ -154,7 +93,7 @@ export default function SearchScreen() {
|
|
|
154
93
|
|
|
155
94
|
setIsLoading(true);
|
|
156
95
|
|
|
157
|
-
// Debounce search (300ms)
|
|
96
|
+
// Debounce dummy search (300ms)
|
|
158
97
|
setTimeout(() => {
|
|
159
98
|
const filtered = PLANETS.filter(
|
|
160
99
|
planet =>
|
|
@@ -194,7 +133,7 @@ export default function SearchScreen() {
|
|
|
194
133
|
accentColor="#E50914"
|
|
195
134
|
cardWidth={280}
|
|
196
135
|
cardHeight={420}
|
|
197
|
-
overlayTitleSize={18}
|
|
136
|
+
overlayTitleSize={18}
|
|
198
137
|
style={{ flex: 1 }}
|
|
199
138
|
/>
|
|
200
139
|
</LinearGradient>
|
|
@@ -202,9 +141,13 @@ export default function SearchScreen() {
|
|
|
202
141
|
}
|
|
203
142
|
```
|
|
204
143
|
|
|
205
|
-
|
|
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
|
|
206
149
|
|
|
207
|
-
Explore all
|
|
150
|
+
Explore all configurations in the [demo app](https://github.com/keiver/expo-tvos-search-demo).
|
|
208
151
|
|
|
209
152
|
### Portrait Cards
|
|
210
153
|
|
|
@@ -268,6 +211,28 @@ Explore all 7 configurations in the [demo app](https://github.com/keiver/expo-tv
|
|
|
268
211
|
/>
|
|
269
212
|
```
|
|
270
213
|
|
|
214
|
+
### Apple TV Hardware Keyboard Support (v1.3.2+)
|
|
215
|
+
|
|
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.
|
|
219
|
+
|
|
220
|
+
For additional control, you can use the focus callbacks with `TVEventControl`:
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
import { TVEventControl } from 'react-native';
|
|
224
|
+
|
|
225
|
+
<TvosSearchView
|
|
226
|
+
onSearchFieldFocused={() => {
|
|
227
|
+
TVEventControl.disableGestureHandlersCancelTouches();
|
|
228
|
+
}}
|
|
229
|
+
onSearchFieldBlurred={() => {
|
|
230
|
+
TVEventControl.enableGestureHandlersCancelTouches();
|
|
231
|
+
}}
|
|
232
|
+
// ... other props
|
|
233
|
+
/>
|
|
234
|
+
```
|
|
235
|
+
|
|
271
236
|
### Customizing Colors and Card Dimensions
|
|
272
237
|
|
|
273
238
|
```tsx
|
|
@@ -310,101 +275,8 @@ Explore all 7 configurations in the [demo app](https://github.com/keiver/expo-tv
|
|
|
310
275
|
/>
|
|
311
276
|
```
|
|
312
277
|
|
|
313
|
-
## TypeScript Support
|
|
314
|
-
|
|
315
|
-
The library provides comprehensive type definitions for all events and props.
|
|
316
|
-
|
|
317
|
-
### Event Types
|
|
318
|
-
|
|
319
|
-
```typescript
|
|
320
|
-
import type {
|
|
321
|
-
SearchEvent,
|
|
322
|
-
SelectItemEvent,
|
|
323
|
-
SearchViewErrorEvent,
|
|
324
|
-
ValidationWarningEvent,
|
|
325
|
-
SearchResult,
|
|
326
|
-
} from 'expo-tvos-search';
|
|
327
|
-
|
|
328
|
-
// Search event - fired on text change
|
|
329
|
-
interface SearchEvent {
|
|
330
|
-
nativeEvent: {
|
|
331
|
-
query: string;
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Selection event - fired when result is selected
|
|
336
|
-
interface SelectItemEvent {
|
|
337
|
-
nativeEvent: {
|
|
338
|
-
id: string;
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Error event - fatal errors (v1.2.0+)
|
|
343
|
-
interface SearchViewErrorEvent {
|
|
344
|
-
nativeEvent: {
|
|
345
|
-
category: 'module_unavailable' | 'validation_failed' | 'image_load_failed' | 'unknown';
|
|
346
|
-
message: string;
|
|
347
|
-
context?: string;
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Validation warning - non-fatal issues (v1.2.0+)
|
|
352
|
-
interface ValidationWarningEvent {
|
|
353
|
-
nativeEvent: {
|
|
354
|
-
type: 'field_truncated' | 'value_clamped' | 'url_invalid' | 'validation_failed';
|
|
355
|
-
message: string;
|
|
356
|
-
context?: string;
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Search result shape
|
|
361
|
-
interface SearchResult {
|
|
362
|
-
id: string; // Required, max 500 chars
|
|
363
|
-
title: string; // Required, max 500 chars
|
|
364
|
-
subtitle?: string; // Optional, max 500 chars
|
|
365
|
-
imageUrl?: string; // Optional, HTTPS recommended
|
|
366
|
-
}
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
### Typed Usage
|
|
370
|
-
|
|
371
|
-
```typescript
|
|
372
|
-
const handleSearch = (event: SearchEvent) => {
|
|
373
|
-
const query = event.nativeEvent.query;
|
|
374
|
-
// TypeScript knows query is a string
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
const handleSelect = (event: SelectItemEvent) => {
|
|
378
|
-
const id = event.nativeEvent.id;
|
|
379
|
-
// TypeScript knows id is a string
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
const handleError = (event: SearchViewErrorEvent) => {
|
|
383
|
-
const { category, message, context } = event.nativeEvent;
|
|
384
|
-
// Full autocomplete for category values
|
|
385
|
-
if (category === 'image_load_failed') {
|
|
386
|
-
logger.warn(`Image failed to load: ${message}`, { context });
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
## Demo Apps & Examples
|
|
392
|
-
|
|
393
|
-
### Official Demo App
|
|
394
|
-
|
|
395
|
-
**[expo-tvos-search-demo](https://github.com/keiver/expo-tvos-search-demo)** - Complete working examples with 7 different layout styles. Browse the [source code](https://github.com/keiver/expo-tvos-search-demo/tree/main/app/(tabs)) for each configuration.
|
|
396
|
-
|
|
397
|
-
### Apps Using This Library
|
|
398
|
-
|
|
399
|
-
**[Tomo TV](https://github.com/keiver/tomotv)** - Full-featured tvOS Jellyfin client
|
|
400
|
-
- Real-world integration with media library API
|
|
401
|
-
- Advanced search with live server calls
|
|
402
|
-
- Complete authentication and navigation flow
|
|
403
|
-
|
|
404
|
-
## See it in action:
|
|
405
|
-
|
|
406
278
|
<p align="center">
|
|
407
|
-
<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/>
|
|
408
280
|
</p>
|
|
409
281
|
|
|
410
282
|
## Props
|
|
@@ -415,7 +287,8 @@ const handleError = (event: SearchViewErrorEvent) => {
|
|
|
415
287
|
|------|------|---------|-------------|
|
|
416
288
|
| `results` | `SearchResult[]` | `[]` | Array of search results |
|
|
417
289
|
| `columns` | `number` | `5` | Number of columns in the grid |
|
|
418
|
-
| `placeholder` | `string` | `"Search
|
|
290
|
+
| `placeholder` | `string` | `"Search..."` | Search field placeholder |
|
|
291
|
+
| `searchText` | `string` | — | Programmatically set search field text (restore state, deep links) |
|
|
419
292
|
| `isLoading` | `boolean` | `false` | Shows loading indicator |
|
|
420
293
|
|
|
421
294
|
### Card Dimensions & Spacing
|
|
@@ -457,7 +330,7 @@ const handleError = (event: SearchViewErrorEvent) => {
|
|
|
457
330
|
|
|
458
331
|
| Prop | Type | Default | Description |
|
|
459
332
|
|------|------|---------|-------------|
|
|
460
|
-
| `emptyStateText` | `string` | `"Search
|
|
333
|
+
| `emptyStateText` | `string` | `"Search your library"` | Text shown when search field is empty |
|
|
461
334
|
| `searchingText` | `string` | `"Searching..."` | Text shown during search |
|
|
462
335
|
| `noResultsText` | `string` | `"No results found"` | Text shown when no results found |
|
|
463
336
|
| `noResultsHintText` | `string` | `"Try a different search term"` | Hint text below no results message |
|
|
@@ -470,6 +343,8 @@ const handleError = (event: SearchViewErrorEvent) => {
|
|
|
470
343
|
| `onSelectItem` | `function` | required | Called when result is selected |
|
|
471
344
|
| `onError` | `function` | optional | **(v1.2.0+)** Called when errors occur (image loading failures, validation errors) |
|
|
472
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. |
|
|
473
348
|
|
|
474
349
|
### Other
|
|
475
350
|
|
|
@@ -477,127 +352,15 @@ const handleError = (event: SearchViewErrorEvent) => {
|
|
|
477
352
|
|------|------|---------|-------------|
|
|
478
353
|
| `style` | `ViewStyle` | optional | Style object for the view container |
|
|
479
354
|
|
|
480
|
-
## SearchResult Type
|
|
481
|
-
|
|
482
|
-
```typescript
|
|
483
|
-
interface SearchResult {
|
|
484
|
-
id: string;
|
|
485
|
-
title: string;
|
|
486
|
-
subtitle?: string;
|
|
487
|
-
imageUrl?: string;
|
|
488
|
-
}
|
|
489
|
-
```
|
|
490
|
-
|
|
491
355
|
## Result Handling
|
|
492
356
|
|
|
493
357
|
The native implementation applies the following validation and constraints:
|
|
494
358
|
|
|
495
359
|
- **Maximum results**: The results array is capped at 500 items. Any results beyond this limit are silently ignored.
|
|
496
360
|
- **Required fields**: Results with empty `id` or `title` are automatically filtered out and not displayed.
|
|
497
|
-
- **Image URL schemes**:
|
|
361
|
+
- **Image URL schemes**: HTTP, HTTPS, and `data:` URIs are accepted for `imageUrl`. Other URL schemes (e.g., `file://`) are rejected.
|
|
498
362
|
- **HTTPS recommended**: HTTP URLs may be blocked by App Transport Security on tvOS unless explicitly allowed in Info.plist.
|
|
499
363
|
|
|
500
|
-
## Focus Handling - Do's and Don'ts
|
|
501
|
-
|
|
502
|
-
The native `.searchable` modifier manages focus automatically. Here's what to do and what to avoid:
|
|
503
|
-
|
|
504
|
-
### ✅ Do: Render directly in your screen
|
|
505
|
-
|
|
506
|
-
```tsx
|
|
507
|
-
function SearchScreen() {
|
|
508
|
-
return (
|
|
509
|
-
<TvosSearchView
|
|
510
|
-
results={results}
|
|
511
|
-
onSearch={handleSearch}
|
|
512
|
-
onSelectItem={handleSelect}
|
|
513
|
-
style={{ flex: 1 }}
|
|
514
|
-
/>
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
### ❌ Don't: Wrap in focusable containers
|
|
520
|
-
|
|
521
|
-
```tsx
|
|
522
|
-
// ❌ WRONG - breaks focus navigation
|
|
523
|
-
function SearchScreen() {
|
|
524
|
-
return (
|
|
525
|
-
<Pressable> {/* Don't wrap in Pressable */}
|
|
526
|
-
<TvosSearchView ... />
|
|
527
|
-
</Pressable>
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// ❌ WRONG - interferes with native focus
|
|
532
|
-
function SearchScreen() {
|
|
533
|
-
return (
|
|
534
|
-
<TouchableOpacity> {/* Don't wrap in TouchableOpacity */}
|
|
535
|
-
<TvosSearchView ... />
|
|
536
|
-
</TouchableOpacity>
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
**Why this breaks**: Focusable wrappers steal focus from the native SwiftUI search container, which breaks directional navigation.
|
|
542
|
-
|
|
543
|
-
### ✅ Do: Use non-interactive containers
|
|
544
|
-
|
|
545
|
-
```tsx
|
|
546
|
-
// ✅ CORRECT - View is not focusable
|
|
547
|
-
function SearchScreen() {
|
|
548
|
-
return (
|
|
549
|
-
<View style={{ flex: 1, backgroundColor: '#000' }}>
|
|
550
|
-
<TvosSearchView ... />
|
|
551
|
-
</View>
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// ✅ CORRECT - SafeAreaView is not focusable
|
|
556
|
-
function SearchScreen() {
|
|
557
|
-
return (
|
|
558
|
-
<SafeAreaView style={{ flex: 1 }}>
|
|
559
|
-
<TvosSearchView ... />
|
|
560
|
-
</SafeAreaView>
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
## Troubleshooting
|
|
566
|
-
|
|
567
|
-
### Native module not found
|
|
568
|
-
|
|
569
|
-
If you see `requireNativeViewManager("ExpoTvosSearch") returned null`, the native module hasn't been built:
|
|
570
|
-
|
|
571
|
-
```bash
|
|
572
|
-
# Clean and rebuild with tvOS support
|
|
573
|
-
EXPO_TV=1 npx expo prebuild --clean
|
|
574
|
-
npx expo run:ios
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
**Note:** Expo Go doesn't support this. Build a dev client or native build instead.
|
|
578
|
-
|
|
579
|
-
### Images not loading
|
|
580
|
-
|
|
581
|
-
1. Verify your image URLs are HTTPS (HTTP may be blocked by App Transport Security)
|
|
582
|
-
2. Ensure required authentication parameters are included in image URLs
|
|
583
|
-
3. For local development, ensure your server is accessible from the Apple TV
|
|
584
|
-
|
|
585
|
-
### Focus issues
|
|
586
|
-
|
|
587
|
-
If focus doesn't move correctly:
|
|
588
|
-
|
|
589
|
-
1. Ensure `columns` prop matches your layout (default: 5)
|
|
590
|
-
2. Check `topInset` if the first row is hidden under the tab bar
|
|
591
|
-
3. The native `.searchable` modifier handles focus automatically - avoid wrapping in focusable containers
|
|
592
|
-
|
|
593
|
-
### Marquee not scrolling
|
|
594
|
-
|
|
595
|
-
If long titles don't scroll when focused:
|
|
596
|
-
|
|
597
|
-
1. Verify `enableMarquee={true}` (default)
|
|
598
|
-
2. Check `marqueeDelay` - scrolling starts after this delay (default: 1.5s)
|
|
599
|
-
3. Text only scrolls if it overflows the card width
|
|
600
|
-
|
|
601
364
|
## Testing
|
|
602
365
|
|
|
603
366
|
Run TypeScript tests:
|
|
@@ -609,6 +372,7 @@ npm run test:coverage # Generate coverage report
|
|
|
609
372
|
```
|
|
610
373
|
|
|
611
374
|
Tests cover:
|
|
375
|
+
|
|
612
376
|
- `isNativeSearchAvailable()` behavior on different platforms
|
|
613
377
|
- Component rendering when native module is unavailable
|
|
614
378
|
- Event structure validation
|
|
@@ -616,6 +380,7 @@ Tests cover:
|
|
|
616
380
|
## Contributing
|
|
617
381
|
|
|
618
382
|
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on:
|
|
383
|
+
|
|
619
384
|
- Code of conduct
|
|
620
385
|
- Development setup
|
|
621
386
|
- Testing requirements
|
|
@@ -628,4 +393,4 @@ If you're adding new props to the library, follow the comprehensive checklist in
|
|
|
628
393
|
|
|
629
394
|
## License
|
|
630
395
|
|
|
631
|
-
MIT
|
|
396
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
package/build/index.d.ts
CHANGED
|
@@ -45,13 +45,21 @@ export interface SearchViewErrorEvent {
|
|
|
45
45
|
export interface ValidationWarningEvent {
|
|
46
46
|
nativeEvent: {
|
|
47
47
|
/** Type of validation warning */
|
|
48
|
-
type: "field_truncated" | "value_clamped" | "url_invalid" | "validation_failed";
|
|
48
|
+
type: "field_truncated" | "value_clamped" | "value_truncated" | "results_truncated" | "url_invalid" | "url_insecure" | "validation_failed";
|
|
49
49
|
/** Human-readable warning message */
|
|
50
50
|
message: string;
|
|
51
51
|
/** Optional additional context */
|
|
52
52
|
context?: string;
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Event payload for search field focus changes.
|
|
57
|
+
* Fired when the native search field gains or loses focus.
|
|
58
|
+
* Useful for managing RN gesture handlers via TVEventControl.
|
|
59
|
+
*/
|
|
60
|
+
export interface SearchFieldFocusEvent {
|
|
61
|
+
nativeEvent: Record<string, never>;
|
|
62
|
+
}
|
|
55
63
|
/**
|
|
56
64
|
* Represents a single search result displayed in the grid.
|
|
57
65
|
*/
|
|
@@ -62,7 +70,7 @@ export interface SearchResult {
|
|
|
62
70
|
title: string;
|
|
63
71
|
/** Optional secondary text displayed below the title */
|
|
64
72
|
subtitle?: string;
|
|
65
|
-
/** Optional image URL for the result poster/thumbnail */
|
|
73
|
+
/** Optional image URL for the result poster/thumbnail. Supports HTTPS, HTTP, and data: URIs */
|
|
66
74
|
imageUrl?: string;
|
|
67
75
|
}
|
|
68
76
|
/**
|
|
@@ -73,7 +81,7 @@ export interface SearchResult {
|
|
|
73
81
|
* <TvosSearchView
|
|
74
82
|
* results={searchResults}
|
|
75
83
|
* columns={5}
|
|
76
|
-
* placeholder="Search
|
|
84
|
+
* placeholder="Search..."
|
|
77
85
|
* isLoading={loading}
|
|
78
86
|
* topInset={140}
|
|
79
87
|
* onSearch={(e) => handleSearch(e.nativeEvent.query)}
|
|
@@ -101,9 +109,20 @@ export interface TvosSearchViewProps {
|
|
|
101
109
|
columns?: number;
|
|
102
110
|
/**
|
|
103
111
|
* Placeholder text shown in the search field when empty.
|
|
104
|
-
* @default "Search
|
|
112
|
+
* @default "Search..."
|
|
105
113
|
*/
|
|
106
114
|
placeholder?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Programmatically set the search field text.
|
|
117
|
+
* Works like React Native TextInput's `value` + `onChangeText` pattern.
|
|
118
|
+
* Useful for restoring search state, deep links, or "search for similar" flows.
|
|
119
|
+
*
|
|
120
|
+
* **Warning:** Avoid setting `searchText` inside your `onSearch` handler with
|
|
121
|
+
* transforms (e.g., trimming, lowercasing). The native guard only prevents
|
|
122
|
+
* same-value loops — transformed values will trigger a new `onSearch` event,
|
|
123
|
+
* creating an infinite update cycle.
|
|
124
|
+
*/
|
|
125
|
+
searchText?: string;
|
|
107
126
|
/**
|
|
108
127
|
* Whether to show a loading indicator.
|
|
109
128
|
* @default false
|
|
@@ -155,7 +174,7 @@ export interface TvosSearchViewProps {
|
|
|
155
174
|
marqueeDelay?: number;
|
|
156
175
|
/**
|
|
157
176
|
* Text displayed when the search field is empty and no results are shown.
|
|
158
|
-
* @default "Search
|
|
177
|
+
* @default "Search your library"
|
|
159
178
|
*/
|
|
160
179
|
emptyStateText?: string;
|
|
161
180
|
/**
|
|
@@ -231,6 +250,9 @@ export interface TvosSearchViewProps {
|
|
|
231
250
|
/**
|
|
232
251
|
* Callback fired when the search text changes.
|
|
233
252
|
* Debounce this handler to avoid excessive API calls.
|
|
253
|
+
*
|
|
254
|
+
* **Note:** If using the `searchText` prop, do not set it to a transformed
|
|
255
|
+
* value inside this handler — see `searchText` docs for loop prevention.
|
|
234
256
|
*/
|
|
235
257
|
onSearch: (event: SearchEvent) => void;
|
|
236
258
|
/**
|
|
@@ -262,6 +284,36 @@ export interface TvosSearchViewProps {
|
|
|
262
284
|
* ```
|
|
263
285
|
*/
|
|
264
286
|
onValidationWarning?: (event: ValidationWarningEvent) => void;
|
|
287
|
+
/**
|
|
288
|
+
* Optional callback fired when the native search field gains focus.
|
|
289
|
+
* Use this to disable RN gesture handlers via TVEventControl if the
|
|
290
|
+
* automatic gesture handling doesn't work on your device.
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```tsx
|
|
294
|
+
* import { TVEventControl } from 'react-native';
|
|
295
|
+
*
|
|
296
|
+
* onSearchFieldFocused={() => {
|
|
297
|
+
* TVEventControl.disableGestureHandlersCancelTouches();
|
|
298
|
+
* }}
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
onSearchFieldFocused?: (event: SearchFieldFocusEvent) => void;
|
|
302
|
+
/**
|
|
303
|
+
* Optional callback fired when the native search field loses focus.
|
|
304
|
+
* Use this to re-enable RN gesture handlers via TVEventControl if you
|
|
305
|
+
* disabled them in onSearchFieldFocused.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```tsx
|
|
309
|
+
* import { TVEventControl } from 'react-native';
|
|
310
|
+
*
|
|
311
|
+
* onSearchFieldBlurred={() => {
|
|
312
|
+
* TVEventControl.enableGestureHandlersCancelTouches();
|
|
313
|
+
* }}
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
onSearchFieldBlurred?: (event: SearchFieldFocusEvent) => void;
|
|
265
317
|
/**
|
|
266
318
|
* Optional style for the view container.
|
|
267
319
|
*/
|