Skip to content

Commit 0bdf7ec

Browse files
committed
feat(useHotKey): add new option allowInModal
This allows to also trigger hotkeys in modals. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent aad0e6d commit 0bdf7ec

File tree

4 files changed

+49
-8
lines changed

4 files changed

+49
-8
lines changed

docs/composables/useHotKey.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ where:
2222
See [KeyboardEvent.key Value column](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values) for possible values
2323
- `callback`: a function to be called when the key is pressed. Before called, it will be checked whether keyboard shortcuts are disabled, or interactive element is currently focused, or whether options should be applied
2424
- `options`: options to be applied to the shortcut:
25-
- `push`: whether the event should be triggered on both keydown and keyup (default: `false`)
26-
- `prevent`: prevents the default action of the event (default: `false`)
27-
- `stop`: prevents propagation of the event in the capturing and bubbling phases (default: `false`)
2825
- `ctrl`: whether the Ctrl key (Cmd key on MacOS) should be pressed (default: `false`)
2926
- `alt`: whether the Alt key should be pressed (default: `false`)
3027
- `shift`: whether the Shift key should be pressed (should be explicitly defined as `true`|`false` if needed)
28+
- `push`: whether the event should be triggered on both keydown and keyup (default: `false`)
29+
- `prevent`: prevents the default action of the event (default: `false`)
30+
- `stop`: prevents propagation of the event in the capturing and bubbling phases (default: `false`)
3131
- `caseSensitive`: whether specific case should be listened, e.g. only 'd' and not 'D' (default: `false`)
32+
- `allowInModal`: whether key strokes should also be handled while a modal is shown (default: `false`)
33+
By default this is disabled to not trigger hotkeys of an app while the app is overlaid by a modal.
3234
- `stopCallback`: a callback to stop listening to the event
3335

3436
### Playground

src/components/NcModal/NcModal.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ useHotKey('Escape', () => {
233233
if (trapStack.at(-1) === focusTrap) {
234234
close()
235235
}
236-
})
236+
}, { allowInModal: true })
237237
238238
useHotKey(['ArrowLeft', 'ArrowRight'], (event) => {
239239
// Ignore arrow navigation, if there is a current focus outside the modal.
@@ -248,7 +248,7 @@ useHotKey(['ArrowLeft', 'ArrowRight'], (event) => {
248248
} else {
249249
nextSlide()
250250
}
251-
})
251+
}, { allowInModal: true })
252252
253253
// for developers we should add a warning if used with invalid props combination
254254
onMounted(() => {

src/composables/useHotKey/index.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ export interface UseHotKeyOptions {
3939
* Undefined will be handled the same as `false` and will only run the callback if the 'alt' key is NOT pressed.
4040
*/
4141
alt?: boolean
42+
43+
/**
44+
* Allow hot key to trigger even if a modal/dialog is open.
45+
* By default this is disabled to not trigger hot keys in apps if they are overlaid by a modal/dialog.
46+
*
47+
* @default false
48+
*/
49+
allowInModal?: boolean
4250
}
4351

4452
/**
@@ -47,17 +55,23 @@ export interface UseHotKeyOptions {
4755
*
4856
* @todo Discuss if we should abort on another interactive elements (button, a, e.t.c)
4957
*
50-
* @param event keyboard event
58+
* @param event - The keyboard event
59+
* @param options - The hot key options
5160
* @return Whether it should prevent callback
5261
*/
53-
function shouldIgnoreEvent(event: KeyboardEvent): boolean {
62+
function shouldIgnoreEvent(event: KeyboardEvent, options: UseHotKeyOptions): boolean {
5463
if (!(event.target instanceof HTMLElement)
5564
|| event.target instanceof HTMLInputElement
5665
|| event.target instanceof HTMLTextAreaElement
5766
|| event.target instanceof HTMLSelectElement
5867
|| event.target.isContentEditable) {
5968
return true
6069
}
70+
71+
if (options.allowInModal) {
72+
return false
73+
}
74+
6175
/** Abort if any modal/dialog opened */
6276
return document.getElementsByClassName('modal-mask').length !== 0
6377
}
@@ -90,7 +104,7 @@ function eventHandler(callback: KeyboardEventHandler, options: UseHotKeyOptions)
90104
* option should be explicitly defined
91105
*/
92106
return
93-
} else if (shouldIgnoreEvent(event)) {
107+
} else if (shouldIgnoreEvent(event, options)) {
94108
// Keyboard shortcuts are disabled, because active element assumes input
95109
return
96110
}

tests/unit/composables/useHotKey.spec.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('useHotKey', () => {
1919

2020
afterEach(() => {
2121
mockCallback.mockReset()
22+
document.body.innerHTML = ''
2223
})
2324

2425
it('should register listener and invoke callback', () => {
@@ -39,6 +40,30 @@ describe('useHotKey', () => {
3940
expect(mockCallback).not.toHaveBeenCalled()
4041
})
4142

43+
it('should not invoke callback by default, when a modal is shown', () => {
44+
const modal = document.createElement('div')
45+
modal.className = 'modal-mask'
46+
document.body.appendChild(modal)
47+
48+
const stop = useHotKey(true, mockCallback)
49+
triggerKeyDown({ key: 'a', code: 'KeyA' })
50+
stop()
51+
52+
expect(mockCallback).not.toHaveBeenCalled()
53+
})
54+
55+
it('should invoke callback if modals are allowed, when a modal is shown', () => {
56+
const modal = document.createElement('div')
57+
modal.className = 'modal-mask'
58+
document.body.appendChild(modal)
59+
60+
const stop = useHotKey(true, mockCallback, { allowInModal: true })
61+
triggerKeyDown({ key: 'a', code: 'KeyA' })
62+
stop()
63+
64+
expect(mockCallback).toHaveBeenCalled()
65+
})
66+
4267
describe('options', () => {
4368
it('should accept array of keys and invoke callback for all of them', () => {
4469
const stop = useHotKey(['a', 'b'], mockCallback)

0 commit comments

Comments
 (0)