From 1214ec72988866763397fb867e998cc354fa6b88 Mon Sep 17 00:00:00 2001
From: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
Date: Mon, 18 May 2026 20:48:38 +0200
Subject: [PATCH] Fixed #4109 - DataTable + VirtualScroller spacer drift with
rowGroupMode subheader
Add an optional getItemSize(index): number prop to VirtualScroller so it
can be made variable-row-height aware. When supplied, VS builds a
cumulative-size prefix-sum array and uses it for spacer total, content
translation, scroll-to-index, and viewport-size calculation. When null
(default), VS retains its existing uniform-height code path byte-for-byte.
DataTable supplies a getItemSize that returns itemSize plus an auto-
measured group-header height at cluster boundaries, and 0 for hidden
rows inside collapsed clusters. The spacer-tbody subtraction sums the
actually-rendered window's sizes. This corrects both drift sources:
collapsed clusters no longer over-contribute to the cumulative total,
and subheader rows are now accounted for in the spacer formula.
Two related pre-existing bugs are addressed in the same change:
- BodyRow.shouldRenderRowGroupHeader / shouldRenderRowGroupFooter use
this.value[this.rowIndex - 1], which mixes a windowed value array
with an absolute rowIndex in VS mode. Switched to this.index for
windowed position lookup. Non-VS behavior unchanged.
- TableBody's inline top on every sticky group-header causes them to
stack at the same threshold when groups are dense in VS mode. Gated
on isVirtualScrollerDisabled so non-VS sticky behavior is preserved.
Tests: 11 new VirtualScroller specs (uniform/variable paths, cumulative
correctness, binary-search boundary cases, reference-change invalidation)
and 3 new DataTable specs (getItemSize boundary detection, collapsed-
cluster handling). All 410 existing specs continue to pass.
Signed-off-by: Oliver Sluke <22557015+oliversluke@users.noreply.github.com>
---
packages/primevue/src/datatable/BodyRow.vue | 4 +-
.../primevue/src/datatable/DataTable.spec.js | 58 ++++++++
packages/primevue/src/datatable/DataTable.vue | 70 +++++++++-
packages/primevue/src/datatable/TableBody.vue | 2 +-
.../virtualscroller/BaseVirtualScroller.vue | 4 +
.../virtualscroller/VirtualScroller.spec.js | 126 +++++++++++++++++
.../src/virtualscroller/VirtualScroller.vue | 127 ++++++++++++++++--
7 files changed, 373 insertions(+), 18 deletions(-)
create mode 100644 packages/primevue/src/virtualscroller/VirtualScroller.spec.js
diff --git a/packages/primevue/src/datatable/BodyRow.vue b/packages/primevue/src/datatable/BodyRow.vue
index 07502945e1..3dc39d4d85 100644
--- a/packages/primevue/src/datatable/BodyRow.vue
+++ b/packages/primevue/src/datatable/BodyRow.vue
@@ -554,7 +554,7 @@ export default {
},
shouldRenderRowGroupHeader() {
const currentRowFieldData = resolveFieldData(this.rowData, this.groupRowsBy);
- const prevRowData = this.value[this.rowIndex - 1];
+ const prevRowData = this.value[this.index - 1];
if (prevRowData) {
const previousRowFieldData = resolveFieldData(prevRowData, this.groupRowsBy);
@@ -569,7 +569,7 @@ export default {
return false;
} else {
let currentRowFieldData = resolveFieldData(this.rowData, this.groupRowsBy);
- let nextRowData = this.value[this.rowIndex + 1];
+ let nextRowData = this.value[this.index + 1];
if (nextRowData) {
let nextRowFieldData = resolveFieldData(nextRowData, this.groupRowsBy);
diff --git a/packages/primevue/src/datatable/DataTable.spec.js b/packages/primevue/src/datatable/DataTable.spec.js
index 4c1899f8ee..645b9cb632 100644
--- a/packages/primevue/src/datatable/DataTable.spec.js
+++ b/packages/primevue/src/datatable/DataTable.spec.js
@@ -1540,4 +1540,62 @@ describe('DataTable.vue', () => {
expect(rowCheckboxIcons[1].text()).toBe('CustomIcon');
expect(rowCheckboxIcons[2].text()).toBe('CustomIcon');
});
+
+ // VirtualScroller + rowGroupMode="subheader" item-size contract
+ it('should expose getItemSize as null when rowGroupMode is unset', () => {
+ expect(wrapper.vm.getItemSize).toBe(null);
+ });
+
+ it('should report itemSize plus a header contribution at group boundaries', async () => {
+ const grouped = [
+ { id: 0, rep: 'Amy' },
+ { id: 1, rep: 'Amy' },
+ { id: 2, rep: 'John' },
+ { id: 3, rep: 'John' },
+ { id: 4, rep: 'Maria' }
+ ];
+
+ await wrapper.setProps({
+ value: grouped,
+ rowGroupMode: 'subheader',
+ groupRowsBy: 'rep',
+ virtualScrollerOptions: { itemSize: 36 }
+ });
+ wrapper.vm.d_rowGroupHeaderHeight = 40;
+
+ const fn = wrapper.vm.getItemSize;
+
+ expect(typeof fn).toBe('function');
+ expect(fn(0)).toBe(76); // boundary (first row) + data row
+ expect(fn(1)).toBe(36); // continuation of Amy
+ expect(fn(2)).toBe(76); // boundary into John
+ expect(fn(3)).toBe(36); // continuation of John
+ expect(fn(4)).toBe(76); // boundary into Maria
+ });
+
+ it('should report 0 for non-anchor rows of a collapsed group', async () => {
+ const grouped = [
+ { id: 0, rep: 'Amy' },
+ { id: 1, rep: 'Amy' },
+ { id: 2, rep: 'John' },
+ { id: 3, rep: 'John' }
+ ];
+
+ await wrapper.setProps({
+ value: grouped,
+ rowGroupMode: 'subheader',
+ groupRowsBy: 'rep',
+ expandableRowGroups: true,
+ expandedRowGroups: ['John'],
+ virtualScrollerOptions: { itemSize: 36 }
+ });
+ wrapper.vm.d_rowGroupHeaderHeight = 40;
+
+ const fn = wrapper.vm.getItemSize;
+
+ expect(fn(0)).toBe(40); // Amy anchor: header only (collapsed)
+ expect(fn(1)).toBe(0); // Amy continuation: hidden
+ expect(fn(2)).toBe(76); // John anchor: header + data row (expanded)
+ expect(fn(3)).toBe(36); // John continuation: data row only
+ });
});
diff --git a/packages/primevue/src/datatable/DataTable.vue b/packages/primevue/src/datatable/DataTable.vue
index fee84ef25f..c98c1df8d3 100755
--- a/packages/primevue/src/datatable/DataTable.vue
+++ b/packages/primevue/src/datatable/DataTable.vue
@@ -82,6 +82,7 @@
:style="scrollHeight !== 'flex' ? { height: scrollHeight } : undefined"
:scrollHeight="scrollHeight !== 'flex' ? undefined : '100%'"
:disabled="virtualScrollerDisabled"
+ :getItemSize="getItemSize"
loaderDisabled
inline
autoSize
@@ -242,7 +243,7 @@
@@ -423,7 +424,8 @@ export default {
d_editingMeta: {},
d_filters: this.cloneFilters(this.filters),
d_columns: new HelperSet({ type: 'Column' }),
- d_columnGroups: new HelperSet({ type: 'ColumnGroup' })
+ d_columnGroups: new HelperSet({ type: 'ColumnGroup' }),
+ d_rowGroupHeaderHeight: 0
};
},
rowTouched: false,
@@ -499,6 +501,8 @@ export default {
if (this.editMode === 'row' && this.dataKey && !this.d_editingRowKeys) {
this.updateEditingRowKeys(this.editingRows);
}
+
+ this.updateRowGroupHeaderHeight();
},
beforeUnmount() {
this.unbindColumnResizeEvents();
@@ -515,6 +519,8 @@ export default {
if (this.editMode === 'row' && this.dataKey && !this.d_editingRowKeys) {
this.updateEditingRowKeys(this.editingRows);
}
+
+ this.updateRowGroupHeaderHeight();
},
methods: {
columnProp(col, prop) {
@@ -2045,6 +2051,40 @@ export default {
},
hasSpacerStyle(style) {
return isNotEmpty(style);
+ },
+ updateRowGroupHeaderHeight() {
+ if (this.rowGroupMode !== 'subheader' || !this.groupRowsBy || this.virtualScrollerDisabled) {
+ return;
+ }
+
+ const header = this.$refs.table && findSingle(this.$refs.table, 'tr.p-datatable-row-group-header');
+
+ if (header) {
+ const height = getOuterHeight(header);
+
+ if (height && height !== this.d_rowGroupHeaderHeight) {
+ this.d_rowGroupHeaderHeight = height;
+ }
+ }
+ },
+ getRenderedWindowSize(slotProps) {
+ if (!slotProps || !slotProps.rows || !slotProps.rows.length) {
+ return 0;
+ }
+
+ const fn = this.getItemSize;
+
+ if (typeof fn !== 'function') {
+ return slotProps.rows.length * slotProps.itemSize;
+ }
+
+ let total = 0;
+
+ for (let i = 0; i < slotProps.rows.length; i++) {
+ total += fn(slotProps.getItemOptions(i).index);
+ }
+
+ return total;
}
},
computed: {
@@ -2111,6 +2151,32 @@ export default {
return !data || data.length === 0;
},
+ getItemSize() {
+ const baseSize = (this.virtualScrollerOptions && this.virtualScrollerOptions.itemSize) || 0;
+ const groupRowsBy = this.groupRowsBy;
+ const subheader = this.rowGroupMode === 'subheader' && !!groupRowsBy;
+
+ if (!subheader) {
+ return null;
+ }
+
+ const items = this.processedData;
+ const expandable = this.expandableRowGroups;
+ const expandedGroups = this.expandedRowGroups;
+ const headerHeight = this.d_rowGroupHeaderHeight;
+
+ return (index) => {
+ if (!items || index < 0 || index >= items.length) {
+ return baseSize;
+ }
+
+ const groupValue = resolveFieldData(items[index], groupRowsBy);
+ const isFirstOfGroup = index === 0 || resolveFieldData(items[index - 1], groupRowsBy) !== groupValue;
+ const isExpanded = !expandable || (expandedGroups && expandedGroups.indexOf(groupValue) > -1);
+
+ return (isFirstOfGroup ? headerHeight : 0) + (isExpanded ? baseSize : 0);
+ };
+ },
paginatorTop() {
return this.paginator && (this.paginatorPosition !== 'bottom' || this.paginatorPosition === 'both');
},
diff --git a/packages/primevue/src/datatable/TableBody.vue b/packages/primevue/src/datatable/TableBody.vue
index 9d052807af..0a9f0158ef 100755
--- a/packages/primevue/src/datatable/TableBody.vue
+++ b/packages/primevue/src/datatable/TableBody.vue
@@ -279,7 +279,7 @@ export default {
},
computed: {
rowGroupHeaderStyle() {
- if (this.scrollable) {
+ if (this.scrollable && this.isVirtualScrollerDisabled) {
return { top: this.rowGroupHeaderStyleObject.top };
}
diff --git a/packages/primevue/src/virtualscroller/BaseVirtualScroller.vue b/packages/primevue/src/virtualscroller/BaseVirtualScroller.vue
index ddecde33cc..135ad9fa10 100644
--- a/packages/primevue/src/virtualscroller/BaseVirtualScroller.vue
+++ b/packages/primevue/src/virtualscroller/BaseVirtualScroller.vue
@@ -20,6 +20,10 @@ export default {
type: [Number, Array],
default: 0
},
+ getItemSize: {
+ type: Function,
+ default: null
+ },
scrollHeight: null,
scrollWidth: null,
orientation: {
diff --git a/packages/primevue/src/virtualscroller/VirtualScroller.spec.js b/packages/primevue/src/virtualscroller/VirtualScroller.spec.js
new file mode 100644
index 0000000000..4a0f02ed03
--- /dev/null
+++ b/packages/primevue/src/virtualscroller/VirtualScroller.spec.js
@@ -0,0 +1,126 @@
+import { mount } from '@vue/test-utils';
+import PrimeVue from 'primevue/config';
+import VirtualScroller from './VirtualScroller.vue';
+
+window.ResizeObserver = class {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+};
+
+const buildItems = (count) => Array.from({ length: count }, (_, i) => ({ id: i }));
+
+const mountScroller = (props = {}) => {
+ const wrapper = mount(VirtualScroller, {
+ global: { plugins: [PrimeVue] },
+ props: {
+ items: buildItems(20),
+ itemSize: 30,
+ style: { height: '120px', width: '200px' },
+ ...props
+ }
+ });
+
+ // jsdom does not report element visibility, so viewInit() skips init().
+ // getContentPosition reads parseFloat('') -> NaN in jsdom; stub it to 0.
+ wrapper.vm.getContentPosition = () => ({ left: 0, right: 0, top: 0, bottom: 0, x: 0, y: 0 });
+ wrapper.vm.init();
+
+ return wrapper;
+};
+
+describe('VirtualScroller.vue', () => {
+ it('should exist', () => {
+ const wrapper = mountScroller();
+
+ expect(wrapper.find('.p-virtualscroller').exists()).toBe(true);
+ });
+
+ describe('uniform-size path (getItemSize not provided)', () => {
+ it('should not build cumulative sizes', () => {
+ const wrapper = mountScroller();
+
+ expect(wrapper.vm.cumulativeSizes).toBe(null);
+ });
+
+ it('should compute spacer height from items.length * itemSize', () => {
+ const wrapper = mountScroller();
+
+ expect(wrapper.vm.spacerStyle.height).toBe('600px');
+ });
+
+ it('should compute index from scrollTop via floor division', () => {
+ const wrapper = mountScroller();
+
+ expect(wrapper.vm.getIndexAtOffset(0)).toBe(0);
+ expect(wrapper.vm.getIndexAtOffset(90)).toBe(3);
+ expect(wrapper.vm.getIndexAtOffset(599)).toBe(19);
+ });
+ });
+
+ describe('variable-size path (getItemSize provided)', () => {
+ // header on every fifth row: sizes 80, 30, 30, 30, 30, 80, 30, ...
+ const stepSize = (index) => (index % 5 === 0 ? 80 : 30);
+
+ it('should build cumulative sizes from getItemSize', () => {
+ const wrapper = mountScroller({ getItemSize: stepSize });
+
+ const sizes = wrapper.vm.cumulativeSizes;
+
+ expect(sizes.length).toBe(21);
+ expect(sizes[0]).toBe(0);
+ expect(sizes[1]).toBe(80);
+ expect(sizes[5]).toBe(80 + 30 * 4);
+ expect(sizes[20]).toBe(80 * 4 + 30 * 16);
+ });
+
+ it('should compute spacer height from cumulative total', () => {
+ const wrapper = mountScroller({ getItemSize: stepSize });
+
+ expect(wrapper.vm.spacerStyle.height).toBe(`${80 * 4 + 30 * 16}px`);
+ });
+
+ it('getItemOffset should return the y-coordinate of items[index]', () => {
+ const wrapper = mountScroller({ getItemSize: stepSize });
+
+ expect(wrapper.vm.getItemOffset(0)).toBe(0);
+ expect(wrapper.vm.getItemOffset(1)).toBe(80);
+ expect(wrapper.vm.getItemOffset(5)).toBe(80 + 30 * 4);
+ });
+
+ it('getIndexAtOffset should binary-search the cumulative array', () => {
+ const wrapper = mountScroller({ getItemSize: stepSize });
+
+ expect(wrapper.vm.getIndexAtOffset(0)).toBe(0);
+ expect(wrapper.vm.getIndexAtOffset(79)).toBe(0);
+ expect(wrapper.vm.getIndexAtOffset(80)).toBe(1);
+ expect(wrapper.vm.getIndexAtOffset(199)).toBe(4);
+ expect(wrapper.vm.getIndexAtOffset(200)).toBe(5);
+ expect(wrapper.vm.getIndexAtOffset(-50)).toBe(0);
+ expect(wrapper.vm.getIndexAtOffset(99999)).toBe(19);
+ });
+
+ it('should rebuild cumulative sizes when getItemSize reference changes', async () => {
+ const wrapper = mountScroller({ getItemSize: stepSize });
+
+ expect(wrapper.vm.spacerStyle.height).toBe(`${80 * 4 + 30 * 16}px`);
+
+ await wrapper.setProps({ getItemSize: () => 40 });
+ wrapper.vm.init();
+
+ expect(wrapper.vm.spacerStyle.height).toBe('800px');
+ });
+
+ it('should fall back to uniform math when getItemSize is removed', async () => {
+ const wrapper = mountScroller({ getItemSize: stepSize });
+
+ expect(wrapper.vm.cumulativeSizes).not.toBe(null);
+
+ await wrapper.setProps({ getItemSize: null });
+ wrapper.vm.init();
+
+ expect(wrapper.vm.cumulativeSizes).toBe(null);
+ expect(wrapper.vm.spacerStyle.height).toBe('600px');
+ });
+ });
+});
diff --git a/packages/primevue/src/virtualscroller/VirtualScroller.vue b/packages/primevue/src/virtualscroller/VirtualScroller.vue
index d5b9dc2784..30a8b2db7e 100644
--- a/packages/primevue/src/virtualscroller/VirtualScroller.vue
+++ b/packages/primevue/src/virtualscroller/VirtualScroller.vue
@@ -71,6 +71,7 @@ export default {
},
element: null,
content: null,
+ cumulativeSizes: null,
lastScrollPos: null,
scrollTimeout: null,
resizeTimeout: null,
@@ -105,6 +106,10 @@ export default {
this.init();
this.calculateAutoSize();
},
+ getItemSize() {
+ this.init();
+ this.calculateAutoSize();
+ },
orientation() {
this.lastScrollPos = this.isBoth() ? { top: 0, left: 0 } : 0;
},
@@ -152,10 +157,84 @@ export default {
init() {
if (!this.disabled) {
this.setSize();
+ this.buildCumulativeSizes();
this.calculateOptions();
this.setSpacerSize();
}
},
+ buildCumulativeSizes() {
+ if (typeof this.getItemSize !== 'function' || this.isBoth() || this.isHorizontal()) {
+ this.cumulativeSizes = null;
+
+ return;
+ }
+
+ const items = this.items || [];
+ const sizes = new Array(items.length + 1);
+
+ sizes[0] = 0;
+
+ for (let i = 0; i < items.length; i++) {
+ sizes[i + 1] = sizes[i] + this.getItemSize(i);
+ }
+
+ this.cumulativeSizes = sizes;
+ },
+ getItemOffset(index) {
+ if (this.cumulativeSizes) {
+ const i = Math.max(0, Math.min(index, this.cumulativeSizes.length - 1));
+
+ return this.cumulativeSizes[i];
+ }
+
+ return index * this.itemSize;
+ },
+ getIndexAtOffset(offset) {
+ if (!this.cumulativeSizes) {
+ return Math.floor(offset / (this.itemSize || offset));
+ }
+
+ const sizes = this.cumulativeSizes;
+
+ if (offset <= 0) return 0;
+
+ const last = sizes.length - 1;
+
+ if (offset >= sizes[last]) return Math.max(0, last - 1);
+
+ let lo = 0;
+ let hi = last;
+
+ while (lo < hi) {
+ const mid = (lo + hi) >>> 1;
+
+ if (sizes[mid + 1] <= offset) lo = mid + 1;
+ else hi = mid;
+ }
+
+ return lo;
+ },
+ getLastByCumulative(first, numToleratedItems) {
+ if (!this.cumulativeSizes) return this.getLast(first);
+
+ const sizes = this.cumulativeSizes;
+ const contentPos = this.getContentPosition();
+ const contentHeight = this.element ? this.element.offsetHeight - contentPos.top : 0;
+ const startOffset = sizes[Math.min(first, sizes.length - 1)];
+ const target = startOffset + contentHeight + 2 * numToleratedItems * this.itemSize;
+
+ let lo = first;
+ let hi = sizes.length - 1;
+
+ while (lo < hi) {
+ const mid = (lo + hi) >>> 1;
+
+ if (sizes[mid] < target) lo = mid + 1;
+ else hi = mid;
+ }
+
+ return lo;
+ },
isVertical() {
return this.orientation === 'vertical';
},
@@ -194,7 +273,15 @@ export default {
isRangeChanged = newFirst.rows !== first.rows || newFirst.cols !== first.cols;
} else {
newFirst = calculateFirst(index, numToleratedItems);
- horizontal ? scrollTo(calculateCoord(newFirst, itemSize, contentPos.left), scrollTop) : scrollTo(scrollLeft, calculateCoord(newFirst, itemSize, contentPos.top));
+
+ if (horizontal) {
+ scrollTo(calculateCoord(newFirst, itemSize, contentPos.left), scrollTop);
+ } else if (this.cumulativeSizes) {
+ scrollTo(scrollLeft, this.getItemOffset(newFirst) + contentPos.top);
+ } else {
+ scrollTo(scrollLeft, calculateCoord(newFirst, itemSize, contentPos.top));
+ }
+
isScrollChanged = this.lastScrollPos !== (horizontal ? scrollLeft : scrollTop);
isRangeChanged = newFirst !== first;
}
@@ -224,7 +311,8 @@ export default {
}
} else {
if (viewport.first - first > index) {
- const pos = (viewport.first - 1) * this.itemSize;
+ const target = viewport.first - 1;
+ const pos = !horizontal && this.cumulativeSizes ? this.getItemOffset(target) : target * this.itemSize;
horizontal ? scrollTo(pos, 0) : scrollTo(0, pos);
}
@@ -238,7 +326,8 @@ export default {
}
} else {
if (viewport.last - first <= index + 1) {
- const pos = (viewport.first + 1) * this.itemSize;
+ const target = viewport.first + 1;
+ const pos = !horizontal && this.cumulativeSizes ? this.getItemOffset(target) : target * this.itemSize;
horizontal ? scrollTo(pos, 0) : scrollTo(0, pos);
}
@@ -266,7 +355,7 @@ export default {
} else {
const scrollPos = horizontal ? scrollLeft : scrollTop;
- firstInViewport = calculateFirstInViewport(scrollPos, this.itemSize);
+ firstInViewport = !horizontal && this.cumulativeSizes ? this.getIndexAtOffset(scrollPos) : calculateFirstInViewport(scrollPos, this.itemSize);
lastInViewport = firstInViewport + this.numItemsInViewport;
}
}
@@ -299,12 +388,19 @@ export default {
},
calculateOptions() {
const both = this.isBoth();
+ const horizontal = this.isHorizontal();
const first = this.first;
const { numItemsInViewport, numToleratedItems } = this.calculateNumItems();
const calculateLast = (_first, _num, _numT, _isCols = false) => this.getLast(_first + _num + (_first < _numT ? 2 : 3) * _numT, _isCols);
- const last = both
- ? { rows: calculateLast(first.rows, numItemsInViewport.rows, numToleratedItems[0]), cols: calculateLast(first.cols, numItemsInViewport.cols, numToleratedItems[1], true) }
- : calculateLast(first, numItemsInViewport, numToleratedItems);
+ let last;
+
+ if (both) {
+ last = { rows: calculateLast(first.rows, numItemsInViewport.rows, numToleratedItems[0]), cols: calculateLast(first.cols, numItemsInViewport.cols, numToleratedItems[1], true) };
+ } else if (!horizontal && this.cumulativeSizes) {
+ last = this.getLastByCumulative(first, numToleratedItems);
+ } else {
+ last = calculateLast(first, numItemsInViewport, numToleratedItems);
+ }
this.last = last;
this.numItemsInViewport = numItemsInViewport;
@@ -396,12 +492,17 @@ export default {
const horizontal = this.isHorizontal();
const contentPos = this.getContentPosition();
const setProp = (_name, _value, _size, _cpos = 0) => (this.spacerStyle = { ...this.spacerStyle, ...{ [`${_name}`]: (_value || []).length * _size + _cpos + 'px' } });
+ const setVerticalSize = (_cpos = 0) => (this.spacerStyle = { ...this.spacerStyle, height: this.cumulativeSizes[this.cumulativeSizes.length - 1] + _cpos + 'px' });
if (both) {
setProp('height', items, this.itemSize[0], contentPos.y);
setProp('width', this.columns || items[1], this.itemSize[1], contentPos.x);
+ } else if (horizontal) {
+ setProp('width', this.columns || items, this.itemSize, contentPos.x);
+ } else if (this.cumulativeSizes) {
+ setVerticalSize(contentPos.y);
} else {
- horizontal ? setProp('width', this.columns || items, this.itemSize, contentPos.x) : setProp('height', items, this.itemSize, contentPos.y);
+ setProp('height', items, this.itemSize, contentPos.y);
}
}
},
@@ -415,10 +516,10 @@ export default {
if (both) {
setTransform(calculateTranslateVal(first.cols, this.itemSize[1]), calculateTranslateVal(first.rows, this.itemSize[0]));
+ } else if (horizontal) {
+ setTransform(calculateTranslateVal(first, this.itemSize), 0);
} else {
- const translateVal = calculateTranslateVal(first, this.itemSize);
-
- horizontal ? setTransform(translateVal, 0) : setTransform(0, translateVal);
+ setTransform(0, this.cumulativeSizes ? this.getItemOffset(first) : calculateTranslateVal(first, this.itemSize));
}
}
},
@@ -488,11 +589,11 @@ export default {
const isScrollDownOrRight = this.lastScrollPos <= scrollPos;
if (!this.appendOnly || (this.appendOnly && isScrollDownOrRight)) {
- const currentIndex = calculateCurrentIndex(scrollPos, this.itemSize);
+ const currentIndex = !horizontal && this.cumulativeSizes ? this.getIndexAtOffset(scrollPos) : calculateCurrentIndex(scrollPos, this.itemSize);
const triggerIndex = calculateTriggerIndex(currentIndex, this.first, this.last, this.numItemsInViewport, this.d_numToleratedItems, isScrollDownOrRight);
newFirst = calculateFirst(currentIndex, triggerIndex, this.first, this.last, this.numItemsInViewport, this.d_numToleratedItems, isScrollDownOrRight);
- newLast = calculateLast(currentIndex, newFirst, this.last, this.numItemsInViewport, this.d_numToleratedItems);
+ newLast = !horizontal && this.cumulativeSizes ? this.getLastByCumulative(newFirst, this.d_numToleratedItems) : calculateLast(currentIndex, newFirst, this.last, this.numItemsInViewport, this.d_numToleratedItems);
isRangeChanged = newFirst !== this.first || newLast !== this.last || this.isRangeChanged;
newScrollPos = scrollPos;
}