Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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/add-ember-hotkeys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/ember-hotkeys": minor
---

Add `@tanstack/ember-hotkeys` — Ember adapter for TanStack Hotkeys with `{{on-hotkey}}` and `{{on-hotkey-sequence}}` helpers, test support utilities, and Glint template registry.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objec
> - [**Vue Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/vue/reference)
> - [**Lit Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/lit/reference)
> - [**Svelte Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/svelte/reference)
> - [**Ember Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/ember/reference)

## Get Involved

Expand Down
47 changes: 47 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@
"to": "framework/svelte/quick-start"
}
]
},
{
"label": "ember",
"children": [
{
"label": "Quick Start",
"to": "framework/ember/quick-start"
}
]
}
]
},
Expand Down Expand Up @@ -294,6 +303,35 @@
"to": "framework/svelte/guides/formatting-display"
}
]
},
{
"label": "ember",
"children": [
{
"label": "Hotkeys",
"to": "framework/ember/guides/hotkeys"
},
{
"label": "Sequences",
"to": "framework/ember/guides/sequences"
},
{
"label": "Hotkey Recording",
"to": "framework/ember/guides/hotkey-recording"
},
{
"label": "Hotkey Sequence Recording",
"to": "framework/ember/guides/sequence-recording"
},
{
"label": "Key State Tracking",
"to": "framework/ember/guides/key-state-tracking"
},
{
"label": "Formatting & Display",
"to": "framework/ember/guides/formatting-display"
}
]
}
]
},
Expand Down Expand Up @@ -368,6 +406,15 @@
"to": "framework/svelte/reference/index"
}
]
},
{
"label": "ember",
"children": [
{
"label": "Ember Helpers",
"to": "framework/ember/reference/index"
}
]
}
]
},
Expand Down
66 changes: 66 additions & 0 deletions docs/framework/ember/guides/formatting-display.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Formatting & Display Guide
id: formatting-display
---

TanStack Hotkeys includes utilities for turning hotkey strings into display-friendly labels. These utilities are framework-agnostic and are re-exported from `@tanstack/ember-hotkeys`.

## `formatForDisplay`

```ts
import { formatForDisplay } from '@tanstack/ember-hotkeys';

formatForDisplay('Mod+S');
// Mac: "⌘S" | Windows: "Ctrl+S"

formatForDisplay('Mod+Shift+Z');
// Mac: "⌘⇧Z" | Windows: "Ctrl+Shift+Z"
```

## `formatWithLabels`

```ts
import { formatWithLabels } from '@tanstack/ember-hotkeys';

formatWithLabels('Mod+S');
formatWithLabels('Mod+Shift+Z');
```

## Using in Templates

### Keyboard Shortcut Badges

```gts
import { formatForDisplay } from '@tanstack/ember-hotkeys';
import onHotkey from '@tanstack/ember-hotkeys/helpers/on-hotkey';

<template>
{{onHotkey "Mod+S" @onSave}}
<button type="button">
Save <kbd>{{formatForDisplay "Mod+S"}}</kbd>
</button>
</template>
```

### Menu Items with Hotkeys

```gts
import { formatForDisplay } from '@tanstack/ember-hotkeys';
import onHotkey from '@tanstack/ember-hotkeys/helpers/on-hotkey';

<template>
{{onHotkey @hotkey @onAction}}
<div class="menu-item">
<span>{{@label}}</span>
<span class="menu-shortcut">{{formatForDisplay @hotkey}}</span>
</div>
</template>
```

## Validation

```ts
import { validateHotkey } from '@tanstack/ember-hotkeys';

const result = validateHotkey('Alt+A');
```
66 changes: 66 additions & 0 deletions docs/framework/ember/guides/hotkey-recording.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Hotkey Recording Guide
id: hotkey-recording
---

TanStack Hotkeys provides the `HotkeyRecorder` class for building shortcut customization UIs. The class is re-exported from `@tanstack/ember-hotkeys` and can be used directly in Ember components.

## Basic Usage

```gts
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { HotkeyRecorder, formatForDisplay } from '@tanstack/ember-hotkeys';
import type { Hotkey } from '@tanstack/ember-hotkeys';

export default class ShortcutRecorder extends Component {
@tracked recordedHotkey: Hotkey | null = null;
@tracked isRecording = false;

recorder = new HotkeyRecorder({
onRecord: (hotkey) => {
this.recordedHotkey = hotkey;
this.isRecording = false;
},
onCancel: () => {
this.isRecording = false;
},
});

@action startRecording() {
this.recorder.start();
this.isRecording = true;
}

@action cancelRecording() {
this.recorder.cancel();
}

<template>
<button type="button" {{on "click" this.startRecording}}>
{{#if this.isRecording}}
Press a key combination...
{{else if this.recordedHotkey}}
{{formatForDisplay this.recordedHotkey}}
{{else}}
Click to record
{{/if}}
</button>
{{#if this.isRecording}}
<button type="button" {{on "click" this.cancelRecording}}>Cancel</button>
{{/if}}
</template>
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

## `ignoreInputs`

The `HotkeyRecorder` supports an `ignoreInputs` option (defaults to `true`). When `true`, the recorder will not intercept normal typing in text inputs, textareas, selects, or contentEditable elements — keystrokes pass through to the input as usual. Pressing **Escape** still cancels recording even when focused on an input.

```ts
const recorder = new HotkeyRecorder({
ignoreInputs: false, // record even from inside inputs
onRecord: (hotkey) => console.log(hotkey),
});
```
154 changes: 154 additions & 0 deletions docs/framework/ember/guides/hotkeys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
title: Hotkeys Guide
id: hotkeys
---

The `{{onHotkey}}` helper is the primary way to register keyboard shortcuts in Ember applications. It wraps the singleton `HotkeyManager` with Ember's helper lifecycle: when the helper enters the template the key is registered, and when the template is torn down the listener is removed.

## Basic Usage

```gts
import onHotkey from '@tanstack/ember-hotkeys/helpers/on-hotkey';

const save = () => {
saveDocument();
};

<template>
{{onHotkey "Mod+S" save}}
</template>
```

The callback receives the original `KeyboardEvent` as the first argument and a `HotkeyCallbackContext` as the second:

```gts
import onHotkey from '@tanstack/ember-hotkeys/helpers/on-hotkey';

const save = (event: KeyboardEvent, context: HotkeyCallbackContext) => {
console.log(context.hotkey);
console.log(context.parsedHotkey);
};

<template>
{{onHotkey "Mod+S" save}}
</template>
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Options

All options are passed as named arguments in the template:

```gts
{{onHotkey "Mod+S" @onSave
preventDefault=true
stopPropagation=true
eventType="keydown"
enabled=true
}}
```

### `enabled`

When `enabled` is false, the hotkey **stays registered** (visible in devtools); only the callback is suppressed.

```gts
import onHotkey from '@tanstack/ember-hotkeys/helpers/on-hotkey';

<template>
{{onHotkey "Escape" @onClose enabled=@isOpen}}
</template>
```

### `target`

Scope a hotkey to a specific DOM element instead of the entire document:

```gts
import onHotkey from '@tanstack/ember-hotkeys/helpers/on-hotkey';

<template>
<div tabindex="0" {{onHotkey "Escape" @onClosePanel target=this}}>
Panel content
</div>
</template>
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### `preventDefault`

Prevent the browser default action (e.g., browser save dialog for `Mod+S`). This is `true` by default.

```gts
{{onHotkey "Mod+S" @onSave preventDefault=true}}
```

### `stopPropagation`

Stop the event from propagating to parent elements:

```gts
{{onHotkey "Escape" @onClose stopPropagation=true}}
```

### `eventType`

Listen for `keyup` events instead of the default `keydown`:

```gts
{{onHotkey "Escape" @onClose eventType="keyup"}}
```

### `requireReset`

When `true`, the hotkey fires at most once per key-hold. The user must release the key and press again to fire the callback a second time.

```gts
{{onHotkey "Escape" @onClose requireReset=true}}
```

### `ignoreInputs`

By default, hotkeys are suppressed when an input element is focused. Set `ignoreInputs` to `false` to fire the callback even when an input, textarea, or contenteditable element has focus:

```gts
{{onHotkey "Enter" @onSubmit ignoreInputs=false}}
```

### `conflictBehavior`

Control what happens when the same hotkey is registered twice:

```gts
{{onHotkey "Mod+S" @onSave conflictBehavior="replace"}}
```

### `platform`

Force a specific platform for `Mod` resolution:

```gts
{{onHotkey "Mod+S" @onSave platform="mac"}}
```

## Metadata (name & description)

Every hotkey registration can carry a `meta` object with a `name` and `description`. This metadata is informational only -- it does not affect hotkey behavior -- but it flows through to registrations and devtools, making it easy to build shortcut palettes and help screens.

```gts
{{onHotkey "Mod+S" @onSave meta=(hash name="Save" description="Save the document")}}
```

## Automatic Cleanup

Registrations are cleaned up automatically when the helper is destroyed -- for example, when the component is removed from the DOM or when an `{{#if}}` block becomes falsy.

## The Hotkey Manager

You can access the underlying manager directly when needed:

```ts
import { getHotkeyManager } from '@tanstack/ember-hotkeys';

const manager = getHotkeyManager();
manager.isRegistered('Mod+S');
manager.getRegistrationCount();
```
Loading