Skip to content
Open
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/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');
```
72 changes: 72 additions & 0 deletions docs/framework/ember/guides/hotkey-recording.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
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 { registerDestructor } from '@ember/destroyable';
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;
},
});

constructor(owner: unknown, args: Record<string, unknown>) {
super(owner, args);
registerDestructor(this, () => this.recorder.cancel());
}

@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),
});
```
166 changes: 166 additions & 0 deletions docs/framework/ember/guides/hotkeys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
---
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';
import type { HotkeyCallbackContext } from '@tanstack/ember-hotkeys';

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 Component from '@glimmer/component';
import { action } from '@ember/object';
import onHotkey from '@tanstack/ember-hotkeys/helpers/on-hotkey';

export default class Panel extends Component {
panelEl: HTMLElement | null = null;

@action setRef(el: HTMLElement) {
this.panelEl = el;
}

<template>
<div tabindex="0" {{did-insert this.setRef}}>
{{onHotkey "Escape" @onClosePanel target=this.panelEl}}
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