diff --git a/README.md b/README.md index d3ccb28..26c6c86 100755 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/functions/useKey/index.ts b/src/functions/useKey/index.ts new file mode 100755 index 0000000..f4aa1eb --- /dev/null +++ b/src/functions/useKey/index.ts @@ -0,0 +1 @@ +export * from './useKey' diff --git a/src/functions/useKey/stories/UseKeyDemo.vue b/src/functions/useKey/stories/UseKeyDemo.vue new file mode 100755 index 0000000..eca75a2 --- /dev/null +++ b/src/functions/useKey/stories/UseKeyDemo.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/src/functions/useKey/stories/useKey.md b/src/functions/useKey/stories/useKey.md new file mode 100755 index 0000000..94f6245 --- /dev/null +++ b/src/functions/useKey/stories/useKey.md @@ -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 + isTracking: Ref + 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` whether the key is currently pressed or not +- `isTracking: Ref` 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 + + + +``` + +Example where se use the `callback` and when pressing the key +**it will update the value only on keyUp**. + +```html + + + +``` + +Example where we `watch` the `isPressed` value and when pressing +the key without releasing **it will update the value only once**. + +```html + + + +``` diff --git a/src/functions/useKey/stories/useKey.story.ts b/src/functions/useKey/stories/useKey.story.ts new file mode 100755 index 0000000..f128f60 --- /dev/null +++ b/src/functions/useKey/stories/useKey.story.ts @@ -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: ` +
+ + + + + +
` +}) + +storiesOf('sensors|useKey', module) + .addParameters({ notes }) + .add('Demo', basicDemo) diff --git a/src/functions/useKey/useKey.spec.ts b/src/functions/useKey/useKey.spec.ts new file mode 100755 index 0000000..282d286 --- /dev/null +++ b/src/functions/useKey/useKey.spec.ts @@ -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: ` +
+
+
+ + +
+ `, + 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) + }) +}) diff --git a/src/functions/useKey/useKey.ts b/src/functions/useKey/useKey.ts new file mode 100755 index 0000000..4aebc43 --- /dev/null +++ b/src/functions/useKey/useKey.ts @@ -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 } +} diff --git a/src/vue-use-kit.ts b/src/vue-use-kit.ts index a49505e..b3a33a0 100755 --- a/src/vue-use-kit.ts +++ b/src/vue-use-kit.ts @@ -8,6 +8,7 @@ export * from './functions/useGeolocation' export * from './functions/useHover' export * from './functions/useIdle' export * from './functions/useIntersection' +export * from './functions/useKey' export * from './functions/useLocation' export * from './functions/useMedia' export * from './functions/useMediaDevices'