add-keyboard-nav
Add keyboard navigation to a feature using CommandRegistryService. Use when implementing keyboard shortcuts, vim-style navigation, or hotkeys for a page or component.
$ Installer
git clone https://github.com/majiayu000/claude-skill-registry /tmp/claude-skill-registry && cp -r /tmp/claude-skill-registry/skills/product/add-keyboard-nav ~/.claude/skills/claude-skill-registry// tip: Run this command in your terminal to install the skill
SKILL.md
name: add-keyboard-nav description: Add keyboard navigation to a feature using CommandRegistryService. Use when implementing keyboard shortcuts, vim-style navigation, or hotkeys for a page or component.
Add Keyboard Navigation Skill
Implement keyboard shortcuts using the hnews command registry pattern.
Architecture Overview
CommandRegistryService <- Central command registry (string â callback)
â
KeyboardNavigationService <- Story list navigation (j/k/o/c)
BaseCommentNavigationService <- Comment thread navigation (abstract)
â
SidebarKeyboardNavigationService <- Sidebar-specific commands
ItemKeyboardNavigationService <- Item page-specific commands
Step 1: Register Commands
Inject CommandRegistryService and register commands in constructor:
import { inject } from '@angular/core';
import { CommandRegistryService } from '../services/command-registry.service';
@Injectable({ providedIn: 'root' })
export class MyFeatureNavigationService {
private commandRegistry = inject(CommandRegistryService);
constructor() {
this.registerCommands();
}
private registerCommands(): void {
// Use namespaced command IDs: 'feature.action'
this.commandRegistry.register('myFeature.next', () => this.selectNext());
this.commandRegistry.register('myFeature.previous', () => this.selectPrevious());
this.commandRegistry.register('myFeature.open', () => this.openSelected());
}
private selectNext(): void {
// Implementation
}
}
Step 2: Handle Keyboard Events
In the component or app-level, listen for keydown and execute commands:
@HostListener('document:keydown', ['$event'])
handleKeydown(event: KeyboardEvent): void {
// Skip if user is typing in an input
if (this.isTyping(event)) return;
const keyMap: Record<string, string> = {
'j': 'myFeature.next',
'k': 'myFeature.previous',
'Enter': 'myFeature.open',
};
const command = keyMap[event.key];
if (command) {
event.preventDefault();
this.commandRegistry.execute(command);
}
}
private isTyping(event: KeyboardEvent): boolean {
const target = event.target as HTMLElement;
return target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable;
}
Step 3: DOM Attributes for Navigation
Use data attributes to find navigable elements:
<!-- Story list items -->
<article
[attr.data-story-index]="index"
[attr.data-story-id]="story.id"
[class.selected]="isSelected(index)"
></article>
<!-- Comment threads use role="treeitem" -->
<div role="treeitem" [attr.data-comment-id]="comment.id"></div>
<!-- Load more buttons -->
<button class="load-more-btn" (click)="loadMore()"></button>
Step 4: Selection State with Signals
readonly selectedIndex = signal<number | null>(null);
readonly totalItems = signal<number>(0);
isSelected = computed(() => {
const index = this.selectedIndex();
return (itemIndex: number) => index === itemIndex;
});
selectNext(): boolean {
const current = this.selectedIndex();
const total = this.totalItems();
if (current === null) {
this.selectedIndex.set(0);
return true;
}
if (current < total - 1) {
this.selectedIndex.set(current + 1);
return true;
}
return false;
}
Step 5: Scroll Into View
private scrollSelectedIntoView(): void {
const element = document.querySelector(`[data-item-index="${this.selectedIndex()}"]`);
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
Command Naming Convention
Use hierarchical naming: {feature}.{action}
Examples:
story.next,story.previous,story.opencomment.next,comment.previous,comment.collapsesidebar.close,sidebar.scrollTopnavigation.previousTab,navigation.nextTab
For Comment Threads
Extend BaseCommentNavigationService:
@Injectable()
export class MyCommentNavigationService extends BaseCommentNavigationService {
protected getCommentElements(): HTMLElement[] {
return Array.from(document.querySelectorAll('[role="treeitem"]'));
}
protected getContainerElement(): HTMLElement | null {
return document.querySelector('.comments-container');
}
}
Testing
describe('MyFeatureNavigationService', () => {
let service: MyFeatureNavigationService;
let commandRegistry: CommandRegistryService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(MyFeatureNavigationService);
commandRegistry = TestBed.inject(CommandRegistryService);
});
it('should register commands', () => {
expect(commandRegistry.hasCommand('myFeature.next')).toBe(true);
});
});
Repository

majiayu000
Author
majiayu000/claude-skill-registry/skills/product/add-keyboard-nav
0
Stars
0
Forks
Updated15h ago
Added1w ago