diff --git a/.changeset/dry-ducks-remain.md b/.changeset/dry-ducks-remain.md
new file mode 100644
index 00000000..b54c9d71
--- /dev/null
+++ b/.changeset/dry-ducks-remain.md
@@ -0,0 +1,6 @@
+---
+"@interactors/globals": patch
+"@interactors/keyboard": patch
+---
+
+Improve keyboard layout, by adding missing chars and fix pressing functional keys
diff --git a/packages/globals/src/index.ts b/packages/globals/src/index.ts
index 7602e714..650ba722 100644
--- a/packages/globals/src/index.ts
+++ b/packages/globals/src/index.ts
@@ -1,2 +1,2 @@
export * from './globals'
-export { KeyboardLayout, KeyCode } from './keyboard-layout'
+export * from './keyboard-layout'
diff --git a/packages/globals/src/keyboard-layout.ts b/packages/globals/src/keyboard-layout.ts
index 54cfd98c..1d3d701c 100644
--- a/packages/globals/src/keyboard-layout.ts
+++ b/packages/globals/src/keyboard-layout.ts
@@ -1,3 +1,355 @@
+// NOTE: List of keys from https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
+export type KeyValue =
+ // Special keys
+ | 'Unidentified'
+ // Modifier keys
+ | 'Alt'
+ | 'AltGraph'
+ | 'CapsLock'
+ | 'Control'
+ | 'Fn'
+ | 'FnLock'
+ | 'Hyper'
+ | 'Meta'
+ | 'NumLock'
+ | 'ScrollLock'
+ | 'Shift'
+ | 'Super'
+ | 'Symbol'
+ | 'SymbolLock'
+ // Whitespace keys
+ | 'Enter'
+ | 'Tab'
+ | ' '
+ // Navigation keys
+ | 'ArrowDown'
+ | 'ArrowLeft'
+ | 'ArrowRight'
+ | 'ArrowUp'
+ | 'End'
+ | 'Home'
+ | 'PageDown'
+ | 'PageUp'
+ // Editing keys
+ | 'Backspace'
+ | 'Clear'
+ | 'Copy'
+ | 'CrSel'
+ | 'Cut'
+ | 'Delete'
+ | 'EraseEof'
+ | 'ExSel'
+ | 'Insert'
+ | 'Paste'
+ | 'Redo'
+ | 'Undo'
+ // UI keys
+ | 'Accept'
+ | 'Again'
+ | 'Attn'
+ | 'Cancel'
+ | 'ContextMenu'
+ | 'Escape'
+ | 'Execute'
+ | 'Find'
+ | 'Finish'
+ | 'Help'
+ | 'Pause'
+ | 'Play'
+ | 'Props'
+ | 'Select'
+ | 'ZoomIn'
+ | 'ZoomOut'
+ // Device keys
+ | 'BrightnessDown'
+ | 'BrightnessUp'
+ | 'Eject'
+ | 'LogOff'
+ | 'Power'
+ | 'PowerOff'
+ | 'PrintScreen'
+ | 'Hibernate'
+ | 'Standby'
+ | 'WakeUp'
+ // Common IME keys
+ | 'AllCandidates'
+ | 'Alphanumeric'
+ | 'CodeInput'
+ | 'Compose'
+ | 'Convert'
+ | 'Dead'
+ | 'FinalMode'
+ | 'GroupFirst'
+ | 'GroupLast'
+ | 'GroupNext'
+ | 'GroupPrevious'
+ | 'ModeChange'
+ | 'NextCandidate'
+ | 'NonConvert'
+ | 'PreviousCandidate'
+ | 'Process'
+ | 'SingleCandidate'
+ // Korean keyboards only
+ | 'HangulMode'
+ | 'HanjaMode'
+ | 'JunjaMode'
+ // Japanese keyboards only
+ | 'Eisu'
+ | 'Hankaku'
+ | 'Hiragana'
+ | 'HiraganaKatakana'
+ | 'KanaMode'
+ | 'KanjiMode'
+ | 'Katakana'
+ | 'Romaji'
+ | 'Zenkaku'
+ | 'ZenkakuHanaku'
+ // Function keys
+ | 'F1'
+ | 'F2'
+ | 'F3'
+ | 'F4'
+ | 'F5'
+ | 'F6'
+ | 'F7'
+ | 'F8'
+ | 'F9'
+ | 'F10'
+ | 'F11'
+ | 'F12'
+ | 'F13'
+ | 'F14'
+ | 'F15'
+ | 'F16'
+ | 'F17'
+ | 'F18'
+ | 'F19'
+ | 'F20'
+ | 'Soft1'
+ | 'Soft2'
+ | 'Soft3'
+ | 'Soft4'
+ // Phone keys
+ | 'AppSwitch'
+ | 'Call'
+ | 'Camera'
+ | 'CameraFocus'
+ | 'EndCall'
+ | 'GoBack'
+ | 'GoHome'
+ | 'HeadsetHook'
+ | 'LastNumberRedial'
+ | 'Notification'
+ | 'MannerMode'
+ | 'VoiceDial'
+ // Multimedia keys
+ | 'ChannelDown'
+ | 'ChannelUp'
+ | 'MediaFastForward'
+ | 'MediaPause'
+ | 'MediaPlay'
+ | 'MediaPlayPause'
+ | 'MediaRecord'
+ | 'MediaRewind'
+ | 'MediaStop'
+ | 'MediaTrackNext'
+ | 'MediaTrackPrevious'
+ // Audio control keys
+ | 'AudioBalanceLeft'
+ | 'AudioBalanceRight'
+ | 'AudioBassDown'
+ | 'AudioBassBoostDown'
+ | 'AudioBassBoostToggle'
+ | 'AudioBassBoostUp'
+ | 'AudioBassUp'
+ | 'AudioFaderFront'
+ | 'AudioFaderRear'
+ | 'AudioSurroundModeNext'
+ | 'AudioTrebleDown'
+ | 'AudioTrebleUp'
+ | 'AudioVolumeDown'
+ | 'AudioVolumeMute'
+ | 'AudioVolumeUp'
+ | 'MicrophoneToggle'
+ | 'MicrophoneVolumeDown'
+ | 'MicrophoneVolumeMute'
+ | 'MicrophoneVolumeUp'
+ // TV control keys
+ | 'TV'
+ | 'TV3DMode'
+ | 'TVAntennaCable'
+ | 'TVAudioDescription'
+ | 'TVAudioDescriptionMixDown'
+ | 'TVAudioDescriptionMixUp'
+ | 'TVContentsMenu'
+ | 'TVDataService'
+ | 'TVInput'
+ | 'TVInputComponent1'
+ | 'TVInputComponent2'
+ | 'TVInputComposite1'
+ | 'TVInputComposite2'
+ | 'TVInputHDMI1'
+ | 'TVInputHDMI2'
+ | 'TVInputHDMI3'
+ | 'TVInputHDMI4'
+ | 'TVInputVGA1'
+ | 'TVMediaContext'
+ | 'TVNetwork'
+ | 'TVNumberEntry'
+ | 'TVPower'
+ | 'TVRadioService'
+ | 'TVSatellite'
+ | 'TVSatelliteBS'
+ | 'TVSatelliteCS'
+ | 'TVSatelliteToggle'
+ | 'TVTerrestrialAnalog'
+ | 'TVTerrestrialDigital'
+ | 'TVTimer'
+ // Media controller keys
+ | 'AVRInput'
+ | 'AVRPower'
+ | 'ColorF0Red'
+ | 'ColorF1Green'
+ | 'ColorF2Yellow'
+ | 'ColorF3Blue'
+ | 'ColorF4Grey'
+ | 'ColorF5Brown'
+ | 'ClosedCaptionToggle'
+ | 'Dimmer'
+ | 'DisplaySwap'
+ | 'DVR'
+ | 'Exit'
+ | 'FavoriteClear0'
+ | 'FavoriteClear1'
+ | 'FavoriteClear2'
+ | 'FavoriteClear3'
+ | 'FavoriteRecall0'
+ | 'FavoriteRecall1'
+ | 'FavoriteRecall2'
+ | 'FavoriteRecall3'
+ | 'FavoriteStore0'
+ | 'FavoriteStore1'
+ | 'FavoriteStore2'
+ | 'FavoriteStore3'
+ | 'Guide'
+ | 'GuideNextDay'
+ | 'GuidePreviousDay'
+ | 'Info'
+ | 'InstantReplay'
+ | 'Link'
+ | 'ListProgram'
+ | 'LiveContent'
+ | 'Lock'
+ | 'MediaApps'
+ | 'MediaAudioTrack'
+ | 'MediaLast'
+ | 'MediaSkipBackward'
+ | 'MediaSkipForward'
+ | 'MediaStepBackward'
+ | 'MediaStepForward'
+ | 'MediaTopMenu'
+ | 'NavigateIn'
+ | 'NavigateNext'
+ | 'NavigateOut'
+ | 'NavigatePrevious'
+ | 'NextFavoriteChannel'
+ | 'NextUserProfile'
+ | 'OnDemand'
+ | 'Pairing'
+ | 'PinPDown'
+ | 'PinPMove'
+ | 'PinPToggle'
+ | 'PinPUp'
+ | 'PlaySpeedDown'
+ | 'PlaySpeedReset'
+ | 'PlaySpeedUp'
+ | 'RandomToggle'
+ | 'RcLowBattery'
+ | 'RecordSpeedNext'
+ | 'RfBypass'
+ | 'ScanChannelsToggle'
+ | 'ScreenModeNext'
+ | 'Settings'
+ | 'SplitScreenToggle'
+ | 'STBInput'
+ | 'STBPower'
+ | 'Subtitle'
+ | 'Teletext'
+ | 'VideoModeNext'
+ | 'Wink'
+ | 'ZoomToggle'
+ // Speech recognition keys
+ | 'SpeechCorrectionList'
+ | 'SpeechInputToggle'
+ // Document keys
+ | 'Close'
+ | 'New'
+ | 'Open'
+ | 'Print'
+ | 'Save'
+ | 'SpellCheck'
+ | 'MailForward'
+ | 'MailReply'
+ | 'MailSend'
+ // Application selector keys
+ | 'LaunchCalculator'
+ | 'LaunchCalendar'
+ | 'LaunchContacts'
+ | 'LaunchMail'
+ | 'LaunchMediaPlayer'
+ | 'LaunchMusicPlayer'
+ | 'LaunchMyComputer'
+ | 'LaunchPhone'
+ | 'LaunchScreenSaver'
+ | 'LaunchSpreadsheet'
+ | 'LaunchWebBrowser'
+ | 'LaunchWebCam'
+ | 'LaunchWordProcessor'
+ | 'LaunchApplication1'
+ | 'LaunchApplication2'
+ | 'LaunchApplication3'
+ | 'LaunchApplication4'
+ | 'LaunchApplication5'
+ | 'LaunchApplication6'
+ | 'LaunchApplication7'
+ | 'LaunchApplication8'
+ | 'LaunchApplication9'
+ | 'LaunchApplication10'
+ | 'LaunchApplication11'
+ | 'LaunchApplication12'
+ | 'LaunchApplication13'
+ | 'LaunchApplication14'
+ | 'LaunchApplication15'
+ | 'LaunchApplication16'
+ // Browser control keys
+ | 'BrowserBack'
+ | 'BrowserFavorites'
+ | 'BrowserForward'
+ | 'BrowserHome'
+ | 'BrowserRefresh'
+ | 'BrowserSearch'
+ | 'BrowserStop'
+ // Numeric keypad keys
+ | 'Decimal'
+ | 'Key11'
+ | 'Key12'
+ | 'Multiply'
+ | 'Add'
+ | 'Clear'
+ | 'Divide'
+ | 'Subtract'
+ | 'Separator'
+ | '0'
+ | '1'
+ | '2'
+ | '3'
+ | '4'
+ | '5'
+ | '6'
+ | '7'
+ | '8'
+ | '9'
+
export type KeyCode =
| "Backspace"
| "Tab"
@@ -99,11 +451,25 @@ export type KeyCode =
| "Slash"
| "Backquote"
| "BracketLeft"
- | "Backslack"
+ | "Backslash"
| "BracketRight"
| "Quote";
+interface CommonKey {
+ code: KeyCode;
+ key: string;
+ shiftKey?: true;
+}
+
+interface SpecialKey {
+ code: KeyCode;
+ key: KeyValue;
+ shiftKey?: true;
+}
+
+export type Key = CommonKey | SpecialKey
+
export interface KeyboardLayout {
- getKey(code: KeyCode): string | undefined;
- getCode(key: string): KeyCode | undefined;
+ getByCode(code: KeyCode): Key | undefined;
+ getByKey(key: string): Key | undefined;
}
diff --git a/packages/keyboard/src/keyboard.ts b/packages/keyboard/src/keyboard.ts
index ad4a77d7..9fd25613 100644
--- a/packages/keyboard/src/keyboard.ts
+++ b/packages/keyboard/src/keyboard.ts
@@ -1,9 +1,10 @@
-import { globals, KeyCode } from '@interactors/globals';
+import { globals, KeyCode, KeyValue } from '@interactors/globals';
import { createInteractor } from '@interactors/core';
import { dispatchInput, dispatchKeyDown, dispatchKeyUp } from './dispatch';
+import { NonPrintableKeys } from './keys';
-export type KeyOptions = {
- key?: string;
+export interface KeyOptions {
+ key?: K;
code?: KeyCode;
ctrlKey?: boolean;
shiftKey?: boolean;
@@ -15,16 +16,17 @@ export type KeyOptions = {
const KeyboardInteractor = createInteractor('Keyboard')
.selector(':root')
.actions({
- async press(interactor, options: KeyOptions = {}) {
+ async press(interactor, options: KeyOptions | KeyOptions = {}) {
await interactor.perform((element) => {
let activeElement = (element.ownerDocument.activeElement || element.ownerDocument.body) as HTMLElement;
if(options.key && !options.code) {
- options.code = globals.keyboardLayout.getCode(options.key);
+ options = { ...options, ...globals.keyboardLayout.getByKey(options.key) };
}
if(options.code && !options.key) {
- options.key = globals.keyboardLayout.getKey(options.code);
+ options = { ...options, ...globals.keyboardLayout.getByCode(options.code) };
}
- if(dispatchKeyDown(activeElement, options) && isTextElement(activeElement)) {
+ let isFunctionalKey = options.key ? NonPrintableKeys.has(options.key as KeyValue) : false;
+ if(dispatchKeyDown(activeElement, options) && isTextElement(activeElement) && !isFunctionalKey) {
// don't change the value if the keydown event was stopped
setValue(activeElement, activeElement.value + options.key);
// input is not dispatched if the keydown event was stopped
diff --git a/packages/keyboard/src/keys.ts b/packages/keyboard/src/keys.ts
new file mode 100644
index 00000000..d57ee092
--- /dev/null
+++ b/packages/keyboard/src/keys.ts
@@ -0,0 +1,342 @@
+import { KeyValue } from '@interactors/globals';
+
+export const NonPrintableKeys: Set = new Set([
+ // Special keys,
+ 'Unidentified',
+ // Modifier keys
+ 'Alt',
+ 'AltGraph',
+ 'CapsLock',
+ 'Control',
+ 'Fn',
+ 'FnLock',
+ 'Hyper',
+ 'Meta',
+ 'NumLock',
+ 'ScrollLock',
+ 'Shift',
+ 'Super',
+ 'Symbol',
+ 'SymbolLock',
+ // Whitespace keys
+ 'Enter',
+ 'Tab',
+ // Navigation keys
+ 'ArrowDown',
+ 'ArrowLeft',
+ 'ArrowRight',
+ 'ArrowUp',
+ 'End',
+ 'Home',
+ 'PageDown',
+ 'PageUp',
+ // Editing keys
+ 'Backspace',
+ 'Clear',
+ 'Copy',
+ 'CrSel',
+ 'Cut',
+ 'Delete',
+ 'EraseEof',
+ 'ExSel',
+ 'Insert',
+ 'Paste',
+ 'Redo',
+ 'Undo',
+ // UI keys
+ 'Accept',
+ 'Again',
+ 'Attn',
+ 'Cancel',
+ 'ContextMenu',
+ 'Escape',
+ 'Execute',
+ 'Find',
+ 'Finish',
+ 'Help',
+ 'Pause',
+ 'Play',
+ 'Props',
+ 'Select',
+ 'ZoomIn',
+ 'ZoomOut',
+ // Device keys
+ 'BrightnessDown',
+ 'BrightnessUp',
+ 'Eject',
+ 'LogOff',
+ 'Power',
+ 'PowerOff',
+ 'PrintScreen',
+ 'Hibernate',
+ 'Standby',
+ 'WakeUp',
+ // Common IME keys
+ 'AllCandidates',
+ 'Alphanumeric',
+ 'CodeInput',
+ 'Compose',
+ 'Convert',
+ 'Dead',
+ 'FinalMode',
+ 'GroupFirst',
+ 'GroupLast',
+ 'GroupNext',
+ 'GroupPrevious',
+ 'ModeChange',
+ 'NextCandidate',
+ 'NonConvert',
+ 'PreviousCandidate',
+ 'Process',
+ 'SingleCandidate',
+ // Korean keyboards only
+ 'HangulMode',
+ 'HanjaMode',
+ 'JunjaMode',
+ // Japanese keyboards only
+ 'Eisu',
+ 'Hankaku',
+ 'Hiragana',
+ 'HiraganaKatakana',
+ 'KanaMode',
+ 'KanjiMode',
+ 'Katakana',
+ 'Romaji',
+ 'Zenkaku',
+ 'ZenkakuHanaku',
+ // Function keys
+ 'F1',
+ 'F2',
+ 'F3',
+ 'F4',
+ 'F5',
+ 'F6',
+ 'F7',
+ 'F8',
+ 'F9',
+ 'F10',
+ 'F11',
+ 'F12',
+ 'F13',
+ 'F14',
+ 'F15',
+ 'F16',
+ 'F17',
+ 'F18',
+ 'F19',
+ 'F20',
+ 'Soft1',
+ 'Soft2',
+ 'Soft3',
+ 'Soft4',
+ // Phone keys
+ 'AppSwitch',
+ 'Call',
+ 'Camera',
+ 'CameraFocus',
+ 'EndCall',
+ 'GoBack',
+ 'GoHome',
+ 'HeadsetHook',
+ 'LastNumberRedial',
+ 'Notification',
+ 'MannerMode',
+ 'VoiceDial',
+ // Multimedia keys
+ 'ChannelDown',
+ 'ChannelUp',
+ 'MediaFastForward',
+ 'MediaPause',
+ 'MediaPlay',
+ 'MediaPlayPause',
+ 'MediaRecord',
+ 'MediaRewind',
+ 'MediaStop',
+ 'MediaTrackNext',
+ 'MediaTrackPrevious',
+ // Audio control keys
+ 'AudioBalanceLeft',
+ 'AudioBalanceRight',
+ 'AudioBassDown',
+ 'AudioBassBoostDown',
+ 'AudioBassBoostToggle',
+ 'AudioBassBoostUp',
+ 'AudioBassUp',
+ 'AudioFaderFront',
+ 'AudioFaderRear',
+ 'AudioSurroundModeNext',
+ 'AudioTrebleDown',
+ 'AudioTrebleUp',
+ 'AudioVolumeDown',
+ 'AudioVolumeMute',
+ 'AudioVolumeUp',
+ 'MicrophoneToggle',
+ 'MicrophoneVolumeDown',
+ 'MicrophoneVolumeMute',
+ 'MicrophoneVolumeUp',
+ // TV control keys
+ 'TV',
+ 'TV3DMode',
+ 'TVAntennaCable',
+ 'TVAudioDescription',
+ 'TVAudioDescriptionMixDown',
+ 'TVAudioDescriptionMixUp',
+ 'TVContentsMenu',
+ 'TVDataService',
+ 'TVInput',
+ 'TVInputComponent1',
+ 'TVInputComponent2',
+ 'TVInputComposite1',
+ 'TVInputComposite2',
+ 'TVInputHDMI1',
+ 'TVInputHDMI2',
+ 'TVInputHDMI3',
+ 'TVInputHDMI4',
+ 'TVInputVGA1',
+ 'TVMediaContext',
+ 'TVNetwork',
+ 'TVNumberEntry',
+ 'TVPower',
+ 'TVRadioService',
+ 'TVSatellite',
+ 'TVSatelliteBS',
+ 'TVSatelliteCS',
+ 'TVSatelliteToggle',
+ 'TVTerrestrialAnalog',
+ 'TVTerrestrialDigital',
+ 'TVTimer',
+ // Media controller keys
+ 'AVRInput',
+ 'AVRPower',
+ 'ColorF0Red',
+ 'ColorF1Green',
+ 'ColorF2Yellow',
+ 'ColorF3Blue',
+ 'ColorF4Grey',
+ 'ColorF5Brown',
+ 'ClosedCaptionToggle',
+ 'Dimmer',
+ 'DisplaySwap',
+ 'DVR',
+ 'Exit',
+ 'FavoriteClear0',
+ 'FavoriteClear1',
+ 'FavoriteClear2',
+ 'FavoriteClear3',
+ 'FavoriteRecall0',
+ 'FavoriteRecall1',
+ 'FavoriteRecall2',
+ 'FavoriteRecall3',
+ 'FavoriteStore0',
+ 'FavoriteStore1',
+ 'FavoriteStore2',
+ 'FavoriteStore3',
+ 'Guide',
+ 'GuideNextDay',
+ 'GuidePreviousDay',
+ 'Info',
+ 'InstantReplay',
+ 'Link',
+ 'ListProgram',
+ 'LiveContent',
+ 'Lock',
+ 'MediaApps',
+ 'MediaAudioTrack',
+ 'MediaLast',
+ 'MediaSkipBackward',
+ 'MediaSkipForward',
+ 'MediaStepBackward',
+ 'MediaStepForward',
+ 'MediaTopMenu',
+ 'NavigateIn',
+ 'NavigateNext',
+ 'NavigateOut',
+ 'NavigatePrevious',
+ 'NextFavoriteChannel',
+ 'NextUserProfile',
+ 'OnDemand',
+ 'Pairing',
+ 'PinPDown',
+ 'PinPMove',
+ 'PinPToggle',
+ 'PinPUp',
+ 'PlaySpeedDown',
+ 'PlaySpeedReset',
+ 'PlaySpeedUp',
+ 'RandomToggle',
+ 'RcLowBattery',
+ 'RecordSpeedNext',
+ 'RfBypass',
+ 'ScanChannelsToggle',
+ 'ScreenModeNext',
+ 'Settings',
+ 'SplitScreenToggle',
+ 'STBInput',
+ 'STBPower',
+ 'Subtitle',
+ 'Teletext',
+ 'VideoModeNext',
+ 'Wink',
+ 'ZoomToggle',
+ // Speech recognition keys
+ 'SpeechCorrectionList',
+ 'SpeechInputToggle',
+ // Document keys
+ 'Close',
+ 'New',
+ 'Open',
+ 'Print',
+ 'Save',
+ 'SpellCheck',
+ 'MailForward',
+ 'MailReply',
+ 'MailSend',
+ // Application selector keys
+ 'LaunchCalculator',
+ 'LaunchCalendar',
+ 'LaunchContacts',
+ 'LaunchMail',
+ 'LaunchMediaPlayer',
+ 'LaunchMusicPlayer',
+ 'LaunchMyComputer',
+ 'LaunchPhone',
+ 'LaunchScreenSaver',
+ 'LaunchSpreadsheet',
+ 'LaunchWebBrowser',
+ 'LaunchWebCam',
+ 'LaunchWordProcessor',
+ 'LaunchApplication1',
+ 'LaunchApplication2',
+ 'LaunchApplication3',
+ 'LaunchApplication4',
+ 'LaunchApplication5',
+ 'LaunchApplication6',
+ 'LaunchApplication7',
+ 'LaunchApplication8',
+ 'LaunchApplication9',
+ 'LaunchApplication10',
+ 'LaunchApplication11',
+ 'LaunchApplication12',
+ 'LaunchApplication13',
+ 'LaunchApplication14',
+ 'LaunchApplication15',
+ 'LaunchApplication16',
+ // Browser control keys
+ 'BrowserBack',
+ 'BrowserFavorites',
+ 'BrowserForward',
+ 'BrowserHome',
+ 'BrowserRefresh',
+ 'BrowserSearch',
+ 'BrowserStop',
+ // Numeric keypad keys
+ 'Decimal',
+ 'Key11',
+ 'Key12',
+ 'Multiply',
+ 'Add',
+ 'Clear',
+ 'Divide',
+ 'Subtract',
+ 'Separator'
+]);
diff --git a/packages/keyboard/src/layout.ts b/packages/keyboard/src/layout.ts
index 9d5f56d9..1ef090c2 100644
--- a/packages/keyboard/src/layout.ts
+++ b/packages/keyboard/src/layout.ts
@@ -1,122 +1,96 @@
-import { KeyCode, KeyboardLayout } from '@interactors/globals';
+import { Key, KeyCode, KeyboardLayout } from "@interactors/globals";
-export function createKeyboardLayout(map: Iterable<[KeyCode, string]>): KeyboardLayout {
- let keyMap = new Map(map);
- let codeMap = new Map(Array.from(map).map(([key, value]) => [value, key]));
+export const defaultKeyMap = [
+ ..."0123456789".split("").map((digit) => ({ code: `Digit${digit}`, key: digit })),
+ ...")!@#$%^&*(".split("").map((key, digit) => ({ code: `Digit${digit}`, key, shiftKey: true })),
+ ..."abcdefghijklmnopqrstuvwxyz".split("").map((key) => ({ code: `Key${key.toUpperCase()}`, key })),
+ ..."ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((key) => ({ code: `Key${key}`, key, shiftKey: true })),
+ ...Array.from({ length: 12 })
+ .map((_, digit) => `F${digit + 1}`)
+ .map((key) => ({ code: key, key })),
+ ...[
+ "Backspace",
+ "Tab",
+ "Enter",
+ "Pause",
+ "CapsLock",
+ "Escape",
+ "PageUp",
+ "PageDown",
+ "End",
+ "Home",
+ "ArrowLeft",
+ "ArrowUp",
+ "ArrowRight",
+ "ArrowDown",
+ "PrintScreen",
+ "Insert",
+ "Delete",
+ "ContextMenu",
+ "NumLock",
+ "ScrollLock",
+ ].map((code) => ({ code, key: code })),
+
+ { code: "Space", key: " " },
+ { code: "AltLeft", key: "Alt" },
+ { code: "AltRight", key: "Alt" },
+ { code: "ShiftLeft", key: "Shift" },
+ { code: "ShiftRight", key: "Shift" },
+ { code: "ControlLeft", key: "Control" },
+ { code: "ControlRight", key: "Control" },
+ { code: "MetaLeft", key: "Meta" },
+ { code: "MetaRight", key: "Meta" },
+
+ { code: "Semicolon", key: ";" },
+ { code: "Equal", key: "=" },
+ { code: "Comma", key: "," },
+ { code: "Minus", key: "-" },
+ { code: "Period", key: "." },
+ { code: "Slash", key: "/" },
+ { code: "Backquote", key: "`" },
+ { code: "BracketLeft", key: "[" },
+ { code: "Backslash", key: "\\" },
+ { code: "BracketRight", key: "]" },
+ { code: "Quote", key: "'" },
+
+ { code: "Semicolon", key: ":", shiftKey: true },
+ { code: "Equal", key: "+", shiftKey: true },
+ { code: "Comma", key: "<", shiftKey: true },
+ { code: "Minus", key: "_", shiftKey: true },
+ { code: "Period", key: ">", shiftKey: true },
+ { code: "Slash", key: "?", shiftKey: true },
+ { code: "Backquote", key: "~", shiftKey: true },
+ { code: "BracketLeft", key: "{", shiftKey: true },
+ { code: "Backslash", key: "|", shiftKey: true },
+ { code: "BracketRight", key: "}", shiftKey: true },
+ { code: "Quote", key: '"', shiftKey: true },
+
+ ..."0123456789".split("").map((digit) => ({ code: `Numpad${digit}`, key: digit })),
+ { code: "NumpadMultiply", key: "*" },
+ { code: "NumpadAdd", key: "+" },
+ { code: "NumpadSubtract", key: "-" },
+ { code: "NumpadDecimal", key: "." },
+ { code: "NumpadDivide", key: "/" },
+] as Key[];
+
+export function createKeyboardLayout(override: (Key)[] = []): KeyboardLayout {
+ let codeMap = new Map();
+ let keyMap = new Map();
+
+ [...override, ...defaultKeyMap].forEach((key) => {
+ codeMap.get(key.code)?.push(key) ?? codeMap.set(key.code, [key]);
+ keyMap.get(key.key)?.push(key) ?? keyMap.set(key.key, [key]);
+ });
return {
- getKey(code: KeyCode): string | undefined {
- return keyMap.get(code);
+ getByCode(code: KeyCode): Key | undefined {
+ return codeMap.get(code)?.[0];
},
- getCode(key: string): KeyCode | undefined {
- return codeMap.get(key);
+ getByKey(key: string): Key | undefined {
+ return keyMap.get(key)?.[0];
},
- }
+ };
}
-export const US_INTERNATIONAL = createKeyboardLayout([
- ["Backspace", "Backspace"],
- ["Tab", "Tab"],
- ["Enter", "Enter"],
- ["ShiftLeft", "Shift"],
- ["ShiftRight", "Shift"],
- ["ControlLeft", "Control"],
- ["ControlRight", "Control"],
- ["AltLeft", "Alt"],
- ["AltRight", "Alt"],
- ["Pause", "Pause"],
- ["CapsLock", "CapsLock"],
- ["Escape", "Escape"],
- ["Space", " "],
- ["PageUp", "PageUp"],
- ["PageDown", "PageDown"],
- ["End", "End"],
- ["Home", "Home"],
- ["ArrowLeft", "ArrowLeft"],
- ["ArrowUp", "ArrowUp"],
- ["ArrowRight", "ArrowRight"],
- ["ArrowDown", "ArrowDown"],
- ["PrintScreen", "PrintScreen"],
- ["Insert", "Insert"],
- ["Delete", "Delete"],
- ["Digit0", "0"],
- ["Digit1", "1"],
- ["Digit2", "2"],
- ["Digit3", "3"],
- ["Digit4", "4"],
- ["Digit5", "5"],
- ["Digit6", "6"],
- ["Digit7", "7"],
- ["Digit8", "8"],
- ["Digit9", "9"],
- ["KeyA", "a"],
- ["KeyB", "b"],
- ["KeyC", "c"],
- ["KeyD", "d"],
- ["KeyE", "e"],
- ["KeyF", "f"],
- ["KeyG", "g"],
- ["KeyH", "h"],
- ["KeyI", "i"],
- ["KeyJ", "j"],
- ["KeyK", "k"],
- ["KeyL", "l"],
- ["KeyM", "m"],
- ["KeyN", "n"],
- ["KeyO", "o"],
- ["KeyP", "p"],
- ["KeyQ", "q"],
- ["KeyR", "r"],
- ["KeyS", "s"],
- ["KeyT", "t"],
- ["KeyU", "u"],
- ["KeyV", "v"],
- ["KeyW", "w"],
- ["KeyX", "x"],
- ["KeyY", "y"],
- ["KeyZ", "z"],
- ["MetaLeft", "Meta"],
- ["MetaRight", "Meta"],
- ["ContextMenu", "ContextMenu"],
- ["Numpad0", "0"],
- ["Numpad1", "1"],
- ["Numpad2", "2"],
- ["Numpad3", "3"],
- ["Numpad4", "4"],
- ["Numpad5", "5"],
- ["Numpad6", "6"],
- ["Numpad7", "7"],
- ["Numpad8", "8"],
- ["Numpad9", "9"],
- ["NumpadMultiply", "*"],
- ["NumpadAdd", "+"],
- ["NumpadSubtract", "-"],
- ["NumpadDecimal", "."],
- ["NumpadDivide", "/"],
- ["F1", "F1"],
- ["F2", "F2"],
- ["F3", "F3"],
- ["F4", "F4"],
- ["F5", "F5"],
- ["F6", "F6"],
- ["F7", "F7"],
- ["F8", "F8"],
- ["F9", "F9"],
- ["F10", "F10"],
- ["F11", "F11"],
- ["F12", "F12"],
- ["NumLock", "NumLock"],
- ["ScrollLock", "ScrollLock"],
- ["Semicolon", ";"],
- ["Equal", "="],
- ["Comma", ","],
- ["Minus", "-"],
- ["Period", "."],
- ["Slash", "/"],
- ["Backquote", "`"],
- ["BracketLeft", "["],
- ["Backslack", "\\"],
- ["BracketRight", "]"],
- ["Quote", "'"],
-]);
+export const US_INTERNATIONAL = createKeyboardLayout();
diff --git a/packages/keyboard/test/keyboard.test.ts b/packages/keyboard/test/keyboard.test.ts
index b0ed9bef..eba76b30 100644
--- a/packages/keyboard/test/keyboard.test.ts
+++ b/packages/keyboard/test/keyboard.test.ts
@@ -5,7 +5,7 @@ import { Keyboard, createKeyboardLayout } from '../src';
import { dom } from './helpers';
const TextField = createInteractor('text field')
- .selector('input')
+ .selector('input,textarea')
.filters({
id: (e) => e.id,
})
@@ -22,6 +22,26 @@ const Heading = createInteractor('heading')
describe('Keyboard', () => {
describe('press', () => {
+ let domFixture = `
+
+
+
+
+
+
+
+ `;
+
it('dispatches events with the given key to the body when no element active', async () => {
dom(`
@@ -39,34 +59,14 @@ describe('Keyboard', () => {
`);
- globals.document.getElementById('nameField');
await Keyboard.press({ key: 'w' });
await Heading({ id: 'keydown' }).has({ text: 'success w KeyW' });
await Heading({ id: 'keyup' }).has({ text: 'success w KeyW' });
});
it('dispatches events with the given key to the active element', async () => {
- dom(`
-
-
-
-
-
-
-
- `);
+ dom(domFixture);
- globals.document.getElementById('nameField');
await TextField({ id: 'nameField' }).focus();
await Keyboard.press({ key: 'w' });
await Heading({ id: 'keydown' }).has({ text: 'success hello w KeyW' });
@@ -75,32 +75,45 @@ describe('Keyboard', () => {
});
it('dispatches events with the given code to the active element', async () => {
- dom(`
-
-
-
-
-
-
-
- `);
+ dom(domFixture);
+
+
+ await TextField({ id: "nameField" }).focus();
+ await Keyboard.press({ code: "KeyW" });
+ await Heading({ id: "keydown" }).has({ text: "success hello w KeyW" });
+ await Heading({ id: "keyup" }).has({ text: "success hellow w KeyW" });
+ await Heading({ id: "input" }).has({ text: "success hellow insertText w" });
+ });
+
+ it("dispatches events with the `Enter` code to the active element", async () => {
+ dom(domFixture);
- globals.document.getElementById('nameField');
await TextField({ id: 'nameField' }).focus();
- await Keyboard.press({ code: 'KeyW' });
- await Heading({ id: 'keydown' }).has({ text: 'success hello w KeyW' });
- await Heading({ id: 'keyup' }).has({ text: 'success hellow w KeyW' });
- await Heading({ id: 'input' }).has({ text: 'success hellow insertText w' });
+ await Keyboard.press({ code: 'Home' });
+ await Keyboard.press({ code: 'Enter' });
+ await Heading({ id: 'keydown' }).has({ text: 'success hello Enter Enter' });
+ await Heading({ id: 'keyup' }).has({ text: 'success hello Enter Enter' });
+ await Heading({ id: "input" }).has({ text: "" });
+ });
+
+ it("dispatches events with the `Dead` key value to the active element", async () => {
+ dom(domFixture);
+
+ await TextField({ id: 'nameField' }).focus();
+ await Keyboard.press({ key: 'Dead' });
+ await Heading({ id: 'keydown' }).has({ text: 'success hello Dead ' });
+ await Heading({ id: 'keyup' }).has({ text: 'success hello Dead ' });
+ await Heading({ id: "input" }).has({ text: "" });
+ });
+
+ it("dispatches events with the uppercased `w` to the active element", async () => {
+ dom(domFixture);
+
+ await TextField({ id: 'nameField' }).focus();
+ await Keyboard.press({ key: 'W' });
+ await Heading({ id: "keydown" }).has({ text: "success hello W KeyW" });
+ await Heading({ id: "keyup" }).has({ text: "success helloW W KeyW" });
+ await Heading({ id: "input" }).has({ text: "success helloW insertText W" });
});
});
@@ -127,8 +140,8 @@ describe('Keyboard', () => {
describe('layout', () => {
it('can change keyboard layout', async () => {
setKeyboardLayout(createKeyboardLayout([
- ['KeyW', 'q'],
- ['KeyX', 'b'],
+ { code: 'KeyW', key: 'q' },
+ { code: 'KeyX', key: 'b' },
]));
dom(`