revert: simplify VirtualGrid item size measurement

This commit is contained in:
Rizumu Ayaka
2026-02-04 20:18:33 +08:00
parent 48e7f1b2ca
commit 893c237e33
2 changed files with 19 additions and 124 deletions

View File

@@ -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<TestItem>, {
props: {
items,
gridStyle: defaultGridStyle,
defaultItemHeight: 50,
defaultItemWidth: 100,
maxColumns: 1,
bufferRows: 0
},
slots: {
item: `<template #item="{ index }">
<div class="test-index">{{ index }}</div>
</template>`
},
attachTo: document.body
})
await nextTick()
const initialIndices = wrapper
.findAll('.test-index')
.map((node) => node.text())
expect(initialIndices[0]).toBe('2')
const renderedItemEls = wrapper
.findAll<HTMLElement>('[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

View File

@@ -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<HTMLElement | null>(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<CSSProperties>(() => {
@@ -78,8 +78,8 @@ const mergedGridStyle = computed<CSSProperties>(() => {
}
})
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<GridState>(() => {
@@ -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<CSSProperties>(() => ({
height: spacerRowsToHeight(topSpacerRows.value)
height: rowsToHeight(state.value.start)
}))
const bottomSpacerStyle = computed<CSSProperties>(() => ({
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)