Skip to content

Commit bd832b7

Browse files
authored
Merge pull request #116 from github/normalize-uppercase-macos
Normalize key to uppercase on MacOS when `Shift` is held
2 parents df28670 + fcedce0 commit bd832b7

File tree

4 files changed

+77
-8
lines changed

4 files changed

+77
-8
lines changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,17 @@ for (const el of document.querySelectorAll('[data-shortcut]')) {
108108
6. `"Mod"` is a special modifier that localizes to `Meta` on MacOS/iOS, and `Control` on Windows/Linux.
109109
1. `"Mod+"` can appear in any order in a hotkey string. For example: `"Mod+Alt+Shift+KEY"`
110110
2. Neither the `Control` or `Meta` modifiers should appear in a hotkey string with `Mod`.
111-
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`.
112111
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.
113112
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`.
114-
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.
113+
9. `"Shift"` should be included if it would be held and the key is uppercase: ie, `Shift+A` not `A`
114+
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.
115115

116116
### Example
117117

118118
The following hotkey would match if the user typed the key sequence `a` and then `b`, OR if the user held down the `Control`, `Alt` and `/` keys at the same time.
119119

120120
```js
121121
'a b,Control+Alt+/'
122-
123122
```
124123

125124
🔬 **Hotkey Mapper** is a tool to help you determine the correct hotkey string for your key combination: <https://github.github.io/hotkey/pages/hotkey_mapper.html>

src/hotkey.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {NormalizedSequenceString} from './sequence.js'
22
import {macosSymbolLayerKeys} from './macos-symbol-layer.js'
3+
import {macosUppercaseLayerKeys} from './macos-uppercase-layer.js'
34

45
const normalizedHotkeyBrand = Symbol('normalizedHotkey')
56

@@ -47,9 +48,19 @@ export function eventToHotkeyString(
4748
}
4849

4950
if (!modifierKeyNames.includes(key)) {
50-
const nonOptionPlaneKey =
51+
// MacOS outputs symbols when `Alt` is held, so we map them back to the key symbol if we can
52+
const altNormalizedKey =
5153
hotkeyString.includes('Alt') && matchApplePlatform.test(platform) ? macosSymbolLayerKeys[key] ?? key : key
52-
const syntheticKey = syntheticKeyNames[nonOptionPlaneKey] ?? nonOptionPlaneKey
54+
55+
// MacOS outputs lowercase characters when `Command+Shift` is held, so we map them back to uppercase if we can
56+
const shiftNormalizedKey =
57+
hotkeyString.includes('Shift') && matchApplePlatform.test(platform)
58+
? macosUppercaseLayerKeys[altNormalizedKey] ?? altNormalizedKey
59+
: altNormalizedKey
60+
61+
// Some symbols can't be used because of hotkey string format, so we replace them with 'synthetic' named keys
62+
const syntheticKey = syntheticKeyNames[shiftNormalizedKey] ?? shiftNormalizedKey
63+
5364
hotkeyString.push(syntheticKey)
5465
}
5566

@@ -84,12 +95,12 @@ function localizeMod(hotkey: string, platform: string = navigator.platform): str
8495

8596
function sortModifiers(hotkey: string): string {
8697
const key = hotkey.split('+').pop()
87-
const modifiers = []
98+
const modifiers: string[] = []
8899
for (const modifier of ['Control', 'Alt', 'Meta', 'Shift']) {
89100
if (hotkey.includes(modifier)) {
90101
modifiers.push(modifier)
91102
}
92103
}
93-
modifiers.push(key)
104+
if (key) modifiers.push(key)
94105
return modifiers.join('+')
95106
}

src/macos-uppercase-layer.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Map of 'uppercase' symbols to the keys that would be pressed (while holding `Shift`) to type them on MacOS on an
3+
* English layout. Most of these are standardized across most language layouts, so this won't work 100% in every
4+
* language but it should work most of the time.
5+
*
6+
*/
7+
export const macosUppercaseLayerKeys: Record<string, string> = {
8+
['`']: '~',
9+
['1']: '!',
10+
['2']: '@',
11+
['3']: '#',
12+
['4']: '$',
13+
['5']: '%',
14+
['6']: '^',
15+
['7']: '&',
16+
['8']: '*',
17+
['9']: '(',
18+
['0']: ')',
19+
['-']: '_',
20+
['=']: '+',
21+
['[']: '{',
22+
[']']: '}',
23+
['\\']: '|',
24+
[';']: ':',
25+
["'"]: '"',
26+
[',']: '<',
27+
['.']: '>',
28+
['/']: '?',
29+
['q']: 'Q',
30+
['w']: 'W',
31+
['e']: 'E',
32+
['r']: 'R',
33+
['t']: 'T',
34+
['y']: 'Y',
35+
['u']: 'U',
36+
['i']: 'I',
37+
['o']: 'O',
38+
['p']: 'P',
39+
['a']: 'A',
40+
['s']: 'S',
41+
['d']: 'D',
42+
['f']: 'F',
43+
['g']: 'G',
44+
['h']: 'H',
45+
['j']: 'J',
46+
['k']: 'K',
47+
['l']: 'L',
48+
['z']: 'Z',
49+
['x']: 'X',
50+
['c']: 'C',
51+
['v']: 'V',
52+
['b']: 'B',
53+
['n']: 'N',
54+
['m']: 'M'
55+
}

test/test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,11 @@ describe('hotkey', function () {
286286
['Alt+Shift+ArrowLeft', {altKey: true, shiftKey: true, key: 'ArrowLeft'}],
287287
['Alt+Shift+ArrowLeft', {altKey: true, shiftKey: true, key: 'ArrowLeft'}, 'mac'],
288288
['Control+Space', {ctrlKey: true, key: ' '}],
289-
['Shift+Plus', {shiftKey: true, key: '+'}]
289+
['Shift+Plus', {shiftKey: true, key: '+'}],
290+
['Meta+Shift+X', {metaKey: true, shiftKey: true, key: 'x'}, 'mac'],
291+
['Control+Shift+X', {ctrlKey: true, shiftKey: true, key: 'X'}],
292+
['Meta+Shift+!', {metaKey: true, shiftKey: true, key: '1'}, 'mac'],
293+
['Control+Shift+!', {ctrlKey: true, shiftKey: true, key: '!'}]
290294
]
291295
for (const [expected, keyEvent, platform = 'win / linux'] of tests) {
292296
it(`${JSON.stringify(keyEvent)} => ${expected}`, function (done) {

0 commit comments

Comments
 (0)