From b932b83aaa3b14088299cfe3ebbb7e4fda216880 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Thu, 7 Dec 2023 19:05:54 +0000 Subject: [PATCH 1/7] Add shift-normalization on MacOS --- src/hotkey.ts | 15 ++++++++-- src/macos-uppercase-layer.ts | 55 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/macos-uppercase-layer.ts diff --git a/src/hotkey.ts b/src/hotkey.ts index 649c1bb..4eaf10e 100644 --- a/src/hotkey.ts +++ b/src/hotkey.ts @@ -1,5 +1,6 @@ import {NormalizedSequenceString} from './sequence' import {macosSymbolLayerKeys} from './macos-symbol-layer' +import {macosUppercaseLayerKeys} from './macos-uppercase-layer' const normalizedHotkeyBrand = Symbol('normalizedHotkey') @@ -47,9 +48,19 @@ export function eventToHotkeyString( } if (!modifierKeyNames.includes(key)) { - const nonOptionPlaneKey = + // MacOS outputs cymbols when `Alt` is held, so we map them back to the key symbol if we can + const altNormalizedKey = hotkeyString.includes('Alt') && matchApplePlatform.test(platform) ? macosSymbolLayerKeys[key] ?? key : key - const syntheticKey = syntheticKeyNames[nonOptionPlaneKey] ?? nonOptionPlaneKey + + // MacOS outputs lowercase characters when `Command+Shift` is held, so we map them back to uppercase if we can + const shiftNormalizedKey = + hotkeyString.includes('Shift') && matchApplePlatform.test(platform) + ? macosUppercaseLayerKeys[altNormalizedKey] ?? altNormalizedKey + : altNormalizedKey + + // Some symbols can't be used because of hotkey string format, so we replace them with 'synthetic' named keys + const syntheticKey = syntheticKeyNames[shiftNormalizedKey] ?? shiftNormalizedKey + hotkeyString.push(syntheticKey) } diff --git a/src/macos-uppercase-layer.ts b/src/macos-uppercase-layer.ts new file mode 100644 index 0000000..4b835af --- /dev/null +++ b/src/macos-uppercase-layer.ts @@ -0,0 +1,55 @@ +/** + * Map of 'uppercase' symbols to the keys that would be pressed (while holding `Shift`) to type them on MacOS on an + * English layout. Most of these are standardized across most language layouts, so this won't work 100% in every + * language but it should work most of the time. + * + */ +export const macosUppercaseLayerKeys: Record = { + ['`']: '~', + ['1']: '!', + ['2']: '@', + ['3']: '#', + ['4']: '$', + ['5']: '%', + ['6']: '^', + ['7']: '&', + ['8']: '*', + ['9']: '(', + ['0']: ')', + ['-']: '_', + ['=']: '+', + ['[']: '{', + [']']: '}', + ['\\']: '|', + [';']: ':', + ["'"]: '"', + [',']: '<', + ['.']: '>', + ['/']: '?', + ['q']: 'Q', + ['w']: 'W', + ['e']: 'E', + ['r']: 'R', + ['t']: 'T', + ['y']: 'Y', + ['u']: 'U', + ['i']: 'I', + ['o']: 'O', + ['p']: 'P', + ['a']: 'A', + ['s']: 'S', + ['d']: 'D', + ['f']: 'F', + ['g']: 'G', + ['h']: 'H', + ['j']: 'J', + ['k']: 'K', + ['l']: 'L', + ['z']: 'Z', + ['x']: 'X', + ['c']: 'C', + ['v']: 'V', + ['b']: 'B', + ['n']: 'N', + ['m']: 'M' +} From 5a360d19c5561b7bebbc6eaf3d33a48f85cc6f20 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Thu, 7 Dec 2023 19:07:27 +0000 Subject: [PATCH 2/7] Update tests --- test/test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/test.js b/test/test.js index 17b33cd..83aab23 100644 --- a/test/test.js +++ b/test/test.js @@ -286,7 +286,11 @@ describe('hotkey', function () { ['Alt+Shift+ArrowLeft', {altKey: true, shiftKey: true, key: 'ArrowLeft'}], ['Alt+Shift+ArrowLeft', {altKey: true, shiftKey: true, key: 'ArrowLeft'}, 'mac'], ['Control+Space', {ctrlKey: true, key: ' '}], - ['Shift+Plus', {shiftKey: true, key: '+'}] + ['Shift+Plus', {shiftKey: true, key: '+'}], + ['Command+Shift+X', {metaKey: true, shiftKey: true, key: 'x'}, 'mac'], + ['Control+Shift+X', {ctrlKey: true, shiftKey: true, key: 'X'}], + ['Command+Shift+!', {metaKey: true, shiftKey: true, key: '1'}, 'mac'], + ['Control+Shift+!', {ctrlKey: true, shiftKey: true, key: '!'}] ] for (const [expected, keyEvent, platform = 'win / linux'] of tests) { it(`${JSON.stringify(keyEvent)} => ${expected}`, function (done) { From 1ddb484b5a1f84757f90736a5903807fa1ef3b1c Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Thu, 7 Dec 2023 19:07:58 +0000 Subject: [PATCH 3/7] Remove note in docs --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 57485a0..aa67945 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,6 @@ for (const el of document.querySelectorAll('[data-shortcut]')) { 6. `"Mod"` is a special modifier that localizes to `Meta` on MacOS/iOS, and `Control` on Windows/Linux. 1. `"Mod+"` can appear in any order in a hotkey string. For example: `"Mod+Alt+Shift+KEY"` 2. Neither the `Control` or `Meta` modifiers should appear in a hotkey string with `Mod`. - 3. Due to the inconsistent lowercasing of `event.key` on Mac and iOS when `Meta` is pressed along with `Shift`, it is recommended to avoid hotkey strings containing both `Mod` and `Shift`. 7. `"Plus"` and `"Space"` are special key names to represent the `+` and ` ` keys respectively, because these symbols cannot be represented in the normal hotkey string syntax. 8. You can use the comma key `,` as a hotkey, e.g. `a,,` would activate if the user typed `a` or `,`. `Control+,,x` would activate for `Control+,` or `x`. 9. `"Shift"` should be included if it would be held, and the key name should match the 'uppercase' key name: `Shift+?`, `Shift+A`, `Shift+$` From feaf0329be16f006614d2df5ec0e9db3dfa54e14 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Thu, 7 Dec 2023 19:10:14 +0000 Subject: [PATCH 4/7] Fix tests --- test/test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test.js b/test/test.js index 83aab23..5119ded 100644 --- a/test/test.js +++ b/test/test.js @@ -287,9 +287,9 @@ describe('hotkey', function () { ['Alt+Shift+ArrowLeft', {altKey: true, shiftKey: true, key: 'ArrowLeft'}, 'mac'], ['Control+Space', {ctrlKey: true, key: ' '}], ['Shift+Plus', {shiftKey: true, key: '+'}], - ['Command+Shift+X', {metaKey: true, shiftKey: true, key: 'x'}, 'mac'], + ['Meta+Shift+X', {metaKey: true, shiftKey: true, key: 'x'}, 'mac'], ['Control+Shift+X', {ctrlKey: true, shiftKey: true, key: 'X'}], - ['Command+Shift+!', {metaKey: true, shiftKey: true, key: '1'}, 'mac'], + ['Meta+Shift+!', {metaKey: true, shiftKey: true, key: '1'}, 'mac'], ['Control+Shift+!', {ctrlKey: true, shiftKey: true, key: '!'}] ] for (const [expected, keyEvent, platform = 'win / linux'] of tests) { From 6873531c861700589caa49fc2410746f17613154 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Thu, 7 Dec 2023 17:04:42 -0500 Subject: [PATCH 5/7] Fix typo Co-authored-by: Ned Schwartz --- src/hotkey.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hotkey.ts b/src/hotkey.ts index 4eaf10e..8a0902e 100644 --- a/src/hotkey.ts +++ b/src/hotkey.ts @@ -48,7 +48,7 @@ export function eventToHotkeyString( } if (!modifierKeyNames.includes(key)) { - // MacOS outputs cymbols when `Alt` is held, so we map them back to the key symbol if we can + // MacOS outputs symbols when `Alt` is held, so we map them back to the key symbol if we can const altNormalizedKey = hotkeyString.includes('Alt') && matchApplePlatform.test(platform) ? macosSymbolLayerKeys[key] ?? key : key From f2c43dcec2c8db448c506f4328933cf62c06d4b2 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Thu, 7 Dec 2023 22:13:21 +0000 Subject: [PATCH 6/7] Update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 934a0ad..259cfa4 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,8 @@ for (const el of document.querySelectorAll('[data-shortcut]')) { 2. Neither the `Control` or `Meta` modifiers should appear in a hotkey string with `Mod`. 7. `"Plus"` and `"Space"` are special key names to represent the `+` and ` ` keys respectively, because these symbols cannot be represented in the normal hotkey string syntax. 8. You can use the comma key `,` as a hotkey, e.g. `a,,` would activate if the user typed `a` or `,`. `Control+,,x` would activate for `Control+,` or `x`. -9. `"Shift"` should be included if it would be held and the key is uppercase: ie, `Shift+A` not `A`. Note however that MacOS outputs lowercase keys when `Meta+Shift` is held (ie, `Meta+Shift+a`); see 6.3 above. +9. `"Shift"` should be included if it would be held and the key is uppercase: ie, `Shift+A` not `A` + 1. MacOS outputs lowercase key names when `Meta+Shift` is held (ie, `Meta+Shift+a`). In an attempt to normalize this, `hotkey` will automatically map these key names to uppercase, so the uppercase keys should still be used (ie, `"Meta+Shift+A"` or `"Mod+Shift+A"`). **However**, this normalization only works on US keyboard layouts. ### Example @@ -118,7 +119,6 @@ The following hotkey would match if the user typed the key sequence `a` and then ```js 'a b,Control+Alt+/' - ``` 🔬 **Hotkey Mapper** is a tool to help you determine the correct hotkey string for your key combination: From fcedce08d3581155e8fbd38bcf7dadf5fbf93aa0 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Thu, 7 Dec 2023 22:16:21 +0000 Subject: [PATCH 7/7] Fix types --- src/hotkey.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hotkey.ts b/src/hotkey.ts index 6a02fbb..551c718 100644 --- a/src/hotkey.ts +++ b/src/hotkey.ts @@ -1,6 +1,6 @@ import {NormalizedSequenceString} from './sequence.js' import {macosSymbolLayerKeys} from './macos-symbol-layer.js' -import {macosUppercaseLayerKeys} from './macos-uppercase-layer' +import {macosUppercaseLayerKeys} from './macos-uppercase-layer.js' const normalizedHotkeyBrand = Symbol('normalizedHotkey') @@ -95,12 +95,12 @@ function localizeMod(hotkey: string, platform: string = navigator.platform): str function sortModifiers(hotkey: string): string { const key = hotkey.split('+').pop() - const modifiers = [] + const modifiers: string[] = [] for (const modifier of ['Control', 'Alt', 'Meta', 'Shift']) { if (hotkey.includes(modifier)) { modifiers.push(modifier) } } - modifiers.push(key) + if (key) modifiers.push(key) return modifiers.join('+') }