Skip to content
This repository was archived by the owner on Jul 1, 2023. It is now read-only.

Commit c7364c9

Browse files
committed
* Fix jumping scrolling after horizontal resize
* More stable scrolling when items height is too inconsistent
1 parent 155d866 commit c7364c9

File tree

8 files changed

+130
-102
lines changed

8 files changed

+130
-102
lines changed

.github/workflows/nodejs.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
22
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
33

4-
name: Node.js CI
4+
name: tests
55

66
on:
77
push:
@@ -25,5 +25,6 @@ jobs:
2525
with:
2626
node-version: ${{ matrix.node-version }}
2727
- run: npm ci
28+
- run: npm run lint
2829
- run: npm run build
2930
- run: npm run test

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ npm run serve
8383

8484
* `bottom`: emits when the last item is rendered. Used for infinite scroll
8585

86+
**Note:** You need to disable [scroll anchoring](https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-anchor/Guide_to_scroll_anchoring) on the outer container. Without it after adding new items browser will scroll back to the bottom and bottom event will fire again.
87+
```css
88+
.container {
89+
overflow-anchor: none;
90+
}
91+
```
92+
8693

8794
# Similar projects
8895

dev/serve.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div>
2+
<div class="container">
33
<div class="box"></div>
44
<list-scroller
55
:itemComponent="item"
@@ -64,7 +64,12 @@ export default {
6464
border: solid;
6565
border-width: 1px;
6666
}
67+
6768
.list {
6869
margin-bottom: 1.1em;
6970
}
71+
72+
.container {
73+
overflow-anchor: none;
74+
}
7075
</style>

jest.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ module.exports = {
22
preset: '@vue/cli-plugin-unit-jest',
33
testMatch: ['<rootDir>/tests/*.test.js'],
44
collectCoverage: true,
5-
collectCoverageFrom: ["src/**/*.{js,vue}"],
6-
coverageReporters: ["text-summary"]
5+
collectCoverageFrom: ['src/**/*.{js,vue}'],
6+
coverageReporters: ['text'],
77
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vue-list-scroller",
3-
"version": "1.0.1",
3+
"version": "1.0.3",
44
"description": "Simple Vue.js component for efficiant rendering large lists",
55
"homepage": "https://github.com/IvanSafonov/vue-list-scroller",
66
"license": "MIT",

src/listscroller.vue

Lines changed: 42 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,15 @@ export default {
114114
/**
115115
* Resize event handler
116116
* Clears known items' heights if window width changes
117+
* Keeps visible heights
117118
*/
118119
resizeHandler() {
119-
if (!pxEq(this.innerWidth, window.innerWidth)) this.heights.clear()
120+
if (!pxEq(this.prevWidth, this.width())) {
121+
const old = this.heights
122+
this.heights = new Map()
123+
for (let i = this.start; i < this.end; i++)
124+
if (old.has(i)) this.heights.set(i, old.get(i))
125+
}
120126
},
121127
122128
/**
@@ -179,11 +185,10 @@ export default {
179185
this.setStart(this.start)
180186
}
181187
182-
this.fixHeight()
183-
184-
const bottom = this.itemsData.length === this.end
185-
if (bottom && !this.bottom) this.$emit('bottom')
186-
this.bottom = this.itemsData.length === this.end
188+
this.$nextTick(() => {
189+
this.fixSpacerMargin()
190+
this.fixHeight()
191+
})
187192
},
188193
189194
observe(from, to) {
@@ -203,14 +208,15 @@ export default {
203208
* Calculates end index and updates observed items in ResizeObserver
204209
*/
205210
setStart(index) {
206-
this.innerWidth = window.innerWidth
211+
this.prevWidth = this.width()
207212
const count = Math.round(
208213
this.renderViewports * Math.ceil(window.innerHeight / this.average),
209214
)
210215
const prevStart = this.start
211216
const prevEnd = this.end
212217
this.start = Math.max(index, 0)
213218
this.end = Math.min(this.start + count, this.itemsData.length)
219+
if (prevStart === this.start && prevEnd === this.end) return
214220
215221
if (this.start != prevStart || this.end != prevEnd)
216222
this.unobserve(0, Math.min(this.start, prevEnd) - prevStart)
@@ -220,80 +226,62 @@ export default {
220226
prevEnd - prevStart,
221227
)
222228
223-
this.fixSpacerMargin()
224-
this.updateHeight()
225-
226229
if (this.start < prevStart)
227230
this.observe(0, Math.min(prevStart, this.end) - this.start)
228231
if (prevEnd < this.end)
229232
this.observe(
230233
Math.max(prevEnd, this.start) - this.start,
231234
this.end - this.start,
232235
)
233-
},
234236
235-
getBottoms() {
236-
const { list, spacer } = this.$refs
237-
return spacer && list
238-
? [
239-
spacer.getBoundingClientRect().bottom,
240-
list.getBoundingClientRect().bottom,
241-
]
242-
: [0, 0]
237+
const bottom = this.itemsData.length === this.end
238+
if (bottom && !this.bottom) this.$emit('bottom')
239+
this.bottom = bottom
243240
},
244241
245242
/**
246243
* Fixes spacerMargin errors
247244
*/
248245
fixSpacerMargin() {
249-
if (
250-
(this.start === 0 && !pxEq(this.spacerMargin, 0)) ||
251-
pxGt(0, this.spacerMargin)
252-
) {
246+
if (this.start === 0) {
247+
if (pxEq(this.spacerMargin, 0)) return
253248
window.scroll(0, window.scrollY - this.spacerMargin)
254249
this.spacerMargin = 0
255-
return
256-
}
257-
258-
const avMargin = this.spacerMargin - this.average * this.start
259-
if (Math.abs(avMargin) > 0.05 * this.itemsData.length * this.average) {
250+
} else if (pxGt(0, this.spacerMargin)) {
251+
const avMargin = this.spacerMargin - this.average * this.start
260252
this.spacerMargin -= avMargin
261253
window.scroll(0, window.scrollY - avMargin)
262254
}
263255
},
264256
265257
/**
266-
* Updates list height according to average item height
267-
* Updates only if the difference is more than 5%
258+
* Fixes height error
268259
*/
269-
updateHeight() {
270-
const avHeight = this.average * this.itemsData.length
271-
const [btSpacer, btRoot] = this.getBottoms()
272-
273-
if (
274-
this.itemsData.length === 0 ||
275-
Math.abs(this.height - avHeight) >
276-
0.05 * this.itemsData.length * this.average ||
277-
(this.itemsData.length !== this.end && pxGt(btSpacer, btRoot))
260+
fixHeight() {
261+
const { list, spacer } = this.$refs
262+
const diff =
263+
spacer.getBoundingClientRect().bottom -
264+
list.getBoundingClientRect().bottom
265+
266+
if (this.itemsData.length == this.end) {
267+
if (pxEq(diff, 0)) return
268+
this.height += diff
269+
} else if (pxGt(diff, 0)) {
270+
this.height = Math.max(
271+
this.height + diff,
272+
this.average * this.itemsData.length,
273+
)
274+
} else if (
275+
Math.abs(this.height - this.average * this.itemsData.length) >
276+
0.1 * this.itemsData.length * this.average
278277
) {
279-
this.height = avHeight
278+
this.height = this.average * this.itemsData.length
280279
}
281280
},
282281
283-
/**
284-
* Fixes height error when the bottom of the list is rendered
285-
*/
286-
fixHeight(sync = false) {
287-
if (!sync) return this.$nextTick(() => this.fixHeight(true))
288-
289-
const [btSpacer, btRoot] = this.getBottoms()
290-
if (
291-
(this.itemsData.length == this.end && pxGt(btRoot, btSpacer)) ||
292-
pxGt(btSpacer, btRoot)
293-
) {
294-
const fix = btSpacer - btRoot
295-
this.height += fix
296-
}
282+
width() {
283+
const { list } = this.$refs
284+
return list ? list.getBoundingClientRect().width : 0
297285
},
298286
},
299287

tests/__snapshots__/listscroller.test.js.snap

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
exports[`ListScroller component with 1000 items corrects spacer margin on scrolling up 1`] = `
44
<div
5-
style="height: 18750px; position: relative; overflow: hidden;"
5+
style="height: 20000px; position: relative; overflow: hidden;"
66
>
77
<div
88
style="transform: translateY(1875px);"
@@ -76,7 +76,7 @@ exports[`ListScroller component with 1000 items handles horisontal resize 1`] =
7676
style="height: 30000px; position: relative; overflow: hidden;"
7777
>
7878
<div
79-
style="transform: translateY(4500px);"
79+
style="transform: translateY(2880px);"
8080
>
8181
<div
8282
data-item-index="150"
@@ -129,10 +129,10 @@ exports[`ListScroller component with 1000 items handles horisontal resize 1`] =
129129

130130
exports[`ListScroller component with 1000 items handles vertical resize 1`] = `
131131
<div
132-
style="height: 20000px; position: relative; overflow: hidden;"
132+
style="height: 30000px; position: relative; overflow: hidden;"
133133
>
134134
<div
135-
style="transform: translateY(3000px);"
135+
style="transform: translateY(2880px);"
136136
>
137137
<div
138138
data-item-index="150"
@@ -179,21 +179,6 @@ exports[`ListScroller component with 1000 items handles vertical resize 1`] = `
179179
>
180180
158 158
181181
</div>
182-
<div
183-
data-item-index="159"
184-
>
185-
159 159
186-
</div>
187-
<div
188-
data-item-index="160"
189-
>
190-
160 160
191-
</div>
192-
<div
193-
data-item-index="161"
194-
>
195-
161 161
196-
</div>
197182
</div>
198183
</div>
199184
`;

0 commit comments

Comments
 (0)