Skip to content

Commit 71ef66f

Browse files
committed
feat(useIntersection): Adding useIntersection function
1 parent 59c1b89 commit 71ef66f

File tree

9 files changed

+314
-1
lines changed

9 files changed

+314
-1
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ Vue.use(VueCompositionAPI);
6161
- Sensors
6262
- [`useHover`](./src/components/useHover/stories/useHover.md) — tracks mouse hover state of a given element.
6363
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usehover--demo)
64+
- [`useIntersection`](./src/components/useIntersection/stories/useIntersection.md) — tracks intersection of target element with an ancestor element.
65+
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-useintersection--demo)
6466
- [`useMedia`](./src/components/useMedia/stories/useMedia.md) — tracks state of a CSS media query.
6567
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemedia--demo)
6668
[![Demo](https://img.shields.io/badge/advanced_demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemedia--advanced-demo)
@@ -70,7 +72,7 @@ Vue.use(VueCompositionAPI);
7072
- [`useMouseElement`](./src/components/useMouseElement/stories/useMouseElement.md) — tracks the mouse position relative to given element.
7173
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouseelement--demo)
7274
- Animations
73-
- [`useInterval`](./src/components/useInterval/stories/useInterval.md) — updates the `counter` value repeatedly on a fixed time delay.
75+
- [`useInterval`](./src/components/useInterval/stories/useInterval.md) — updates `counter` value repeatedly on a fixed time delay.
7476
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/animations-useinterval--demo)
7577
- [`useIntervalFn`](./src/components/useIntervalFn/stories/useIntervalFn.md) — calls function repeatedly on a fixed time delay.
7678
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/animations-useintervalfn--demo)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useIntersection'
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<template>
2+
<div class="section">
3+
<div class="intersection" v-for="el in divElements">
4+
<use-intersection-element-demo
5+
class="intersection__el"
6+
:options="intersectionOpts"
7+
@change="handleIntersectionChange"
8+
/>
9+
</div>
10+
</div>
11+
</template>
12+
13+
<script lang="ts">
14+
import Vue from 'vue'
15+
import UseIntersectionElementDemo from './UseIntersectionElementDemo.vue'
16+
17+
export default Vue.extend({
18+
name: 'UseIntersectionDemo',
19+
components: { UseIntersectionElementDemo },
20+
setup() {
21+
const intersectionOpts = {
22+
root: null,
23+
rootMargin: '-40px 0px -40px',
24+
threshold: 1
25+
}
26+
27+
const divElements = new Array(100)
28+
29+
const handleIntersectionChange = ({
30+
target,
31+
intersectionRatio
32+
}: IntersectionObserverEntry) => {
33+
const isVisible = intersectionRatio === 1
34+
target.classList.toggle('-is-active', isVisible)
35+
}
36+
37+
return { divElements, intersectionOpts, handleIntersectionChange }
38+
}
39+
})
40+
</script>
41+
42+
<style>
43+
.section {
44+
padding: 20px 0;
45+
}
46+
47+
.intersection {
48+
display: flex;
49+
align-items: center;
50+
justify-content: center;
51+
height: 120px;
52+
}
53+
54+
/* El */
55+
.intersection__el {
56+
position: relative;
57+
width: 80px;
58+
height: 80px;
59+
}
60+
61+
/* Circle */
62+
.intersection__el:after {
63+
content: 'OFF';
64+
display: flex;
65+
justify-content: center;
66+
align-items: center;
67+
position: absolute;
68+
top: 50%;
69+
left: 50%;
70+
width: 40px;
71+
height: 40px;
72+
border-radius: 50%;
73+
background: red;
74+
color: white;
75+
text-transform: uppercase;
76+
font-size: 12px;
77+
transition: all 0.7s ease-in-out;
78+
transform: translate(-50%, -50%) scale(1);
79+
}
80+
81+
.intersection__el.-is-active:after {
82+
content: 'ON';
83+
background: green;
84+
transform: translate(-50%, -50%) scale(1.4);
85+
}
86+
</style>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<template>
2+
<div ref="elRef">
3+
<slot></slot>
4+
</div>
5+
</template>
6+
7+
<script lang="ts">
8+
import Vue from 'vue'
9+
import { ref, watch } from '@src/api'
10+
import { useIntersection } from '@src/vue-use-kit'
11+
export default Vue.extend({
12+
name: 'UseIntersectionElementDemo',
13+
props: {
14+
options: {
15+
default: {
16+
root: null,
17+
rootMargin: '0px',
18+
threshold: 0
19+
}
20+
}
21+
},
22+
setup({ options }, { emit }) {
23+
const elRef = ref(null)
24+
const { observedEntry } = useIntersection(
25+
elRef,
26+
options as IntersectionObserverInit
27+
)
28+
watch(() => {
29+
if (!observedEntry.value) return
30+
emit('change', observedEntry.value)
31+
})
32+
33+
return { elRef }
34+
}
35+
})
36+
</script>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# useIntersection
2+
3+
Vue function that tracks the changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.
4+
5+
It is based on the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
6+
7+
## Reference
8+
9+
```typescript
10+
useIntersection(
11+
elRef: Ref<null | Element>,
12+
options?: IntersectionObserverInit,
13+
runOnMount?: boolean
14+
): {
15+
observedEntry: Ref<IntersectionObserverEntry | null>;
16+
start: () => void;
17+
stop: () => void;
18+
};
19+
```
20+
21+
### Parameters
22+
23+
- `elRef: Ref<null | Element>` the element to observe
24+
- `options: IntersectionObserverInit` the [IntersectionObserver options](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver#Properties)
25+
- `runOnMount: boolean` whether to observe the element on mount, `true` by default
26+
27+
### Returns
28+
29+
- `observedEntry: Ref<IntersectionObserverEntry | null>` the [observed entry](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry)
30+
- `start: Function` the function used for starting observing
31+
- `stop: Function` the function used for stopping observing
32+
33+
## Usage
34+
35+
```html
36+
<template>
37+
<div>
38+
<div class="el-wrap">
39+
<div ref="el1Ref" :class="el1Class" class="el"></div>
40+
</div>
41+
<div class="el-wrap">
42+
<div ref="el2Ref" :class="el2Class" class="el"></div>
43+
</div>
44+
<div class="el-wrap">
45+
<div ref="el3Ref" :class="el3Class" class="el"></div>
46+
</div>
47+
</div>
48+
</template>
49+
50+
<script lang="ts">
51+
import Vue from 'vue'
52+
import { ref, watch } from '@src/api'
53+
import { useIntersection } from 'vue-use-kit'
54+
55+
export default Vue.extend({
56+
name: 'UseIntersectionDemo',
57+
setup() {
58+
const options = {
59+
root: null,
60+
rootMargin: '0px 0px -30px',
61+
threshold: 1.0
62+
}
63+
64+
const watchClass = (className, observedEntry, isEnabled) => {
65+
if (!isEnabled) return
66+
const isVisible = observedEntry.value.intersectionRatio === 1
67+
className.value = isVisible ? '-is-visible' : ''
68+
}
69+
70+
const el1Ref = ref(null)
71+
const el1Class = ref('')
72+
const { observedEntry: el1Ob } = useIntersection(el1Ref, options)
73+
watch(() => watchClass(el1Class, el1Ob, el1Ob.value))
74+
75+
const el2Ref = ref(null)
76+
const el2Class = ref('')
77+
const { observedEntry: el2Ob } = useIntersection(el2Ref, options)
78+
watch(() => watchClass(el2Class, el2Ob, el2Ob.value))
79+
80+
const el3Ref = ref(null)
81+
const el3Class = ref('')
82+
const { observedEntry: el3Ob } = useIntersection(el3Ref, options)
83+
watch(() => watchClass(el3Class, el3Ob, el3Ob.value))
84+
85+
return {
86+
el1Ref,
87+
el1Class,
88+
el2Ref,
89+
el2Class,
90+
el3Ref,
91+
el3Class
92+
}
93+
}
94+
})
95+
</script>
96+
97+
<style>
98+
.el-wrap {
99+
margin: 500px 0;
100+
}
101+
102+
.el {
103+
width: 40px;
104+
height: 40px;
105+
background: red;
106+
transition: 0.3s ease-in-out;
107+
}
108+
109+
.el.-is-visible {
110+
background: green;
111+
}
112+
</style>
113+
```
Lines changed: 28 additions & 0 deletions
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 UseIntersectionDemo from './UseIntersectionDemo.vue'
5+
6+
const functionName = 'useIntersection'
7+
const functionPath = path.resolve(__dirname, '..')
8+
const notes = require(`./${functionName}.md`).default
9+
10+
const basicDemo = () => ({
11+
components: { StoryTitle, demo: UseIntersectionDemo },
12+
template: `
13+
<div class="container">
14+
<story-title
15+
function-path="${functionPath}"
16+
source-name="${functionName}"
17+
demo-name="UseIntersectionDemo.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|useIntersection', module)
27+
.addParameters({ notes })
28+
.add('Demo', basicDemo)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// import { mount } from '@src/helpers/test'
2+
// import { useIntersection } from '@src/vue-use-kit'
3+
4+
describe('useIntersection', () => {
5+
it('should do something', () => {
6+
// Add test here
7+
})
8+
})
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ref, onMounted, onUnmounted, Ref } from '@src/api'
2+
3+
const errorMsg =
4+
'IntersectionObserver is not supported, please install a polyfill'
5+
6+
export function useIntersection(
7+
elRef: Ref<null | Element>,
8+
options: IntersectionObserverInit = {},
9+
runOnMount = true
10+
) {
11+
const observedEntry: Ref<IntersectionObserverEntry | null> = ref(null)
12+
13+
const handleObserver = (entries: IntersectionObserverEntry[]) => {
14+
observedEntry.value = entries[0]
15+
}
16+
17+
let observer: IntersectionObserver | null = null
18+
19+
const start = () => {
20+
if (!('IntersectionObserver' in window)) throw new Error(errorMsg)
21+
22+
// Do not start if the observer is already initialized
23+
if (observer || !elRef.value) return
24+
observer = new IntersectionObserver(handleObserver, options)
25+
observer.observe(elRef.value)
26+
}
27+
28+
const stop = () => {
29+
if (!observer) return
30+
observer.disconnect()
31+
observer = null
32+
}
33+
34+
onMounted(() => runOnMount && start())
35+
onUnmounted(stop)
36+
37+
return { observedEntry, start, stop }
38+
}

src/vue-use-kit.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './components/getQuery'
22
export * from './components/useClickAway'
3+
export * from './components/useIntersection'
34
export * from './components/useIntervalFn'
45
export * from './components/useInterval'
56
export * from './components/useMedia'

0 commit comments

Comments
 (0)