@24i/bigscreen-sdk 1.0.54-alpha.2836 → 1.0.54-alpha.2838

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@24i/bigscreen-sdk",
3
- "version": "1.0.54-alpha.2836",
3
+ "version": "1.0.54-alpha.2838",
4
4
  "description": "SmartApps BIGscreen SDK monorepo",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -193,6 +193,7 @@
193
193
  "./l10n": "./packages/l10n/src/index.ts",
194
194
  "./list/AttachDetachItem": "./packages/list/AttachDetachItem/index.ts",
195
195
  "./list/Basic": "./packages/list/BasicList/index.ts",
196
+ "./list/Double": "./packages/list/DoubleList/index.ts",
196
197
  "./list/Basic/static": "./packages/list/BasicList/static.ts",
197
198
  "./list/Centered": "./packages/list/CenteredList/index.ts",
198
199
  "./list/Centered/static": "./packages/list/CenteredList/static.ts",
@@ -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
+ }
@@ -0,0 +1,72 @@
1
+ import { createRef } from '@24i/bigscreen-sdk/jsx';
2
+ import { ListBase } from '../Base';
3
+ import { UnifiedListController } from '../UnifiedListController/UnifiedListController';
4
+ import { RowList, RowType, ROW, Props } from './interface';
5
+ import { Item } from '../types';
6
+
7
+ /*
8
+ * DoubleListAdapter class to manage two RowList instances
9
+ */
10
+ export class DoubleListAdapter<T extends Item<U>, U> {
11
+ readonly topRowList: RowList<T, U>;
12
+
13
+ readonly bottomRowList: RowList<T, U>;
14
+
15
+ constructor(props: Props<T, U>) {
16
+ this.topRowList = new RowList<T, U>(createRef<ListBase<T, U>>(), props);
17
+ this.bottomRowList = new RowList<T, U>(createRef<ListBase<T, U>>(), props);
18
+ }
19
+
20
+ mount(): void {
21
+ this.topRowList.controller.componentDidMount();
22
+ this.bottomRowList.controller.componentDidMount();
23
+ }
24
+
25
+ unmount(): void {
26
+ this.topRowList.controller.componentWillUnmount();
27
+ this.bottomRowList.controller.componentWillUnmount();
28
+ }
29
+
30
+ splitData = (data: U[]): { topData: U[]; bottomData: U[] } => {
31
+ const topData: U[] = [];
32
+ const bottomData: U[] = [];
33
+ data.forEach((item, index) => ((index % 2 === 0) ? topData : bottomData).push(item));
34
+ return { topData, bottomData };
35
+ };
36
+
37
+ appendData(data: U[]): void {
38
+ const { topData, bottomData } = this.splitData(data);
39
+ this.topRowList.appendData(topData);
40
+ this.bottomRowList.appendData(bottomData);
41
+ }
42
+
43
+ getAbsoluteIndex = (row: RowType, columnIndex: number): number => (
44
+ row === ROW.TOP ? columnIndex * 2 : (columnIndex * 2) + 1
45
+ );
46
+
47
+ prependData(data: U[]): void {
48
+ const { topData, bottomData } = this.splitData(data);
49
+ this.topRowList.prependData(topData);
50
+ this.bottomRowList.prependData(bottomData);
51
+ }
52
+
53
+ getController(row: RowType): UnifiedListController<U> {
54
+ return row === ROW.TOP ? this.topRowList.controller : this.bottomRowList.controller;
55
+ }
56
+
57
+ getList(row: RowType): RowList<T, U> {
58
+ return row === ROW.TOP ? this.topRowList : this.bottomRowList;
59
+ }
60
+
61
+ getActiveRow(row: RowType): { controller: UnifiedListController<U>; list: RowList<T, U> } {
62
+ return row === ROW.TOP
63
+ ? { controller: this.topRowList.controller, list: this.topRowList }
64
+ : { controller: this.bottomRowList.controller, list: this.bottomRowList };
65
+ }
66
+
67
+ getInactiveRow(row: RowType): { controller: UnifiedListController<U>; list: RowList<T, U> } {
68
+ return row === ROW.TOP
69
+ ? { controller: this.bottomRowList.controller, list: this.bottomRowList }
70
+ : { controller: this.topRowList.controller, list: this.topRowList };
71
+ }
72
+ }
@@ -0,0 +1,15 @@
1
+ import { AnimationUtils } from '@24i/bigscreen-sdk/animations';
2
+ import { AnimationConfiguration } from '../types';
3
+ import { animate } from '../animate';
4
+
5
+ export const CONFIG = {
6
+ defaultAnimation: {
7
+ normal: {
8
+ time: 700,
9
+ easing: AnimationUtils.easeOutQuart,
10
+ },
11
+ } as AnimationConfiguration,
12
+ animate,
13
+ };
14
+
15
+ export const VISIBLE_ITEMS_OFFSET = 3;
@@ -0,0 +1,82 @@
1
+ import { IFocusable } from '@24i/bigscreen-sdk/focus';
2
+ import { Component } from '@24i/bigscreen-sdk/jsx';
3
+ import { ListBase } from '../Base';
4
+ import { UnifiedListController } from '../UnifiedListController/UnifiedListController';
5
+ import { Item, SharedProps, AnimationConfiguration } from '../types';
6
+ import { CONFIG } from './config';
7
+
8
+ /**
9
+ * Constants for row identification
10
+ */
11
+ export const ROW = {
12
+ TOP: 'top',
13
+ BOTTOM: 'bottom',
14
+ } as const;
15
+
16
+ export type RowType = typeof ROW[keyof typeof ROW];
17
+
18
+ /*
19
+ * Constants for horizontal scroll direction
20
+ */
21
+ export const DIRECTION = {
22
+ FORWARD: 'forward',
23
+ BACKWARD: 'backward',
24
+ } as const;
25
+
26
+ export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION];
27
+
28
+ export type Props<T extends Item<U>, U> = SharedProps<T, U> & {
29
+ firstScrollStep: number,
30
+ animation?: Partial<AnimationConfiguration>,
31
+ rowClassName?: string,
32
+ };
33
+
34
+ /**
35
+ * Interface for DoubleList component
36
+ */
37
+ export interface DoubleRowList<T extends Item<U>, U>
38
+ extends IFocusable, Component<SharedProps<T, U>> {
39
+ appendData(data: U[]): void;
40
+ prependData(data: U[]): void;
41
+ scrollTo(index: number, animate?: boolean): void;
42
+ focus(options?: FocusOptions): void;
43
+ forward(): boolean;
44
+ backward(): boolean;
45
+ }
46
+
47
+ /*
48
+ * RowList class representing a single row in the DoubleList
49
+ */
50
+ export class RowList<T extends Item<U>, U> {
51
+ base: React.RefObject<ListBase<T, U>>;
52
+
53
+ props: Props<T, U>;
54
+
55
+ controller: UnifiedListController<U>;
56
+
57
+ constructor(
58
+ base: React.RefObject<ListBase<T, U>>,
59
+ props: Props<T, U>,
60
+ ) {
61
+ this.base = base;
62
+ this.props = props;
63
+ this.controller = new UnifiedListController<U>(this as any, CONFIG);
64
+ }
65
+
66
+ get current() {
67
+ return this.base.current;
68
+ }
69
+
70
+ setIndex(index: number): void {
71
+ if (!this.base.current) return;
72
+ this.base.current.index = index;
73
+ }
74
+
75
+ appendData(data: U[]): void {
76
+ this.controller.appendData(data);
77
+ }
78
+
79
+ prependData(data: U[]): void {
80
+ this.controller.prependData(data);
81
+ }
82
+ }