@amazon-devices/vega-carousel 1.0.0 → 1.0.1

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 (20) hide show
  1. package/dist/Carousel/Carousel.js +1 -1
  2. package/dist/CarouselScrollable/native/CarouselNativeItemWrapper.d.ts +19 -36
  3. package/dist/CarouselScrollable/native/CarouselNativeItemWrapper.js +48 -77
  4. package/dist/CarouselScrollable/native/CarouselNativeLogic.d.ts +23 -11
  5. package/dist/CarouselScrollable/native/CarouselNativeLogic.js +120 -36
  6. package/dist/CarouselScrollable/native/CarouselNativeWrapper.js +26 -21
  7. package/dist/CarouselScrollable/recyclerview/JSRecyclerViewList.js +10 -3
  8. package/dist/kepler/aarch64-debug/lib/com.amazon.keplerscript.carousel.so +0 -0
  9. package/dist/kepler/aarch64-debug/lib/com.amazon.keplerscript.carousel.so.debug +0 -0
  10. package/dist/kepler/aarch64-release/lib/com.amazon.keplerscript.carousel.so +0 -0
  11. package/dist/kepler/aarch64-release/lib/com.amazon.keplerscript.carousel.so.debug +0 -0
  12. package/dist/kepler/armv7-debug/lib/com.amazon.keplerscript.carousel.so +0 -0
  13. package/dist/kepler/armv7-debug/lib/com.amazon.keplerscript.carousel.so.debug +0 -0
  14. package/dist/kepler/armv7-release/lib/com.amazon.keplerscript.carousel.so +0 -0
  15. package/dist/kepler/armv7-release/lib/com.amazon.keplerscript.carousel.so.debug +0 -0
  16. package/dist/kepler/x86_64-debug/lib/com.amazon.keplerscript.carousel.so +0 -0
  17. package/dist/kepler/x86_64-debug/lib/com.amazon.keplerscript.carousel.so.debug +0 -0
  18. package/dist/kepler/x86_64-release/lib/com.amazon.keplerscript.carousel.so +0 -0
  19. package/dist/kepler/x86_64-release/lib/com.amazon.keplerscript.carousel.so.debug +0 -0
  20. package/package.json +1 -1
@@ -33,7 +33,7 @@ const react_1 = __importStar(require("react"));
33
33
  const CarouselScrollable_1 = require("../CarouselScrollable");
34
34
  const Utils_1 = require("./Utils");
35
35
  function Scrollable({ dataAdapter: { getItem, getItemCount, getItemKey, notifyDataError }, renderItem: renderItemProp, testID, uniqueId, orientation = 'horizontal', renderedItemsCount = 8, numOffsetItems = 2, navigableScrollAreaMargin = 0, hasPreferredFocus = false, initialStartIndex = 0, hideItemsBeforeSelection = false, trapSelectionOnOrientation = false, containerStyle, itemStyle = {
36
- itemPadding: 20,
36
+ itemPadding: 10,
37
37
  pressedItemScaleFactor: 0.8,
38
38
  selectedItemScaleFactor: 1,
39
39
  }, animationDuration = {
@@ -34,13 +34,6 @@ export type CarouselNativeItemProps<ItemT, KeyT = React.Key> = ViewProps & Carou
34
34
  * @see {@link parentId}
35
35
  */
36
36
  dataIndex?: number;
37
- /**
38
- * The shift associated with this item.
39
- *
40
- * @default undefined
41
- * @see {@link parentId}
42
- */
43
- itemShift?: ScaleFactor;
44
37
  /**
45
38
  * When this flag is set to `true`, the selected item will have a styleable
46
39
  * border enveloping the selected item.
@@ -78,15 +71,6 @@ export type CarouselNativeItemProps<ItemT, KeyT = React.Key> = ViewProps & Carou
78
71
  */
79
72
  focusedItemScaleFactor: number;
80
73
  shiftItemsScaleFactor: ScaleFactor;
81
- /**
82
- * This element is used on the very first time the {@link CarouselNativeItemWrapper}
83
- * resolvers.
84
- *
85
- * Further resolvers will use the {@link dataAdapter} and {@link renderItem}
86
- * properties to generate the {@link React.ReactElement} to be used.
87
- */
88
- initialElement?: React.ReactElement;
89
- initialKey?: KeyT;
90
74
  /**
91
75
  * This function is called whenever the item receives a message from the
92
76
  * native layer telling it to rebind itself to another data item.
@@ -129,7 +113,10 @@ export type CarouselNativeItemState = {
129
113
  */
130
114
  refreshToggle: boolean;
131
115
  /**
132
- * This field is used for specifying per-item shift value.
116
+ * Per-item shift (layout offset) resolved from the data adapter. The child
117
+ * is the source of truth for its own shift — the parent only tells us which
118
+ * `dataIndex` to display, and we read the shift directly from the adapter
119
+ * here. See VEGATV-115306.
133
120
  */
134
121
  itemShift?: ScaleFactor;
135
122
  };
@@ -154,8 +141,12 @@ export type CarouselNativeItemState = {
154
141
  */
155
142
  export declare class CarouselNativeItemWrapper<ItemT, KeyT = React.Key> extends React.Component<CarouselNativeItemProps<ItemT, KeyT>, CarouselNativeItemState> {
156
143
  private currentKey;
157
- private useInitialElement;
158
144
  constructor(props: CarouselNativeItemProps<ItemT, KeyT>);
145
+ /**
146
+ * Snapshot the rendered key after every committed update.
147
+ * This runs in the commit phase (safe for CR). See VEGATV-115306.
148
+ */
149
+ componentDidUpdate(): void;
159
150
  /**
160
151
  * Function called by the framework right before calling {@link render}, both
161
152
  * on the initial mount, and on the following updates.
@@ -202,13 +193,8 @@ export declare class CarouselNativeItemWrapper<ItemT, KeyT = React.Key> extends
202
193
  */
203
194
  shouldComponentUpdate(nextProps: Readonly<CarouselNativeItemProps<ItemT, KeyT>>, nextState: Readonly<CarouselNativeItemState>): boolean;
204
195
  /**
205
- * Called whenever the framework needs the component's definition to render a
206
- * view in the screen.
207
- *
208
- * When this method is called, there is nothing for this component to resolve,
209
- * so this call is as fast as it can be. However, be mindful that the other
210
- * lifecycle methods in this class were already called, and the _perceived_
211
- * render performance is a combination of all these methods.
196
+ * Pure render — computes the item element fresh from props/state each time.
197
+ * Does NOT write to `this`. See VEGATV-115306.
212
198
  *
213
199
  * @see [Official API documentation](https://react.dev/reference/react/Component#render)
214
200
  */
@@ -232,24 +218,21 @@ export declare class CarouselNativeItemWrapper<ItemT, KeyT = React.Key> extends
232
218
  *
233
219
  * @param {CarouselNativeItemState} state An optional state used to read the
234
220
  * item's data index instead of the component's state.
235
- *
236
- * @param {CarouselNativeItemProps} props Optional props used to override the current props if newer props are known
237
- * @return {React.ReactElement | undefined} Returns the fully resolved
238
- * element or `undefined` if it is not found.
221
+ * @param {CarouselNativeItemProps} props Optional props used to override the
222
+ * current props if newer props are known.
223
+ * @return {React.ReactElement} Returns the fully resolved element, or an
224
+ * empty fragment if it cannot be resolved.
239
225
  *
240
226
  * @see {@link CarouselNativeItemState}
241
227
  * @see {@link resolveDataItems}
242
228
  */
243
229
  private resolveUpdatedItemContent;
244
230
  /**
245
- * Convenience method to render a native component with the
231
+ * Convenience method to render a native component with the given
246
232
  * {@link itemElement} as its child component.
247
- */
248
- private renderStateItem;
249
- /**
250
- * This property is used to cache the resolved element of this item.
251
233
  *
252
- * @private
234
+ * @param {React.ReactElement} itemElement The resolved element to render
235
+ * inside the native host.
253
236
  */
254
- private itemElement;
237
+ private renderStateItem;
255
238
  }
@@ -43,18 +43,27 @@ const defaultBorderStyle = {
43
43
  * @see {@link CarouselScrollable}
44
44
  */
45
45
  class CarouselNativeItemWrapper extends react_1.default.Component {
46
+ // Snapshot of the last-rendered key. Seeded in the constructor and updated
47
+ // in commit-phase only (componentDidUpdate/onBind) for CR safety.
48
+ // See VEGATV-115306.
46
49
  currentKey;
47
- useInitialElement;
48
50
  constructor(props) {
49
51
  super(props);
52
+ const dataIndex = this.props.dataIndex ?? -1;
50
53
  this.state = {
51
- dataIndex: this.props.dataIndex ?? -1,
54
+ dataIndex,
52
55
  parentId: this.props.parentId,
53
56
  refreshToggle: false,
54
- itemShift: this.props.itemShift,
57
+ itemShift: (0, CarouselData_1.getSelectedItemOffset)(this.props.dataAdapter, dataIndex),
55
58
  };
56
- this.currentKey = this.props.initialKey;
57
- this.useInitialElement = true;
59
+ this.currentKey = (0, CarouselData_1.getItemKey)(this.props.dataAdapter, dataIndex);
60
+ }
61
+ /**
62
+ * Snapshot the rendered key after every committed update.
63
+ * This runs in the commit phase (safe for CR). See VEGATV-115306.
64
+ */
65
+ componentDidUpdate() {
66
+ this.currentKey = (0, CarouselData_1.getItemKey)(this.props.dataAdapter, this.state.dataIndex);
58
67
  }
59
68
  ////////////////////////////////////////
60
69
  /// Lifecycle methods
@@ -112,69 +121,35 @@ class CarouselNativeItemWrapper extends react_1.default.Component {
112
121
  * @see [Official API documentation](https://react.dev/reference/react/Component#shouldcomponentupdate)
113
122
  */
114
123
  shouldComponentUpdate(nextProps, nextState) {
115
- let shouldUpdate = false;
116
124
  const nextKey = (0, CarouselData_1.getItemKey)(nextProps.dataAdapter, nextState.dataIndex);
117
125
  if (this.currentKey !== nextKey) {
118
- shouldUpdate = true;
126
+ return true;
119
127
  }
120
128
  // the state will govern whether we should update the component
121
- if (!shouldUpdate &&
122
- (this.state.dataIndex !== nextState.dataIndex ||
123
- this.state.parentId !== nextState.parentId ||
124
- this.state.refreshToggle !== nextState.refreshToggle)) {
125
- shouldUpdate = true;
129
+ if (this.state.dataIndex !== nextState.dataIndex ||
130
+ this.state.parentId !== nextState.parentId ||
131
+ this.state.refreshToggle !== nextState.refreshToggle) {
132
+ return true;
126
133
  }
127
- if (!shouldUpdate &&
128
- (this.props.focusedItemScaleFactor !== nextProps.focusedItemScaleFactor ||
129
- this.props.shiftItemsScaleFactor.width !==
130
- nextProps.shiftItemsScaleFactor.width ||
131
- this.props.shiftItemsScaleFactor.height !==
132
- nextProps.shiftItemsScaleFactor.height)) {
134
+ if (this.props.focusedItemScaleFactor !== nextProps.focusedItemScaleFactor ||
135
+ this.props.shiftItemsScaleFactor.width !==
136
+ nextProps.shiftItemsScaleFactor.width ||
137
+ this.props.shiftItemsScaleFactor.height !==
138
+ nextProps.shiftItemsScaleFactor.height) {
133
139
  utils_1.Log.d('shouldComponentUpdate: PROPS CHANGED');
134
- shouldUpdate = true;
135
- }
136
- // If the state did not determine if we should update, then check the new and current key
137
- if (!shouldUpdate) {
138
- if (nextKey !== this.currentKey) {
139
- utils_1.Log.d('shouldComponentUpdate: KEYS CHANGED');
140
- shouldUpdate = true;
141
- }
142
- }
143
- // If we should update, ensure that the itemElement gets updated as well
144
- if (shouldUpdate) {
145
- this.currentKey = nextKey;
146
- this.itemElement = this.resolveUpdatedItemContent(nextState, nextProps);
140
+ return true;
147
141
  }
148
- return shouldUpdate;
142
+ return false;
149
143
  }
150
144
  /**
151
- * Called whenever the framework needs the component's definition to render a
152
- * view in the screen.
153
- *
154
- * When this method is called, there is nothing for this component to resolve,
155
- * so this call is as fast as it can be. However, be mindful that the other
156
- * lifecycle methods in this class were already called, and the _perceived_
157
- * render performance is a combination of all these methods.
145
+ * Pure render — computes the item element fresh from props/state each time.
146
+ * Does NOT write to `this`. See VEGATV-115306.
158
147
  *
159
148
  * @see [Official API documentation](https://react.dev/reference/react/Component#render)
160
149
  */
161
150
  render() {
162
- try {
163
- if (!this.itemElement) {
164
- // This is the initial resolver of the item
165
- if (this.useInitialElement && this.props.initialElement) {
166
- this.itemElement = this.props.initialElement;
167
- }
168
- else {
169
- this.itemElement = <></>;
170
- this.useInitialElement = false;
171
- }
172
- }
173
- return this.renderStateItem();
174
- }
175
- finally {
176
- this.useInitialElement = false;
177
- }
151
+ const itemElement = this.resolveUpdatedItemContent();
152
+ return this.renderStateItem(itemElement);
178
153
  }
179
154
  ////////////////////////////////////////
180
155
  /// Private callback functions
@@ -196,10 +171,11 @@ class CarouselNativeItemWrapper extends react_1.default.Component {
196
171
  dataIndex: nativeEvent.nativeEvent.index,
197
172
  itemShift: (0, CarouselData_1.getSelectedItemOffset)(this.props.dataAdapter, nativeEvent.nativeEvent.index),
198
173
  };
199
- this.props.unbindCallback(this.state.dataIndex);
200
- this.itemElement = this.resolveUpdatedItemContent(newState);
201
- // Every time a frame gets re-bound/recycled update the currentKey
174
+ // Every time a frame gets re-bound/recycled update the currentKey.
175
+ // This runs in an event handler (commit-safe), not during render.
176
+ // See VEGATV-115306.
202
177
  this.currentKey = (0, CarouselData_1.getItemKey)(this.props.dataAdapter, nativeEvent.nativeEvent.index);
178
+ this.props.unbindCallback(this.state.dataIndex);
203
179
  this.setState(newState);
204
180
  this.props.bindCallback(nativeEvent.nativeEvent.index);
205
181
  }
@@ -221,31 +197,34 @@ class CarouselNativeItemWrapper extends react_1.default.Component {
221
197
  *
222
198
  * @param {CarouselNativeItemState} state An optional state used to read the
223
199
  * item's data index instead of the component's state.
224
- *
225
- * @param {CarouselNativeItemProps} props Optional props used to override the current props if newer props are known
226
- * @return {React.ReactElement | undefined} Returns the fully resolved
227
- * element or `undefined` if it is not found.
200
+ * @param {CarouselNativeItemProps} props Optional props used to override the
201
+ * current props if newer props are known.
202
+ * @return {React.ReactElement} Returns the fully resolved element, or an
203
+ * empty fragment if it cannot be resolved.
228
204
  *
229
205
  * @see {@link CarouselNativeItemState}
230
206
  * @see {@link resolveDataItems}
231
207
  */
232
208
  resolveUpdatedItemContent = (state, props) => {
209
+ const targetIndex = state?.dataIndex ?? this.state.dataIndex;
210
+ const adapter = props?.dataAdapter ?? this.props.dataAdapter;
233
211
  let element;
234
- (0, CarouselData_1.resolveDataItems)(props?.dataAdapter ?? this.props.dataAdapter, props?.renderItem ?? this.props.renderItem, (0, CarouselData_1.dataReadAtTargetConfig)(state?.dataIndex ?? this.state.dataIndex), (resolved) => {
212
+ (0, CarouselData_1.resolveDataItems)(adapter, props?.renderItem ?? this.props.renderItem, (0, CarouselData_1.dataReadAtTargetConfig)(targetIndex), (resolved) => {
235
213
  element = resolved.element;
236
214
  });
237
215
  // We need to update all undefined elements with an empty view in order to account for the loading state
238
216
  // TODO : Update this logic once Scrollable getItem APIs support a loading state instead of just valid and invalid cases
239
- if (!element) {
240
- element = <></>;
241
- }
217
+ element ??= <></>;
242
218
  return element;
243
219
  };
244
220
  /**
245
- * Convenience method to render a native component with the
221
+ * Convenience method to render a native component with the given
246
222
  * {@link itemElement} as its child component.
223
+ *
224
+ * @param {React.ReactElement} itemElement The resolved element to render
225
+ * inside the native host.
247
226
  */
248
- renderStateItem = () => {
227
+ renderStateItem = (itemElement) => {
249
228
  return (<CarouselNativeItemHostComponent_1.CarouselNativeItemHost testID={this.props.testID} parentId={this.state.parentId} dataIndex={this.state.dataIndex} showSelectionBorder={this.props.showSelectionBorder ?? false} selectionBorderColor={this.props.showSelectionBorder
250
229
  ? (0, react_native_1.processColor)(this.props.selectionBorderStyle?.borderColor ??
251
230
  defaultBorderStyle.borderColor)
@@ -263,16 +242,8 @@ class CarouselNativeItemWrapper extends react_1.default.Component {
263
242
  : this.props.shiftItemsScaleFactor.width} shiftItemsHeightScaleFactor={this.state.itemShift
264
243
  ? this.state.itemShift.height
265
244
  : this.props.shiftItemsScaleFactor.height} onBind={this.onBind}>
266
- {this.itemElement}
245
+ {itemElement}
267
246
  </CarouselNativeItemHostComponent_1.CarouselNativeItemHost>);
268
247
  };
269
- ////////////////////////////////////////
270
- /// Private attributes
271
- /**
272
- * This property is used to cache the resolved element of this item.
273
- *
274
- * @private
275
- */
276
- itemElement = undefined;
277
248
  }
278
249
  exports.CarouselNativeItemWrapper = CarouselNativeItemWrapper;
@@ -1,7 +1,7 @@
1
1
  import { HostComponent, StyleProp, ViewStyle } from 'react-native';
2
2
  import { ListOrientation, OnItemBindFunc, OnItemUnbindFunc, OnSelectionChangedCallback } from '../CarouselScrollableTypes';
3
3
  import { CarouselNativeHostComponentSelectionChangedEvent, CarouselNativeHostComponentProps } from './CarouselNativeHostComponent';
4
- import { CarouselDataAdapter, CarouselDataChange, CarouselResolvedDataValues, RenderItemFunc } from '../../CarouselData';
4
+ import { CarouselDataAdapter, CarouselDataChange } from '../../CarouselData';
5
5
  import { AnyValue } from '../../utils';
6
6
  /**
7
7
  * Auxiliary function use to invoke the `enableInputEvents` native function on
@@ -113,8 +113,14 @@ export declare function callReset(hostComponent: HostComponent<CarouselNativeHos
113
113
  */
114
114
  export declare function addFlexDirectionStyle(baseStyle: StyleProp<ViewStyle>, orientation: ListOrientation): StyleProp<ViewStyle>;
115
115
  /**
116
- * This function is the core logic to retrieve items on the {@link CarouselScrollable}
117
- * initial render.
116
+ * This function determines which data indices should fill the
117
+ * {@link CarouselScrollable}'s initial render window.
118
+ *
119
+ * It does NOT eagerly resolve per-item content (key, shift, JSX). Each child
120
+ * component looks those up itself from the same adapter using its assigned
121
+ * `dataIndex`. This function only checks **whether data is available** at each
122
+ * candidate index — it walks the adapter, calling `getItem` to confirm a slot
123
+ * is fillable, and collects the indices that succeed.
118
124
  *
119
125
  * It will try in different ways to get as many items as defined by the
120
126
  * `dataLimit` argument.
@@ -133,9 +139,11 @@ export declare function addFlexDirectionStyle(baseStyle: StyleProp<ViewStyle>, o
133
139
  * **before** the last item retrieved at step 2 until we have a total of
134
140
  * `dataLimit` items, or the last item we retrieved was at position `0`;
135
141
  *
136
- * Internally, `getInitialData` uses the {@link resolveDataItems} function to
137
- * guarantee that failed items are resolved (and accounted for) by the given
138
- * adapter.
142
+ * This function walks the adapter with retry logic to handle transient item
143
+ * retrieval failures (same retry semantics as resolveDataItems). It returns
144
+ * only the indices of items that exist; per-item properties (key, shift,
145
+ * resolved element) are looked up by each child component from the same
146
+ * adapter. See VEGATV-115306.
139
147
  *
140
148
  * @template ItemT The type of data items the list will use to render its items.
141
149
  * @template KeyT The type used as items unique identifiers (also called _keys_,
@@ -143,12 +151,16 @@ export declare function addFlexDirectionStyle(baseStyle: StyleProp<ViewStyle>, o
143
151
  *
144
152
  * @param {CarouselDataAdapter<ItemT, KeyT>} adapter An object with the data access
145
153
  * APIs that allow us to retrieve items from an external source.
146
- * @param {RenderItemFunc<ItemT>} resolver
147
- * @param {number} initialIndex
148
- * @param {number} preferredBackCount
149
- * @param {number} dataLimit
154
+ * @param {number} initialIndex The data position to start retrieving from.
155
+ * @param {number} preferredBackCount Maximum number of items to place before
156
+ * the initial selection.
157
+ * @param {number} dataLimit Maximum total items to retrieve.
158
+ *
159
+ * @return {number[]} The data indices of the items that should fill the
160
+ * initial render window. Each child reads its own key, shift, and
161
+ * resolved element from the adapter using these indices.
150
162
  */
151
- export declare function getInitialData<ItemT, KeyT>(adapter: CarouselDataAdapter<ItemT, KeyT>, resolver: RenderItemFunc<ItemT>, initialIndex: number, preferredBackCount: number, dataLimit: number): Array<CarouselResolvedDataValues<KeyT>>;
163
+ export declare function computeInitialIndexWindow<ItemT, KeyT>(adapter: CarouselDataAdapter<ItemT, KeyT>, initialIndex: number, preferredBackCount: number, dataLimit: number): number[];
152
164
  /**
153
165
  * Function to hoist the workflow logic used when the `onSelectionChanged` event
154
166
  * happens.
@@ -5,7 +5,7 @@
5
5
  * PROPRIETARY/CONFIDENTIAL. USE IS SUBJECT TO LICENSE TERMS.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.triggerOnItemBindCallback = exports.triggerOnItemUnbindCallback = exports.triggerOnSelectionChangeCallback = exports.getInitialData = exports.addFlexDirectionStyle = exports.callReset = exports.callNativeNotifyDataChange = exports.callNativeSetOverflowItemsVisible = exports.callNativeSetSelectedItem = exports.callNativeEnableInputEvents = void 0;
8
+ exports.triggerOnItemBindCallback = exports.triggerOnItemUnbindCallback = exports.triggerOnSelectionChangeCallback = exports.computeInitialIndexWindow = exports.addFlexDirectionStyle = exports.callReset = exports.callNativeNotifyDataChange = exports.callNativeSetOverflowItemsVisible = exports.callNativeSetSelectedItem = exports.callNativeEnableInputEvents = void 0;
9
9
  const react_native_1 = require("react-native");
10
10
  const CarouselScrollableTypes_1 = require("../CarouselScrollableTypes");
11
11
  const CarouselNativeHostComponent_1 = require("./CarouselNativeHostComponent");
@@ -155,8 +155,24 @@ function addFlexDirectionStyle(baseStyle, orientation) {
155
155
  }
156
156
  exports.addFlexDirectionStyle = addFlexDirectionStyle;
157
157
  /**
158
- * This function is the core logic to retrieve items on the {@link CarouselScrollable}
159
- * initial render.
158
+ * Step values used by {@link computeInitialIndexWindow}'s walk helper. The
159
+ * numeric value doubles as the arithmetic increment used to advance the
160
+ * position: `BACKWARD` is `-1`, `FORWARD` is `+1`.
161
+ */
162
+ var WalkStep;
163
+ (function (WalkStep) {
164
+ WalkStep[WalkStep["BACKWARD"] = -1] = "BACKWARD";
165
+ WalkStep[WalkStep["FORWARD"] = 1] = "FORWARD";
166
+ })(WalkStep || (WalkStep = {}));
167
+ /**
168
+ * This function determines which data indices should fill the
169
+ * {@link CarouselScrollable}'s initial render window.
170
+ *
171
+ * It does NOT eagerly resolve per-item content (key, shift, JSX). Each child
172
+ * component looks those up itself from the same adapter using its assigned
173
+ * `dataIndex`. This function only checks **whether data is available** at each
174
+ * candidate index — it walks the adapter, calling `getItem` to confirm a slot
175
+ * is fillable, and collects the indices that succeed.
160
176
  *
161
177
  * It will try in different ways to get as many items as defined by the
162
178
  * `dataLimit` argument.
@@ -175,9 +191,11 @@ exports.addFlexDirectionStyle = addFlexDirectionStyle;
175
191
  * **before** the last item retrieved at step 2 until we have a total of
176
192
  * `dataLimit` items, or the last item we retrieved was at position `0`;
177
193
  *
178
- * Internally, `getInitialData` uses the {@link resolveDataItems} function to
179
- * guarantee that failed items are resolved (and accounted for) by the given
180
- * adapter.
194
+ * This function walks the adapter with retry logic to handle transient item
195
+ * retrieval failures (same retry semantics as resolveDataItems). It returns
196
+ * only the indices of items that exist; per-item properties (key, shift,
197
+ * resolved element) are looked up by each child component from the same
198
+ * adapter. See VEGATV-115306.
181
199
  *
182
200
  * @template ItemT The type of data items the list will use to render its items.
183
201
  * @template KeyT The type used as items unique identifiers (also called _keys_,
@@ -185,59 +203,125 @@ exports.addFlexDirectionStyle = addFlexDirectionStyle;
185
203
  *
186
204
  * @param {CarouselDataAdapter<ItemT, KeyT>} adapter An object with the data access
187
205
  * APIs that allow us to retrieve items from an external source.
188
- * @param {RenderItemFunc<ItemT>} resolver
189
- * @param {number} initialIndex
190
- * @param {number} preferredBackCount
191
- * @param {number} dataLimit
206
+ * @param {number} initialIndex The data position to start retrieving from.
207
+ * @param {number} preferredBackCount Maximum number of items to place before
208
+ * the initial selection.
209
+ * @param {number} dataLimit Maximum total items to retrieve.
210
+ *
211
+ * @return {number[]} The data indices of the items that should fill the
212
+ * initial render window. Each child reads its own key, shift, and
213
+ * resolved element from the adapter using these indices.
192
214
  */
193
- function getInitialData(adapter, resolver, initialIndex, preferredBackCount, dataLimit) {
215
+ function computeInitialIndexWindow(adapter, initialIndex, preferredBackCount, dataLimit) {
194
216
  const initialData = [];
195
217
  let startIndex = initialIndex;
196
218
  let endIndex = initialIndex;
219
+ const maxRetries = CarouselData_1.ITEM_FAILURE_DEFAULT_MAX_RETRIES;
220
+ // Helper: attempt to read a single item at `index` and add its index to the
221
+ // collection via `addIndex` on success. Retries on transient adapter failures
222
+ // up to `maxRetries` or until `notifyDataError` returns false. Returns true
223
+ // if an index was added, false if the read failed permanently.
224
+ function tryReadAndAddItem(index, errorReadConfig, addIndex) {
225
+ let retryCount = 0;
226
+ while (retryCount < maxRetries) {
227
+ const itemData = adapter.getItem(index);
228
+ if (itemData !== undefined) {
229
+ addIndex(index);
230
+ return true;
231
+ }
232
+ // Notify the adapter about the read failure and check if we should retry
233
+ const shouldRetry = adapter.notifyDataError({
234
+ index,
235
+ readConfig: { ...errorReadConfig, maxRetries },
236
+ reason: CarouselData_1.CarouselDataErrorReason.CANNOT_READ,
237
+ });
238
+ if (!shouldRetry) {
239
+ break;
240
+ }
241
+ retryCount++;
242
+ }
243
+ return false;
244
+ }
245
+ // Helper: walk in a direction (forward or backward) reading items until the
246
+ // predicate stops or a read fails permanently. Each per-position read with
247
+ // retries is delegated to tryReadAndAddItem so retry semantics live in one
248
+ // place.
249
+ //
250
+ // The `step` enum value doubles as the arithmetic increment for advancing
251
+ // the position. We map it to the corresponding CarouselDataReadDirection
252
+ // value so the notifyDataError payload uses the public direction enum.
253
+ //
254
+ // Note: shouldContinue is consulted once per position rather than on every
255
+ // retry within tryReadAndAddItem. Existing callers' predicates depend only
256
+ // on `index` (which doesn't change mid-retry) and on initialData.length
257
+ // (which only grows on success), so this is observably equivalent to
258
+ // checking on every attempt.
259
+ function walkItems(start, step, shouldContinue, addIndex) {
260
+ const errorReadConfig = {
261
+ startIndex: start,
262
+ readDirection: step === WalkStep.BACKWARD
263
+ ? CarouselData_1.CarouselDataReadDirection.BACKWARDS
264
+ : CarouselData_1.CarouselDataReadDirection.FORWARD,
265
+ };
266
+ let index = start;
267
+ let lastSuccessIndex;
268
+ while (shouldContinue(index)) {
269
+ if (!tryReadAndAddItem(index, errorReadConfig, addIndex)) {
270
+ break;
271
+ }
272
+ lastSuccessIndex = index;
273
+ index += step;
274
+ }
275
+ return lastSuccessIndex;
276
+ }
197
277
  // To get our initial data set without information about how many items are
198
278
  // available, we'll need to preemptively request items from our data adapter,
199
279
  // and stop when there is no more data available, or we got `maxRenderItems`
200
280
  // items.
201
- // We start by pulling the item in the selected index position...
202
- (0, CarouselData_1.resolveDataItems)(adapter, resolver, (0, CarouselData_1.dataReadAtTargetConfig)(initialIndex), (resolved) => {
203
- initialData.push(resolved);
281
+ // Step 1: We start by pulling the item in the selected index position...
282
+ tryReadAndAddItem(initialIndex, {
283
+ startIndex: initialIndex,
284
+ readDirection: CarouselData_1.CarouselDataReadDirection.AT_TARGET,
285
+ }, index => {
286
+ initialData.push(index);
204
287
  });
205
288
  // If our adapter did not return an item for the initial position, we can
206
289
  // assume this list is empty for now.
207
290
  if (initialData.length > 0) {
208
- // If we have the initial index, we try to get items to place **before** the
209
- // selected one; we do this because of the `scrollOffsetCount` property that
210
- // limits how many items we can have before the selected one, and not after
211
- // it.
212
- startIndex =
213
- (0, CarouselData_1.resolveDataItems)(adapter, resolver, (0, CarouselData_1.dataReadBackwardsConfig)(initialIndex - 1, index => index >= 0 &&
214
- initialData.length <= dataLimit &&
215
- initialIndex - index <= preferredBackCount), (resolved) => {
216
- initialData.unshift(resolved);
217
- }) ?? initialIndex;
218
- // Once we have all the items before the selected one, we're free to request
219
- // the rest of the items in our initial data set after the selected item.
220
- endIndex =
221
- (0, CarouselData_1.resolveDataItems)(adapter, resolver, (0, CarouselData_1.dataReadForwardConfig)(initialIndex + 1, () => initialData.length < dataLimit), (resolved) => {
222
- initialData.push(resolved);
223
- }) ?? initialIndex;
224
- // Sometimes, after retrieving items before and after the selected one, we
225
- // still don't have enough items to match `renderedItemsCount` count, and when
226
- // this happens, there are two possible reasons:
291
+ // Step 2: If we have the initial index, we try to get items to place
292
+ // **before** the selected one; we do this because of the
293
+ // `scrollOffsetCount` property that limits how many items we can have
294
+ // before the selected one, and not after it.
295
+ const backResult = walkItems(initialIndex - 1, WalkStep.BACKWARD, index => index >= 0 &&
296
+ initialData.length <= dataLimit &&
297
+ initialIndex - index <= preferredBackCount, index => {
298
+ initialData.unshift(index);
299
+ });
300
+ startIndex = backResult ?? initialIndex;
301
+ // Step 3: Once we have all the items before the selected one, we're free
302
+ // to request the rest of the items in our initial data set after the
303
+ // selected item.
304
+ const forwardResult = walkItems(initialIndex + 1, WalkStep.FORWARD, () => initialData.length < dataLimit, index => {
305
+ initialData.push(index);
306
+ });
307
+ endIndex = forwardResult ?? initialIndex;
308
+ // Step 4: Sometimes, after retrieving items before and after the selected
309
+ // one, we still don't have enough items to match `renderedItemsCount`
310
+ // count, and when this happens, there are two possible reasons:
227
311
  // 1. There is no more data available in the data set;
228
312
  // 2. The `initialStartIndex` is close to the end of the data set;
229
313
  // In the first case, there is nothing for us to do, but on the second one,
230
314
  // we should try to go backwards while we still have data to fill our
231
315
  // initial data array.
232
316
  if (endIndex - startIndex < dataLimit - 1) {
233
- (0, CarouselData_1.resolveDataItems)(adapter, resolver, (0, CarouselData_1.dataReadBackwardsConfig)(startIndex - 1, index => index >= 0 && initialData.length < dataLimit), (resolved) => {
234
- initialData.unshift(resolved);
317
+ walkItems(startIndex - 1, WalkStep.BACKWARD, index => index >= 0 && initialData.length < dataLimit, index => {
318
+ initialData.unshift(index);
235
319
  });
236
320
  }
237
321
  }
238
322
  return initialData;
239
323
  }
240
- exports.getInitialData = getInitialData;
324
+ exports.computeInitialIndexWindow = computeInitialIndexWindow;
241
325
  /**
242
326
  * Function to hoist the workflow logic used when the `onSelectionChanged` event
243
327
  * happens.
@@ -62,9 +62,7 @@ function CarouselNativeInner(rawProps, ref) {
62
62
  // To conform to the logic on the native layer and to accomplish the same behavior we multiple the two scale factors together to yield 1.316. Instead of applying this adjusted scale factor to the selected dimensions
63
63
  // we apply this scale factor to the unselected dimensions which the user perceives as the item shrinking when the item is pressed
64
64
  const adjustedPressedItemScaleFactor = focusedItemScaleFactor * pressedItemScaleFactor;
65
- const renderedFrames = react_1.default.useRef(renderedItemsCount);
66
- const lastInitialData = react_1.default.useRef(undefined);
67
- const lastParentId = react_1.default.useRef(undefined);
65
+ const [renderedFrames] = react_1.default.useState(() => renderedItemsCount);
68
66
  const onNativeSelectionChanged = react_1.default.useCallback(event => {
69
67
  (0, CarouselNativeLogic_1.triggerOnSelectionChangeCallback)(onSelectionChanged, event);
70
68
  }, [onSelectionChanged]);
@@ -84,34 +82,41 @@ function CarouselNativeInner(rawProps, ref) {
84
82
  };
85
83
  });
86
84
  const styleWithFlexDirection = react_1.default.useMemo(() => (0, CarouselNativeLogic_1.addFlexDirectionStyle)(style, orientation), [style, orientation]);
87
- let initialData = lastInitialData.current && lastInitialData.current.length !== 0
88
- ? lastInitialData.current
89
- : (0, CarouselNativeLogic_1.getInitialData)(dataAdapter, renderItem, initialStartIndexInternal, numOffsetItems, renderedFrames.current);
90
- if (!lastParentId.current || !lastInitialData.current) {
91
- lastParentId.current = uniqueId;
92
- lastInitialData.current = initialData;
93
- }
94
- if (lastParentId.current !== uniqueId) {
95
- initialData = (0, CarouselNativeLogic_1.getInitialData)(dataAdapter, renderItem, initialStartIndexInternal, numOffsetItems, renderedFrames.current);
96
- lastParentId.current = uniqueId;
97
- lastInitialData.current = initialData;
98
- }
85
+ const initialDataLimit = Math.min(renderedFrames, dataAdapter.getItemCount());
86
+ const hasData = initialDataLimit > 0;
87
+ // Cache initialData (the array of dataIndices to seed the children with)
88
+ // on (uniqueId, hasData). The events where the parent's initialData is
89
+ // actually consumed by the rest of the system are first render, uniqueId
90
+ // change (soft rebind), and the empty -> non-empty data arrival. Outside
91
+ // those moments children are driven by native onBind callbacks and
92
+ // shouldComponentUpdate blocks parent-driven re-renders, so a wider cache
93
+ // key would just do unnecessary work.
94
+ //
95
+ // react-hooks/exhaustive-deps is suppressed because the closure references
96
+ // dataAdapter / initialStartIndexInternal / numOffsetItems /
97
+ // initialDataLimit, which we intentionally do NOT include in the dep array
98
+ // — those values are re-read fresh from the closure when the memo
99
+ // recomputes, and adding them to the deps would invalidate on every
100
+ // adapter reference change without a corresponding need to refresh
101
+ // initialData. See VEGATV-115306.
102
+ const initialData = react_1.default.useMemo(() => (0, CarouselNativeLogic_1.computeInitialIndexWindow)(dataAdapter, initialStartIndexInternal, numOffsetItems, initialDataLimit),
103
+ // eslint-disable-next-line react-hooks/exhaustive-deps
104
+ [uniqueId, hasData]);
99
105
  (0, react_1.useEffect)(() => {
100
106
  dataAdapter.registerDataChangeCallback?.(onDataChangeCallback);
101
107
  }, [dataAdapter, onDataChangeCallback]);
102
108
  const startIndex = 0;
103
109
  const endIndex = dataAdapter.getItemCount();
104
- const indices = initialData.map(data => data.index);
105
110
  const relativeInitialStartIndex = react_1.default.useMemo(() => {
106
- const relativeIndex = indices.indexOf(initialStartIndexInternal);
111
+ const relativeIndex = initialData.indexOf(initialStartIndexInternal);
107
112
  return relativeIndex < 0 ? 0 : relativeIndex;
108
- }, [indices, initialStartIndexInternal]);
109
- utils_1.Log.i(`[CarouselScrollable] Rendering Carousel Native Wrapper "${uniqueId || ''}", startIndex = ${startIndex}, endIndex = ${endIndex}, initialSelectedIndex : ${initialStartIndexInternal}, initialData Length : ${initialData.length}, initialData Indices : ${JSON.stringify(indices)}`);
113
+ }, [initialData, initialStartIndexInternal]);
114
+ utils_1.Log.i(`[CarouselScrollable] Rendering Carousel Native Wrapper "${uniqueId ?? ''}", startIndex = ${startIndex}, endIndex = ${endIndex}, initialSelectedIndex : ${initialStartIndexInternal}, initialData Length : ${initialData.length}, initialData Indices : ${JSON.stringify(initialData)}`);
110
115
  return (<CarouselNativeHostComponent_1.CarouselNativeHostComponent {...props} initialStartIndex={initialStartIndexInternal} relativeInitialStartIndex={relativeInitialStartIndex} dataStartIndex={startIndex} dataLimitIndex={endIndex} style={styleWithFlexDirection} scaleItemOnPress={pressedItemScaleFactor != 1.0} scaleItemOnSelection={focusedItemScaleFactor !== 1.0} shiftItemsOnSelection={shiftItemsOnSelection.width !== 1.0 ||
111
116
  shiftItemsOnSelection.height !== 1.0} onSelectionChanged={onNativeSelectionChanged} ref={hostRef}>
112
- {Array(renderedFrames.current)
117
+ {Array(renderedFrames)
113
118
  .fill({})
114
- .map((_, index) => (<CarouselNativeItemWrapper_1.CarouselNativeItemWrapper key={index} parentId={uniqueId} dataIndex={initialData[index]?.index} itemShift={initialData[index]?.shift} initialElement={initialData[index]?.element} initialKey={initialData[index]?.key} showSelectionBorder={showSelectionBorder} selectionBorderStyle={selectionBorderStyle} selectionBorderStrategy={selectionBorderStrategy} focusedItemScaleFactor={focusedItemScaleFactor} pressedItemScaleFactor={adjustedPressedItemScaleFactor} shiftItemsScaleFactor={shiftItemsOnSelection} dataAdapter={dataAdapter} renderItem={renderItem} bindCallback={bindCallback} unbindCallback={unbindCallback}/>))}
119
+ .map((_, index) => (<CarouselNativeItemWrapper_1.CarouselNativeItemWrapper key={index} parentId={uniqueId} dataIndex={initialData[index]} showSelectionBorder={showSelectionBorder} selectionBorderStyle={selectionBorderStyle} selectionBorderStrategy={selectionBorderStrategy} focusedItemScaleFactor={focusedItemScaleFactor} pressedItemScaleFactor={adjustedPressedItemScaleFactor} shiftItemsScaleFactor={shiftItemsOnSelection} dataAdapter={dataAdapter} renderItem={renderItem} bindCallback={bindCallback} unbindCallback={unbindCallback}/>))}
115
120
  </CarouselNativeHostComponent_1.CarouselNativeHostComponent>);
116
121
  }
117
122
  /**
@@ -101,9 +101,14 @@ function JSRecyclerViewListInner(props, _ref) {
101
101
  if (!c || !c.props) {
102
102
  return c;
103
103
  }
104
+ // Build override props without mutating the original element. React 18+
105
+ // freezes element.props in development, so direct assignment to
106
+ // c.props throws TypeError. cloneElement returns a new element with
107
+ // the merged props instead. See VEGATV-115306.
108
+ const overrideProps = {};
104
109
  if (hasProperty(c.props, 'onFocusChanged')) {
105
110
  const { onFocusChanged } = c.props;
106
- c.props.onFocusChanged = (event) => {
111
+ overrideProps.onFocusChanged = (event) => {
107
112
  if (event.focused) {
108
113
  scrollableRef.current?.scrollToIndex(index, true);
109
114
  }
@@ -112,14 +117,16 @@ function JSRecyclerViewListInner(props, _ref) {
112
117
  }
113
118
  if (hasProperty(c.props, 'onItemFocusChanged')) {
114
119
  const { onItemFocusChanged } = c.props;
115
- c.props.onItemFocusChanged = (childItem) => {
120
+ overrideProps.onItemFocusChanged = (childItem) => {
116
121
  if (childItem.childEvent.focused) {
117
122
  scrollableRef.current?.scrollToIndex(index, true);
118
123
  }
119
124
  onItemFocusChanged(childItem);
120
125
  };
121
126
  }
122
- return c;
127
+ return Object.keys(overrideProps).length > 0
128
+ ? React.cloneElement(c, overrideProps)
129
+ : c;
123
130
  }, [dataAdapter, renderItem]);
124
131
  const minStyle = {
125
132
  minHeight: 1,
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name": "@amazon-devices/vega-carousel", "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "files": ["dist", "react-native.config.js", "THIRD_PARTY_NOTICES.txt"], "scripts": {"clean": "rm -rf build dist generated logs node_modules", "test:snapshot": "jest --colors --updateSnapshot", "start": "node node_modules/react-native/local-cli/cli.js start", "test": "jest --colors", "lint": "eslint src --ext .ts,.tsx", "lint:fix": "eslint src --ext .ts,.tsx --fix", "lint-staged": "lint-staged --allow-empty", "format": "clang-format -i --glob=\"native/**/*.{cpp,h}\"", "git-commit-template": "git-commit-template .git-commit-template", "compile": "tsc -p tsconfig-build.json", "release": "npm-run-all format build", "build:js": "tsc -p tsconfig-build.json", "build:native:debug": "vega build -b Debug", "build:native:release": "vega build -b Release", "build:debug": "npm-run-all build:js build:native:debug", "build": "npm-run-all build:js build:native:debug build:native:release"}, "dependencies": {"ansi-colors": "^4.1.3"}, "devDependencies": {"@amazon-devices/react-native-kepler": "^4.0.0", "@babel/core": "^7.20.0", "@babel/plugin-transform-private-methods": "^7.23.3", "@babel/runtime": "^7.20.0", "@bam.tech/jest-junit-reports-merger": "^1.1.0", "@react-native-community/eslint-config": "^3.2.0", "@testing-library/jest-native": "^5.4.3", "@testing-library/react-native": "^13.0.0", "@types/jest": "^28.0.0", "@types/react": "^19.0.0", "@types/react-test-renderer": "^19.0.0", "@typescript-eslint/eslint-plugin": "^5.18.0", "babel-jest": "^29.0.0", "clang-format": "^1.8.0", "eslint": "^8.19.0", "eslint-plugin-react-native": "^4.1.0", "git-commit-template": "^1.0.5", "lint-staged": "^12.3.7", "jest": "^29.0.0", "jest-junit": "^16.0.0", "metro-react-native-babel-preset": "^0.76.5", "npm-run-all": "^4.1.5", "prettier": "^3.0.0", "react": "^19.0.0", "react-native": "^0.83.0", "react-test-renderer": "^19.0.0", "recyclerlistview": "4.2.1", "ts-jest": "^29.0.0", "typedoc": "^0.25.7", "typescript": "4.9.5", "@amazon-devices/kepler-cli-platform": "^0", "@react-native-community/cli": "^11.2.0"}, "peerDependencies": {"@amazon-devices/react-native-kepler": "^4.0.0", "react": "^19.0.0", "react-native": "^0.83.0"}, "kepler": {"projectType": "turbomodule", "toolchain": "clang", "targets": ["tv"], "os": ["vega"]}, "eslintConfig": {"extends": "@react-native-community", "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint"], "root": true}, "prettier": {"arrowParens": "avoid", "bracketSameLine": true, "bracketSpacing": false, "singleQuote": true, "trailingComma": "all"}, "npm-pretty-much": {"allowUnsafeName": "reason for using a unsafe name"}, "license": "See license in LICENSE"}
1
+ {"name": "@amazon-devices/vega-carousel", "version": "1.0.1", "main": "dist/index.js", "types": "dist/index.d.ts", "files": ["dist", "react-native.config.js", "THIRD_PARTY_NOTICES.txt"], "scripts": {"clean": "rm -rf build dist generated logs node_modules", "test:snapshot": "jest --colors --updateSnapshot", "start": "node node_modules/react-native/local-cli/cli.js start", "test": "jest --colors", "lint": "eslint src --ext .ts,.tsx", "lint:fix": "eslint src --ext .ts,.tsx --fix", "lint-staged": "lint-staged --allow-empty", "format": "clang-format -i --glob=\"native/**/*.{cpp,h}\"", "git-commit-template": "git-commit-template .git-commit-template", "compile": "tsc -p tsconfig-build.json", "release": "npm-run-all format build", "build:js": "tsc -p tsconfig-build.json", "build:native:debug": "vega build -b Debug", "build:native:release": "vega build -b Release", "build:debug": "npm-run-all build:js build:native:debug", "build": "npm-run-all build:js build:native:debug build:native:release"}, "dependencies": {"ansi-colors": "^4.1.3"}, "devDependencies": {"@amazon-devices/react-native-kepler": "^4.0.0", "@babel/core": "^7.20.0", "@babel/plugin-transform-private-methods": "^7.23.3", "@babel/runtime": "^7.20.0", "@bam.tech/jest-junit-reports-merger": "^1.1.0", "@react-native-community/eslint-config": "^3.2.0", "@testing-library/jest-native": "^5.4.3", "@testing-library/react-native": "^13.0.0", "@types/jest": "^28.0.0", "@types/react": "^19.0.0", "@types/react-test-renderer": "^19.0.0", "@typescript-eslint/eslint-plugin": "^5.18.0", "babel-jest": "^29.0.0", "clang-format": "^1.8.0", "eslint": "^8.19.0", "eslint-plugin-react-native": "^4.1.0", "git-commit-template": "^1.0.5", "lint-staged": "^12.3.7", "jest": "^29.0.0", "jest-junit": "^16.0.0", "metro-react-native-babel-preset": "^0.76.5", "npm-run-all": "^4.1.5", "prettier": "^3.0.0", "react": "^19.0.0", "react-native": "^0.83.0", "react-test-renderer": "^19.0.0", "recyclerlistview": "4.2.1", "ts-jest": "^29.0.0", "typedoc": "^0.25.7", "typescript": "4.9.5", "@amazon-devices/kepler-cli-platform": "^0", "@react-native-community/cli": "^11.2.0"}, "peerDependencies": {"@amazon-devices/react-native-kepler": "^4.0.0", "react": "^19.0.0", "react-native": "^0.83.0"}, "kepler": {"projectType": "turbomodule", "toolchain": "clang", "targets": ["tv"], "os": ["vega"]}, "eslintConfig": {"extends": "@react-native-community", "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint"], "root": true}, "prettier": {"arrowParens": "avoid", "bracketSameLine": true, "bracketSpacing": false, "singleQuote": true, "trailingComma": "all"}, "npm-pretty-much": {"allowUnsafeName": "reason for using a unsafe name"}, "license": "See license in LICENSE"}