mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
feat: add keyboard navigation to session grid
- Enable arrow key navigation (up/down/left/right) through sessions - Press Enter to open selected session - Add visual feedback for keyboard-selected sessions - Fix global event listener conflict by scoping to component - Make session-list focusable with proper focus indicators - Pass selected state to session-card for visual highlighting Adopts PR #322 with improvements based on review feedback
This commit is contained in:
parent
fcbe9e7be5
commit
1052354394
2 changed files with 65 additions and 2 deletions
|
|
@ -57,6 +57,7 @@ export class SessionCard extends LitElement {
|
|||
|
||||
@property({ type: Object }) session!: Session;
|
||||
@property({ type: Object }) authClient!: AuthClient;
|
||||
@property({ type: Boolean }) selected = false;
|
||||
@state() private killing = false;
|
||||
@state() private killingFrame = 0;
|
||||
@state() private isActive = false;
|
||||
|
|
@ -357,7 +358,7 @@ export class SessionCard extends LitElement {
|
|||
this.isActive && this.session.status === 'running'
|
||||
? 'ring-2 ring-primary shadow-glow-sm'
|
||||
: ''
|
||||
}"
|
||||
} ${this.selected ? 'ring-2 ring-accent-primary shadow-card-hover' : ''}"
|
||||
style="view-transition-name: session-${this.session.id}; --session-id: session-${
|
||||
this.session.id
|
||||
}"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,67 @@ export class SessionList extends LitElement {
|
|||
@state() private cleaningExited = false;
|
||||
private previousRunningCount = 0;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Make the component focusable
|
||||
this.tabIndex = 0;
|
||||
// Add keyboard listener only to this component
|
||||
this.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
private getVisibleSessions() {
|
||||
const running = this.sessions.filter((s) => s.status === 'running');
|
||||
const exited = this.sessions.filter((s) => s.status === 'exited');
|
||||
return this.hideExited ? running : running.concat(exited);
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
const { key } = e;
|
||||
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter'].includes(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're inside an input element - since we're now listening on the component
|
||||
// itself, we need to stop propagation for child inputs
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target !== this &&
|
||||
(target.closest('input, textarea, select') || target.isContentEditable)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessions = this.getVisibleSessions();
|
||||
if (sessions.length === 0) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevent event from bubbling up
|
||||
|
||||
let index = this.selectedSessionId
|
||||
? sessions.findIndex((s) => s.id === this.selectedSessionId)
|
||||
: 0;
|
||||
if (index < 0) index = 0;
|
||||
|
||||
if (key === 'Enter') {
|
||||
this.handleSessionSelect({ detail: sessions[index] } as CustomEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'ArrowLeft' || key === 'ArrowUp') {
|
||||
index = (index - 1 + sessions.length) % sessions.length;
|
||||
} else if (key === 'ArrowRight' || key === 'ArrowDown') {
|
||||
index = (index + 1) % sessions.length;
|
||||
}
|
||||
|
||||
this.selectedSessionId = sessions[index].id;
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private handleRefresh() {
|
||||
this.dispatchEvent(new CustomEvent('refresh'));
|
||||
}
|
||||
|
|
@ -260,7 +321,7 @@ export class SessionList extends LitElement {
|
|||
const showExitedSection = !this.hideExited && hasExitedSessions;
|
||||
|
||||
return html`
|
||||
<div class="font-mono text-sm" data-testid="session-list-container">
|
||||
<div class="font-mono text-sm focus:outline-none focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary rounded-lg" data-testid="session-list-container">
|
||||
<div class="p-4 pt-5">
|
||||
${
|
||||
!hasRunningSessions && (!hasExitedSessions || this.hideExited)
|
||||
|
|
@ -524,6 +585,7 @@ export class SessionList extends LitElement {
|
|||
<session-card
|
||||
.session=${session}
|
||||
.authClient=${this.authClient}
|
||||
.selected=${session.id === this.selectedSessionId}
|
||||
@session-select=${this.handleSessionSelect}
|
||||
@session-killed=${this.handleSessionKilled}
|
||||
@session-kill-error=${this.handleSessionKillError}
|
||||
|
|
|
|||
Loading…
Reference in a new issue