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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ Vue.use(VueCompositionAPI);
- [`useMedia`](./src/components/useMedia/stories/useMedia.md) — tracks state of a CSS media query.
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemedia--demo)
[![Demo](https://img.shields.io/badge/advanced_demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemedia--advanced-demo)
- [`useMediaDevices`](./src/components/useMediaDevices/stories/useMediaDevices.md) — tracks connected hardware devices.
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemediadevices--demo)
- [`useMouse`](./src/components/useMouse/stories/useMouse.md) — tracks the mouse position.
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouse--demo)
[![Demo](https://img.shields.io/badge/advanced_demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouse--advanced-demo)
Expand Down
1 change: 1 addition & 0 deletions src/components/useMediaDevices/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useMediaDevices'
51 changes: 51 additions & 0 deletions src/components/useMediaDevices/stories/UseMediaDevicesDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<template>
<table class="table is-fullwidth">
<thead>
<tr>
<th>Prop</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>devicesState</td>
<td>
<pre>{{ JSON.stringify(devicesState, null, 2) }}</pre>
</td>
</tr>
<tr>
<td>isTracked</td>
<td>{{ isTracked }}</td>
</tr>
<tr>
<td colspan="2">
<button class="button is-primary" @click="start" v-if="!isTracking">
Start tracking media devices
</button>
<button class="button is-danger" @click="stop" v-else>
Stop tracking media devices
</button>
</td>
</tr>
</tbody>
</table>
</template>

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

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

Vue function that tracks connected hardware devices.

## Reference

```typescript
interface UseMediaDevicesState {
deviceId: string
groupId: string
kind: string
label: string
}

function useMediaDevices(
runOnMount?: boolean
): {
devicesState: Ref<UseMediaDevicesState[]>
isTracking: Ref<boolean>
isTracked: Ref<boolean>
start: () => void
stop: () => void
}
```

### Parameters

- `runOnMount: boolean` whether to run the connected media devices tracking on mount, `true` by default

### Returns

- `devicesState: Ref<UseMediaDevicesState[]>` the list of connected media devices
- `isTracking: Ref<boolean>` whether the function is tracking the connected media devices or not
- `isTracked: Ref<boolean>` whether the connected devices have been successfully tracked
- `start: Function` the function used to start tracking the connected media devices
- `stop: Function` the function used to stop tracking the connected media devices

## Usage

```html
<template>
<div>
<div>
devicesState:
<pre>{{ JSON.stringify(devicesState, null, 2) }}</pre>
</div>
<div>
isTracked: {{ isTracked }}
</div>
<div>
<button @click="start" v-if="!isTracking">
Start tracking media devices
</button>
<button @click="stop" v-else>Stop tracking media devices</button>
</div>
</div>
</template>

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

export default Vue.extend({
name: 'UseMediaDevicesDemo',
setup() {
const { devicesState, isTracked, start, stop } = useMediaDevices(false)
return { devicesState, isTracked, start, stop }
}
})
</script>
```
28 changes: 28 additions & 0 deletions src/components/useMediaDevices/stories/useMediaDevices.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 UseMediaDevicesDemo from './UseMediaDevicesDemo.vue'

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

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

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

const mediaDeviceInfo = {
deviceId: 'string',
groupId: 'string',
kind: 'string',
label: 'string'
}
const mediaDevices = [mediaDeviceInfo, mediaDeviceInfo]
let enumerateDevices: any
beforeEach(() => {
enumerateDevices = () => Promise.resolve(mediaDevices)
;(navigator as any).mediaDevices = {
enumerateDevices,
addEventListener: jest.fn(),
removeEventListener: jest.fn()
}
})

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

const testComponent = (onMount = true) => ({
template: `
<div>
<div id="isTracking" v-if="isTracking"></div>
<div id="devicesState">{{JSON.stringify(devicesState)}}</div>
<button id="start" @click="start"></button>
<button id="stop" @click="stop"></button>
</div>
`,
setup() {
const { devicesState, isTracking, start, stop } = useMediaDevices(onMount)
return { devicesState, isTracking, start, stop }
}
})

describe('useMediaDevices', () => {
const event = 'devicechange'
it('should call devicechange onMounted', async () => {
const addEventListenerSpy = jest.spyOn(
navigator.mediaDevices,
'addEventListener'
)
const removeEventListenerSpy = jest.spyOn(
navigator.mediaDevices,
'removeEventListener'
)
const wrapper = mount(testComponent())
await wrapper.vm.$nextTick()
expect(addEventListenerSpy).toHaveBeenCalledTimes(1)
expect(addEventListenerSpy).toBeCalledWith(event, expect.any(Function))

// Destroy instance to check if the remove event listener is being called
wrapper.destroy()
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1)
expect(removeEventListenerSpy).toBeCalledWith(event, expect.any(Function))
})

it('should call document.addEventListener again when start is called', async () => {
const addEventListenerSpy = jest.spyOn(
navigator.mediaDevices,
'addEventListener'
)
const wrapper = mount(testComponent())
expect(addEventListenerSpy).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(addEventListenerSpy).toHaveBeenCalledTimes(1 * 2)
})

it('should call document.removeEventListener when stop is called', async () => {
const removeEventListenerSpy = jest.spyOn(
navigator.mediaDevices,
'removeEventListener'
)
const wrapper = mount(testComponent())
wrapper.find('#stop').trigger('click')

// Wait for Vue to append #start in the DOM
await wrapper.vm.$nextTick()
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1)
})

it('should show #isTracking when onMount is true', async () => {
const wrapper = mount(testComponent(true))
await wrapper.vm.$nextTick()
expect(wrapper.find('#isTracking').exists()).toBe(true)
})

it('should not show #isTracking when onMount is false', async () => {
const wrapper = mount(testComponent(false))
await wrapper.vm.$nextTick()
expect(wrapper.find('#isTracking').exists()).toBe(false)
})
})
60 changes: 60 additions & 0 deletions src/components/useMediaDevices/useMediaDevices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ref, onMounted, onUnmounted, Ref } from '@vue/composition-api'

export interface UseMediaDevicesState {
deviceId: string
groupId: string
kind: string
label: string
}

export function useMediaDevices(runOnMount = true) {
const devicesState: Ref<UseMediaDevicesState[]> = ref([])
const isTracking = ref(false)
const isTracked = ref(false)

const deviceMap = ({
deviceId,
groupId,
kind,
label
}: UseMediaDevicesState) => ({
deviceId,
groupId,
kind,
label
})

const handleDeviceChange = () => {
navigator.mediaDevices
.enumerateDevices()
.then(deviceList => {
if (!isTracking.value) return
isTracked.value = true
devicesState.value = deviceList.map(deviceMap)
})
.catch(() => {
isTracked.value = false
})
}

const start = () => {
if (isTracking.value) return
handleDeviceChange()
navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange)
isTracking.value = true
}

const stop = () => {
if (!isTracking.value) return
navigator.mediaDevices.removeEventListener(
'devicechange',
handleDeviceChange
)
isTracking.value = false
}

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

return { devicesState, isTracking, isTracked, start, stop }
}
1 change: 1 addition & 0 deletions src/vue-use-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './components/useIdle'
export * from './components/useIntersection'
export * from './components/useLocation'
export * from './components/useMedia'
export * from './components/useMediaDevices'
export * from './components/useMouse'
export * from './components/useMouseElement'
export * from './components/useSearchParams'
Expand Down