Skip to content

Commit e9e3ee2

Browse files
authored
Merge pull request #20 from microcipcip/feature/useKey
Feature/use key
2 parents 3fd3cfd + 78f9905 commit e9e3ee2

File tree

8 files changed

+384
-0
lines changed

8 files changed

+384
-0
lines changed

Diff for: README.md

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ Vue.use(VueCompositionAPI)
7272
- [`useIntersection`](./src/functions/useIntersection/stories/useIntersection.md) — tracks intersection of target element with an ancestor element.
7373
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-useintersection--demo)
7474
[![Demo](https://img.shields.io/badge/advanced_demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-useintersection--advanced-demo)
75+
- [`useKey`](./src/functions/useKey/stories/useKey.md) — executes a handler when a keyboard key is pressed.
76+
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usekey--demo)
7577
- [`useLocation`](./src/functions/useLocation/stories/useLocation.md) — tracks bar navigation location state.
7678
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-uselocation--demo)
7779
- [`useMedia`](./src/functions/useMedia/stories/useMedia.md) — tracks state of a CSS media query.

Diff for: src/functions/useKey/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useKey'

Diff for: src/functions/useKey/stories/UseKeyDemo.vue

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<template>
2+
<div>
3+
<div class="canvas">
4+
<div class="canvas__target" :style="canvasTargetPos">🐱‍👤</div>
5+
</div>
6+
<br />
7+
<button class="button is-primary" @click="start" v-if="!isTracking">
8+
Start tracking key press
9+
</button>
10+
<button class="button is-danger" @click="stop" v-else>
11+
Stop tracking key press
12+
</button>
13+
</div>
14+
</template>
15+
16+
<script lang="ts">
17+
import Vue from 'vue'
18+
import { ref } from '@src/api'
19+
import { useKey } from '@src/vue-use-kit'
20+
import { computed } from '@vue/composition-api'
21+
22+
const add = (coord: { value: number }) => () => (coord.value = coord.value + 4)
23+
const sub = (coord: { value: number }) => () => (coord.value = coord.value - 4)
24+
25+
export default Vue.extend({
26+
name: 'UseKeyDemo',
27+
setup() {
28+
const x = ref(0)
29+
const y = ref(0)
30+
31+
const arrowUp = useKey('ArrowUp', sub(y))
32+
const arrowDown = useKey('ArrowDown', add(y))
33+
const arrowLeft = useKey('ArrowLeft', sub(x))
34+
const arrowRight = useKey('ArrowRight', add(x))
35+
36+
const isTracking = ref(true)
37+
const start = () => {
38+
isTracking.value = true
39+
arrowUp.start()
40+
arrowDown.start()
41+
arrowLeft.start()
42+
arrowRight.start()
43+
}
44+
45+
const stop = () => {
46+
isTracking.value = false
47+
arrowUp.stop()
48+
arrowDown.stop()
49+
arrowLeft.stop()
50+
arrowRight.stop()
51+
}
52+
53+
const canvasTargetPos = computed(() => ({
54+
transform: `translate(-50%, -50%) translate(${x.value}px, ${y.value}px)`
55+
}))
56+
57+
return { canvasTargetPos, isTracking, start, stop }
58+
}
59+
})
60+
</script>
61+
62+
<style scoped>
63+
.canvas {
64+
position: relative;
65+
width: 100%;
66+
height: 200px;
67+
background: #f1f1f1;
68+
}
69+
70+
.canvas__target {
71+
position: absolute;
72+
top: 50%;
73+
left: 50%;
74+
transform: translate(-50%, -50%);
75+
font-size: 30px;
76+
}
77+
</style>

Diff for: src/functions/useKey/stories/useKey.md

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# useKey
2+
3+
Vue function that executes a handler when a keyboard key is pressed.
4+
5+
## Reference
6+
7+
```typescript
8+
type UseKeyFilter = string | ((event: KeyboardEvent) => boolean)
9+
```
10+
11+
```typescript
12+
function useKey(
13+
filter: UseKeyFilter,
14+
callback?: any,
15+
runOnMount?: boolean
16+
): {
17+
isPressed: Ref<boolean>
18+
isTracking: Ref<boolean>
19+
start: () => void
20+
stop: () => void
21+
}
22+
```
23+
24+
### Parameters
25+
26+
- `filter: UseKeyFilter` the filter string or function to use for triggering the key event
27+
- `callback: Function` the function called when the given key is pressed
28+
- `runOnMount: boolean` whether to track the given key on mount, `true` by default.
29+
30+
### Returns
31+
32+
- `isPressed: Ref<boolean>` whether the key is currently pressed or not
33+
- `isTracking: Ref<boolean>` whether this function events are running or not
34+
- `start: Function` the function used for start tracking the key event
35+
- `stop: Function` the function used for stop tracking the key event
36+
37+
## Usage
38+
39+
Example where se use the `callback` and when pressing the key without
40+
releasing **it will update the value continuously**.
41+
42+
```html
43+
<template>
44+
<div>
45+
<p>isPressed {{ isPressed }}</p>
46+
<p>keyPressCount {{ keyPressCount }}</p>
47+
<div>
48+
<button @click="start" v-if="!isTracking">
49+
Start tracking key press
50+
</button>
51+
<button @click="stop" v-else>
52+
Stop tracking key press
53+
</button>
54+
</div>
55+
</div>
56+
</template>
57+
58+
<script lang="ts">
59+
import Vue from 'vue'
60+
import { ref } from '@src/api'
61+
import { useKey } from 'vue-use-kit'
62+
63+
export default Vue.extend({
64+
name: 'UseKeyDemo',
65+
setup() {
66+
const keyPressCount = ref(0)
67+
68+
// Increase value continuously when 'a' key is kept pressed
69+
const handleKey = () => keyPressCount.value++
70+
71+
const { isPressed, isTracking, start, stop } = useKey('a', handleKey)
72+
return { keyPressCount, isPressed, isTracking, start, stop }
73+
}
74+
})
75+
</script>
76+
```
77+
78+
Example where se use the `callback` and when pressing the key
79+
**it will update the value only on keyUp**.
80+
81+
```html
82+
<template>
83+
<div>
84+
<p>isPressed {{ isPressed }}</p>
85+
<p>keyPressCount {{ keyPressCount }}</p>
86+
<div>
87+
<button @click="start" v-if="!isTracking">
88+
Start tracking key press
89+
</button>
90+
<button @click="stop" v-else>
91+
Stop tracking key press
92+
</button>
93+
</div>
94+
</div>
95+
</template>
96+
97+
<script lang="ts">
98+
import Vue from 'vue'
99+
import { ref } from '@src/api'
100+
import { useKey } from 'vue-use-kit'
101+
102+
export default Vue.extend({
103+
name: 'UseKeyDemo',
104+
setup() {
105+
const keyPressCount = ref(0)
106+
107+
// Increase value when 'a' key is released
108+
const handleKey = e => {
109+
if (e.type === 'keyup') keyPressCount.value++
110+
}
111+
112+
const { isPressed, isTracking, start, stop } = useKey('a', handleKey)
113+
return { keyPressCount, isPressed, isTracking, start, stop }
114+
}
115+
})
116+
</script>
117+
```
118+
119+
Example where we `watch` the `isPressed` value and when pressing
120+
the key without releasing **it will update the value only once**.
121+
122+
```html
123+
<template>
124+
<div>
125+
<p>isPressed {{ isPressed }}</p>
126+
<p>keyPressCount {{ keyPressCount }}</p>
127+
<div>
128+
<button @click="start" v-if="!isTracking">
129+
Start tracking key press
130+
</button>
131+
<button @click="stop" v-else>
132+
Stop tracking key press
133+
</button>
134+
</div>
135+
</div>
136+
</template>
137+
138+
<script lang="ts">
139+
import Vue from 'vue'
140+
import { watch, ref } from '@src/api'
141+
import { useKey } from 'vue-use-kit'
142+
143+
export default Vue.extend({
144+
name: 'UseKeyDemo',
145+
setup() {
146+
const keyPressCount = ref(0)
147+
const { isPressed, isTracking, start, stop } = useKey('a')
148+
149+
// Increase value when 'a' key is pressed
150+
watch(isPressed, isPress => isPress && keyPressCount.value++)
151+
152+
return { keyPressCount, isPressed, isTracking, start, stop }
153+
}
154+
})
155+
</script>
156+
```

Diff for: src/functions/useKey/stories/useKey.story.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { storiesOf } from '@storybook/vue'
2+
import path from 'path'
3+
import StoryTitle from '@src/helpers/StoryTitle.vue'
4+
import UseKeyDemo from './UseKeyDemo.vue'
5+
6+
const functionName = 'useKey'
7+
const functionPath = path.resolve(__dirname, '..')
8+
const notes = require(`./${functionName}.md`).default
9+
10+
const basicDemo = () => ({
11+
components: { StoryTitle, demo: UseKeyDemo },
12+
template: `
13+
<div class="container">
14+
<story-title
15+
function-path="${functionPath}"
16+
source-name="${functionName}"
17+
demo-name="UseKeyDemo.vue"
18+
>
19+
<template v-slot:title></template>
20+
<template v-slot:intro>
21+
<p>
22+
Press an arrow key (←, →, ↑, ↓) to move the ninja cat around
23+
the screen.
24+
</p>
25+
</template>
26+
</story-title>
27+
<demo />
28+
</div>`
29+
})
30+
31+
storiesOf('sensors|useKey', module)
32+
.addParameters({ notes })
33+
.add('Demo', basicDemo)

Diff for: src/functions/useKey/useKey.spec.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {
2+
mount,
3+
checkOnMountAndUnmountEvents,
4+
checkOnStartEvents,
5+
checkOnStopEvents,
6+
checkElementExistenceOnMount
7+
} from '@src/helpers/test'
8+
import { useKey } from '@src/vue-use-kit'
9+
10+
afterEach(() => {
11+
jest.clearAllMocks()
12+
})
13+
14+
const testComponent = (filter = 'a', callback = () => ``, onMount = true) => ({
15+
template: `
16+
<div>
17+
<div id="isTracking" v-if="isTracking"></div>
18+
<div id="isPressed" v-if="isPressed"></div>
19+
<button id="start" @click="start"></button>
20+
<button id="stop" @click="stop"></button>
21+
</div>
22+
`,
23+
setup() {
24+
const { isPressed, isTracking, start, stop } = useKey(
25+
filter,
26+
callback,
27+
onMount
28+
)
29+
return { isPressed, isTracking, start, stop }
30+
}
31+
})
32+
33+
describe('useKey', () => {
34+
const noop = () => ``
35+
const events = ['keyup', 'keydown']
36+
37+
it('should add events on mounted and remove them on unmounted', async () => {
38+
await checkOnMountAndUnmountEvents(document, events, testComponent)
39+
})
40+
41+
it('should add events again when start is called', async () => {
42+
await checkOnStartEvents(document, events, testComponent)
43+
})
44+
45+
it('should remove events when stop is called', async () => {
46+
await checkOnStopEvents(document, events, testComponent)
47+
})
48+
49+
it('should show #isTracking when runOnMount is true', async () => {
50+
await checkElementExistenceOnMount(true, testComponent('a', noop, true))
51+
})
52+
53+
it('should not show #isTracking when runOnMount is false', async () => {
54+
await checkElementExistenceOnMount(false, testComponent('a', noop, false))
55+
})
56+
57+
it('should not show #isPressed when no key has been pressed', async () => {
58+
const wrapper = mount(testComponent())
59+
await wrapper.vm.$nextTick()
60+
expect(wrapper.find('#isPressed').exists()).toBe(false)
61+
})
62+
})

Diff for: src/functions/useKey/useKey.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { ref, onMounted, onUnmounted, Ref } from '@src/api'
2+
3+
type UseKeyFilter = string | ((event: KeyboardEvent) => boolean)
4+
5+
export function useKey(
6+
filter: UseKeyFilter,
7+
callback: any = () => ``,
8+
runOnMount = true
9+
) {
10+
const isTracking = ref(false)
11+
const isPressed = ref(false)
12+
13+
const getFilter = () => {
14+
if (typeof filter === 'function') return filter
15+
return (event: KeyboardEvent) => event.key === filter
16+
}
17+
18+
const handleKeyDown = (event: KeyboardEvent) => {
19+
const filterFn = getFilter()
20+
if (!filterFn(event)) return
21+
22+
isPressed.value = true
23+
callback(event)
24+
}
25+
26+
const handleKeyUp = (event: KeyboardEvent) => {
27+
const filterFn = getFilter()
28+
if (!filterFn(event)) return
29+
30+
isPressed.value = false
31+
callback(event)
32+
}
33+
34+
const start = () => {
35+
if (isTracking.value) return
36+
document.addEventListener('keydown', handleKeyDown)
37+
document.addEventListener('keyup', handleKeyUp)
38+
isTracking.value = true
39+
}
40+
41+
const stop = () => {
42+
if (!isTracking.value) return
43+
document.removeEventListener('keydown', handleKeyDown)
44+
document.removeEventListener('keyup', handleKeyUp)
45+
isTracking.value = false
46+
}
47+
48+
onMounted(() => runOnMount && start())
49+
onUnmounted(stop)
50+
51+
return { isPressed, isTracking, start, stop }
52+
}

0 commit comments

Comments
 (0)