diff --git a/README.md b/README.md index 9b0e68e..259cfa4 100644 --- a/README.md +++ b/README.md @@ -108,10 +108,10 @@ 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 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 @@ -119,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: diff --git a/src/hotkey.ts b/src/hotkey.ts index 4840fcf..551c718 100644 --- a/src/hotkey.ts +++ b/src/hotkey.ts @@ -1,5 +1,6 @@ import {NormalizedSequenceString} from './sequence.js' import {macosSymbolLayerKeys} from './macos-symbol-layer.js' +import {macosUppercaseLayerKeys} from './macos-uppercase-layer.js' const normalizedHotkeyBrand = Symbol('normalizedHotkey') @@ -47,9 +48,19 @@ export function eventToHotkeyString( } if (!modifierKeyNames.includes(key)) { - const nonOptionPlaneKey = + // 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 - 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) } @@ -84,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('+') } 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' +} diff --git a/test/test.js b/test/test.js index 17b33cd..5119ded 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: '+'}], + ['Meta+Shift+X', {metaKey: true, shiftKey: true, key: 'x'}, 'mac'], + ['Control+Shift+X', {ctrlKey: true, shiftKey: true, key: 'X'}], + ['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) { it(`${JSON.stringify(keyEvent)} => ${expected}`, function (done) {