@24i/bigscreen-sdk 1.0.52 → 1.0.53-alpha.2837

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.
Files changed (51) hide show
  1. package/package.json +4 -3
  2. package/packages/analytics/src/clients/GoogleAnalytics/constants.ts +10 -2
  3. package/packages/analytics/src/clients/GoogleAnalytics/interface.ts +5 -1
  4. package/packages/analytics/src/clients/TwentyFourIQ/TwentyFourIQClient.ts +2 -0
  5. package/packages/analytics/src/clients/TwentyFourIQ/constants.ts +15 -4
  6. package/packages/analytics/src/clients/TwentyFourIQ/interface.ts +7 -4
  7. package/packages/analytics/src/clients/TwentyFourIQ/mappers/mapAdEvent.ts +20 -0
  8. package/packages/analytics/src/clients/TwentyFourIQ/mappers/mapBase.ts +2 -0
  9. package/packages/analytics/src/clients/TwentyFourIQ/mappers/mapPlayerClose.ts +2 -1
  10. package/packages/analytics/src/clients/TwentyFourIQ/mappers/mapVideoComplete.ts +2 -1
  11. package/packages/analytics/src/clients/TwentyFourIQ/mappers/mapVideoPause.ts +2 -1
  12. package/packages/analytics/src/clients/TwentyFourIQ/mappers/mapVideoProgress.ts +2 -1
  13. package/packages/analytics/src/clients/TwentyFourIQ/types.ts +38 -29
  14. package/packages/driver-androidtv/src/DeviceAndroidTV.ts +14 -0
  15. package/packages/driver-androidtv/src/__mocks__/javaScriptBridge.ts +5 -0
  16. package/packages/driver-androidtv/src/index.ts +1 -0
  17. package/packages/driver-androidtv/src/types.ts +22 -0
  18. package/packages/driver-base/src/DeviceBase.ts +14 -0
  19. package/packages/driver-base/src/__mocks__/DeviceBase.ts +13 -0
  20. package/packages/driver-base/src/index.ts +3 -0
  21. package/packages/driver-base/src/types/InAppPurchase.ts +25 -0
  22. package/packages/driver-browser/src/DeviceBrowser.ts +14 -0
  23. package/packages/driver-entone/src/DeviceEntone.ts +19 -1
  24. package/packages/driver-firetv/src/DeviceFireTV.ts +59 -1
  25. package/packages/driver-firetv/src/__mocks__/javaScriptBridge.ts +5 -0
  26. package/packages/driver-firetv/src/types.ts +1 -0
  27. package/packages/driver-hbbtv/src/DeviceHbbTV.ts +14 -0
  28. package/packages/driver-kreatv/src/DeviceKreaTV.ts +14 -0
  29. package/packages/driver-saphi/src/DeviceSaphi.ts +14 -0
  30. package/packages/driver-smartcast/src/DeviceSmartCast.ts +49 -0
  31. package/packages/driver-smartcast/src/__mocks__/api.ts +26 -0
  32. package/packages/driver-smartcast/src/types.ts +36 -0
  33. package/packages/driver-tizen/src/DeviceTizen.ts +102 -7
  34. package/packages/driver-tizen/src/ITizen.ts +15116 -202
  35. package/packages/driver-tizen/src/IWebapis.ts +8641 -92
  36. package/packages/driver-tizen/src/TizenKeys.ts +2 -2
  37. package/packages/driver-tizen/src/__mocks__/tizen.ts +0 -8
  38. package/packages/driver-tizen/src/constants.ts +1 -195
  39. package/packages/driver-vidaa/src/DeviceVidaa.ts +14 -0
  40. package/packages/driver-webos/src/DeviceWebos.ts +14 -0
  41. package/packages/driver-xbox/src/DeviceXbox.ts +19 -1
  42. package/packages/input/src/Input.tsx +6 -0
  43. package/packages/list/DoubleList/README.md +105 -0
  44. package/packages/list/DoubleList/index.ts +1 -0
  45. package/packages/list/src/DoubleList/DoubleList.tsx +267 -0
  46. package/packages/list/src/DoubleList/DoubleListAdapter.ts +72 -0
  47. package/packages/list/src/DoubleList/config.ts +15 -0
  48. package/packages/list/src/DoubleList/interface.ts +82 -0
  49. package/packages/analytics/src/clients/TwentyFourIQ/mappers/mapAdLoaded.ts +0 -13
  50. package/packages/analytics/src/clients/TwentyFourIQ/mappers/mapAdSkipped.ts +0 -16
  51. package/packages/driver-tizen/src/types.ts +0 -14
@@ -1,7 +1,7 @@
1
1
  import { KeyMap, KeycodeKeyMap } from '@24i/bigscreen-sdk/driver-base';
2
- import { ITizen } from './types';
2
+ import { Tizen } from './ITizen';
3
3
 
4
- declare const tizen: ITizen;
4
+ declare const tizen: Tizen;
5
5
 
6
6
  type KeyCodeMap = { [key: string]: number };
7
7
 
@@ -32,11 +32,3 @@ export const tizen = {
32
32
  getPropertyValue: async () => {},
33
33
  },
34
34
  };
35
-
36
- declare global {
37
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
38
- interface Window {
39
- webapis: typeof webapis,
40
- tizen: typeof tizen,
41
- }
42
- }
@@ -1,197 +1,3 @@
1
- /* eslint-disable no-magic-numbers */
2
-
3
1
  export const VENDOR = 'Samsung Tizen';
4
2
 
5
- // MARK: TVAudioControl
6
-
7
- export type AudioOutputMode = (
8
- | 'PCM'
9
- | 'DOLBY'
10
- | 'DTS'
11
- | 'AAC'
12
- );
13
-
14
- export type AudioBeepType = (
15
- | 'UP'
16
- | 'DOWN'
17
- | 'LEFT'
18
- | 'RIGHT'
19
- | 'PAGE_LEFT'
20
- | 'PAGE_RIGHT'
21
- | 'BACK'
22
- | 'SELECT'
23
- | 'CANCEL'
24
- | 'KEYPAD'
25
- | 'KEYPAD_ENTER'
26
- | 'KEYPAD_DEL'
27
- | 'MOVE'
28
- | 'PREPARING'
29
- );
30
-
31
- // MARK: SystemInfo
32
-
33
- export type SystemInfoPropertyId = (
34
- | 'BATTERY'
35
- | 'CPU'
36
- | 'STORAGE'
37
- | 'DISPLAY'
38
- | 'DEVICE_ORIENTATION'
39
- | 'BUILD'
40
- | 'LOCALE'
41
- | 'NETWORK'
42
- | 'WIFI_NETWORK'
43
- | 'ETHERNET_NETWORK'
44
- | 'CELLULAR_NETWORK'
45
- | 'NET_PROXY_NETWORK'
46
- | 'SIM'
47
- | 'PERIPHERAL'
48
- | 'MEMORY'
49
- | 'VIDEOSOURCE'
50
- | 'CAMERA_FLASH'
51
- | 'ADS'
52
- | 'SERVICE_COUNTRY'
53
- | 'SOURCE_INFO'
54
- | 'PANEL'
55
- );
56
-
57
- export type SystemInfoNetworkType = (
58
- | 'NONE'
59
- | '2G'
60
- | '2.5G'
61
- | '3G'
62
- | '4G'
63
- | 'WIFI'
64
- | 'ETHERNET'
65
- | 'NET_PROXY'
66
- | 'UNKNOWN'
67
- );
68
-
69
- export type SystemInfoWifiSecurityMode = (
70
- | 'NONE'
71
- | 'WEP'
72
- | 'WPA_PSK'
73
- | 'WPA2_PSK'
74
- | 'EAP'
75
- );
76
-
77
- export type SystemInfoWifiEncryptionType = (
78
- | 'NONE'
79
- | 'WEP'
80
- | 'TKIP'
81
- | 'AES'
82
- | 'TKIP_AES_MIXED'
83
- );
84
-
85
- export type SystemInfoNetworkIpMode = (
86
- | 'NONE'
87
- | 'STATIC'
88
- | 'DYNAMIC'
89
- | 'AUTO'
90
- | 'FIXED'
91
- );
92
-
93
- export type SystemInfoDeviceOrientationStatus = (
94
- | 'PORTRAIT_PRIMARY'
95
- | 'PORTRAIT_SECONDARY'
96
- | 'LANDSCAPE_PRIMARY'
97
- | 'LANDSCAPE_SECONDARY'
98
- );
99
-
100
- export type SystemInfoSimState = (
101
- | 'ABSENT'
102
- | 'INITIALIZING'
103
- | 'READY'
104
- | 'PIN_REQUIRED'
105
- | 'PUK_REQUIRED'
106
- | 'NETWORK_LOCKED'
107
- | 'SIM_LOCKED'
108
- | 'UNKNOWN'
109
- );
110
-
111
- export type SystemInfoProfile = 'MOBILE_FULL' | 'MOBILE_WEB' | 'MOBILE' | 'WEARABLE' | 'TV';
112
-
113
- export type SystemInfoLowMemoryStatus = 'NORMAL' | 'WARNING';
114
-
115
- export type SystemInfoVideoSourceType = (
116
- | 'TV'
117
- | 'AV'
118
- | 'SVIDEO'
119
- | 'COMP'
120
- | 'PC'
121
- | 'HDMI'
122
- | 'SCART'
123
- | 'DVI'
124
- | 'MEDIA'
125
- );
126
-
127
- // MARK: ProductInfo
128
-
129
- export enum ProductInfoConfigKey {
130
- CONFIG_KEY_DATA_SERVICE = 0,
131
- CONFIG_KEY_SERVICE_COUNTRY = 1,
132
- }
133
-
134
- export enum ProductInfoNoGlass3dSupport {
135
- NO_GLASS_3D_NOT_SUPPORTED = 0,
136
- NO_GLASS_3D_SUPPORTED = 1,
137
- }
138
-
139
- export enum ProductInfoSiServerType {
140
- SI_TYPE_OPERATIING_SERVER = 0,
141
- SI_TYPE_DEVELOPMENT_SERVER = 1,
142
- SI_TYPE_DEVELOPING_SERVER = 2,
143
- }
144
-
145
- // MARK: Network
146
-
147
- export enum NetworkActiveConnectionType {
148
- DISCONNECTED = 0,
149
- WIFI = 1,
150
- CELLULAR = 2,
151
- ETHERNET = 3,
152
- }
153
-
154
- export enum NetworkIpMode {
155
- NONE = 0,
156
- STATIC = 1,
157
- DYNAMIC = 2,
158
- AUTO = 3,
159
- FIXED = 4,
160
- UNKNOWN = 5,
161
- }
162
-
163
- export enum NetworkState {
164
- LAN_CABLE_ATTACHED = 1,
165
- LAN_CABLE_DETACHED = 2,
166
- LAN_CABLE_STATE_UNKNOWN = 3,
167
- GATEWAY_CONNECTED = 4,
168
- GATEWAY_DISCONNECTED = 5,
169
- WIFI_MODULE_STATE_ATTACHED = 6,
170
- WIFI_MODULE_STATE_DETACHED = 7,
171
- WIFI_MODULE_STATE_UNKNOWN = 8,
172
- }
173
-
174
- export enum NetworkWiFiSecurityMode {
175
- WEP = 1,
176
- WPA_PSK = 2,
177
- WPA2_PSK = 3,
178
- EAP = 4,
179
- NONE = 5,
180
- UNKNOWN = 6,
181
- }
182
-
183
- export enum NetworkWiFiEncryptionType {
184
- WEP = 1,
185
- TKIP = 2,
186
- AES = 3,
187
- TKIP_AES_MIXED = 4,
188
- NONE = 5,
189
- UNKNOWN = 6,
190
- }
191
-
192
- // MARK: AppCommon
193
-
194
- export enum AppCommonScreenSaverState {
195
- SCREEN_SAVER_OFF = 0,
196
- SCREEN_SAVER_ON = 1,
197
- }
3
+ export const DPI_SUCCESS = '100000';
@@ -3,6 +3,8 @@ import {
3
3
  ConnectionType,
4
4
  ScreenSaverStatus,
5
5
  VolumeRange,
6
+ IAPSuccessPayload,
7
+ IAPPurchaseProductData,
6
8
  } from '@24i/bigscreen-sdk/driver-base';
7
9
  import { generateUuid } from '@24i/bigscreen-sdk/utils';
8
10
  import { NetworkConnectionTypeMap } from './constants';
@@ -150,4 +152,16 @@ export class DeviceVidaa extends DeviceBase {
150
152
  // not supported on Hisense VIDAA platform
151
153
  return '';
152
154
  }
155
+
156
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
157
+ async purchaseProduct(data: IAPPurchaseProductData): Promise<IAPSuccessPayload> {
158
+ // not supported
159
+ throw new Error('Not supported');
160
+ }
161
+
162
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
163
+ async cancelSubscription(data: IAPPurchaseProductData): Promise<void> {
164
+ // not supported
165
+ throw new Error('Not supported');
166
+ }
153
167
  }
@@ -4,6 +4,8 @@ import {
4
4
  ScreenSaverStatus,
5
5
  VolumeRange,
6
6
  ExitOptions,
7
+ IAPSuccessPayload,
8
+ IAPPurchaseProductData,
7
9
  } from '@24i/bigscreen-sdk/driver-base';
8
10
  import { Storage } from '@24i/bigscreen-sdk/storage';
9
11
  import { initWebOSTVjs } from './webOSTVjs/webOSTVjs';
@@ -343,4 +345,16 @@ export class DeviceWebos extends DeviceBase {
343
345
  // Not supported on LG webOS platform (for date 12.4.2021, latest webOS TV 5.x)
344
346
  return '';
345
347
  }
348
+
349
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
350
+ async purchaseProduct(data: IAPPurchaseProductData): Promise<IAPSuccessPayload> {
351
+ // not supported
352
+ throw new Error('Not supported');
353
+ }
354
+
355
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
356
+ async cancelSubscription(data: IAPPurchaseProductData): Promise<void> {
357
+ // not supported
358
+ throw new Error('Not supported');
359
+ }
346
360
  }
@@ -1,4 +1,10 @@
1
- import { DeviceBase, ScreenSaverStatus, VolumeRange } from '@24i/bigscreen-sdk/driver-base';
1
+ import {
2
+ DeviceBase,
3
+ IAPPurchaseProductData,
4
+ IAPSuccessPayload,
5
+ ScreenSaverStatus,
6
+ VolumeRange,
7
+ } from '@24i/bigscreen-sdk/driver-base';
2
8
  import { runAsync } from '@24i/bigscreen-sdk/utils/timers';
3
9
  import { generateUuid } from '@24i/bigscreen-sdk/utils/generateUuid';
4
10
  import { find } from '@24i/bigscreen-sdk/perf-utils/array';
@@ -239,4 +245,16 @@ export class DeviceXbox extends DeviceBase {
239
245
  getDeviceClass(): string {
240
246
  return 'xbox';
241
247
  }
248
+
249
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
250
+ async purchaseProduct(data: IAPPurchaseProductData): Promise<IAPSuccessPayload> {
251
+ // not supported
252
+ throw new Error('Not supported');
253
+ }
254
+
255
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
256
+ async cancelSubscription(data: IAPPurchaseProductData): Promise<void> {
257
+ // not supported
258
+ throw new Error('Not supported');
259
+ }
242
260
  }
@@ -106,6 +106,12 @@ export class Input extends Component<Props> implements IFocusable {
106
106
 
107
107
  setValue(value: string) {
108
108
  this.nativeInput.current!.value = value;
109
+ setText(this.text, this.filterText(value));
110
+ if (value.length === 0) {
111
+ this.showPlaceholder();
112
+ } else {
113
+ this.hidePlaceholder();
114
+ }
109
115
  }
110
116
 
111
117
  setType(type: Props['type']) {
@@ -0,0 +1,105 @@
1
+ ---
2
+ id: README
3
+ title: DoubleList
4
+ hide_title: true
5
+ sidebar_label: DoubleList
6
+ ---
7
+
8
+ # DoubleList
9
+
10
+ `DoubleList` renders a dataset into two synchronized rows (TOP and BOTTOM) for big-screen navigation.
11
+
12
+ Internally it splits the original `data` into:
13
+ - TOP row: items from original indices `0, 2, 4, ...`
14
+ - BOTTOM row: items from original indices `1, 3, 5, ...`
15
+
16
+ ## List Specific Props
17
+ - `firstScrollStep` - value in pixels to move on the first scroll
18
+ - `scrollStep` - value to move on other scrolls (should be equal to item width + its spacing)
19
+ - `rowClassName` - CSS class applied to each row
20
+ - `animation` - animation configuration overrides
21
+
22
+ ## Default Animation Configuration
23
+ - normal: 700ms with easeOutQuart
24
+
25
+ ## Usage
26
+
27
+ ```tsx
28
+ import { createRef } from '@24i/bigscreen-sdk/jsx';
29
+ import { DoubleList } from '@24i/bigscreen-sdk/list/DoubleList';
30
+
31
+ type Item = { id: string; title: string };
32
+
33
+ const listRef = createRef<DoubleList<any, Item>>();
34
+
35
+ export const Example = ({ items }: { items: Item[] }) => (
36
+ <DoubleList<any, Item>
37
+ ref={listRef as any}
38
+ data={items}
39
+ visibleItems={6}
40
+ firstScrollStep={50}
41
+ scrollStep={100}
42
+ animate={true}
43
+ renderItem={(item, columnIndex, ref) => (
44
+ <div ref={ref as any} className="card">
45
+ {item.title}
46
+ </div>
47
+ )}
48
+ onFocusChanged={(ref, columnIndex) => {
49
+ // `columnIndex` is row-local (column within the active row).
50
+ //
51
+ // To access the component-tracked absolute index, read it from the instance:
52
+ const absoluteIndex = (listRef.current as any)?.absoluteIndex;
53
+ // Use `absoluteIndex` for analytics/selection.
54
+ }}
55
+ />
56
+ );
57
+ ```
58
+
59
+ ### onFocusChanged
60
+
61
+ `onFocusChanged(itemRef, index)` receives:
62
+ - `itemRef`: reference to the focused item
63
+ - `index`: **row-local index** (column index within the focused row)
64
+
65
+ > `DoubleList` tracks an internal `absoluteIndex` value, but it is **not** passed as the callback argument.
66
+ > Read it from the `DoubleList` instance (via `ref`) after focus updates.
67
+
68
+ ## Indices
69
+
70
+ ### Column index (row-local)
71
+
72
+ Used by:
73
+ - `renderItem(item, columnIndex, ref)`
74
+ - `onFocusChanged(ref, columnIndex)`
75
+
76
+ ### Absolute index (tracked by the component)
77
+
78
+ `DoubleList` maintains `absoluteIndex` to represent the position in the original `data[]`.
79
+
80
+ To consume it externally:
81
+ - keep a ref to the component
82
+ - read `ref.current.absoluteIndex` after focus updates
83
+
84
+ ---
85
+
86
+ ## Instance methods (ref API)
87
+
88
+ `DoubleList` implements `DoubleRowList<T, U>`:
89
+
90
+ - `appendData(data: U[]): void`
91
+ - `prependData(data: U[]): void`
92
+ - `scrollTo(index: number, animate?: boolean): void`
93
+ - `focus(options?: FocusOptions): void`
94
+
95
+ Example:
96
+
97
+ ```tsx
98
+ listRef.current?.scrollTo(10, true);
99
+ listRef.current?.focus();
100
+ ```
101
+
102
+ ## Notes
103
+
104
+ - If `data.length` is odd, TOP row will contain one more item than BOTTOM row.
105
+ - For external analytics/selection logic that needs original `data[]` positions, use the component’s tracked `absoluteIndex` via `ref`.
@@ -0,0 +1 @@
1
+ export { DoubleList } from '../src/DoubleList/DoubleList';
@@ -0,0 +1,267 @@
1
+ import { Component, DeclareProps } from '@24i/bigscreen-sdk/jsx';
2
+ import { noop } from '@24i/bigscreen-sdk/utils/noop';
3
+ import { device } from '@24i/bigscreen-sdk/device';
4
+ import { ListBase } from '../Base';
5
+ import { RowType, DirectionType, DIRECTION, ROW, DoubleRowList, Props } from './interface';
6
+ import { Item } from '../types';
7
+ import { DoubleListAdapter } from './DoubleListAdapter';
8
+ import { CONFIG, VISIBLE_ITEMS_OFFSET } from './config';
9
+
10
+ export class DoubleList<T extends Item<U>, U>
11
+ extends Component<Props<T, U>> implements DoubleRowList<T, U> {
12
+ /*
13
+ * Adapter managing the two row lists and their controllers
14
+ */
15
+ adapter: DoubleListAdapter<T, U>;
16
+
17
+ /**
18
+ * Track which row is currently active
19
+ */
20
+ currentRow: RowType = ROW.TOP;
21
+
22
+ /**
23
+ * Track column position for vertical navigation
24
+ */
25
+ columnIndex = 0;
26
+
27
+ /**
28
+ * Track absolute index for onFocusChanged callback
29
+ */
30
+ absoluteIndex: number | null = -1;
31
+
32
+ static defaultProps = {
33
+ animation: CONFIG.defaultAnimation,
34
+ // eslint-disable-next-line react/default-props-match-prop-types
35
+ horizontal: true,
36
+ // eslint-disable-next-line react/default-props-match-prop-types
37
+ onFocusChanged: noop,
38
+ // eslint-disable-next-line react/default-props-match-prop-types
39
+ animate: CONFIG.animate,
40
+ };
41
+
42
+ declare props: DeclareProps<Props<T, U>, typeof DoubleList.defaultProps>;
43
+
44
+ constructor(props: Props<T, U>) {
45
+ super(props);
46
+ this.adapter = new DoubleListAdapter<T, U>(props);
47
+ }
48
+
49
+ componentDidMount(): void {
50
+ this.adapter.mount();
51
+ this.absoluteIndex = -1;
52
+ }
53
+
54
+ componentWillUnmount(): void {
55
+ this.adapter.unmount();
56
+ this.absoluteIndex = null;
57
+ }
58
+
59
+ /**
60
+ * Private helper: Switch between rows at the same column
61
+ * @param targetRow - The row to switch to (ROW.TOP or ROW.BOTTOM)
62
+ */
63
+ private switchToRow = (targetRow: RowType): boolean => {
64
+ const { data } = this.props;
65
+ const targetAbsoluteIndex = this.adapter.getAbsoluteIndex(targetRow, this.columnIndex);
66
+
67
+ // Check if target item exists in data
68
+ if (targetAbsoluteIndex >= data.length) {
69
+ return false;
70
+ }
71
+
72
+ const targetList = this.adapter.getList(targetRow);
73
+ const targetController = this.adapter.getController(targetRow);
74
+
75
+ if (!targetList) {
76
+ return false;
77
+ }
78
+ targetList.setIndex(this.columnIndex);
79
+ this.currentRow = targetRow;
80
+ this.absoluteIndex = targetAbsoluteIndex;
81
+
82
+ targetController.focusCurrentItem();
83
+ return true;
84
+ };
85
+
86
+ /**
87
+ * Private helper: Handle DOWN key navigation
88
+ * @param event - The keyboard event
89
+ */
90
+ private handleDownKey = (event: KeyboardEvent): boolean => {
91
+ if (this.currentRow !== ROW.TOP) {
92
+ return false;
93
+ }
94
+
95
+ const switched = this.switchToRow(ROW.BOTTOM);
96
+ if (switched) {
97
+ event.preventDefault();
98
+ event.stopPropagation();
99
+ }
100
+ return switched;
101
+ };
102
+
103
+ /**
104
+ * Private helper: Handle UP key navigation
105
+ * @param event - The keyboard event
106
+ */
107
+ private handleUpKey = (event: KeyboardEvent): boolean => {
108
+ if (this.currentRow !== ROW.BOTTOM) {
109
+ return false;
110
+ }
111
+
112
+ const switched = this.switchToRow(ROW.TOP);
113
+ if (switched) {
114
+ event.preventDefault();
115
+ event.stopPropagation();
116
+ }
117
+ return switched;
118
+ };
119
+
120
+ /**
121
+ * Handle keyboard events
122
+ * Intercepts UP/DOWN keys for vertical navigation
123
+ * Delegates LEFT/RIGHT keys to forward/backward handlers
124
+ * @param event - The keyboard event
125
+ */
126
+ handleKeyDown = (event: KeyboardEvent): boolean => {
127
+ const { horizontal } = this.props;
128
+ if (!horizontal) return false;
129
+ if (device.isDirectionDown(event)) return this.handleDownKey(event);
130
+ if (device.isDirectionUp(event)) return this.handleUpKey(event);
131
+ return false;
132
+ };
133
+
134
+ /**
135
+ * Handle LEFT/RIGHT navigation
136
+ * @param direction
137
+ * @returns boolean - whether the move was successful
138
+ */
139
+ move = (direction: DirectionType): boolean => {
140
+ const { visibleItems, firstScrollStep, scrollStep } = this.props;
141
+ const { controller, list } = this.adapter.getActiveRow(this.currentRow);
142
+ const { controller: inactiveController } = this.adapter.getInactiveRow(this.currentRow);
143
+ const currentList = list.current;
144
+
145
+ if (!currentList) return false;
146
+
147
+ const delta = direction === DIRECTION.FORWARD ? 1 : -1;
148
+ const nextIndex = currentList.index + delta;
149
+ if (nextIndex < 0 || nextIndex > currentList.data.length - 1) return false;
150
+
151
+ currentList.index = nextIndex;
152
+
153
+ if (delta > 0) {
154
+ const scrollTargetIndex = currentList.index - visibleItems + VISIBLE_ITEMS_OFFSET;
155
+ if (scrollTargetIndex > controller.scrollIndex) {
156
+ controller.scroll(scrollTargetIndex);
157
+ inactiveController.scroll(scrollTargetIndex);
158
+ }
159
+ } else {
160
+ const offset = firstScrollStep < scrollStep ? 1 : 0;
161
+ if (currentList.index - offset < controller.scrollIndex) {
162
+ controller.scroll(currentList.index);
163
+ inactiveController.scroll(currentList.index);
164
+ }
165
+ }
166
+
167
+ this.columnIndex = currentList.index;
168
+ this.absoluteIndex = this.adapter.getAbsoluteIndex(this.currentRow, this.columnIndex);
169
+ controller.focusCurrentItem();
170
+ return true;
171
+ };
172
+
173
+ forward = (): boolean => this.move(DIRECTION.FORWARD);
174
+
175
+ backward = (): boolean => this.move(DIRECTION.BACKWARD);
176
+
177
+ appendData = (data: U[]): void => this.adapter.appendData(data);
178
+
179
+ prependData = (data: U[]): void => this.adapter.prependData(data);
180
+
181
+ focus(options?: FocusOptions) {
182
+ const { controller } = this.adapter.getActiveRow(this.currentRow);
183
+ this.absoluteIndex = this.adapter.getAbsoluteIndex(this.currentRow, this.columnIndex);
184
+ controller.focusCurrentItem(options);
185
+ }
186
+
187
+ scrollTo(index: number, animate?: boolean): void {
188
+ const { data, visibleItems, firstScrollStep, scrollStep } = this.props;
189
+
190
+ const safeIndex = Math.max(0, Math.min(index, data.length - 1));
191
+ const targetRow = (safeIndex % 2 === 0) ? ROW.TOP : ROW.BOTTOM;
192
+ const targetColumnIndex = Math.floor(safeIndex / 2);
193
+
194
+ // Keep internal state consistent with requested target.
195
+ this.currentRow = targetRow;
196
+ this.columnIndex = targetColumnIndex;
197
+ this.absoluteIndex = safeIndex;
198
+
199
+ const { topRowList, bottomRowList } = this.adapter;
200
+ const topList = topRowList.current;
201
+ const bottomList = bottomRowList.current;
202
+
203
+ if (!topList || !bottomList) return;
204
+
205
+ // Keep both row indices aligned so switching rows preserves column.
206
+ topRowList.setIndex(Math.min(targetColumnIndex, Math.max(0, topList.data.length - 1)));
207
+ const bottomMaxIndex = Math.max(0, bottomList.data.length - 1);
208
+ bottomRowList.setIndex(Math.min(targetColumnIndex, bottomMaxIndex));
209
+
210
+ const targetController = this.adapter.getController(targetRow);
211
+
212
+ // Scroll both controllers in sync using the same logic as forward/backward.
213
+ const offset = firstScrollStep < scrollStep ? 1 : 0;
214
+ if (targetColumnIndex >= targetController.scrollIndex) {
215
+ const scrollTargetIndex = targetColumnIndex - visibleItems + VISIBLE_ITEMS_OFFSET;
216
+ if (scrollTargetIndex > targetController.scrollIndex) {
217
+ topRowList.controller.scroll(scrollTargetIndex, animate);
218
+ bottomRowList.controller.scroll(scrollTargetIndex, animate);
219
+ }
220
+ } else if (targetColumnIndex - offset < targetController.scrollIndex) {
221
+ topRowList.controller.scroll(targetColumnIndex, animate);
222
+ bottomRowList.controller.scroll(targetColumnIndex, animate);
223
+ }
224
+ targetController.focusCurrentItem();
225
+ }
226
+
227
+ render() {
228
+ const {
229
+ data,
230
+ className,
231
+ rowClassName,
232
+ renderItem,
233
+ visibleItems,
234
+ horizontal,
235
+ } = this.props;
236
+ const { topData, bottomData } = this.adapter.splitData(data);
237
+ const { topRowList, bottomRowList } = this.adapter;
238
+ return (
239
+ <div className={className} onKeyDown={this.handleKeyDown}>
240
+ <ListBase
241
+ ref={topRowList.base}
242
+ data={topData}
243
+ renderItem={renderItem}
244
+ visibleItems={visibleItems}
245
+ forward={this.forward}
246
+ backward={this.backward}
247
+ shouldApplySlowdown={topRowList.controller.shouldApplySlowdown}
248
+ className={rowClassName}
249
+ horizontal={horizontal}
250
+ limit={topRowList.controller.itemsCount}
251
+ />
252
+ <ListBase
253
+ ref={bottomRowList.base}
254
+ data={bottomData}
255
+ renderItem={renderItem}
256
+ visibleItems={visibleItems}
257
+ forward={this.forward}
258
+ backward={this.backward}
259
+ shouldApplySlowdown={bottomRowList.controller.shouldApplySlowdown}
260
+ className={rowClassName}
261
+ horizontal={horizontal}
262
+ limit={bottomRowList.controller.itemsCount}
263
+ />
264
+ </div>
265
+ );
266
+ }
267
+ }