Skip to content

Commit 135d507

Browse files
andylou0102AndyLuoOrbisKantfu
authored
fix(useScroll): use mutationObserver to update arrivedState when the DOM is changed (#4433)
Co-authored-by: AndyLuo <AndyLuo@OmnichatdeMBP-5.localdomain> Co-authored-by: Robin <robin.kehl@singular-it.de> Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
1 parent ffc1ae3 commit 135d507

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { page } from '@vitest/browser/context'
2+
import { describe, expect, it } from 'vitest'
3+
import { computed, defineComponent, shallowRef, useTemplateRef } from 'vue'
4+
import { useScroll } from '.'
5+
6+
const Component = defineComponent({
7+
template: `
8+
<div style='padding: 12px; display: flex; gap: 8px'>
9+
<button data-testId="left" @click="goToLeft">goToLeft</button>
10+
<button data-testId="right" @click="goToRight">goToRight</button>
11+
<button data-testId="top" @click="goToTop">goToTop</button>
12+
<button data-testId="bottom" @click="goToBottom">goToBottom</button>
13+
<button data-testId="toggleWidth" @click="toggleWidth">toggleWidth</button>
14+
<button data-testId="toggleHeight" @click="toggleHeight">toggleHeight</button>
15+
<button data-testId="toggleBox" @click="toggleBox">toggleBox</button>
16+
</div>
17+
<pre data-testId="arrivedState">{{ arrivedState }}</pre>
18+
<div
19+
ref="el"
20+
style="width: 300px; height: 300px; margin: auto; overflow: auto;"
21+
>
22+
<div v-if="showBox" :style></div>
23+
</div>
24+
`,
25+
props: ['observe'],
26+
setup(props) {
27+
const el = useTemplateRef<HTMLElement>('el')
28+
const { x, y, arrivedState } = useScroll(el, { observe: props.observe ?? false })
29+
function triggerScrollManually() {
30+
el.value?.dispatchEvent(new Event('scroll'))
31+
}
32+
function goToLeft() {
33+
x.value = 0
34+
triggerScrollManually()
35+
}
36+
function goToRight() {
37+
x.value = el.value?.scrollWidth || 300
38+
triggerScrollManually()
39+
}
40+
function goToTop() {
41+
y.value = 0
42+
triggerScrollManually()
43+
}
44+
function goToBottom() {
45+
y.value = el.value?.scrollHeight || 300
46+
triggerScrollManually()
47+
}
48+
const height = shallowRef(500)
49+
function toggleHeight() {
50+
if (height.value < 500)
51+
height.value = 500
52+
else
53+
height.value = 300
54+
}
55+
const width = shallowRef(500)
56+
function toggleWidth() {
57+
if (width.value < 500)
58+
width.value = 500
59+
else
60+
width.value = 300
61+
}
62+
const style = computed(() => `width: ${width.value}px; height: ${height.value}px; position: relative;`)
63+
const showBox = shallowRef(true)
64+
function toggleBox() {
65+
showBox.value = !showBox.value
66+
}
67+
return {
68+
el,
69+
style,
70+
arrivedState,
71+
showBox,
72+
goToLeft,
73+
goToRight,
74+
goToTop,
75+
goToBottom,
76+
toggleHeight,
77+
toggleWidth,
78+
toggleBox,
79+
}
80+
},
81+
})
82+
83+
describe('useScroll', () => {
84+
it('should correctly detect leftArrived and rightArrived states when reaching the X-axis boundaries', async () => {
85+
const screen = page.render(Component, { props: { observe: true } })
86+
expect(screen).toBeDefined()
87+
const arrivedState = screen.getByTestId('arrivedState')
88+
await expect.element(arrivedState).toBeVisible()
89+
const rightButton = screen.getByTestId('right')
90+
await expect.element(rightButton).toBeVisible()
91+
await rightButton.click()
92+
expect(arrivedState.query()?.textContent).toMatchInlineSnapshot(`
93+
"{
94+
"left": false,
95+
"right": true,
96+
"top": true,
97+
"bottom": false
98+
}"
99+
`)
100+
const leftButton = screen.getByTestId('left')
101+
await expect.element(leftButton).toBeVisible()
102+
await leftButton.click()
103+
expect(arrivedState.query()?.textContent).toMatchInlineSnapshot(`
104+
"{
105+
"left": true,
106+
"right": false,
107+
"top": true,
108+
"bottom": false
109+
}"
110+
`)
111+
})
112+
it('should correctly detect topArrived and bottomArrived states when reaching the Y-axis boundaries', async () => {
113+
const screen = page.render(Component, { props: { observe: true } })
114+
expect(screen).toBeDefined()
115+
const arrivedState = screen.getByTestId('arrivedState')
116+
await expect.element(arrivedState).toBeVisible()
117+
const bottomButton = screen.getByTestId('bottom')
118+
await expect.element(bottomButton).toBeVisible()
119+
await bottomButton.click()
120+
expect(arrivedState.query()?.textContent).toMatchInlineSnapshot(`
121+
"{
122+
"left": true,
123+
"right": false,
124+
"top": false,
125+
"bottom": true
126+
}"
127+
`)
128+
const topButton = screen.getByTestId('top')
129+
await expect.element(topButton).toBeVisible()
130+
await topButton.click()
131+
expect(arrivedState.query()?.textContent).toMatchInlineSnapshot(`
132+
"{
133+
"left": true,
134+
"right": false,
135+
"top": true,
136+
"bottom": false
137+
}"
138+
`)
139+
})
140+
describe('observe DOM mutations when observe is enabled', () => {
141+
it('should detect boundary changes when child element size is modified', async () => {
142+
const screen = page.render(Component, { props: { observe: true } })
143+
expect(screen).toBeDefined()
144+
const arrivedState = screen.getByTestId('arrivedState')
145+
await expect.element(arrivedState).toBeVisible()
146+
const toggleHeightButton = screen.getByTestId('toggleHeight')
147+
const toggleWidthButton = screen.getByTestId('toggleWidth')
148+
await expect.element(toggleHeightButton).toBeVisible()
149+
await expect.element(toggleWidthButton).toBeVisible()
150+
await toggleHeightButton.click()
151+
await toggleWidthButton.click()
152+
expect(arrivedState.query()?.textContent).toMatchInlineSnapshot(`
153+
"{
154+
"left": true,
155+
"right": true,
156+
"top": true,
157+
"bottom": true
158+
}"
159+
`)
160+
await toggleHeightButton.click()
161+
await toggleWidthButton.click()
162+
expect(arrivedState.query()?.textContent).toMatchInlineSnapshot(`
163+
"{
164+
"left": true,
165+
"right": false,
166+
"top": true,
167+
"bottom": false
168+
}"
169+
`)
170+
})
171+
it('should detect boundary changes when child element is added or removed', async () => {
172+
const screen = page.render(Component, { props: { observe: true } })
173+
expect(screen).toBeDefined()
174+
const arrivedState = screen.getByTestId('arrivedState')
175+
await expect.element(arrivedState).toBeVisible()
176+
const toggleBoxButton = screen.getByTestId('toggleBox')
177+
await expect.element(toggleBoxButton).toBeVisible()
178+
await toggleBoxButton.click()
179+
expect(arrivedState.query()?.textContent).toMatchInlineSnapshot(`
180+
"{
181+
"left": true,
182+
"right": true,
183+
"top": true,
184+
"bottom": true
185+
}"
186+
`)
187+
await toggleBoxButton.click()
188+
expect(arrivedState.query()?.textContent).toMatchInlineSnapshot(`
189+
"{
190+
"left": true,
191+
"right": false,
192+
"top": true,
193+
"bottom": false
194+
}"
195+
`)
196+
})
197+
})
198+
})

packages/core/useScroll/index.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { computed, reactive, shallowRef, toValue } from 'vue'
55
import { defaultWindow } from '../_configurable'
66
import { unrefElement } from '../unrefElement'
77
import { useEventListener } from '../useEventListener'
8+
import { useMutationObserver } from '../useMutationObserver'
89

910
export interface UseScrollOptions extends ConfigurableWindow {
1011
/**
@@ -33,6 +34,15 @@ export interface UseScrollOptions extends ConfigurableWindow {
3334
bottom?: number
3435
}
3536

37+
/**
38+
* Use MutationObserver to monitor specific DOM changes,
39+
* such as attribute modifications, child node additions or removals, or subtree changes.
40+
* @default { mutation: boolean }
41+
*/
42+
observe?: boolean | {
43+
mutation?: boolean
44+
}
45+
3646
/**
3747
* Trigger it when scrolling.
3848
*
@@ -98,6 +108,9 @@ export function useScroll(
98108
top: 0,
99109
bottom: 0,
100110
},
111+
observe: _observe = {
112+
mutation: false,
113+
},
101114
eventListenerOptions = {
102115
capture: false,
103116
passive: true,
@@ -107,6 +120,12 @@ export function useScroll(
107120
onError = (e) => { console.error(e) },
108121
} = options
109122

123+
const observe = typeof _observe === 'boolean'
124+
? {
125+
mutation: _observe,
126+
}
127+
: _observe
128+
110129
const internalX = shallowRef(0)
111130
const internalY = shallowRef(0)
112131

@@ -279,6 +298,23 @@ export function useScroll(
279298
}
280299
})
281300

301+
if (observe?.mutation && element != null && element !== window && element !== document) {
302+
useMutationObserver(
303+
element as MaybeRefOrGetter<HTMLElement | SVGElement>,
304+
() => {
305+
const _element = toValue(element)
306+
if (!_element)
307+
return
308+
setArrivedState(_element)
309+
},
310+
{
311+
attributes: true,
312+
childList: true,
313+
subtree: true,
314+
},
315+
)
316+
}
317+
282318
useEventListener(
283319
element,
284320
'scrollend',

0 commit comments

Comments
 (0)