Skip to content

Commit

Permalink
Merge pull request #116 from github/normalize-uppercase-macos
Browse files Browse the repository at this point in the history
Normalize key to uppercase on MacOS when `Shift` is held
  • Loading branch information
iansan5653 committed Dec 12, 2023
2 parents df28670 + fcedce0 commit bd832b7
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 8 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,18 +108,17 @@ 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

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.

```js
'a b,Control+Alt+/'

```

🔬 **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>
Expand Down
19 changes: 15 additions & 4 deletions src/hotkey.ts
Original file line number Diff line number Diff line change
@@ -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')

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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('+')
}
55 changes: 55 additions & 0 deletions src/macos-uppercase-layer.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
['`']: '~',
['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'
}
6 changes: 5 additions & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit bd832b7

Please sign in to comment.