-
-
Notifications
You must be signed in to change notification settings - Fork 42
feat: add ember-hotkeys #117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
IgnaceMaes
wants to merge
11
commits into
TanStack:main
Choose a base branch
from
IgnaceMaes:add-ember
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
c80ef58
feat: add ember-hotkeys
IgnaceMaes 1bac0fc
chore: document ember-hotkeys in readmes
IgnaceMaes 9b42116
docs: document ember-hotkeys
IgnaceMaes b42a336
chore: remove demo app
IgnaceMaes 29597d8
fix: broken docs link
IgnaceMaes d90a324
chore: add changeset
IgnaceMaes 0839708
docs: use tracker properly
IgnaceMaes fe190b0
chore: format helpers for modern import kebabCase
IgnaceMaes 23c6ea2
chore: fix coderabbit comments
IgnaceMaes 81e7694
chore: address final coderabbit comments
IgnaceMaes ac5fb80
feat: make test helpers await settled state
IgnaceMaes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'); | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| } | ||
| ``` | ||
|
|
||
| ## `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), | ||
| }); | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ``` | ||
|
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> | ||
| } | ||
| ``` | ||
|
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(); | ||
| ``` | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.