From 893c237e330aa741c22b730b19d4e42d96387ba6 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Wed, 4 Feb 2026 20:18:33 +0800 Subject: [PATCH] revert: simplify VirtualGrid item size measurement --- src/components/common/VirtualGrid.test.ts | 58 ---------------- src/components/common/VirtualGrid.vue | 85 +++++------------------ 2 files changed, 19 insertions(+), 124 deletions(-) diff --git a/src/components/common/VirtualGrid.test.ts b/src/components/common/VirtualGrid.test.ts index 6c7a636af..477d0ad74 100644 --- a/src/components/common/VirtualGrid.test.ts +++ b/src/components/common/VirtualGrid.test.ts @@ -105,64 +105,6 @@ describe('VirtualGrid', () => { wrapper.unmount() }) - it('uses measured row step (including gap) to compute visible range', async () => { - const items = createItems(100) - mockedWidth.value = 100 - mockedHeight.value = 60 - mockedScrollY.value = 110 - - const wrapper = mount(VirtualGrid, { - props: { - items, - gridStyle: defaultGridStyle, - defaultItemHeight: 50, - defaultItemWidth: 100, - maxColumns: 1, - bufferRows: 0 - }, - slots: { - item: `` - }, - attachTo: document.body - }) - - await nextTick() - - const initialIndices = wrapper - .findAll('.test-index') - .map((node) => node.text()) - expect(initialIndices[0]).toBe('2') - - const renderedItemEls = wrapper - .findAll('[data-virtual-grid-item]') - .map((node) => node.element) - - expect(renderedItemEls.length).toBeGreaterThanOrEqual(2) - - Object.defineProperty(renderedItemEls[0], 'clientHeight', { value: 50 }) - Object.defineProperty(renderedItemEls[0], 'clientWidth', { value: 100 }) - Object.defineProperty(renderedItemEls[0], 'offsetTop', { value: 0 }) - Object.defineProperty(renderedItemEls[0], 'offsetLeft', { value: 0 }) - - Object.defineProperty(renderedItemEls[1], 'clientHeight', { value: 50 }) - Object.defineProperty(renderedItemEls[1], 'clientWidth', { value: 100 }) - Object.defineProperty(renderedItemEls[1], 'offsetTop', { value: 60 }) - Object.defineProperty(renderedItemEls[1], 'offsetLeft', { value: 0 }) - - await wrapper.setProps({ items: [...items] }) - await nextTick() - await nextTick() - - const updatedIndices = wrapper - .findAll('.test-index') - .map((node) => node.text()) - expect(updatedIndices[0]).toBe('1') - - wrapper.unmount() - }) - it('respects maxColumns prop', async () => { const items = createItems(10) mockedWidth.value = 400 diff --git a/src/components/common/VirtualGrid.vue b/src/components/common/VirtualGrid.vue index 7226a4884..571038abb 100644 --- a/src/components/common/VirtualGrid.vue +++ b/src/components/common/VirtualGrid.vue @@ -57,8 +57,8 @@ const emit = defineEmits<{ 'approach-end': [] }>() -const rowHeight = ref(defaultItemHeight) -const colWidth = ref(defaultItemWidth) +const itemHeight = ref(defaultItemHeight) +const itemWidth = ref(defaultItemWidth) const container = ref(null) const { width, height } = useElementSize(container) const { y: scrollY } = useScroll(container, { @@ -67,7 +67,7 @@ const { y: scrollY } = useScroll(container, { }) const cols = computed(() => - Math.min(Math.floor(width.value / colWidth.value) || 1, maxColumns) + Math.min(Math.floor(width.value / itemWidth.value) || 1, maxColumns) ) const mergedGridStyle = computed(() => { @@ -78,8 +78,8 @@ const mergedGridStyle = computed(() => { } }) -const viewRows = computed(() => Math.ceil(height.value / rowHeight.value)) -const offsetRows = computed(() => Math.floor(scrollY.value / rowHeight.value)) +const viewRows = computed(() => Math.ceil(height.value / itemHeight.value)) +const offsetRows = computed(() => Math.floor(scrollY.value / itemHeight.value)) const isValidGrid = computed(() => height.value && width.value && items?.length) const state = computed(() => { @@ -101,28 +101,15 @@ const renderedItems = computed(() => isValidGrid.value ? items.slice(state.value.start, state.value.end) : [] ) -function spacerRowsToHeight(rows: number): string { - return `${rows * rowHeight.value}px` +function rowsToHeight(rows: number): string { + return `${(rows / cols.value) * itemHeight.value}px` } -const topSpacerRows = computed(() => { - if (!isValidGrid.value) return 0 - return Math.floor(state.value.start / cols.value) -}) - -const bottomSpacerRows = computed(() => { - if (!isValidGrid.value) return 0 - - const totalRows = Math.ceil(items.length / cols.value) - const renderedEndRow = Math.ceil(state.value.end / cols.value) - return Math.max(0, totalRows - renderedEndRow) -}) - const topSpacerStyle = computed(() => ({ - height: spacerRowsToHeight(topSpacerRows.value) + height: rowsToHeight(state.value.start) })) const bottomSpacerStyle = computed(() => ({ - height: spacerRowsToHeight(bottomSpacerRows.value) + height: rowsToHeight(items.length - state.value.end) })) whenever( @@ -132,53 +119,19 @@ whenever( } ) -const ITEM_SIZE_EPSILON_PX = 1 - -/** - * Measures the effective grid row/column step (including `gap`) from rendered - * items to keep spacer math stable and prevent scroll jitter near the end. - */ function updateItemSize(): void { - if (!container.value) return + if (container.value) { + const firstItem = container.value.querySelector('[data-virtual-grid-item]') - const itemElements = Array.from( - container.value.querySelectorAll('[data-virtual-grid-item]') - ).filter((node): node is HTMLElement => node instanceof HTMLElement) + // Don't update item size if the first item is not rendered yet + if (!firstItem?.clientHeight || !firstItem?.clientWidth) return - const firstItem = itemElements[0] - - if (!firstItem?.clientHeight || !firstItem?.clientWidth) return - - const nextRowItem = itemElements.find( - (item) => item.offsetTop > firstItem.offsetTop - ) - - const measuredRowHeight = nextRowItem - ? nextRowItem.offsetTop - firstItem.offsetTop - : firstItem.clientHeight - - const nextColItem = itemElements.find( - (item) => - item.offsetTop === firstItem.offsetTop && - item.offsetLeft > firstItem.offsetLeft - ) - - const measuredColWidth = nextColItem - ? nextColItem.offsetLeft - firstItem.offsetLeft - : firstItem.clientWidth - - if ( - measuredRowHeight > 0 && - Math.abs(rowHeight.value - measuredRowHeight) >= ITEM_SIZE_EPSILON_PX - ) { - rowHeight.value = measuredRowHeight - } - - if ( - measuredColWidth > 0 && - Math.abs(colWidth.value - measuredColWidth) >= ITEM_SIZE_EPSILON_PX - ) { - colWidth.value = measuredColWidth + if (itemHeight.value !== firstItem.clientHeight) { + itemHeight.value = firstItem.clientHeight + } + if (itemWidth.value !== firstItem.clientWidth) { + itemWidth.value = firstItem.clientWidth + } } } const onResize = debounce(updateItemSize, resizeDebounce)