Skip to content

Commit dd7c8aa

Browse files
authored
Merge pull request #16 from microcipcip/feature/useScroll
feat(useScroll): Adding useScroll feature
2 parents 0c142fa + cf14283 commit dd7c8aa

File tree

10 files changed

+334
-2
lines changed

10 files changed

+334
-2
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ export default Vue.extend({
8888
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouseleavepage--demo)
8989
- [`useOrientation`](./src/functions/useOrientation/stories/useOrientation.md) — tracks state of device's screen orientation.
9090
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-useorientation--demo)
91+
- [`useScroll`](./src/functions/useScroll/stories/useScroll.md) — tracks an HTML element's scroll position.
92+
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usescroll--demo)
9193
- [`useSearchParams`](./src/functions/useSearchParams/stories/useSearchParams.md) — tracks browser's location search params.
9294
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usesearchparams--demo)
9395
- Animations

src/functions/useMouse/stories/useMouse.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function useMouse(): {
2828
import { useMouse } from 'vue-use-kit'
2929
3030
export default Vue.extend({
31-
name: 'useMouse',
31+
name: 'useMouseDemo',
3232
setup() {
3333
const { docX, docY } = useMouse()
3434
return { docX, docY }

src/functions/useOrientation/stories/useOrientation.story.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ const basicDemo = () => ({
1717
demo-name="UseOrientationDemo.vue"
1818
>
1919
<template v-slot:title></template>
20-
<template v-slot:intro></template>
20+
<template v-slot:intro>
21+
<p>
22+
<strong>Try to rotate your device</strong> to see the value changes.
23+
Please note that this will work only on supported devices.
24+
</p>
25+
</template>
2126
</story-title>
2227
<demo />
2328
</div>`

src/functions/useScroll/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useScroll'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<template>
2+
<div>
3+
<table class="table is-fullwidth">
4+
<thead>
5+
<tr>
6+
<th>Prop</th>
7+
<th>Value</th>
8+
</tr>
9+
</thead>
10+
<tbody>
11+
<tr>
12+
<td>isScrolling</td>
13+
<td>{{ isScrolling }}</td>
14+
</tr>
15+
<tr>
16+
<td>x, y</td>
17+
<td>{{ x }}px - {{ y }}px</td>
18+
</tr>
19+
<tr>
20+
<td colspan="2">
21+
<button class="button is-primary" @click="start" v-if="!isTracking">
22+
Enable scroll tracking
23+
</button>
24+
<button class="button is-danger" @click="stop" v-else>
25+
Disable scroll tracking
26+
</button>
27+
</td>
28+
</tr>
29+
</tbody>
30+
</table>
31+
<div class="scrollme" ref="scrollRef">
32+
<span class="scrollme__msg">Scroll me!</span>
33+
<div class="scrollme__height"></div>
34+
</div>
35+
</div>
36+
</template>
37+
38+
<script lang="ts">
39+
import Vue from 'vue'
40+
import { useScroll } from '@src/vue-use-kit'
41+
import { ref } from '@src/api'
42+
43+
export default Vue.extend({
44+
name: 'UseScrollDemo',
45+
setup() {
46+
const scrollRef = ref(null)
47+
const { x, y, isScrolling, isTracking, start, stop } = useScroll(scrollRef)
48+
return { scrollRef, x, y, isScrolling, isTracking, start, stop }
49+
}
50+
})
51+
</script>
52+
53+
<style scoped>
54+
.scrollme {
55+
position: relative;
56+
overflow-y: scroll;
57+
height: 200px;
58+
background: #f1f1f1;
59+
}
60+
61+
.scrollme__msg {
62+
padding: 10px;
63+
}
64+
65+
.scrollme__height {
66+
width: 2000px;
67+
height: 40000px;
68+
}
69+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# useScroll
2+
3+
Vue function that tracks an HTML element's scroll position.
4+
5+
## Reference
6+
7+
```typescript
8+
function useScroll(
9+
elRef: Ref<null | HTMLElement | Window>,
10+
ms?: number,
11+
runOnMount?: boolean
12+
): {
13+
x: Ref<number>
14+
y: Ref<number>
15+
isTracking: Ref<boolean>
16+
isScrolling: Ref<boolean>
17+
start: () => void
18+
stop: () => void
19+
}
20+
```
21+
22+
### Parameters
23+
24+
- `elRef: Ref<null | HTMLElement | Window>` target element used for tracking the `x` and `y` scroll position
25+
- `ms: number` how many milliseconds of delay before `isScrolling` goes back to false (basically when user is idle), `150` by default
26+
- `runOnMount: boolean` whether to run the scroll tracking on mount, `true` by default
27+
28+
### Returns
29+
30+
- `x: Ref<number>` the `x` scroll position relative to the elRef
31+
- `y: Ref<number>` the `y` scroll position relative to the elRef
32+
- `isScrolling: Ref<boolean>` whether the element is currently being scrolled or not
33+
- `isTracking: Ref<boolean>` whether this function events are running or not
34+
- `start: Function` the function used for starting the scroll tracking
35+
- `stop: Function` the function used for stopping the scroll tracking
36+
37+
## Usage
38+
39+
```html
40+
<template>
41+
<div>
42+
<div>x, y: {{ x }}px - {{ y }}px</div>
43+
<div>isScrolling: {{ isScrolling }}</div>
44+
<button @click="start" v-if="!isTracking">Start tracking</button>
45+
<button @click="stop" v-else>Stop tracking</button>
46+
<div ref="scrollRef" class="scrollme">
47+
<div class="scrollme__height"></div>
48+
</div>
49+
</div>
50+
</template>
51+
52+
<script lang="ts">
53+
import Vue from 'vue'
54+
import { useScroll } from 'vue-use-kit'
55+
import { ref } from '@src/api'
56+
57+
export default Vue.extend({
58+
name: 'useScrollDemo',
59+
setup() {
60+
const scrollRef = ref(null)
61+
const { x, y, isScrolling, isTracking, start, stop } = useScroll(scrollRef)
62+
return { scrollRef, x, y, isScrolling, isTracking, start, stop }
63+
}
64+
})
65+
</script>
66+
67+
<style scoped>
68+
.scrollme {
69+
position: relative;
70+
overflow-y: scroll;
71+
height: 200px;
72+
background: #f1f1f1;
73+
}
74+
75+
.scrollme__height {
76+
width: 2000px;
77+
height: 40000px;
78+
}
79+
</style>
80+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { storiesOf } from '@storybook/vue'
2+
import path from 'path'
3+
import StoryTitle from '@src/helpers/StoryTitle.vue'
4+
import UseScrollDemo from './UseScrollDemo.vue'
5+
6+
const functionName = 'useScroll'
7+
const functionPath = path.resolve(__dirname, '..')
8+
const notes = require(`./${functionName}.md`).default
9+
10+
const basicDemo = () => ({
11+
components: { StoryTitle, demo: UseScrollDemo },
12+
template: `
13+
<div class="container">
14+
<story-title
15+
function-path="${functionPath}"
16+
source-name="${functionName}"
17+
demo-name="UseScrollDemo.vue"
18+
>
19+
<template v-slot:title></template>
20+
<template v-slot:intro></template>
21+
</story-title>
22+
<demo />
23+
</div>`
24+
})
25+
26+
storiesOf('sensors|useScroll', module)
27+
.addParameters({ notes })
28+
.add('Demo', basicDemo)
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {
2+
checkElementExistenceOnMount,
3+
checkOnStartEvents,
4+
checkOnStopEvents,
5+
mount
6+
} from '@src/helpers/test'
7+
import { ref } from '@src/api'
8+
import { useScroll } from '@src/vue-use-kit'
9+
10+
afterEach(() => {
11+
jest.clearAllMocks()
12+
})
13+
14+
const testComponent = (onMount = true) => ({
15+
template: `
16+
<div>
17+
<div id="isTracking" v-if="isTracking"></div>
18+
<div id="isScrolling" v-if="isScrolling"></div>
19+
<div id="x" v-if="x === 0">{{ x }}px</div>
20+
<div id="y" v-if="y === 0">{{ y }}px</div>
21+
<button id="start" @click="start"></button>
22+
<button id="stop" @click="stop"></button>
23+
</div>
24+
`,
25+
setup() {
26+
const { x, y, isScrolling, isTracking, start, stop } = useScroll(
27+
ref(window),
28+
150,
29+
onMount
30+
)
31+
return { x, y, isScrolling, isTracking, start, stop }
32+
}
33+
})
34+
35+
describe('useScroll', () => {
36+
const events = ['scroll']
37+
38+
it('should add events on mounted and remove them on unmounted', async () => {
39+
const addEventListenerSpy = jest.spyOn(window, 'addEventListener')
40+
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener')
41+
const wrapper = mount(testComponent())
42+
await wrapper.vm.$nextTick()
43+
expect(addEventListenerSpy).toHaveBeenCalledTimes(events.length)
44+
events.forEach(event => {
45+
expect(addEventListenerSpy).toBeCalledWith(
46+
event,
47+
expect.any(Function),
48+
expect.any(Object)
49+
)
50+
})
51+
52+
// Destroy instance to check if the remove event listener is being called
53+
wrapper.destroy()
54+
expect(removeEventListenerSpy).toHaveBeenCalledTimes(events.length)
55+
events.forEach(event => {
56+
expect(removeEventListenerSpy).toBeCalledWith(event, expect.any(Function))
57+
})
58+
})
59+
60+
it('should add events again when start is called', async () => {
61+
await checkOnStartEvents(window, events, testComponent)
62+
})
63+
64+
it('should remove events when stop is called', async () => {
65+
await checkOnStopEvents(window, events, testComponent)
66+
})
67+
68+
it('should show #isTracking when runOnMount is true', async () => {
69+
await checkElementExistenceOnMount(true, testComponent(true))
70+
})
71+
72+
it('should not show #isTracking when runOnMount is false', async () => {
73+
await checkElementExistenceOnMount(false, testComponent(false))
74+
})
75+
76+
it('should show #x and #y elements and not show #isScrolling', async () => {
77+
const wrapper = mount(testComponent())
78+
await wrapper.vm.$nextTick()
79+
expect(wrapper.find('#x').exists()).toBe(true)
80+
expect(wrapper.find('#y').exists()).toBe(true)
81+
expect(wrapper.find('#isScrolling').exists()).toBe(false)
82+
})
83+
})

src/functions/useScroll/useScroll.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { ref, onMounted, onUnmounted, Ref } from '@src/api'
2+
3+
export function useScroll(
4+
elRef: Ref<null | HTMLElement | Window>,
5+
ms = 150,
6+
runOnMount = true
7+
) {
8+
const isTracking = ref(false)
9+
const isScrolling = ref(false)
10+
11+
const x = ref(0)
12+
const y = ref(0)
13+
14+
let scrollingTimeout: any = null
15+
const updateScrollStatus = () => {
16+
isScrolling.value = true
17+
clearTimeout(scrollingTimeout)
18+
scrollingTimeout = setTimeout(() => (isScrolling.value = false), ms)
19+
}
20+
21+
const updateWindowElement = () => {
22+
x.value = window.pageXOffset
23+
y.value = window.pageYOffset
24+
}
25+
26+
const updateHTMLElement = () => {
27+
x.value = (elRef.value as HTMLElement).scrollLeft
28+
y.value = (elRef.value as HTMLElement).scrollTop
29+
}
30+
31+
const updateElScrollPos = () => {
32+
if (!elRef.value) return
33+
elRef.value === window ? updateWindowElement() : updateHTMLElement()
34+
}
35+
36+
const handleScroll = () => {
37+
updateElScrollPos()
38+
updateScrollStatus()
39+
}
40+
41+
const start = () => {
42+
if (isTracking.value) return
43+
if (elRef.value) {
44+
elRef.value.addEventListener('scroll', handleScroll, {
45+
capture: false,
46+
passive: true
47+
})
48+
updateElScrollPos()
49+
}
50+
isTracking.value = true
51+
}
52+
53+
const stop = () => {
54+
if (!isTracking.value) return
55+
if (elRef.value) elRef.value.removeEventListener('scroll', handleScroll)
56+
isTracking.value = false
57+
}
58+
59+
onMounted(() => runOnMount && start())
60+
onUnmounted(stop)
61+
62+
return { x, y, isTracking, isScrolling, start, stop }
63+
}

src/vue-use-kit.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export * from './functions/useMouseElement'
1616
export * from './functions/useMouseLeavePage'
1717
export * from './functions/useOrientation'
1818
export * from './functions/useSearchParams'
19+
export * from './functions/useScroll'
1920
// Animations
2021
export * from './functions/useIntervalFn'
2122
export * from './functions/useInterval'

0 commit comments

Comments
 (0)