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 @@ -72,6 +72,8 @@ Vue.use(VueCompositionAPI)
- [`useIntersection`](./src/functions/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)
- [`useKey`](./src/functions/useKey/stories/useKey.md) — executes a handler when a keyboard key is pressed.
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usekey--demo)
- [`useLocation`](./src/functions/useLocation/stories/useLocation.md) — tracks bar navigation location state.
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-uselocation--demo)
- [`useMedia`](./src/functions/useMedia/stories/useMedia.md) — tracks state of a CSS media query.
Expand Down
1 change: 1 addition & 0 deletions src/functions/useKey/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useKey'
77 changes: 77 additions & 0 deletions src/functions/useKey/stories/UseKeyDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<template>
<div>
<div class="canvas">
<div class="canvas__target" :style="canvasTargetPos">🐱‍👤</div>
</div>
<br />
<button class="button is-primary" @click="start" v-if="!isTracking">
Start tracking key press
</button>
<button class="button is-danger" @click="stop" v-else>
Stop tracking key press
</button>
</div>
</template>

<script lang="ts">
import Vue from 'vue'
import { ref } from '@src/api'
import { useKey } from '@src/vue-use-kit'
import { computed } from '@vue/composition-api'

const add = (coord: { value: number }) => () => (coord.value = coord.value + 4)
const sub = (coord: { value: number }) => () => (coord.value = coord.value - 4)

export default Vue.extend({
name: 'UseKeyDemo',
setup() {
const x = ref(0)
const y = ref(0)

const arrowUp = useKey('ArrowUp', sub(y))
const arrowDown = useKey('ArrowDown', add(y))
const arrowLeft = useKey('ArrowLeft', sub(x))
const arrowRight = useKey('ArrowRight', add(x))

const isTracking = ref(true)
const start = () => {
isTracking.value = true
arrowUp.start()
arrowDown.start()
arrowLeft.start()
arrowRight.start()
}

const stop = () => {
isTracking.value = false
arrowUp.stop()
arrowDown.stop()
arrowLeft.stop()
arrowRight.stop()
}

const canvasTargetPos = computed(() => ({
transform: `translate(-50%, -50%) translate(${x.value}px, ${y.value}px)`
}))

return { canvasTargetPos, isTracking, start, stop }
}
})
</script>

<style scoped>
.canvas {
position: relative;
width: 100%;
height: 200px;
background: #f1f1f1;
}

.canvas__target {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 30px;
}
</style>
156 changes: 156 additions & 0 deletions src/functions/useKey/stories/useKey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# useKey

Vue function that executes a handler when a keyboard key is pressed.

## Reference

```typescript
type UseKeyFilter = string | ((event: KeyboardEvent) => boolean)
```

```typescript
function useKey(
filter: UseKeyFilter,
callback?: any,
runOnMount?: boolean
): {
isPressed: Ref<boolean>
isTracking: Ref<boolean>
start: () => void
stop: () => void
}
```

### Parameters

- `filter: UseKeyFilter` the filter string or function to use for triggering the key event
- `callback: Function` the function called when the given key is pressed
- `runOnMount: boolean` whether to track the given key on mount, `true` by default.

### Returns

- `isPressed: Ref<boolean>` whether the key is currently pressed or not
- `isTracking: Ref<boolean>` whether this function events are running or not
- `start: Function` the function used for start tracking the key event
- `stop: Function` the function used for stop tracking the key event

## Usage

Example where se use the `callback` and when pressing the key without
releasing **it will update the value continuously**.

```html
<template>
<div>
<p>isPressed {{ isPressed }}</p>
<p>keyPressCount {{ keyPressCount }}</p>
<div>
<button @click="start" v-if="!isTracking">
Start tracking key press
</button>
<button @click="stop" v-else>
Stop tracking key press
</button>
</div>
</div>
</template>

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

export default Vue.extend({
name: 'UseKeyDemo',
setup() {
const keyPressCount = ref(0)

// Increase value continuously when 'a' key is kept pressed
const handleKey = () => keyPressCount.value++

const { isPressed, isTracking, start, stop } = useKey('a', handleKey)
return { keyPressCount, isPressed, isTracking, start, stop }
}
})
</script>
```

Example where se use the `callback` and when pressing the key
**it will update the value only on keyUp**.

```html
<template>
<div>
<p>isPressed {{ isPressed }}</p>
<p>keyPressCount {{ keyPressCount }}</p>
<div>
<button @click="start" v-if="!isTracking">
Start tracking key press
</button>
<button @click="stop" v-else>
Stop tracking key press
</button>
</div>
</div>
</template>

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

export default Vue.extend({
name: 'UseKeyDemo',
setup() {
const keyPressCount = ref(0)

// Increase value when 'a' key is released
const handleKey = e => {
if (e.type === 'keyup') keyPressCount.value++
}

const { isPressed, isTracking, start, stop } = useKey('a', handleKey)
return { keyPressCount, isPressed, isTracking, start, stop }
}
})
</script>
```

Example where we `watch` the `isPressed` value and when pressing
the key without releasing **it will update the value only once**.

```html
<template>
<div>
<p>isPressed {{ isPressed }}</p>
<p>keyPressCount {{ keyPressCount }}</p>
<div>
<button @click="start" v-if="!isTracking">
Start tracking key press
</button>
<button @click="stop" v-else>
Stop tracking key press
</button>
</div>
</div>
</template>

<script lang="ts">
import Vue from 'vue'
import { watch, ref } from '@src/api'
import { useKey } from 'vue-use-kit'

export default Vue.extend({
name: 'UseKeyDemo',
setup() {
const keyPressCount = ref(0)
const { isPressed, isTracking, start, stop } = useKey('a')

// Increase value when 'a' key is pressed
watch(isPressed, isPress => isPress && keyPressCount.value++)

return { keyPressCount, isPressed, isTracking, start, stop }
}
})
</script>
```
33 changes: 33 additions & 0 deletions src/functions/useKey/stories/useKey.story.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { storiesOf } from '@storybook/vue'
import path from 'path'
import StoryTitle from '@src/helpers/StoryTitle.vue'
import UseKeyDemo from './UseKeyDemo.vue'

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

const basicDemo = () => ({
components: { StoryTitle, demo: UseKeyDemo },
template: `
<div class="container">
<story-title
function-path="${functionPath}"
source-name="${functionName}"
demo-name="UseKeyDemo.vue"
>
<template v-slot:title></template>
<template v-slot:intro>
<p>
Press an arrow key (←, →, ↑, ↓) to move the ninja cat around
the screen.
</p>
</template>
</story-title>
<demo />
</div>`
})

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

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

const testComponent = (filter = 'a', callback = () => ``, onMount = true) => ({
template: `
<div>
<div id="isTracking" v-if="isTracking"></div>
<div id="isPressed" v-if="isPressed"></div>
<button id="start" @click="start"></button>
<button id="stop" @click="stop"></button>
</div>
`,
setup() {
const { isPressed, isTracking, start, stop } = useKey(
filter,
callback,
onMount
)
return { isPressed, isTracking, start, stop }
}
})

describe('useKey', () => {
const noop = () => ``
const events = ['keyup', 'keydown']

it('should add events on mounted and remove them on unmounted', async () => {
await checkOnMountAndUnmountEvents(document, events, testComponent)
})

it('should add events again when start is called', async () => {
await checkOnStartEvents(document, events, testComponent)
})

it('should remove events when stop is called', async () => {
await checkOnStopEvents(document, events, testComponent)
})

it('should show #isTracking when runOnMount is true', async () => {
await checkElementExistenceOnMount(true, testComponent('a', noop, true))
})

it('should not show #isTracking when runOnMount is false', async () => {
await checkElementExistenceOnMount(false, testComponent('a', noop, false))
})

it('should not show #isPressed when no key has been pressed', async () => {
const wrapper = mount(testComponent())
await wrapper.vm.$nextTick()
expect(wrapper.find('#isPressed').exists()).toBe(false)
})
})
52 changes: 52 additions & 0 deletions src/functions/useKey/useKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ref, onMounted, onUnmounted, Ref } from '@src/api'

type UseKeyFilter = string | ((event: KeyboardEvent) => boolean)

export function useKey(
filter: UseKeyFilter,
callback: any = () => ``,
runOnMount = true
) {
const isTracking = ref(false)
const isPressed = ref(false)

const getFilter = () => {
if (typeof filter === 'function') return filter
return (event: KeyboardEvent) => event.key === filter
}

const handleKeyDown = (event: KeyboardEvent) => {
const filterFn = getFilter()
if (!filterFn(event)) return

isPressed.value = true
callback(event)
}

const handleKeyUp = (event: KeyboardEvent) => {
const filterFn = getFilter()
if (!filterFn(event)) return

isPressed.value = false
callback(event)
}

const start = () => {
if (isTracking.value) return
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
isTracking.value = true
}

const stop = () => {
if (!isTracking.value) return
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
isTracking.value = false
}

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

return { isPressed, isTracking, start, stop }
}
Loading