-
Notifications
You must be signed in to change notification settings - Fork 27.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[DevTools] Add
CMD + .
keyboard shortcut to show/hide (#74878)
This PR added a `cmd + .` keyboard shortcut for the DevTools Indicator. ### Mac Light https://github.com/user-attachments/assets/5d99098a-3983-460b-ad9c-1d4f7c24220d ### Mac Dark https://github.com/user-attachments/assets/de71b668-c79a-4962-a105-fc4b2cbd9a64 ### Windows Light https://github.com/user-attachments/assets/6f615f27-2871-41a3-aa39-7592237e63ad ### Windows Dark https://github.com/user-attachments/assets/d5997095-5161-4d1f-a69c-54e74f032821 Closes NDX-568 --------- Co-authored-by: Jiachi Liu <[email protected]>
- Loading branch information
1 parent
6616e20
commit aa0a819
Showing
5 changed files
with
253 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 133 additions & 0 deletions
133
...t/components/react-dev-overlay/_experimental/internal/hooks/use-keyboard-shortcut.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
/** | ||
* @jest-environment jsdom | ||
*/ | ||
/* eslint-disable import/no-extraneous-dependencies */ | ||
import { renderHook } from '@testing-library/react' | ||
import { useKeyboardShortcut } from './use-keyboard-shortcut' | ||
import { MODIFIERS } from './use-keyboard-shortcut' | ||
|
||
describe('useKeyboardShortcut', () => { | ||
let addEventListenerSpy: jest.SpyInstance | ||
let removeEventListenerSpy: jest.SpyInstance | ||
|
||
beforeEach(() => { | ||
addEventListenerSpy = jest.spyOn(window, 'addEventListener') | ||
removeEventListenerSpy = jest.spyOn(window, 'removeEventListener') | ||
}) | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks() | ||
}) | ||
|
||
it('should add and remove event listener', () => { | ||
const callback = jest.fn() | ||
const { unmount } = renderHook(() => | ||
useKeyboardShortcut({ | ||
key: 'k', | ||
callback, | ||
modifiers: [MODIFIERS.CTRL_CMD], | ||
}) | ||
) | ||
|
||
// When used `expect.any(Function)`, received: | ||
// error TS21228: [ban-function-calls] Constructing functions from strings can lead to XSS. | ||
const eventListener = addEventListenerSpy.mock.calls[0][1] | ||
expect(typeof eventListener).toBe('function') | ||
expect(addEventListenerSpy).toHaveBeenCalledWith('keydown', eventListener) | ||
|
||
unmount() | ||
expect(removeEventListenerSpy).toHaveBeenCalled() | ||
}) | ||
|
||
it('should trigger callback when correct key and modifier are pressed', () => { | ||
const callback = jest.fn() | ||
renderHook(() => | ||
useKeyboardShortcut({ | ||
key: 'k', | ||
callback, | ||
modifiers: [MODIFIERS.CTRL_CMD], | ||
}) | ||
) | ||
|
||
// Simulate keydown event with Cmd/Ctrl + K | ||
window.dispatchEvent( | ||
new KeyboardEvent('keydown', { | ||
key: 'k', | ||
metaKey: true, | ||
}) | ||
) | ||
|
||
expect(callback).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it('should work with multiple modifiers', () => { | ||
const callback = jest.fn() | ||
renderHook(() => | ||
useKeyboardShortcut({ | ||
key: 'k', | ||
callback, | ||
modifiers: [MODIFIERS.CTRL_CMD, MODIFIERS.SHIFT], | ||
}) | ||
) | ||
|
||
// Should not trigger with just Cmd/Ctrl | ||
window.dispatchEvent( | ||
new KeyboardEvent('keydown', { | ||
key: 'k', | ||
metaKey: true, | ||
}) | ||
) | ||
expect(callback).not.toHaveBeenCalled() | ||
|
||
// Should trigger with Cmd/Ctrl + Shift | ||
window.dispatchEvent( | ||
new KeyboardEvent('keydown', { | ||
key: 'k', | ||
metaKey: true, | ||
shiftKey: true, | ||
}) | ||
) | ||
expect(callback).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it('should be case insensitive', () => { | ||
const callback = jest.fn() | ||
renderHook(() => | ||
useKeyboardShortcut({ | ||
key: 'k', | ||
callback, | ||
modifiers: [MODIFIERS.CTRL_CMD], | ||
}) | ||
) | ||
|
||
window.dispatchEvent( | ||
new KeyboardEvent('keydown', { | ||
key: 'K', // uppercase | ||
metaKey: true, | ||
}) | ||
) | ||
|
||
expect(callback).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it('should prevent default event behavior', () => { | ||
const callback = jest.fn() | ||
renderHook(() => | ||
useKeyboardShortcut({ | ||
key: 'k', | ||
callback, | ||
modifiers: [MODIFIERS.CTRL_CMD], | ||
}) | ||
) | ||
|
||
const event = new KeyboardEvent('keydown', { | ||
key: 'k', | ||
metaKey: true, | ||
}) | ||
const preventDefaultSpy = jest.spyOn(event, 'preventDefault') | ||
|
||
window.dispatchEvent(event) | ||
|
||
expect(preventDefaultSpy).toHaveBeenCalled() | ||
}) | ||
}) |
44 changes: 44 additions & 0 deletions
44
...client/components/react-dev-overlay/_experimental/internal/hooks/use-keyboard-shortcut.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { useEffect } from 'react' | ||
|
||
export const MODIFIERS = { | ||
CTRL_CMD: 'CTRL_CMD', | ||
ALT: 'ALT', | ||
SHIFT: 'SHIFT', | ||
} as const | ||
|
||
type KeyboardShortcutProps = { | ||
key: string | ||
callback: () => void | ||
modifiers: (typeof MODIFIERS)[keyof typeof MODIFIERS][] | ||
} | ||
|
||
export function useKeyboardShortcut({ | ||
key, | ||
callback, | ||
modifiers, | ||
}: KeyboardShortcutProps) { | ||
useEffect(() => { | ||
const handleKeyDown = (e: KeyboardEvent) => { | ||
const modifiersPressed = modifiers.every((modifier) => { | ||
switch (modifier) { | ||
case MODIFIERS.CTRL_CMD: | ||
return e.metaKey || e.ctrlKey | ||
case MODIFIERS.ALT: | ||
return e.altKey | ||
case MODIFIERS.SHIFT: | ||
return e.shiftKey | ||
default: | ||
return false | ||
} | ||
}) | ||
|
||
if (modifiersPressed && e.key.toLowerCase() === key.toLowerCase()) { | ||
e.preventDefault() | ||
callback() | ||
} | ||
} | ||
|
||
window.addEventListener('keydown', handleKeyDown) | ||
return () => window.removeEventListener('keydown', handleKeyDown) | ||
}, [key, callback, modifiers]) | ||
} |