@amazon-devices/vega-carousel 0.2.1 → 0.2.5

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.
@@ -1,5 +1,5 @@
1
1
  import { HostComponent, StyleProp, ViewStyle } from 'react-native';
2
- import { ListOrientation, OnItemBindFunc, OnItemUnbindFunc, OnSelectionChangedCallback } from '../CarouselScrollableTypes';
2
+ import { ListOrientation, ListSelectionStrategy, OnItemBindFunc, OnItemUnbindFunc, OnSelectionChangedCallback } from '../CarouselScrollableTypes';
3
3
  import { CarouselNativeHostComponentSelectionChangedEvent, CarouselNativeHostComponentProps } from './CarouselNativeHostComponent';
4
4
  import { CarouselDataAdapter, CarouselDataChange, CarouselResolvedDataValues, RenderItemFunc } from '../../CarouselData';
5
5
  import { AnyValue } from '../../utils';
@@ -112,6 +112,47 @@ export declare function callReset(hostComponent: HostComponent<CarouselNativeHos
112
112
  * `baseStyle` and a _row_ or _column_ flex direction.
113
113
  */
114
114
  export declare function addFlexDirectionStyle(baseStyle: StyleProp<ViewStyle>, orientation: ListOrientation): StyleProp<ViewStyle>;
115
+ /**
116
+ * Resolves how many items {@link getInitialData} should place **before** the
117
+ * `initialStartIndex` in the initial render window.
118
+ *
119
+ * For {@link ListSelectionStrategy.ANCHORED} and
120
+ * {@link ListSelectionStrategy.NATURAL}, the initial pool is a leading-margin
121
+ * window, so the back count is simply `numOffsetItems` (the historical
122
+ * behavior).
123
+ *
124
+ * For {@link ListSelectionStrategy.PINNED} the native layer redistributes the
125
+ * initial pool so the selected item rests at its pin proportion. If JS resolves
126
+ * a different window than native pins to, the leading pin-window frames native
127
+ * wants were never sent by JS, so native fires an async `bindTo` roundtrip to
128
+ * refill them — a visible pop-in on the leading side (VEGATV-116583). To stay
129
+ * in lockstep this MIRRORS native `computePinnedPoolStart`'s `itemsBefore`
130
+ * exactly:
131
+ *
132
+ * pool = min(renderedItemsCount, itemCount) // native items_.size()
133
+ * itemsBefore = clamp(floor(pool * pin), minBefore, maxBefore)
134
+ * minBefore = (hasDataBefore && pool > 2) ? 1 : 0
135
+ * maxBefore = (hasDataAfter && pool > 2) ? pool - 2 : pool - 1
136
+ *
137
+ * A plain `min(floor(pool*pin), pool-1)` diverges from native at the extremes
138
+ * (e.g. pool 8 pin 0.9 → JS 7 vs native 6; pin 0.0 mid-list → JS 0 vs native 1),
139
+ * and that off-by-one frame is exactly the leading frame native would then have
140
+ * to bind async — the roundtrip this change removes. Center pins already match;
141
+ * mirroring the clamp closes the extremes.
142
+ *
143
+ * @param {ListSelectionStrategy} selectionStrategy The list's selection strategy.
144
+ * @param {number} renderedItemsCount The configured render window size.
145
+ * @param {number} numOffsetItems The leading-margin back count used for the
146
+ * non-pinned strategies.
147
+ * @param {number} pinnedFocusOffset The resolved pin proportion (0.0 - 1.0);
148
+ * only used when `selectionStrategy` is `PINNED`.
149
+ * @param {number} initialStartIndex The selected data index at mount (used to
150
+ * detect whether data exists before/after, matching native).
151
+ * @param {number} itemCount Total number of items in the data set.
152
+ *
153
+ * @return {number} The number of items to resolve before `initialStartIndex`.
154
+ */
155
+ export declare function resolveInitialBackCount(selectionStrategy: ListSelectionStrategy, renderedItemsCount: number, numOffsetItems: number, pinnedFocusOffset: number, initialStartIndex: number, itemCount: number): number;
115
156
  /**
116
157
  * This function is the core logic to retrieve items on the {@link CarouselScrollable}
117
158
  * initial render.
@@ -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.getInitialData = exports.resolveInitialBackCount = 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");
@@ -154,6 +154,63 @@ function addFlexDirectionStyle(baseStyle, orientation) {
154
154
  : FlexDirectionColumn);
155
155
  }
156
156
  exports.addFlexDirectionStyle = addFlexDirectionStyle;
157
+ /**
158
+ * Resolves how many items {@link getInitialData} should place **before** the
159
+ * `initialStartIndex` in the initial render window.
160
+ *
161
+ * For {@link ListSelectionStrategy.ANCHORED} and
162
+ * {@link ListSelectionStrategy.NATURAL}, the initial pool is a leading-margin
163
+ * window, so the back count is simply `numOffsetItems` (the historical
164
+ * behavior).
165
+ *
166
+ * For {@link ListSelectionStrategy.PINNED} the native layer redistributes the
167
+ * initial pool so the selected item rests at its pin proportion. If JS resolves
168
+ * a different window than native pins to, the leading pin-window frames native
169
+ * wants were never sent by JS, so native fires an async `bindTo` roundtrip to
170
+ * refill them — a visible pop-in on the leading side (VEGATV-116583). To stay
171
+ * in lockstep this MIRRORS native `computePinnedPoolStart`'s `itemsBefore`
172
+ * exactly:
173
+ *
174
+ * pool = min(renderedItemsCount, itemCount) // native items_.size()
175
+ * itemsBefore = clamp(floor(pool * pin), minBefore, maxBefore)
176
+ * minBefore = (hasDataBefore && pool > 2) ? 1 : 0
177
+ * maxBefore = (hasDataAfter && pool > 2) ? pool - 2 : pool - 1
178
+ *
179
+ * A plain `min(floor(pool*pin), pool-1)` diverges from native at the extremes
180
+ * (e.g. pool 8 pin 0.9 → JS 7 vs native 6; pin 0.0 mid-list → JS 0 vs native 1),
181
+ * and that off-by-one frame is exactly the leading frame native would then have
182
+ * to bind async — the roundtrip this change removes. Center pins already match;
183
+ * mirroring the clamp closes the extremes.
184
+ *
185
+ * @param {ListSelectionStrategy} selectionStrategy The list's selection strategy.
186
+ * @param {number} renderedItemsCount The configured render window size.
187
+ * @param {number} numOffsetItems The leading-margin back count used for the
188
+ * non-pinned strategies.
189
+ * @param {number} pinnedFocusOffset The resolved pin proportion (0.0 - 1.0);
190
+ * only used when `selectionStrategy` is `PINNED`.
191
+ * @param {number} initialStartIndex The selected data index at mount (used to
192
+ * detect whether data exists before/after, matching native).
193
+ * @param {number} itemCount Total number of items in the data set.
194
+ *
195
+ * @return {number} The number of items to resolve before `initialStartIndex`.
196
+ */
197
+ function resolveInitialBackCount(selectionStrategy, renderedItemsCount, numOffsetItems, pinnedFocusOffset, initialStartIndex, itemCount) {
198
+ if (selectionStrategy !== CarouselScrollableTypes_1.ListSelectionStrategy.PINNED) {
199
+ return numOffsetItems;
200
+ }
201
+ // Pool = the number of frames native actually renders (native items_.size()).
202
+ const pool = Math.min(renderedItemsCount, itemCount);
203
+ const rawItemsBefore = Math.floor(pool * pinnedFocusOffset);
204
+ const hasDataBefore = initialStartIndex > 0;
205
+ const hasDataAfter = initialStartIndex < itemCount - 1;
206
+ // The pool > 2 guard matches native (prevents an inverted [minBefore, maxBefore] range for
207
+ // tiny pools, i.e. minBefore > maxBefore).
208
+ const minBefore = hasDataBefore && pool > 2 ? 1 : 0;
209
+ const maxBefore = hasDataAfter && pool > 2 ? pool - 2 : pool - 1;
210
+ // std::clamp(v, lo, hi) == min(max(v, lo), hi)
211
+ return Math.min(Math.max(rawItemsBefore, minBefore), maxBefore);
212
+ }
213
+ exports.resolveInitialBackCount = resolveInitialBackCount;
157
214
  /**
158
215
  * This function is the core logic to retrieve items on the {@link CarouselScrollable}
159
216
  * initial render.
@@ -51,10 +51,21 @@ function CarouselNativeInner(rawProps, ref) {
51
51
  const dataAdapter = (0, CarouselScrollableProps_1.getDataAdapter)(props);
52
52
  const hostRef = react_1.default.useRef(null);
53
53
  const [childrenStates] = react_1.default.useState(new Map());
54
- const { style, uniqueId, initialStartIndex, orientation, renderedItemsCount, numOffsetItems, showSelectionBorder, selectionBorderStyle, selectionBorderStrategy, focusedItemScaleFactor, pressedItemScaleFactor, shiftItemsOnSelection, renderItem, onItemBind, onItemUnbind, onSelectionChanged, } = props;
54
+ const { style, uniqueId, initialStartIndex, orientation, renderedItemsCount, numOffsetItems, selectionStrategy, pinnedFocusOffset, showSelectionBorder, selectionBorderStyle, selectionBorderStrategy, focusedItemScaleFactor, pressedItemScaleFactor, shiftItemsOnSelection, renderItem, onItemBind, onItemUnbind, onSelectionChanged, } = props;
55
55
  const initialStartIndexInternal = react_1.default.useMemo(() => initialStartIndex < dataAdapter.getItemCount()
56
56
  ? initialStartIndex
57
57
  : Math.max(0, dataAdapter.getItemCount() - 1), [initialStartIndex, dataAdapter]);
58
+ // For PINNED, resolve the same pin-centered window native binds to on mount so
59
+ // the leading pin-window frames are already in initialData and native never has
60
+ // to fire an async bindTo roundtrip to refill them (VEGATV-116583).
61
+ const initialBackCount = react_1.default.useMemo(() => (0, CarouselNativeLogic_1.resolveInitialBackCount)(selectionStrategy, renderedItemsCount, numOffsetItems, pinnedFocusOffset, initialStartIndexInternal, dataAdapter.getItemCount()), [
62
+ selectionStrategy,
63
+ renderedItemsCount,
64
+ numOffsetItems,
65
+ pinnedFocusOffset,
66
+ initialStartIndexInternal,
67
+ dataAdapter,
68
+ ]);
58
69
  // Activation animations on the native layer assume pressedItemScaleFactor is based on the unselected dimensions of an item.
59
70
  // However, the prop for this wrapper is documented to be based on the selected dimensions to better align with specs given by BXT.
60
71
  // This means that if a partner passes an pressedItemScaleFactor of 0.94 and a focusedItemScaleFactor of 1.4, when an item is pressed it should
@@ -86,13 +97,13 @@ function CarouselNativeInner(rawProps, ref) {
86
97
  const styleWithFlexDirection = react_1.default.useMemo(() => (0, CarouselNativeLogic_1.addFlexDirectionStyle)(style, orientation), [style, orientation]);
87
98
  let initialData = lastInitialData.current && lastInitialData.current.length !== 0
88
99
  ? lastInitialData.current
89
- : (0, CarouselNativeLogic_1.getInitialData)(dataAdapter, renderItem, initialStartIndexInternal, numOffsetItems, renderedFrames.current);
100
+ : (0, CarouselNativeLogic_1.getInitialData)(dataAdapter, renderItem, initialStartIndexInternal, initialBackCount, renderedFrames.current);
90
101
  if (!lastParentId.current || !lastInitialData.current) {
91
102
  lastParentId.current = uniqueId;
92
103
  lastInitialData.current = initialData;
93
104
  }
94
105
  if (lastParentId.current !== uniqueId) {
95
- initialData = (0, CarouselNativeLogic_1.getInitialData)(dataAdapter, renderItem, initialStartIndexInternal, numOffsetItems, renderedFrames.current);
106
+ initialData = (0, CarouselNativeLogic_1.getInitialData)(dataAdapter, renderItem, initialStartIndexInternal, initialBackCount, renderedFrames.current);
96
107
  lastParentId.current = uniqueId;
97
108
  lastInitialData.current = initialData;
98
109
  }
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name": "@amazon-devices/vega-carousel", "version": "0.2.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", "build": "npm-run-all format compile"}, "dependencies": {"@amazon-devices/react-native-kepler": "^2.0.0", "ansi-colors": "^4.1.3", "react": "^18.2.0", "react-native": "^0.72.0", "recyclerlistview": "4.2.1"}, "devDependencies": {"@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": "^11.5.4", "@types/jest": "^28.0.0", "@types/react-test-renderer": "^18.0.0", "@typescript-eslint/eslint-plugin": "^5.18.0", "babel-jest": "^28.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": "^28.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": "^18.2.0", "react-native": "^0.72.0", "react-test-renderer": "18.2.0", "ts-jest": "^28.0.0", "typedoc": "^0.25.7", "typescript": "4.9.5", "@amazon-devices/kepler-cli-platform": "^0", "@react-native-community/cli": "^11.2.0"}, "resolutions": {"@types/react": "^18"}, "peerDependencies": {"react": "^18.2.0", "react-native": "^0.72.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": "0.2.5", "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", "build": "npm-run-all format compile"}, "dependencies": {"@amazon-devices/react-native-kepler": "^2.0.0", "ansi-colors": "^4.1.3", "react": "^18.2.0", "react-native": "^0.72.0", "recyclerlistview": "4.2.1"}, "devDependencies": {"@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": "^11.5.4", "@types/jest": "^28.0.0", "@types/react-test-renderer": "^18.0.0", "@typescript-eslint/eslint-plugin": "^5.18.0", "babel-jest": "^28.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": "^28.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": "^18.2.0", "react-native": "^0.72.0", "react-test-renderer": "18.2.0", "ts-jest": "^28.0.0", "typedoc": "^0.25.7", "typescript": "4.9.5", "@amazon-devices/kepler-cli-platform": "^0", "@react-native-community/cli": "^11.2.0"}, "resolutions": {"@types/react": "^18"}, "peerDependencies": {"react": "^18.2.0", "react-native": "^0.72.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"}