mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
keyboard moves terminal up
This commit is contained in:
parent
9d775e6b31
commit
4b7a073975
2 changed files with 168 additions and 59 deletions
|
|
@ -88,6 +88,7 @@ export class SessionView extends LitElement {
|
||||||
@state() private useDirectKeyboard = false;
|
@state() private useDirectKeyboard = false;
|
||||||
@state() private showQuickKeys = false;
|
@state() private showQuickKeys = false;
|
||||||
@state() private keyboardHeight = 0;
|
@state() private keyboardHeight = 0;
|
||||||
|
@state() private terminalTransformY = 0;
|
||||||
|
|
||||||
private instanceId = `session-view-${Math.random().toString(36).substr(2, 9)}`;
|
private instanceId = `session-view-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
private createHiddenInputTimeout: ReturnType<typeof setTimeout> | null = null;
|
private createHiddenInputTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
@ -120,6 +121,7 @@ export class SessionView extends LitElement {
|
||||||
}),
|
}),
|
||||||
setShowQuickKeys: (value: boolean) => {
|
setShowQuickKeys: (value: boolean) => {
|
||||||
this.showQuickKeys = value;
|
this.showQuickKeys = value;
|
||||||
|
this.updateTerminalTransform();
|
||||||
},
|
},
|
||||||
setShowFileBrowser: (value: boolean) => {
|
setShowFileBrowser: (value: boolean) => {
|
||||||
this.showFileBrowser = value;
|
this.showFileBrowser = value;
|
||||||
|
|
@ -146,6 +148,7 @@ export class SessionView extends LitElement {
|
||||||
stopLoading: () => this.loadingAnimationManager.stopLoading(),
|
stopLoading: () => this.loadingAnimationManager.stopLoading(),
|
||||||
setKeyboardHeight: (value: number) => {
|
setKeyboardHeight: (value: number) => {
|
||||||
this.keyboardHeight = value;
|
this.keyboardHeight = value;
|
||||||
|
this.updateTerminalTransform();
|
||||||
},
|
},
|
||||||
getTerminalLifecycleManager: () =>
|
getTerminalLifecycleManager: () =>
|
||||||
this.terminalLifecycleManager
|
this.terminalLifecycleManager
|
||||||
|
|
@ -240,6 +243,10 @@ export class SessionView extends LitElement {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
getKeyboardHeight: () => this.keyboardHeight,
|
getKeyboardHeight: () => this.keyboardHeight,
|
||||||
|
setKeyboardHeight: (height: number) => {
|
||||||
|
this.keyboardHeight = height;
|
||||||
|
this.updateTerminalTransform();
|
||||||
|
},
|
||||||
updateShowQuickKeys: (value: boolean) => {
|
updateShowQuickKeys: (value: boolean) => {
|
||||||
this.showQuickKeys = value;
|
this.showQuickKeys = value;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
|
|
@ -597,11 +604,15 @@ export class SessionView extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleKeyboardButtonClick() {
|
private handleKeyboardButtonClick() {
|
||||||
|
// Use a small delay to ensure the button click has fully processed
|
||||||
|
// This prevents the button click from interfering with focus
|
||||||
|
setTimeout(() => {
|
||||||
// Focus the hidden input to show the keyboard
|
// Focus the hidden input to show the keyboard
|
||||||
this.directKeyboardManager.focusHiddenInput();
|
this.directKeyboardManager.focusHiddenInput();
|
||||||
|
|
||||||
// The keyboard visibility will be detected by the visual viewport handler
|
// The keyboard visibility will be detected by the visual viewport handler
|
||||||
// which will automatically show the quick keys when the keyboard appears
|
// which will automatically show the quick keys when the keyboard appears
|
||||||
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleTerminalFitToggle() {
|
private handleTerminalFitToggle() {
|
||||||
|
|
@ -700,6 +711,36 @@ export class SessionView extends LitElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateTerminalTransform(): void {
|
||||||
|
// Calculate total height to move terminal up
|
||||||
|
let totalHeight = 0;
|
||||||
|
|
||||||
|
if (this.showQuickKeys && this.isMobile) {
|
||||||
|
// Quick keys height (approximately 140px based on CSS)
|
||||||
|
const quickKeysHeight = 140;
|
||||||
|
totalHeight += quickKeysHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.keyboardHeight > 0) {
|
||||||
|
totalHeight += this.keyboardHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply transform with smooth transition
|
||||||
|
this.terminalTransformY = totalHeight;
|
||||||
|
|
||||||
|
// If terminal is transformed, try to keep cursor visible
|
||||||
|
if (totalHeight > 0) {
|
||||||
|
// Request terminal to scroll to cursor
|
||||||
|
const terminal = this.querySelector('vibe-terminal') as Terminal;
|
||||||
|
if (terminal) {
|
||||||
|
// Scroll to bottom to keep cursor visible
|
||||||
|
setTimeout(() => {
|
||||||
|
terminal.scrollToBottom();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
refreshTerminalAfterMobileInput() {
|
refreshTerminalAfterMobileInput() {
|
||||||
// After closing mobile input, the viewport changes and the terminal
|
// After closing mobile input, the viewport changes and the terminal
|
||||||
// needs to recalculate its scroll position to avoid getting stuck
|
// needs to recalculate its scroll position to avoid getting stuck
|
||||||
|
|
@ -782,6 +823,11 @@ export class SessionView extends LitElement {
|
||||||
this.session?.status === 'exited' ? 'session-exited' : ''
|
this.session?.status === 'exited' ? 'session-exited' : ''
|
||||||
}"
|
}"
|
||||||
id="terminal-container"
|
id="terminal-container"
|
||||||
|
style="${
|
||||||
|
this.terminalTransformY > 0
|
||||||
|
? `transform: translateY(-${this.terminalTransformY}px); transition: transform 0.3s ease-out;`
|
||||||
|
: ''
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
${
|
${
|
||||||
this.loadingAnimationManager.isLoading()
|
this.loadingAnimationManager.isLoading()
|
||||||
|
|
@ -934,7 +980,15 @@ export class SessionView extends LitElement {
|
||||||
? html`
|
? html`
|
||||||
<div
|
<div
|
||||||
class="keyboard-button"
|
class="keyboard-button"
|
||||||
@click=${() => this.handleKeyboardButtonClick()}
|
@pointerdown=${(e: PointerEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
@click=${(e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.handleKeyboardButtonClick();
|
||||||
|
}}
|
||||||
title="Show keyboard"
|
title="Show keyboard"
|
||||||
>
|
>
|
||||||
⌨
|
⌨
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export interface DirectKeyboardCallbacks {
|
||||||
getDisableFocusManagement(): boolean;
|
getDisableFocusManagement(): boolean;
|
||||||
getVisualViewportHandler(): (() => void) | null;
|
getVisualViewportHandler(): (() => void) | null;
|
||||||
getKeyboardHeight(): number;
|
getKeyboardHeight(): number;
|
||||||
|
setKeyboardHeight(height: number): void;
|
||||||
updateShowQuickKeys(value: boolean): void;
|
updateShowQuickKeys(value: boolean): void;
|
||||||
toggleMobileInput(): void;
|
toggleMobileInput(): void;
|
||||||
clearMobileInputText(): void;
|
clearMobileInputText(): void;
|
||||||
|
|
@ -34,6 +35,7 @@ export class DirectKeyboardManager {
|
||||||
private keyboardMode = false; // Track whether we're in keyboard mode
|
private keyboardMode = false; // Track whether we're in keyboard mode
|
||||||
private keyboardModeTimestamp = 0; // Track when we entered keyboard mode
|
private keyboardModeTimestamp = 0; // Track when we entered keyboard mode
|
||||||
private keyboardActivationTimeout: number | null = null;
|
private keyboardActivationTimeout: number | null = null;
|
||||||
|
private captureClickHandler: ((e: Event) => void) | null = null;
|
||||||
|
|
||||||
constructor(instanceId: string) {
|
constructor(instanceId: string) {
|
||||||
this.instanceId = instanceId;
|
this.instanceId = instanceId;
|
||||||
|
|
@ -84,6 +86,47 @@ export class DirectKeyboardManager {
|
||||||
this.keyboardModeTimestamp = Date.now();
|
this.keyboardModeTimestamp = Date.now();
|
||||||
this.updateHiddenInputPosition();
|
this.updateHiddenInputPosition();
|
||||||
|
|
||||||
|
// Add capture phase click handler to prevent any clicks from stealing focus
|
||||||
|
if (!this.captureClickHandler) {
|
||||||
|
this.captureClickHandler = (e: Event) => {
|
||||||
|
if (this.keyboardMode) {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
|
||||||
|
// Allow clicks on:
|
||||||
|
// 1. Quick keys container (Done button, etc)
|
||||||
|
// 2. Session header (back button, sidebar toggle, etc)
|
||||||
|
// 3. Settings/notification buttons
|
||||||
|
// 4. Any modal overlays
|
||||||
|
if (
|
||||||
|
target.closest('.terminal-quick-keys-container') ||
|
||||||
|
target.closest('session-header') ||
|
||||||
|
target.closest('app-header') ||
|
||||||
|
target.closest('.modal-backdrop') ||
|
||||||
|
target.closest('.modal-content') ||
|
||||||
|
target.closest('.sidebar') ||
|
||||||
|
target.closest('unified-settings') ||
|
||||||
|
target.closest('notification-status')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only prevent clicks on the terminal area itself
|
||||||
|
// This keeps focus on the hidden input when tapping the terminal
|
||||||
|
if (target.closest('#terminal-container') || target.closest('vibe-terminal')) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (this.hiddenInput) {
|
||||||
|
this.hiddenInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Use capture phase to intercept clicks before they reach other elements
|
||||||
|
document.addEventListener('click', this.captureClickHandler, true);
|
||||||
|
document.addEventListener('pointerdown', this.captureClickHandler, true);
|
||||||
|
}
|
||||||
|
|
||||||
// Start focus retention immediately
|
// Start focus retention immediately
|
||||||
if (this.focusRetentionInterval) {
|
if (this.focusRetentionInterval) {
|
||||||
clearInterval(this.focusRetentionInterval);
|
clearInterval(this.focusRetentionInterval);
|
||||||
|
|
@ -133,6 +176,15 @@ export class DirectKeyboardManager {
|
||||||
// Now that we're in keyboard mode, focus the input
|
// Now that we're in keyboard mode, focus the input
|
||||||
if (this.hiddenInput && this.keyboardMode) {
|
if (this.hiddenInput && this.keyboardMode) {
|
||||||
this.hiddenInput.focus();
|
this.hiddenInput.focus();
|
||||||
|
|
||||||
|
// Simulate click to trigger system keyboard on mobile
|
||||||
|
// This helps Safari show the keyboard immediately
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.hiddenInput && this.keyboardMode) {
|
||||||
|
this.hiddenInput.click();
|
||||||
|
logger.log('Simulated click on hidden input to trigger system keyboard');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,68 +294,49 @@ export class DirectKeyboardManager {
|
||||||
|
|
||||||
this.hiddenInput.addEventListener('blur', (e) => {
|
this.hiddenInput.addEventListener('blur', (e) => {
|
||||||
const _event = e as FocusEvent;
|
const _event = e as FocusEvent;
|
||||||
const timeSinceKeyboardMode = Date.now() - this.keyboardModeTimestamp;
|
|
||||||
|
|
||||||
logger.log(`Hidden input blurred. Time since keyboard mode: ${timeSinceKeyboardMode}ms`);
|
logger.log(`Hidden input blurred. Keyboard mode: ${this.keyboardMode}`);
|
||||||
logger.log(
|
logger.log(
|
||||||
`Active element: ${document.activeElement?.tagName}, class: ${document.activeElement?.className}`
|
`Active element: ${document.activeElement?.tagName}, class: ${document.activeElement?.className}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// If we just entered keyboard mode, be more aggressive about keeping focus
|
// If we're in keyboard mode, ALWAYS try to maintain focus
|
||||||
if (this.keyboardMode && timeSinceKeyboardMode < 2000) {
|
// Only the Done button should exit keyboard mode
|
||||||
logger.log('Recently entered keyboard mode, fighting to keep focus');
|
if (this.keyboardMode) {
|
||||||
|
logger.log('In keyboard mode - maintaining focus');
|
||||||
|
|
||||||
// Very short delay to refocus
|
// Immediately try to refocus
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (
|
if (
|
||||||
this.keyboardMode &&
|
this.keyboardMode &&
|
||||||
this.hiddenInput &&
|
this.hiddenInput &&
|
||||||
document.activeElement !== this.hiddenInput
|
document.activeElement !== this.hiddenInput
|
||||||
) {
|
) {
|
||||||
logger.log('Refocusing hidden input during keyboard activation');
|
logger.log('Refocusing hidden input to maintain keyboard');
|
||||||
this.hiddenInput.focus();
|
this.hiddenInput.focus();
|
||||||
}
|
}
|
||||||
}, 10);
|
}, 0);
|
||||||
|
|
||||||
|
// Don't exit keyboard mode or hide quick keys
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Immediately try to recapture focus
|
// Only handle blur normally when NOT in keyboard mode
|
||||||
const disableFocusManagement = this.callbacks?.getDisableFocusManagement() ?? false;
|
const disableFocusManagement = this.callbacks?.getDisableFocusManagement() ?? false;
|
||||||
if (
|
if (!disableFocusManagement && this.showQuickKeys && this.hiddenInput) {
|
||||||
!disableFocusManagement &&
|
// Check if focus went somewhere legitimate
|
||||||
(this.showQuickKeys || this.keyboardMode) &&
|
|
||||||
this.hiddenInput
|
|
||||||
) {
|
|
||||||
// Use a very short timeout to allow any legitimate focus changes to complete
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (
|
|
||||||
!disableFocusManagement &&
|
|
||||||
(this.showQuickKeys || this.keyboardMode) &&
|
|
||||||
this.hiddenInput &&
|
|
||||||
document.activeElement !== this.hiddenInput
|
|
||||||
) {
|
|
||||||
// Check if focus went to a quick key or somewhere else in our component
|
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement;
|
||||||
const isWithinComponent = this.sessionViewElement?.contains(activeElement) ?? false;
|
const isWithinComponent = this.sessionViewElement?.contains(activeElement) ?? false;
|
||||||
|
|
||||||
if (isWithinComponent || !activeElement || activeElement === document.body) {
|
if (!isWithinComponent && activeElement && activeElement !== document.body) {
|
||||||
// Focus was lost to nowhere specific or within our component - recapture it
|
// Focus went somewhere outside our component
|
||||||
logger.log('Recapturing focus on hidden input');
|
|
||||||
this.hiddenInput.focus();
|
|
||||||
} else {
|
|
||||||
// Focus went somewhere legitimate outside our component
|
|
||||||
// Wait a bit longer before hiding quick keys
|
|
||||||
setTimeout(() => {
|
|
||||||
if (document.activeElement !== this.hiddenInput) {
|
|
||||||
this.hiddenInputFocused = false;
|
this.hiddenInputFocused = false;
|
||||||
this.showQuickKeys = false;
|
this.showQuickKeys = false;
|
||||||
// Exit keyboard mode and update position
|
|
||||||
this.keyboardMode = false;
|
|
||||||
this.updateHiddenInputPosition();
|
|
||||||
if (this.callbacks) {
|
if (this.callbacks) {
|
||||||
this.callbacks.updateShowQuickKeys(false);
|
this.callbacks.updateShowQuickKeys(false);
|
||||||
}
|
}
|
||||||
logger.log('Hidden input blurred, hiding quick keys');
|
logger.log('Focus left component, hiding quick keys');
|
||||||
|
|
||||||
// Clear focus retention interval
|
// Clear focus retention interval
|
||||||
if (this.focusRetentionInterval) {
|
if (this.focusRetentionInterval) {
|
||||||
|
|
@ -311,12 +344,9 @@ export class DirectKeyboardManager {
|
||||||
this.focusRetentionInterval = null;
|
this.focusRetentionInterval = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 100);
|
||||||
}
|
} else {
|
||||||
}
|
// Not in keyboard mode and not showing quick keys
|
||||||
}, 10);
|
|
||||||
} else if (!this.keyboardMode) {
|
|
||||||
// If not in keyboard mode, just mark as not focused
|
|
||||||
this.hiddenInputFocused = false;
|
this.hiddenInputFocused = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -460,9 +490,18 @@ export class DirectKeyboardManager {
|
||||||
const disableFocusManagement = this.callbacks?.getDisableFocusManagement() ?? false;
|
const disableFocusManagement = this.callbacks?.getDisableFocusManagement() ?? false;
|
||||||
const showMobileInput = this.callbacks?.getShowMobileInput() ?? false;
|
const showMobileInput = this.callbacks?.getShowMobileInput() ?? false;
|
||||||
const showCtrlAlpha = this.callbacks?.getShowCtrlAlpha() ?? false;
|
const showCtrlAlpha = this.callbacks?.getShowCtrlAlpha() ?? false;
|
||||||
|
|
||||||
|
// In keyboard mode, always maintain focus regardless of other conditions
|
||||||
|
if (this.keyboardMode && this.hiddenInput && document.activeElement !== this.hiddenInput) {
|
||||||
|
logger.log('Keyboard mode: forcing focus on hidden input');
|
||||||
|
this.hiddenInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal focus retention for quick keys
|
||||||
if (
|
if (
|
||||||
!disableFocusManagement &&
|
!disableFocusManagement &&
|
||||||
(this.showQuickKeys || this.keyboardMode) &&
|
this.showQuickKeys &&
|
||||||
this.hiddenInput &&
|
this.hiddenInput &&
|
||||||
document.activeElement !== this.hiddenInput &&
|
document.activeElement !== this.hiddenInput &&
|
||||||
!showMobileInput &&
|
!showMobileInput &&
|
||||||
|
|
@ -471,7 +510,7 @@ export class DirectKeyboardManager {
|
||||||
logger.log('Refocusing hidden input to maintain keyboard');
|
logger.log('Refocusing hidden input to maintain keyboard');
|
||||||
this.hiddenInput.focus();
|
this.hiddenInput.focus();
|
||||||
}
|
}
|
||||||
}, 300) as unknown as number;
|
}, 100) as unknown as number; // More frequent checks (100ms instead of 300ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
private delayedRefocusHiddenInput(): void {
|
private delayedRefocusHiddenInput(): void {
|
||||||
|
|
@ -534,10 +573,19 @@ export class DirectKeyboardManager {
|
||||||
this.keyboardMode = false;
|
this.keyboardMode = false;
|
||||||
this.keyboardModeTimestamp = 0;
|
this.keyboardModeTimestamp = 0;
|
||||||
|
|
||||||
|
// Remove capture click handler
|
||||||
|
if (this.captureClickHandler) {
|
||||||
|
document.removeEventListener('click', this.captureClickHandler, true);
|
||||||
|
document.removeEventListener('pointerdown', this.captureClickHandler, true);
|
||||||
|
this.captureClickHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Hide quick keys
|
// Hide quick keys
|
||||||
this.showQuickKeys = false;
|
this.showQuickKeys = false;
|
||||||
if (this.callbacks) {
|
if (this.callbacks) {
|
||||||
this.callbacks.updateShowQuickKeys(false);
|
this.callbacks.updateShowQuickKeys(false);
|
||||||
|
// Reset keyboard height when dismissing
|
||||||
|
this.callbacks.setKeyboardHeight(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop focus retention
|
// Stop focus retention
|
||||||
|
|
@ -573,6 +621,13 @@ export class DirectKeyboardManager {
|
||||||
this.keyboardActivationTimeout = null;
|
this.keyboardActivationTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove capture click handler
|
||||||
|
if (this.captureClickHandler) {
|
||||||
|
document.removeEventListener('click', this.captureClickHandler, true);
|
||||||
|
document.removeEventListener('pointerdown', this.captureClickHandler, true);
|
||||||
|
this.captureClickHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove hidden input if it exists
|
// Remove hidden input if it exists
|
||||||
if (this.hiddenInput) {
|
if (this.hiddenInput) {
|
||||||
this.hiddenInput.remove();
|
this.hiddenInput.remove();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue