Skip to content

Commit eb01041

Browse files
authored
Merge pull request #9 from microcipcip/feature/useMediaDevices
feat(useMediaDevices): Adding the function useMediaDevices
2 parents 9185917 + 1717c6a commit eb01041

File tree

8 files changed

+315
-0
lines changed

8 files changed

+315
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ Vue.use(VueCompositionAPI);
7777
- [`useMedia`](./src/components/useMedia/stories/useMedia.md) — tracks state of a CSS media query.
7878
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemedia--demo)
7979
[![Demo](https://img.shields.io/badge/advanced_demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemedia--advanced-demo)
80+
- [`useMediaDevices`](./src/components/useMediaDevices/stories/useMediaDevices.md) — tracks connected hardware devices.
81+
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemediadevices--demo)
8082
- [`useMouse`](./src/components/useMouse/stories/useMouse.md) — tracks the mouse position.
8183
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouse--demo)
8284
[![Demo](https://img.shields.io/badge/advanced_demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouse--advanced-demo)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useMediaDevices'
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<template>
2+
<table class="table is-fullwidth">
3+
<thead>
4+
<tr>
5+
<th>Prop</th>
6+
<th>Value</th>
7+
</tr>
8+
</thead>
9+
<tbody>
10+
<tr>
11+
<td>devicesState</td>
12+
<td>
13+
<pre>{{ JSON.stringify(devicesState, null, 2) }}</pre>
14+
</td>
15+
</tr>
16+
<tr>
17+
<td>isTracked</td>
18+
<td>{{ isTracked }}</td>
19+
</tr>
20+
<tr>
21+
<td colspan="2">
22+
<button class="button is-primary" @click="start" v-if="!isTracking">
23+
Start tracking media devices
24+
</button>
25+
<button class="button is-danger" @click="stop" v-else>
26+
Stop tracking media devices
27+
</button>
28+
</td>
29+
</tr>
30+
</tbody>
31+
</table>
32+
</template>
33+
34+
<script lang="ts">
35+
import Vue from 'vue'
36+
import { useMediaDevices } from '@src/vue-use-kit'
37+
38+
export default Vue.extend({
39+
name: 'UseMediaDevicesDemo',
40+
setup() {
41+
const {
42+
devicesState,
43+
isTracking,
44+
isTracked,
45+
start,
46+
stop
47+
} = useMediaDevices(false)
48+
return { devicesState, isTracking, isTracked, start, stop }
49+
}
50+
})
51+
</script>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# useMediaDevices
2+
3+
Vue function that tracks connected hardware devices.
4+
5+
## Reference
6+
7+
```typescript
8+
interface UseMediaDevicesState {
9+
deviceId: string
10+
groupId: string
11+
kind: string
12+
label: string
13+
}
14+
15+
function useMediaDevices(
16+
runOnMount?: boolean
17+
): {
18+
devicesState: Ref<UseMediaDevicesState[]>
19+
isTracking: Ref<boolean>
20+
isTracked: Ref<boolean>
21+
start: () => void
22+
stop: () => void
23+
}
24+
```
25+
26+
### Parameters
27+
28+
- `runOnMount: boolean` whether to run the connected media devices tracking on mount, `true` by default
29+
30+
### Returns
31+
32+
- `devicesState: Ref<UseMediaDevicesState[]>` the list of connected media devices
33+
- `isTracking: Ref<boolean>` whether the function is tracking the connected media devices or not
34+
- `isTracked: Ref<boolean>` whether the connected devices have been successfully tracked
35+
- `start: Function` the function used to start tracking the connected media devices
36+
- `stop: Function` the function used to stop tracking the connected media devices
37+
38+
## Usage
39+
40+
```html
41+
<template>
42+
<div>
43+
<div>
44+
devicesState:
45+
<pre>{{ JSON.stringify(devicesState, null, 2) }}</pre>
46+
</div>
47+
<div>
48+
isTracked: {{ isTracked }}
49+
</div>
50+
<div>
51+
<button @click="start" v-if="!isTracking">
52+
Start tracking media devices
53+
</button>
54+
<button @click="stop" v-else>Stop tracking media devices</button>
55+
</div>
56+
</div>
57+
</template>
58+
59+
<script lang="ts">
60+
import Vue from 'vue'
61+
import { useMediaDevices } from 'vue-use-kit'
62+
63+
export default Vue.extend({
64+
name: 'UseMediaDevicesDemo',
65+
setup() {
66+
const { devicesState, isTracked, start, stop } = useMediaDevices(false)
67+
return { devicesState, isTracked, start, stop }
68+
}
69+
})
70+
</script>
71+
```
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 UseMediaDevicesDemo from './UseMediaDevicesDemo.vue'
5+
6+
const functionName = 'useMediaDevices'
7+
const functionPath = path.resolve(__dirname, '..')
8+
const notes = require(`./${functionName}.md`).default
9+
10+
const basicDemo = () => ({
11+
components: { StoryTitle, demo: UseMediaDevicesDemo },
12+
template: `
13+
<div class="container">
14+
<story-title
15+
function-path="${functionPath}"
16+
source-name="${functionName}"
17+
demo-name="UseMediaDevicesDemo.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|useMediaDevices', module)
27+
.addParameters({ notes })
28+
.add('Demo', basicDemo)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { mount } from '@src/helpers/test'
2+
import { useMediaDevices } from '@src/vue-use-kit'
3+
4+
const mediaDeviceInfo = {
5+
deviceId: 'string',
6+
groupId: 'string',
7+
kind: 'string',
8+
label: 'string'
9+
}
10+
const mediaDevices = [mediaDeviceInfo, mediaDeviceInfo]
11+
let enumerateDevices: any
12+
beforeEach(() => {
13+
enumerateDevices = () => Promise.resolve(mediaDevices)
14+
;(navigator as any).mediaDevices = {
15+
enumerateDevices,
16+
addEventListener: jest.fn(),
17+
removeEventListener: jest.fn()
18+
}
19+
})
20+
21+
afterEach(() => {
22+
jest.clearAllMocks()
23+
})
24+
25+
const testComponent = (onMount = true) => ({
26+
template: `
27+
<div>
28+
<div id="isTracking" v-if="isTracking"></div>
29+
<div id="devicesState">{{JSON.stringify(devicesState)}}</div>
30+
<button id="start" @click="start"></button>
31+
<button id="stop" @click="stop"></button>
32+
</div>
33+
`,
34+
setup() {
35+
const { devicesState, isTracking, start, stop } = useMediaDevices(onMount)
36+
return { devicesState, isTracking, start, stop }
37+
}
38+
})
39+
40+
describe('useMediaDevices', () => {
41+
const event = 'devicechange'
42+
it('should call devicechange onMounted', async () => {
43+
const addEventListenerSpy = jest.spyOn(
44+
navigator.mediaDevices,
45+
'addEventListener'
46+
)
47+
const removeEventListenerSpy = jest.spyOn(
48+
navigator.mediaDevices,
49+
'removeEventListener'
50+
)
51+
const wrapper = mount(testComponent())
52+
await wrapper.vm.$nextTick()
53+
expect(addEventListenerSpy).toHaveBeenCalledTimes(1)
54+
expect(addEventListenerSpy).toBeCalledWith(event, expect.any(Function))
55+
56+
// Destroy instance to check if the remove event listener is being called
57+
wrapper.destroy()
58+
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1)
59+
expect(removeEventListenerSpy).toBeCalledWith(event, expect.any(Function))
60+
})
61+
62+
it('should call document.addEventListener again when start is called', async () => {
63+
const addEventListenerSpy = jest.spyOn(
64+
navigator.mediaDevices,
65+
'addEventListener'
66+
)
67+
const wrapper = mount(testComponent())
68+
expect(addEventListenerSpy).toHaveBeenCalledTimes(1)
69+
wrapper.find('#stop').trigger('click')
70+
71+
// Wait for Vue to append #start in the DOM
72+
await wrapper.vm.$nextTick()
73+
wrapper.find('#start').trigger('click')
74+
expect(addEventListenerSpy).toHaveBeenCalledTimes(1 * 2)
75+
})
76+
77+
it('should call document.removeEventListener when stop is called', async () => {
78+
const removeEventListenerSpy = jest.spyOn(
79+
navigator.mediaDevices,
80+
'removeEventListener'
81+
)
82+
const wrapper = mount(testComponent())
83+
wrapper.find('#stop').trigger('click')
84+
85+
// Wait for Vue to append #start in the DOM
86+
await wrapper.vm.$nextTick()
87+
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1)
88+
})
89+
90+
it('should show #isTracking when onMount is true', async () => {
91+
const wrapper = mount(testComponent(true))
92+
await wrapper.vm.$nextTick()
93+
expect(wrapper.find('#isTracking').exists()).toBe(true)
94+
})
95+
96+
it('should not show #isTracking when onMount is false', async () => {
97+
const wrapper = mount(testComponent(false))
98+
await wrapper.vm.$nextTick()
99+
expect(wrapper.find('#isTracking').exists()).toBe(false)
100+
})
101+
})
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { ref, onMounted, onUnmounted, Ref } from '@vue/composition-api'
2+
3+
export interface UseMediaDevicesState {
4+
deviceId: string
5+
groupId: string
6+
kind: string
7+
label: string
8+
}
9+
10+
export function useMediaDevices(runOnMount = true) {
11+
const devicesState: Ref<UseMediaDevicesState[]> = ref([])
12+
const isTracking = ref(false)
13+
const isTracked = ref(false)
14+
15+
const deviceMap = ({
16+
deviceId,
17+
groupId,
18+
kind,
19+
label
20+
}: UseMediaDevicesState) => ({
21+
deviceId,
22+
groupId,
23+
kind,
24+
label
25+
})
26+
27+
const handleDeviceChange = () => {
28+
navigator.mediaDevices
29+
.enumerateDevices()
30+
.then(deviceList => {
31+
if (!isTracking.value) return
32+
isTracked.value = true
33+
devicesState.value = deviceList.map(deviceMap)
34+
})
35+
.catch(() => {
36+
isTracked.value = false
37+
})
38+
}
39+
40+
const start = () => {
41+
if (isTracking.value) return
42+
handleDeviceChange()
43+
navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange)
44+
isTracking.value = true
45+
}
46+
47+
const stop = () => {
48+
if (!isTracking.value) return
49+
navigator.mediaDevices.removeEventListener(
50+
'devicechange',
51+
handleDeviceChange
52+
)
53+
isTracking.value = false
54+
}
55+
56+
onMounted(() => runOnMount && start())
57+
onUnmounted(stop)
58+
59+
return { devicesState, isTracking, isTracked, start, stop }
60+
}

src/vue-use-kit.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from './components/useIdle'
99
export * from './components/useIntersection'
1010
export * from './components/useLocation'
1111
export * from './components/useMedia'
12+
export * from './components/useMediaDevices'
1213
export * from './components/useMouse'
1314
export * from './components/useMouseElement'
1415
export * from './components/useSearchParams'

0 commit comments

Comments
 (0)