Skip to content
Merged
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/heavy-spiders-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/hotkeys': patch
---

Display macOS shortcut modifiers in Control, Option, Shift, Command order without changing normalized binding identities.
2 changes: 1 addition & 1 deletion docs/framework/angular/guides/formatting-display.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ formatForDisplay('Mod+[Digit2]', { platform: 'windows' }) // 'Ctrl+2'

The same label can represent different bindings. `Mod+S` follows a logical letter; `Mod+[KeyS]` follows a physical position. Physical labels shorten `KeyS` to `S` and `Digit2` to `2`, preserve readable numpad labels, and reuse punctuation and special-key symbols. This does not change the stored code or infer the user's layout.

Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`.
Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`. macOS display orders modifiers as Control, Option, Shift, Command: `Mod+Shift+S` displays as `⇧ ⌘ S`. The normalized binding remains `Mod+Shift+S`.

## Render individual keycaps

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/lit/guides/formatting-display.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ formatForDisplay('Mod+[Digit2]', { platform: 'windows' }) // 'Ctrl+2'

The same label can represent different bindings. `Mod+S` follows a logical letter; `Mod+[KeyS]` follows a physical position. Physical labels shorten `KeyS` to `S` and `Digit2` to `2`, preserve readable numpad labels, and reuse punctuation and special-key symbols. This does not change the stored code or infer the user's layout.

Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`.
Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`. macOS display orders modifiers as Control, Option, Shift, Command: `Mod+Shift+S` displays as `⇧ ⌘ S`. The normalized binding remains `Mod+Shift+S`.

## Render individual keycaps

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/preact/guides/formatting-display.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ formatForDisplay('Mod+[Digit2]', { platform: 'windows' }) // 'Ctrl+2'

The same label can represent different bindings. `Mod+S` follows a logical letter; `Mod+[KeyS]` follows a physical position. Physical labels shorten `KeyS` to `S` and `Digit2` to `2`, preserve readable numpad labels, and reuse punctuation and special-key symbols. This does not change the stored code or infer the user's layout.

Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`.
Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`. macOS display orders modifiers as Control, Option, Shift, Command: `Mod+Shift+S` displays as `⇧ ⌘ S`. The normalized binding remains `Mod+Shift+S`.

## Render individual keycaps

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/react/guides/formatting-display.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ formatForDisplay('Mod+[Digit2]', { platform: 'windows' }) // 'Ctrl+2'

The same label can represent different bindings. `Mod+S` follows a logical letter; `Mod+[KeyS]` follows a physical position. Physical labels shorten `KeyS` to `S` and `Digit2` to `2`, preserve readable numpad labels, and reuse punctuation and special-key symbols. This does not change the stored code or infer the user's layout.

Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`.
Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`. macOS display orders modifiers as Control, Option, Shift, Command: `Mod+Shift+S` displays as `⇧ ⌘ S`. The normalized binding remains `Mod+Shift+S`.

## Render individual keycaps

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/solid/guides/formatting-display.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ formatForDisplay('Mod+[Digit2]', { platform: 'windows' }) // 'Ctrl+2'

The same label can represent different bindings. `Mod+S` follows a logical letter; `Mod+[KeyS]` follows a physical position. Physical labels shorten `KeyS` to `S` and `Digit2` to `2`, preserve readable numpad labels, and reuse punctuation and special-key symbols. This does not change the stored code or infer the user's layout.

Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`.
Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`. macOS display orders modifiers as Control, Option, Shift, Command: `Mod+Shift+S` displays as `⇧ ⌘ S`. The normalized binding remains `Mod+Shift+S`.

## Render individual keycaps

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/svelte/guides/formatting-display.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ formatForDisplay('Mod+[Digit2]', { platform: 'windows' }) // 'Ctrl+2'

The same label can represent different bindings. `Mod+S` follows a logical letter; `Mod+[KeyS]` follows a physical position. Physical labels shorten `KeyS` to `S` and `Digit2` to `2`, preserve readable numpad labels, and reuse punctuation and special-key symbols. This does not change the stored code or infer the user's layout.

Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`.
Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`. macOS display orders modifiers as Control, Option, Shift, Command: `Mod+Shift+S` displays as `⇧ ⌘ S`. The normalized binding remains `Mod+Shift+S`.

## Render individual keycaps

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/vue/guides/formatting-display.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ formatForDisplay('Mod+[Digit2]', { platform: 'windows' }) // 'Ctrl+2'

The same label can represent different bindings. `Mod+S` follows a logical letter; `Mod+[KeyS]` follows a physical position. Physical labels shorten `KeyS` to `S` and `Digit2` to `2`, preserve readable numpad labels, and reuse punctuation and special-key symbols. This does not change the stored code or infer the user's layout.

Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`.
Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`. macOS display orders modifiers as Control, Option, Shift, Command: `Mod+Shift+S` displays as `⇧ ⌘ S`. The normalized binding remains `Mod+Shift+S`.

## Render individual keycaps

Expand Down
21 changes: 12 additions & 9 deletions docs/reference/functions/formatForDisplay.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ title: formatForDisplay
function formatForDisplay(hotkey, options): string[];
```

Defined in: [format.ts:100](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L100)
Defined in: [format.ts:101](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L101)

Formats a hotkey for display in a user interface.

On macOS, uses symbols (βŒ˜β‡§S) in the same modifier order as [normalizeHotkeyFromParsed](normalizeHotkeyFromParsed.md).
On macOS, uses symbols (β‡§βŒ˜S) in Control, Option, Shift, Command order.
Display ordering does not change the normalized hotkey used for registration.
On Windows/Linux, uses text (Ctrl+Shift+S) with `+` separators.
The separator can be customized with `separatorToken`.
Physical codes use conventional display labels: `[KeyS]` becomes `S` and
Expand Down Expand Up @@ -45,7 +46,7 @@ Display text, or individual key labels when parts is true

```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 @@ -60,11 +61,12 @@ formatForDisplay('Escape')
function formatForDisplay(hotkey, options?): string;
```

Defined in: [format.ts:104](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L104)
Defined in: [format.ts:105](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L105)

Formats a hotkey for display in a user interface.

On macOS, uses symbols (βŒ˜β‡§S) in the same modifier order as [normalizeHotkeyFromParsed](normalizeHotkeyFromParsed.md).
On macOS, uses symbols (β‡§βŒ˜S) in Control, Option, Shift, Command order.
Display ordering does not change the normalized hotkey used for registration.
On Windows/Linux, uses text (Ctrl+Shift+S) with `+` separators.
The separator can be customized with `separatorToken`.
Physical codes use conventional display labels: `[KeyS]` becomes `S` and
Expand Down Expand Up @@ -96,7 +98,7 @@ Display text, or individual key labels when parts is true

```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 @@ -111,11 +113,12 @@ formatForDisplay('Escape')
function formatForDisplay(hotkey, options): string | string[];
```

Defined in: [format.ts:108](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L108)
Defined in: [format.ts:109](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L109)

Formats a hotkey for display in a user interface.

On macOS, uses symbols (βŒ˜β‡§S) in the same modifier order as [normalizeHotkeyFromParsed](normalizeHotkeyFromParsed.md).
On macOS, uses symbols (β‡§βŒ˜S) in Control, Option, Shift, Command order.
Display ordering does not change the normalized hotkey used for registration.
On Windows/Linux, uses text (Ctrl+Shift+S) with `+` separators.
The separator can be customized with `separatorToken`.
Physical codes use conventional display labels: `[KeyS]` becomes `S` and
Expand Down Expand Up @@ -147,7 +150,7 @@ Display text, or individual key labels when parts is true

```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 Down
2 changes: 1 addition & 1 deletion docs/reference/functions/formatWithLabels.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ title: formatWithLabels
function formatWithLabels(hotkey, options?): string;
```

Defined in: [format.ts:173](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L173)
Defined in: [format.ts:185](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L185)

## Parameters

Expand Down
14 changes: 7 additions & 7 deletions docs/reference/interfaces/FormatDisplayOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id: FormatDisplayOptions
title: FormatDisplayOptions
---

Defined in: [format.ts:183](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L183)
Defined in: [format.ts:195](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L195)

Options for formatting hotkeys for display.

Expand All @@ -15,7 +15,7 @@ Options for formatting hotkeys for display.
optional keyLabels?: Readonly<Record<string, string>>;
```

Defined in: [format.ts:198](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L198)
Defined in: [format.ts:210](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L210)

Final display overrides for logical keys or physical codes; take precedence over layoutMap. Never changes matching.

Expand All @@ -27,7 +27,7 @@ Final display overrides for logical keys or physical codes; take precedence over
optional layoutMap?: object;
```

Defined in: [format.ts:196](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L196)
Defined in: [format.ts:208](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L208)

An already-resolved code-to-key map, such as KeyboardLayoutMap or Map.
Used only for physical bindings. Mapped keys receive normal display formatting;
Expand Down Expand Up @@ -58,7 +58,7 @@ get: (code) => string | undefined;
optional parts?: boolean;
```

Defined in: [format.ts:189](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L189)
Defined in: [format.ts:201](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L201)

Return individual key labels instead of a joined string. Default: false.

Expand All @@ -70,7 +70,7 @@ Return individual key labels instead of a joined string. Default: false.
optional platform?: "mac" | "windows" | "linux";
```

Defined in: [format.ts:185](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L185)
Defined in: [format.ts:197](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L197)

The target platform. Defaults to auto-detection.

Expand All @@ -82,7 +82,7 @@ The target platform. Defaults to auto-detection.
optional separatorToken?: string | null;
```

Defined in: [format.ts:200](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L200)
Defined in: [format.ts:212](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L212)

Override the separator between display tokens. Defaults to platform-specific formatting when null/undefined.

Expand All @@ -99,6 +99,6 @@ optional useSymbols?:
};
```

Defined in: [format.ts:187](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L187)
Defined in: [format.ts:199](https://github.com/TanStack/hotkeys/blob/main/packages/hotkeys/src/format.ts#L199)

Whether to use symbols for the display. Defaults to true.
18 changes: 15 additions & 3 deletions packages/hotkeys/src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export type DisplayHotkey = RegisterableHotkey | 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 Control, Option, Shift, Command order.
* Display ordering does not change the normalized hotkey used for registration.
* On Windows/Linux, uses text (Ctrl+Shift+S) with `+` separators.
* The separator can be customized with `separatorToken`.
* Physical codes use conventional display labels: `[KeyS]` becomes `S` and
Expand All @@ -88,7 +89,7 @@ export type DisplayHotkey = RegisterableHotkey | 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 Down Expand Up @@ -126,7 +127,18 @@ export function formatForDisplay(
typeof hotkey === 'object' && 'modifiers' in hotkey
? normalizeHotkeyFromParsed(hotkey, platform)
: normalizeRegisterableHotkey(hotkey as RegisterableHotkey, platform)
const parts = splitHotkeyParts(normalized).map((segment) => {
const segments = splitHotkeyParts(normalized)
if (platform === 'mac') {
// Canonical registration puts Mod first; display resolves it to Command.
const modifierIndex = (segment: string) => {
const index = MODIFIER_ORDER.indexOf(
(segment === 'Mod' ? 'Meta' : segment) as CanonicalModifier,
)
return index === -1 ? MODIFIER_ORDER.length : index
}
segments.sort((a, b) => modifierIndex(a) - modifierIndex(b))
}
const parts = segments.map((segment) => {
if (isModifierKey(segment)) {
const modifier = (MODIFIER_ALIASES[segment] ??
MODIFIER_ALIASES[segment.toLowerCase()]) as CanonicalModifier | 'Mod'
Expand Down
52 changes: 43 additions & 9 deletions packages/hotkeys/tests/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {
formatForDisplay,
formatHotkey,
formatHotkeySequence,
formatWithLabels,
} from '../src/format'
import { normalizeRegisterableHotkey } from '../src/parse'
import type { ParsedHotkey, RegisterableHotkey } from '../src/hotkey.types'

/** Strings that parse correctly but are not in the `Hotkey` template union. */
Expand Down Expand Up @@ -86,6 +88,38 @@ describe('formatHotkey', () => {

describe('formatForDisplay', () => {
describe('macOS format', () => {
it.each([
['Mod+Alt+Shift+S', 'βŒ₯ ⇧ ⌘ S'],
['Meta+Shift+Alt+Control+S', 'βŒƒ βŒ₯ ⇧ ⌘ S'],
['Mod+Shift++', '⇧ ⌘ +'],
['Mod+Shift', '⇧ ⌘'],
])('orders %s for display without changing its key', (hotkey, display) => {
expect(formatForDisplay(hotkey, { platform: 'mac' })).toBe(display)
})

it('orders raw physical bindings and parts without changing registration identity', () => {
const binding = { code: 'KeyS', mod: true, alt: true, shift: true }
expect(
formatForDisplay(binding, {
platform: 'mac',
parts: true,
useSymbols: false,
keyLabels: { KeyS: 'Save' },
}),
).toEqual(['Option', 'Shift', 'Cmd', 'Save'])
expect(normalizeRegisterableHotkey(binding, 'mac')).toBe(
'Mod+Alt+Shift+[KeyS]',
)
expect(formatWithLabels('Mod+Shift+S', { platform: 'mac' })).toBe(
'Shift+Cmd+S',
)
for (const platform of ['windows', 'linux'] as const) {
expect(formatForDisplay('Mod+Alt+Shift+S', { platform })).toBe(
'Ctrl+Alt+Shift+S',
)
}
})

it('should use symbols for modifiers with spaces between segments', () => {
expect(formatForDisplay('Control+A', { platform: 'mac' })).toBe('βŒƒ A')
expect(formatForDisplay('Shift+A', { platform: 'mac' })).toBe('⇧ A')
Expand All @@ -98,7 +132,7 @@ describe('formatForDisplay', () => {
'βŒƒ ⇧ A',
)
expect(formatForDisplay(hk('Command+Shift+S'), { platform: 'mac' })).toBe(
'⌘ ⇧ S',
'⇧ ⌘ S',
)
})

Expand All @@ -108,24 +142,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 +229,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 @@ -245,13 +279,13 @@ describe('formatForDisplay', () => {
).toBe('Ctrl+S')
})

it('should handle multiple modifiers in canonical order (Mod first)', () => {
it('should use platform-specific display order', () => {
expect(
formatForDisplay('Mod+Shift+S', {
platform: 'mac',
useSymbols: false,
}),
).toBe('Cmd+Shift+S')
).toBe('Shift+Cmd+S')
expect(
formatForDisplay('Mod+Shift+S', {
platform: 'windows',
Expand Down
Loading