Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tangy-mangos-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/hotkeys': patch
---

`formatForDisplay` and `formatWithLabels` now order macOS modifier keys according to Apple's Human Interface Guidelines: Control β†’ Option β†’ Shift β†’ Command. For example, `Mod+Shift+S` on macOS now renders as `⇧ ⌘ S` instead of `⌘ ⇧ S`. Windows and Linux output is unchanged.
38 changes: 35 additions & 3 deletions packages/hotkeys/src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
PUNCTUATION_KEY_DISPLAY_LABELS,
WINDOWS_MODIFIER_LABELS,
detectPlatform,
resolveModifier,
} from './constants'
import {
isModifierKey,
Expand All @@ -22,6 +23,7 @@ import type {
RegisterableHotkey,
} from './hotkey'


/**
* Converts a hotkey sequence array to a display string.
*
Expand Down Expand Up @@ -69,7 +71,7 @@ export function formatHotkey(parsed: ParsedHotkey): string {
/**
* Formats a hotkey for display in a user interface.
*
* On macOS, uses symbols (βŒ˜β‡§S) in the same modifier order as {@link normalizeHotkeyFromParsed}.
* On macOS, uses symbols (β‡§βŒ˜S) in the same modifier order as {@link normalizeHotkeyFromParsed}.
* On Windows/Linux, uses text (Ctrl+Shift+S) with `+` separators.
* The separator can be customized with `separatorToken`.
*
Expand All @@ -80,7 +82,7 @@ export function formatHotkey(parsed: ParsedHotkey): string {
* @example
* ```ts
* formatForDisplay('Mod+Shift+S', { platform: 'mac' })
* // Returns: '⌘ ⇧ S' (symbols separated by spaces on macOS)
* // Returns: '⇧ ⌘ S' (symbols separated by spaces on macOS)
*
* formatForDisplay('Mod+Shift+S', { platform: 'windows' })
* // Returns: 'Ctrl+Shift+S'
Expand All @@ -101,7 +103,11 @@ export function formatForDisplay(
hotkey as RegisterableHotkey,
platform,
)
return normalizedHotkey

const formattedHotkey =
platform === 'mac' ? toAppleHIGModifierOrder(normalizedHotkey) : normalizedHotkey

return formattedHotkey
.split('+')
.map((segment) => {
if (isModifierKey(segment)) {
Expand Down Expand Up @@ -131,6 +137,32 @@ export function formatForDisplay(
.join(separatorToken)
}


/**
* @paramaters hotkey - The hotkey sequence from the user
* @returns The hotkey sequence but removes the mod key alias and reorders to the Apple HIG modifier order
**/

function toAppleHIGModifierOrder(hotkey: string) {
let modifiers: Array<string> = []
let keys: Array<string> = []
hotkey.split('+').forEach((segment) => {
if (isModifierKey(segment)) {
const modifier = resolveModifier(segment as CanonicalModifier, 'mac');
modifiers = [...modifiers, modifier]
} else {
keys = [...keys, segment]
}
})

modifiers.sort(
(a, b) =>
MODIFIER_ORDER.indexOf(a as CanonicalModifier) -
MODIFIER_ORDER.indexOf(b as CanonicalModifier),
)
return modifiers.concat(keys).join('+')
}

/**
* @deprecated Use {@link formatForDisplay} instead with `useSymbols: false` option.
*/
Expand Down
33 changes: 21 additions & 12 deletions packages/hotkeys/tests/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('formatForDisplay', () => {
'βŒƒ ⇧ A',
)
expect(formatForDisplay(hk('Command+Shift+S'), { platform: 'mac' })).toBe(
'⌘ ⇧ S',
'⇧ ⌘ S',
)
})

Expand All @@ -108,24 +108,24 @@ describe('formatForDisplay', () => {
platform: 'mac',
separatorToken: '',
}),
).toBe('βŒ˜β‡§S')
).toBe('β‡§βŒ˜S')
expect(
formatForDisplay('Mod+Shift+S', {
platform: 'mac',
separatorToken: ' + ',
}),
).toBe('⌘ + ⇧ + S')
).toBe('⇧ + ⌘ + S')
expect(
formatForDisplay('Mod+Shift+S', {
platform: 'mac',
separatorToken: null,
}),
).toBe('⌘ ⇧ S')
).toBe('⇧ ⌘ S')
})

it('should resolve Mod to Command symbol', () => {
expect(formatForDisplay('Mod+S', { platform: 'mac' })).toBe('⌘ S')
expect(formatForDisplay('Mod+Shift+S', { platform: 'mac' })).toBe('⌘ ⇧ S')
expect(formatForDisplay('Mod+Shift+S', { platform: 'mac' })).toBe('⇧ ⌘ S')
})

it('should use symbols for special keys', () => {
Expand Down Expand Up @@ -195,10 +195,10 @@ describe('formatForDisplay', () => {
meta: true,
modifiers: ['Shift', 'Meta'],
}
expect(formatForDisplay(parsed, { platform: 'mac' })).toBe('⌘ ⇧ S')
expect(formatForDisplay(parsed, { platform: 'mac' })).toBe('⇧ ⌘ S')
expect(
formatForDisplay(parsed, { platform: 'mac', useSymbols: false }),
).toBe('Cmd+Shift+S')
).toBe('Shift+Cmd+S')
})
})

Expand Down Expand Up @@ -248,16 +248,25 @@ describe('formatForDisplay', () => {
it('should handle multiple modifiers in canonical order (Mod first)', () => {
expect(
formatForDisplay('Mod+Shift+S', {
platform: 'mac',
platform: 'windows',
useSymbols: false,
}),
).toBe('Cmd+Shift+S')
).toBe('Ctrl+Shift+S')
})
it('when platform is mac, handling multiple modifiers should follow apple human interface guidelines', () => {
// Control β†’ Option β†’ Shift β†’ Command
expect(
formatForDisplay('Mod+Shift+S', {
platform: 'windows',
formatForDisplay('Meta+Shift+Alt+Control+A', {
platform: 'mac',
useSymbols: false,
}),
).toBe('Ctrl+Shift+S')
).toBe('Control+Option+Shift+Cmd+A')
expect(
formatForDisplay('Meta+Shift+Alt+Control+A', {
platform: 'mac',
useSymbols: true,
}),
).toBe('βŒƒ βŒ₯ ⇧ ⌘ A')
})
})
})