diff --git a/README.md b/README.md index 8cfc3f6..08b325c 100755 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ export default Vue.extend({ [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouseleavepage--demo) - [`useOrientation`](./src/functions/useOrientation/stories/useOrientation.md) — tracks state of device's screen orientation. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-useorientation--demo) + - [`useScroll`](./src/functions/useScroll/stories/useScroll.md) — tracks an HTML element's scroll position. + [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usescroll--demo) - [`useSearchParams`](./src/functions/useSearchParams/stories/useSearchParams.md) — tracks browser's location search params. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usesearchparams--demo) - Animations diff --git a/src/functions/useMouse/stories/useMouse.md b/src/functions/useMouse/stories/useMouse.md index 6518434..3b92c2e 100755 --- a/src/functions/useMouse/stories/useMouse.md +++ b/src/functions/useMouse/stories/useMouse.md @@ -28,7 +28,7 @@ function useMouse(): { import { useMouse } from 'vue-use-kit' export default Vue.extend({ - name: 'useMouse', + name: 'useMouseDemo', setup() { const { docX, docY } = useMouse() return { docX, docY } diff --git a/src/functions/useOrientation/stories/useOrientation.story.ts b/src/functions/useOrientation/stories/useOrientation.story.ts index 1d982ec..99318af 100755 --- a/src/functions/useOrientation/stories/useOrientation.story.ts +++ b/src/functions/useOrientation/stories/useOrientation.story.ts @@ -17,7 +17,12 @@ const basicDemo = () => ({ demo-name="UseOrientationDemo.vue" > - + ` diff --git a/src/functions/useScroll/index.ts b/src/functions/useScroll/index.ts new file mode 100755 index 0000000..8908b58 --- /dev/null +++ b/src/functions/useScroll/index.ts @@ -0,0 +1 @@ +export * from './useScroll' diff --git a/src/functions/useScroll/stories/UseScrollDemo.vue b/src/functions/useScroll/stories/UseScrollDemo.vue new file mode 100755 index 0000000..08683fd --- /dev/null +++ b/src/functions/useScroll/stories/UseScrollDemo.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/src/functions/useScroll/stories/useScroll.md b/src/functions/useScroll/stories/useScroll.md new file mode 100755 index 0000000..30a81a1 --- /dev/null +++ b/src/functions/useScroll/stories/useScroll.md @@ -0,0 +1,80 @@ +# useScroll + +Vue function that tracks an HTML element's scroll position. + +## Reference + +```typescript +function useScroll( + elRef: Ref, + ms?: number, + runOnMount?: boolean +): { + x: Ref + y: Ref + isTracking: Ref + isScrolling: Ref + start: () => void + stop: () => void +} +``` + +### Parameters + +- `elRef: Ref` target element used for tracking the `x` and `y` scroll position +- `ms: number` how many milliseconds of delay before `isScrolling` goes back to false (basically when user is idle), `150` by default +- `runOnMount: boolean` whether to run the scroll tracking on mount, `true` by default + +### Returns + +- `x: Ref` the `x` scroll position relative to the elRef +- `y: Ref` the `y` scroll position relative to the elRef +- `isScrolling: Ref` whether the element is currently being scrolled or not +- `isTracking: Ref` whether this function events are running or not +- `start: Function` the function used for starting the scroll tracking +- `stop: Function` the function used for stopping the scroll tracking + +## Usage + +```html + + + + + +``` diff --git a/src/functions/useScroll/stories/useScroll.story.ts b/src/functions/useScroll/stories/useScroll.story.ts new file mode 100755 index 0000000..8294688 --- /dev/null +++ b/src/functions/useScroll/stories/useScroll.story.ts @@ -0,0 +1,28 @@ +import { storiesOf } from '@storybook/vue' +import path from 'path' +import StoryTitle from '@src/helpers/StoryTitle.vue' +import UseScrollDemo from './UseScrollDemo.vue' + +const functionName = 'useScroll' +const functionPath = path.resolve(__dirname, '..') +const notes = require(`./${functionName}.md`).default + +const basicDemo = () => ({ + components: { StoryTitle, demo: UseScrollDemo }, + template: ` +
+ + + + + +
` +}) + +storiesOf('sensors|useScroll', module) + .addParameters({ notes }) + .add('Demo', basicDemo) diff --git a/src/functions/useScroll/useScroll.spec.ts b/src/functions/useScroll/useScroll.spec.ts new file mode 100755 index 0000000..3b68161 --- /dev/null +++ b/src/functions/useScroll/useScroll.spec.ts @@ -0,0 +1,83 @@ +import { + checkElementExistenceOnMount, + checkOnStartEvents, + checkOnStopEvents, + mount +} from '@src/helpers/test' +import { ref } from '@src/api' +import { useScroll } from '@src/vue-use-kit' + +afterEach(() => { + jest.clearAllMocks() +}) + +const testComponent = (onMount = true) => ({ + template: ` +
+
+
+
{{ x }}px
+
{{ y }}px
+ + +
+ `, + setup() { + const { x, y, isScrolling, isTracking, start, stop } = useScroll( + ref(window), + 150, + onMount + ) + return { x, y, isScrolling, isTracking, start, stop } + } +}) + +describe('useScroll', () => { + const events = ['scroll'] + + it('should add events on mounted and remove them on unmounted', async () => { + const addEventListenerSpy = jest.spyOn(window, 'addEventListener') + const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener') + const wrapper = mount(testComponent()) + await wrapper.vm.$nextTick() + expect(addEventListenerSpy).toHaveBeenCalledTimes(events.length) + events.forEach(event => { + expect(addEventListenerSpy).toBeCalledWith( + event, + expect.any(Function), + expect.any(Object) + ) + }) + + // Destroy instance to check if the remove event listener is being called + wrapper.destroy() + expect(removeEventListenerSpy).toHaveBeenCalledTimes(events.length) + events.forEach(event => { + expect(removeEventListenerSpy).toBeCalledWith(event, expect.any(Function)) + }) + }) + + it('should add events again when start is called', async () => { + await checkOnStartEvents(window, events, testComponent) + }) + + it('should remove events when stop is called', async () => { + await checkOnStopEvents(window, events, testComponent) + }) + + it('should show #isTracking when runOnMount is true', async () => { + await checkElementExistenceOnMount(true, testComponent(true)) + }) + + it('should not show #isTracking when runOnMount is false', async () => { + await checkElementExistenceOnMount(false, testComponent(false)) + }) + + it('should show #x and #y elements and not show #isScrolling', async () => { + const wrapper = mount(testComponent()) + await wrapper.vm.$nextTick() + expect(wrapper.find('#x').exists()).toBe(true) + expect(wrapper.find('#y').exists()).toBe(true) + expect(wrapper.find('#isScrolling').exists()).toBe(false) + }) +}) diff --git a/src/functions/useScroll/useScroll.ts b/src/functions/useScroll/useScroll.ts new file mode 100755 index 0000000..8d32c87 --- /dev/null +++ b/src/functions/useScroll/useScroll.ts @@ -0,0 +1,63 @@ +import { ref, onMounted, onUnmounted, Ref } from '@src/api' + +export function useScroll( + elRef: Ref, + ms = 150, + runOnMount = true +) { + const isTracking = ref(false) + const isScrolling = ref(false) + + const x = ref(0) + const y = ref(0) + + let scrollingTimeout: any = null + const updateScrollStatus = () => { + isScrolling.value = true + clearTimeout(scrollingTimeout) + scrollingTimeout = setTimeout(() => (isScrolling.value = false), ms) + } + + const updateWindowElement = () => { + x.value = window.pageXOffset + y.value = window.pageYOffset + } + + const updateHTMLElement = () => { + x.value = (elRef.value as HTMLElement).scrollLeft + y.value = (elRef.value as HTMLElement).scrollTop + } + + const updateElScrollPos = () => { + if (!elRef.value) return + elRef.value === window ? updateWindowElement() : updateHTMLElement() + } + + const handleScroll = () => { + updateElScrollPos() + updateScrollStatus() + } + + const start = () => { + if (isTracking.value) return + if (elRef.value) { + elRef.value.addEventListener('scroll', handleScroll, { + capture: false, + passive: true + }) + updateElScrollPos() + } + isTracking.value = true + } + + const stop = () => { + if (!isTracking.value) return + if (elRef.value) elRef.value.removeEventListener('scroll', handleScroll) + isTracking.value = false + } + + onMounted(() => runOnMount && start()) + onUnmounted(stop) + + return { x, y, isTracking, isScrolling, start, stop } +} diff --git a/src/vue-use-kit.ts b/src/vue-use-kit.ts index 861f1e2..3c13b08 100755 --- a/src/vue-use-kit.ts +++ b/src/vue-use-kit.ts @@ -16,6 +16,7 @@ export * from './functions/useMouseElement' export * from './functions/useMouseLeavePage' export * from './functions/useOrientation' export * from './functions/useSearchParams' +export * from './functions/useScroll' // Animations export * from './functions/useIntervalFn' export * from './functions/useInterval'