Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ Vue.use(VueCompositionAPI);
## APIs

- Sensors
- [`useGeolocation`](./src/components/useGeolocation/stories/useGeolocation.md) — tracks geolocation state of user's device.
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usegeolocation--demo)
- [`useHover`](./src/components/useHover/stories/useHover.md) — tracks mouse hover state of a given element.
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usehover--demo)
- [`useIntersection`](./src/components/useIntersection/stories/useIntersection.md) — tracks intersection of target element with an ancestor element.
- [`useIntersection`](./src/components/useIntersection/stories/useIntersection.md) — tracks intersection of target element with an ancestor element.
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-useintersection--demo)
[![Demo](https://img.shields.io/badge/advanced_demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-useintersection--advanced-demo)
- [`useMedia`](./src/components/useMedia/stories/useMedia.md) — tracks state of a CSS media query.
Expand Down
2 changes: 1 addition & 1 deletion src/components/useFullscreen/useFullscreen.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const testComponent = () => ({
})

describe('useFullscreen', () => {
it('should not be fullscreen when initialized', () => {
it('should not be fullscreen onMounted', () => {
const wrapper = mount(testComponent())
expect(wrapper.find('#isFullscreen').exists()).toBe(false)
})
Expand Down
1 change: 1 addition & 0 deletions src/components/useGeolocation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useGeolocation'
41 changes: 41 additions & 0 deletions src/components/useGeolocation/stories/UseGeolocationDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<template>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Prop</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>geo</td>
<td>
<pre>{{ JSON.stringify(geo, null, 2) }}</pre>
</td>
</tr>
<tr>
<td colspan="2">
<button class="button is-primary" @click="start" v-if="!isTracking">
Enable geolocation tracking
</button>
<button class="button is-danger" @click="stop" v-else>
Disable geolocation tracking
</button>
</td>
</tr>
</tbody>
</table>
</template>

<script lang="ts">
import Vue from 'vue'
import { useGeolocation } from '@src/vue-use-kit'

export default Vue.extend({
name: 'UseGeolocationDemo',
setup() {
const { isTracking, geo, start, stop } = useGeolocation({}, false)
return { isTracking, geo, start, stop }
}
})
</script>
74 changes: 74 additions & 0 deletions src/components/useGeolocation/stories/useGeolocation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# useGeolocation

Vue function that tracks geolocation state of user's device, based on the [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API).

## Reference

```typescript
interface UseGeolocation {
loading: boolean
accuracy: number | null
altitude: number | null
altitudeAccuracy: number | null
heading: number | null
latitude: number | null
longitude: number | null
speed: number | null
timestamp: number | null
error?: Error | PositionError
}
```

```typescript
useGeolocation(
options?: PositionOptions,
runOnMount?: boolean
): {
isTracking: Ref<boolean>;
geo: Ref<UseGeolocation>;
start: () => void;
stop: () => void;
}
```

### Parameters

- `options: PositionOptions` the [geolocation position options](https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions)
- `runOnMount: boolean` whether to run the geolocation tracking on mount, `true` by default

### Returns

- `isTracking: Ref<boolean>` whether the function is tracking the user's location or not
- `geo: Ref<UseGeolocation>` the geolocation object
- `start: Function` the function used for starting the geolocation tracking
- `stop: Function` the function used for stopping the geolocation tracking

## Usage

```html
<template>
<div>
<div>
Geo:
<pre>{{ JSON.stringify(geo, null, 2) }}</pre>
</div>
<button @click="start" v-if="!isTracking">
Enable geolocation tracking
</button>
<button @click="stop" v-else>Disable geolocation tracking</button>
</div>
</template>

<script lang="ts">
import Vue from 'vue'
import { useGeolocation } from 'vue-use-kit'

export default Vue.extend({
name: 'UseGeolocationDemo',
setup() {
const { isTracking, geo, start, stop } = useGeolocation()
return { isTracking, geo, start, stop }
}
})
</script>
```
28 changes: 28 additions & 0 deletions src/components/useGeolocation/stories/useGeolocation.story.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { storiesOf } from '@storybook/vue'
import path from 'path'
import StoryTitle from '@src/helpers/StoryTitle.vue'
import UseGeolocationDemo from './UseGeolocationDemo.vue'

const functionName = 'useGeolocation'
const functionPath = path.resolve(__dirname, '..')
const notes = require(`./${functionName}.md`).default

const basicDemo = () => ({
components: { StoryTitle, demo: UseGeolocationDemo },
template: `
<div class="container">
<story-title
function-path="${functionPath}"
source-name="${functionName}"
demo-name="UseGeolocationDemo.vue"
>
<template v-slot:title></template>
<template v-slot:intro></template>
</story-title>
<demo />
</div>`
})

storiesOf('sensors|useGeolocation', module)
.addParameters({ notes })
.add('Demo', basicDemo)
83 changes: 83 additions & 0 deletions src/components/useGeolocation/useGeolocation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { mount } from '@src/helpers/test'
import { useGeolocation } from '@src/vue-use-kit'

let watchPosition: any
let clearWatch: any
let getCurrentPosition: any

beforeEach(() => {
watchPosition = jest.fn()
clearWatch = jest.fn()
getCurrentPosition = jest.fn().mockImplementationOnce(success =>
Promise.resolve(
success({
coords: {
latitude: 51.1,
longitude: 45.3
}
})
)
)
const inst = ((navigator as any).geolocation = {
watchPosition,
clearWatch,
getCurrentPosition
})
})

afterEach(() => {
jest.clearAllMocks()
})

const testComponent = () => ({
template: `
<div>
<div id="isTracking" v-if="isTracking"></div>
<button id="start" @click="start"></button>
<button id="stop" @click="stop"></button>
</div>
`,
setup() {
const { isTracking, geo, start, stop } = useGeolocation()
return { isTracking, geo, start, stop }
}
})

describe('useGeolocation', () => {
it('should call getCurrentPosition and watchPosition onMounted', () => {
expect(getCurrentPosition).toHaveBeenCalledTimes(0)
expect(watchPosition).toHaveBeenCalledTimes(0)
mount(testComponent())
expect(getCurrentPosition).toHaveBeenCalledTimes(1)
expect(watchPosition).toHaveBeenCalledTimes(1)
})

it('should call clearWatch onUnmounted', () => {
expect(clearWatch).toHaveBeenCalledTimes(0)
const wrapper = mount(testComponent())
wrapper.vm.$destroy()
expect(clearWatch).toHaveBeenCalledTimes(1)
})

it('should call getCurrentPosition again when start is called', async () => {
expect(getCurrentPosition).toHaveBeenCalledTimes(0)
const wrapper = mount(testComponent())
expect(getCurrentPosition).toHaveBeenCalledTimes(1)
wrapper.find('#stop').trigger('click')

// Wait for Vue to append #start in the DOM
await wrapper.vm.$nextTick()
wrapper.find('#start').trigger('click')
expect(getCurrentPosition).toHaveBeenCalledTimes(2)
})

it('should call clearWatch when stop is called', async () => {
expect(clearWatch).toHaveBeenCalledTimes(0)
const wrapper = mount(testComponent())
wrapper.find('#stop').trigger('click')

// Wait for Vue to append #start in the DOM
await wrapper.vm.$nextTick()
expect(clearWatch).toHaveBeenCalledTimes(1)
})
})
101 changes: 101 additions & 0 deletions src/components/useGeolocation/useGeolocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ref, onMounted, onUnmounted, Ref } from '@src/api'

export interface UseGeolocation {
loading: boolean
accuracy: number | null
altitude: number | null
altitudeAccuracy: number | null
heading: number | null
latitude: number | null
longitude: number | null
speed: number | null
timestamp: number | null
error?: Error | PositionError
}

const defaultOpts = {
enableHighAccuracy: false,
timeout: Infinity,
maximumAge: 0
}

export function useGeolocation(
options: PositionOptions = {},
runOnMount = true
) {
options = Object.assign(defaultOpts, options)

// Note: surprisingly the watchId can be 0 (not positive) so
// we have to check if watchId !== null every time
let watchId: number | null = null

const geoInitData = {
loading: false,
accuracy: null,
altitude: null,
altitudeAccuracy: null,
heading: null,
latitude: null,
longitude: null,
speed: null,
timestamp: null
}

const isTracking = ref(false)
const geo: Ref<UseGeolocation> = ref({ ...geoInitData })

const onEventError = (error: PositionError) => {
if (watchId === null) return
geo.value.loading = false
geo.value.error = {
code: error.code,
message: error.message
} as PositionError
isTracking.value = false
}

const handleGeolocation = ({ coords, timestamp }: Position) => {
geo.value = {
loading: false,
accuracy: coords.accuracy,
altitude: coords.altitude,
altitudeAccuracy: coords.altitudeAccuracy,
heading: coords.heading,
latitude: coords.latitude,
longitude: coords.longitude,
speed: coords.speed,
timestamp
}
isTracking.value = true
}

const start = () => {
if (watchId !== null) return
geo.value.loading = true
geo.value.timestamp = Date.now()

navigator.geolocation.getCurrentPosition(
handleGeolocation,
onEventError,
options
)

watchId = navigator.geolocation.watchPosition(
handleGeolocation,
onEventError,
options
)
}

const stop = () => {
if (watchId === null) return
navigator.geolocation.clearWatch(watchId)
watchId = null
isTracking.value = false
}

onMounted(() => runOnMount && start())
onUnmounted(stop)

return { isTracking, geo, start, stop }
}
2 changes: 1 addition & 1 deletion src/components/useIntersection/useIntersection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const testComponent = (onMount = true) => ({
})

describe('useIntersection', () => {
it('should call IntersectionObserver when initialized', () => {
it('should call IntersectionObserver onMounted', () => {
expect(observe).toHaveBeenCalledTimes(0)
mount(testComponent())
expect(observe).toHaveBeenCalledTimes(1)
Expand Down
2 changes: 1 addition & 1 deletion src/components/useIntervalFn/useIntervalFn.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const testComponent = (onMount = true) => ({
})

describe('useIntervalFn', () => {
it('should call setInterval when initialized', () => {
it('should call setInterval onMounted', () => {
expect(setInterval).toHaveBeenCalledTimes(0)
mount(testComponent())
jest.advanceTimersByTime(1500)
Expand Down
2 changes: 1 addition & 1 deletion src/components/useTimeoutFn/useTimeoutFn.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const testComponent = (onMount = true) => ({
})

describe('useTimeoutFn', () => {
it('should call setTimeout when initialized', () => {
it('should call setTimeout onMounted', () => {
expect(setTimeout).toHaveBeenCalledTimes(0)
mount(testComponent())
expect(setTimeout).toHaveBeenCalledTimes(1)
Expand Down
1 change: 1 addition & 0 deletions src/vue-use-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './components/getQuery'
export * from './components/useClickAway'
export * from './components/useFullscreen'

export * from './components/useGeolocation'
export * from './components/useHover'
export * from './components/useIntersection'
export * from './components/useMedia'
Expand Down