keyboard moves terminal up

This commit is contained in:
Peter Steinberger 2025-06-27 01:37:24 +02:00
parent 9d775e6b31
commit 4b7a073975
2 changed files with 168 additions and 59 deletions

View file

@ -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"
> >

View file

@ -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();