mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-22 14:06:02 +00:00
Fix Safari clipboard paste (#336)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
29f938dcc1
commit
00933690a8
8 changed files with 420 additions and 20 deletions
156
web/docs/ios-safari-paste.md
Normal file
156
web/docs/ios-safari-paste.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
# iOS Safari Paste Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation of paste functionality for iOS Safari in VibeTunnel, addressing the limitations of the Clipboard API on mobile Safari and providing a reliable fallback mechanism.
|
||||
|
||||
## The Problem
|
||||
|
||||
### Primary Issues
|
||||
|
||||
1. **Clipboard API Limitations on iOS Safari**
|
||||
- `navigator.clipboard.readText()` only works in secure contexts (HTTPS/localhost)
|
||||
- Even in secure contexts, it requires "transient user activation" (immediate user gesture)
|
||||
- iOS 15 and older versions don't expose `readText()` at all
|
||||
- In HTTP contexts, `navigator.clipboard` is completely undefined
|
||||
|
||||
2. **Focus Management Conflicts**
|
||||
- VibeTunnel uses aggressive focus retention for the hidden input field
|
||||
- Focus retention runs every 100ms to maintain keyboard visibility
|
||||
- "Keyboard mode" forces focus back to hidden input continuously
|
||||
- This prevents any other element from maintaining focus long enough for iOS paste menu
|
||||
|
||||
3. **iOS Native Paste Menu Requirements**
|
||||
- Requires a visible, focusable text input element
|
||||
- Element must maintain focus for the paste menu to appear
|
||||
- User must be able to long-press the element
|
||||
- Hidden or off-screen elements don't trigger the paste menu reliably
|
||||
|
||||
### Failed Approaches
|
||||
|
||||
1. **Off-screen Textarea** (Apple's documented approach)
|
||||
- Created textarea at `position: fixed; left: -9999px`
|
||||
- Focus was immediately stolen by focus retention mechanism
|
||||
- Even after disabling focus retention, keyboard mode continued stealing focus
|
||||
- iOS couldn't show paste menu on an off-screen element
|
||||
|
||||
2. **Temporary Focus Retention Disable**
|
||||
- Attempted to pause focus retention interval during paste
|
||||
- Keyboard mode's focus management still interfered
|
||||
- Complex state management led to race conditions
|
||||
- Restoration of focus states was unreliable
|
||||
|
||||
## The Solution
|
||||
|
||||
### Implementation Strategy
|
||||
|
||||
Use the existing hidden input field and temporarily make it visible for paste operations:
|
||||
|
||||
```typescript
|
||||
private triggerNativePasteWithHiddenInput(): void {
|
||||
// 1. Save original styles
|
||||
const originalStyles = {
|
||||
position: this.hiddenInput.style.position,
|
||||
opacity: this.hiddenInput.style.opacity,
|
||||
// ... all other styles
|
||||
};
|
||||
|
||||
// 2. Make input visible at screen center
|
||||
this.hiddenInput.style.position = 'fixed';
|
||||
this.hiddenInput.style.left = '50%';
|
||||
this.hiddenInput.style.top = '50%';
|
||||
this.hiddenInput.style.transform = 'translate(-50%, -50%)';
|
||||
this.hiddenInput.style.width = '200px';
|
||||
this.hiddenInput.style.height = '40px';
|
||||
this.hiddenInput.style.opacity = '1';
|
||||
this.hiddenInput.style.backgroundColor = 'white';
|
||||
this.hiddenInput.style.border = '2px solid #007AFF';
|
||||
this.hiddenInput.style.borderRadius = '8px';
|
||||
this.hiddenInput.style.padding = '8px';
|
||||
this.hiddenInput.style.zIndex = '10000';
|
||||
this.hiddenInput.placeholder = 'Long-press to paste';
|
||||
|
||||
// 3. Add paste event listener
|
||||
this.hiddenInput.addEventListener('paste', handlePasteEvent);
|
||||
|
||||
// 4. Focus and select
|
||||
this.hiddenInput.focus();
|
||||
this.hiddenInput.select();
|
||||
|
||||
// 5. Clean up after paste or timeout
|
||||
}
|
||||
```
|
||||
|
||||
### Why This Works
|
||||
|
||||
1. **No Focus Conflicts**: Uses the same input that already has focus management
|
||||
2. **Visible Target**: iOS can show paste menu on a visible, centered element
|
||||
3. **User-Friendly**: Clear visual feedback with "Long-press to paste" placeholder
|
||||
4. **Simple State Management**: Just style changes, no complex focus juggling
|
||||
5. **Maintains User Gesture Context**: Called directly from touch event handler
|
||||
|
||||
### User Flow
|
||||
|
||||
1. User taps "Paste" button in quick keys
|
||||
2. Hidden input becomes visible at screen center with blue border
|
||||
3. User long-presses the visible input
|
||||
4. iOS shows native paste menu
|
||||
5. User taps "Paste" from menu
|
||||
6. Text is pasted and sent to terminal
|
||||
7. Input returns to hidden state
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Key Files
|
||||
|
||||
- `web/src/client/components/session-view/direct-keyboard-manager.ts:695-784` - Main paste implementation
|
||||
- `web/src/client/components/terminal-quick-keys.ts:198-207` - Paste button handler
|
||||
|
||||
### Fallback Logic
|
||||
|
||||
```typescript
|
||||
// In handleQuickKeyPress for 'Paste' key:
|
||||
1. Try modern Clipboard API if available (HTTPS contexts)
|
||||
2. If that fails or unavailable, use triggerNativePasteWithHiddenInput()
|
||||
3. Show visible input for native iOS paste menu
|
||||
```
|
||||
|
||||
### Autocorrect Disable
|
||||
|
||||
To prevent iOS text editing interference, the hidden input has comprehensive attributes:
|
||||
|
||||
```typescript
|
||||
this.hiddenInput.autocapitalize = 'none';
|
||||
this.hiddenInput.autocomplete = 'off';
|
||||
this.hiddenInput.setAttribute('autocorrect', 'off');
|
||||
this.hiddenInput.setAttribute('spellcheck', 'false');
|
||||
this.hiddenInput.setAttribute('data-autocorrect', 'off');
|
||||
this.hiddenInput.setAttribute('data-gramm', 'false');
|
||||
this.hiddenInput.setAttribute('data-ms-editor', 'false');
|
||||
this.hiddenInput.setAttribute('data-smartpunctuation', 'false');
|
||||
this.hiddenInput.setAttribute('inputmode', 'text');
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Scenarios
|
||||
|
||||
1. **HTTPS Context**: Clipboard API should work directly
|
||||
2. **HTTP Context**: Should fall back to visible input method
|
||||
3. **Focus Retention Active**: Paste should still work without conflicts
|
||||
4. **Multiple Paste Operations**: Each should work independently
|
||||
5. **Timeout Handling**: Input should restore after 10 seconds if no paste
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. Requires user to long-press and select paste (two taps total)
|
||||
2. Shows visible UI element temporarily
|
||||
3. 10-second timeout if user doesn't paste
|
||||
4. Only works with text content (no rich text/images)
|
||||
|
||||
## References
|
||||
|
||||
- [iOS Safari Clipboard API Documentation](https://developer.apple.com/documentation/webkit/clipboard_api)
|
||||
- [WebKit Bug Tracker - Clipboard API Issues](https://bugs.webkit.org/)
|
||||
- Original working implementation: Commit `44f69c45a`
|
||||
- Issue #317: Safari paste functionality
|
||||
|
|
@ -60,6 +60,7 @@ export class DirectKeyboardManager {
|
|||
private keyboardModeTimestamp = 0; // Track when we entered keyboard mode
|
||||
private keyboardActivationTimeout: number | null = null;
|
||||
private captureClickHandler: ((e: Event) => void) | null = null;
|
||||
private globalPasteHandler: ((e: Event) => void) | null = null;
|
||||
|
||||
// IME composition state tracking for Japanese/CJK input
|
||||
private isComposing = false;
|
||||
|
|
@ -67,6 +68,9 @@ export class DirectKeyboardManager {
|
|||
|
||||
constructor(instanceId: string) {
|
||||
this.instanceId = instanceId;
|
||||
|
||||
// Add global paste listener for environments where Clipboard API doesn't work
|
||||
this.setupGlobalPasteListener();
|
||||
}
|
||||
|
||||
setInputManager(inputManager: InputManager): void {
|
||||
|
|
@ -214,10 +218,17 @@ export class DirectKeyboardManager {
|
|||
this.hiddenInput.style.cursor = 'default';
|
||||
this.hiddenInput.style.pointerEvents = 'none'; // Start with pointer events disabled
|
||||
this.hiddenInput.style.webkitUserSelect = 'text'; // iOS specific
|
||||
this.hiddenInput.autocapitalize = 'off';
|
||||
this.hiddenInput.autocapitalize = 'none'; // More explicit than 'off'
|
||||
this.hiddenInput.autocomplete = 'off';
|
||||
this.hiddenInput.setAttribute('autocorrect', 'off');
|
||||
this.hiddenInput.setAttribute('spellcheck', 'false');
|
||||
this.hiddenInput.setAttribute('data-autocorrect', 'off');
|
||||
this.hiddenInput.setAttribute('data-gramm', 'false'); // Disable Grammarly
|
||||
this.hiddenInput.setAttribute('data-ms-editor', 'false'); // Disable Microsoft Editor
|
||||
this.hiddenInput.setAttribute('data-smartpunctuation', 'false'); // Disable smart quotes/dashes
|
||||
this.hiddenInput.setAttribute('data-form-type', 'other'); // Hint this isn't a form field
|
||||
this.hiddenInput.setAttribute('inputmode', 'text'); // Allow keyboard but disable optimizations
|
||||
this.hiddenInput.setAttribute('enterkeyhint', 'done'); // Prevent iOS enter key behavior
|
||||
this.hiddenInput.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Set initial position based on mode
|
||||
|
|
@ -420,7 +431,9 @@ export class DirectKeyboardManager {
|
|||
handleQuickKeyPress = async (
|
||||
key: string,
|
||||
isModifier?: boolean,
|
||||
isSpecial?: boolean
|
||||
isSpecial?: boolean,
|
||||
isToggle?: boolean,
|
||||
pasteText?: string
|
||||
): Promise<void> => {
|
||||
if (!this.inputManager) {
|
||||
logger.error('No input manager found');
|
||||
|
|
@ -468,15 +481,50 @@ export class DirectKeyboardManager {
|
|||
}
|
||||
return;
|
||||
} else if (key === 'Paste') {
|
||||
// Handle Paste key
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text && this.inputManager) {
|
||||
this.inputManager.sendInputText(text);
|
||||
// Handle Paste key - following iOS Safari best practices
|
||||
logger.log('Paste button pressed - attempting clipboard read');
|
||||
|
||||
// Log environment details for debugging
|
||||
logger.log('Clipboard context:', {
|
||||
hasClipboard: !!navigator.clipboard,
|
||||
hasReadText: !!navigator.clipboard?.readText,
|
||||
isSecureContext: window.isSecureContext,
|
||||
protocol: window.location.protocol,
|
||||
userAgent: navigator.userAgent.includes('Safari') ? 'Safari' : 'Other',
|
||||
});
|
||||
|
||||
// Check if we're in a secure context (HTTPS/localhost/PWA)
|
||||
if (window.isSecureContext && navigator.clipboard && navigator.clipboard.readText) {
|
||||
try {
|
||||
logger.log('Secure context detected - trying modern clipboard API...');
|
||||
const text = await navigator.clipboard.readText();
|
||||
logger.log('Clipboard read successful, text length:', text?.length || 0);
|
||||
|
||||
if (text && this.inputManager) {
|
||||
logger.log('Sending clipboard text to terminal');
|
||||
this.inputManager.sendInputText(text);
|
||||
return; // Success - exit early
|
||||
} else if (!text) {
|
||||
logger.warn('Clipboard is empty or contains no text');
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.warn('Clipboard API failed despite secure context:', {
|
||||
name: error?.name,
|
||||
message: error?.message,
|
||||
});
|
||||
// Continue to fallback
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to read clipboard:', err);
|
||||
} else {
|
||||
logger.log(
|
||||
'Not in secure context (HTTP) - clipboard API unavailable, using textarea fallback'
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback: Use existing hidden input with paste event
|
||||
logger.log('Using iOS native paste fallback with existing hidden input');
|
||||
this.triggerNativePasteWithHiddenInput();
|
||||
} else if (key === 'Ctrl+A') {
|
||||
// Send Ctrl+A (start of line)
|
||||
this.inputManager.sendControlSequence('\x01');
|
||||
|
|
@ -645,6 +693,118 @@ export class DirectKeyboardManager {
|
|||
}
|
||||
}
|
||||
|
||||
private triggerNativePasteWithHiddenInput(): void {
|
||||
if (!this.hiddenInput) {
|
||||
logger.error('No hidden input available for paste fallback');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('Making hidden input temporarily visible for paste');
|
||||
|
||||
// Store original styles to restore later
|
||||
const originalStyles = {
|
||||
position: this.hiddenInput.style.position,
|
||||
opacity: this.hiddenInput.style.opacity,
|
||||
left: this.hiddenInput.style.left,
|
||||
top: this.hiddenInput.style.top,
|
||||
width: this.hiddenInput.style.width,
|
||||
height: this.hiddenInput.style.height,
|
||||
backgroundColor: this.hiddenInput.style.backgroundColor,
|
||||
border: this.hiddenInput.style.border,
|
||||
borderRadius: this.hiddenInput.style.borderRadius,
|
||||
padding: this.hiddenInput.style.padding,
|
||||
zIndex: this.hiddenInput.style.zIndex,
|
||||
};
|
||||
|
||||
// Make the input visible and positioned for interaction
|
||||
this.hiddenInput.style.position = 'fixed';
|
||||
this.hiddenInput.style.left = '50%';
|
||||
this.hiddenInput.style.top = '50%';
|
||||
this.hiddenInput.style.transform = 'translate(-50%, -50%)';
|
||||
this.hiddenInput.style.width = '200px';
|
||||
this.hiddenInput.style.height = '40px';
|
||||
this.hiddenInput.style.opacity = '1';
|
||||
this.hiddenInput.style.backgroundColor = 'white';
|
||||
this.hiddenInput.style.border = '2px solid #007AFF';
|
||||
this.hiddenInput.style.borderRadius = '8px';
|
||||
this.hiddenInput.style.padding = '8px';
|
||||
this.hiddenInput.style.zIndex = '10000';
|
||||
this.hiddenInput.placeholder = 'Long-press to paste';
|
||||
|
||||
const restoreStyles = () => {
|
||||
if (!this.hiddenInput) return;
|
||||
|
||||
// Restore all original styles
|
||||
Object.entries(originalStyles).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
(this.hiddenInput!.style as any)[key] = value;
|
||||
}
|
||||
});
|
||||
this.hiddenInput.placeholder = '';
|
||||
logger.log('Restored hidden input to original state');
|
||||
};
|
||||
|
||||
// Create a one-time paste event handler
|
||||
const handlePasteEvent = (e: ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const clipboardData = e.clipboardData?.getData('text/plain');
|
||||
logger.log('Native paste event received, text length:', clipboardData?.length || 0);
|
||||
|
||||
if (clipboardData && this.inputManager) {
|
||||
logger.log('Sending native paste text to terminal');
|
||||
this.inputManager.sendInputText(clipboardData);
|
||||
} else {
|
||||
logger.warn('No clipboard data received in paste event');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
this.hiddenInput?.removeEventListener('paste', handlePasteEvent);
|
||||
restoreStyles();
|
||||
logger.log('Removed paste event listener and restored styles');
|
||||
};
|
||||
|
||||
// Add paste event listener
|
||||
this.hiddenInput.addEventListener('paste', handlePasteEvent);
|
||||
|
||||
// Focus and select the now-visible input
|
||||
this.hiddenInput.focus();
|
||||
this.hiddenInput.select();
|
||||
|
||||
logger.log('Input is now visible and focused - long-press to see paste menu');
|
||||
|
||||
// Clean up after timeout if no paste occurs
|
||||
setTimeout(() => {
|
||||
if (this.hiddenInput) {
|
||||
this.hiddenInput.removeEventListener('paste', handlePasteEvent);
|
||||
restoreStyles();
|
||||
logger.log('Paste timeout - restored input to hidden state');
|
||||
}
|
||||
}, 10000); // 10 second timeout
|
||||
}
|
||||
|
||||
private setupGlobalPasteListener(): void {
|
||||
// Listen for paste events anywhere in the document
|
||||
// This catches CMD+V or context menu paste when the hidden input is focused
|
||||
this.globalPasteHandler = (e: Event) => {
|
||||
const pasteEvent = e as ClipboardEvent;
|
||||
// Only handle if our hidden input is focused and we're in keyboard mode
|
||||
if (this.hiddenInput && document.activeElement === this.hiddenInput && this.showQuickKeys) {
|
||||
const clipboardData = pasteEvent.clipboardData?.getData('text/plain');
|
||||
if (clipboardData && this.inputManager) {
|
||||
logger.log('Global paste event captured, text length:', clipboardData.length);
|
||||
this.inputManager.sendInputText(clipboardData);
|
||||
pasteEvent.preventDefault();
|
||||
pasteEvent.stopPropagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('paste', this.globalPasteHandler);
|
||||
logger.log('Global paste listener setup for CMD+V support');
|
||||
}
|
||||
|
||||
private dismissKeyboard(): void {
|
||||
// Exit keyboard mode
|
||||
this.keyboardMode = false;
|
||||
|
|
@ -705,6 +865,12 @@ export class DirectKeyboardManager {
|
|||
this.captureClickHandler = null;
|
||||
}
|
||||
|
||||
// Remove global paste listener
|
||||
if (this.globalPasteHandler) {
|
||||
document.removeEventListener('paste', this.globalPasteHandler);
|
||||
this.globalPasteHandler = null;
|
||||
}
|
||||
|
||||
// Remove hidden input if it exists
|
||||
if (this.hiddenInput) {
|
||||
this.hiddenInput.remove();
|
||||
|
|
|
|||
|
|
@ -244,8 +244,15 @@ export class MobileInputOverlay extends LitElement {
|
|||
style="height: 120px; background: rgb(var(--color-bg)); color: rgb(var(--color-text)); border: none; padding: 12px;"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
autocapitalize="none"
|
||||
spellcheck="false"
|
||||
data-autocorrect="off"
|
||||
data-gramm="false"
|
||||
data-ms-editor="false"
|
||||
data-smartpunctuation="false"
|
||||
data-form-type="other"
|
||||
inputmode="text"
|
||||
enterkeyhint="done"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ export class TerminalQuickKeys extends LitElement {
|
|||
@property({ type: Function }) onKeyPress?: (
|
||||
key: string,
|
||||
isModifier?: boolean,
|
||||
isSpecial?: boolean
|
||||
isSpecial?: boolean,
|
||||
isToggle?: boolean,
|
||||
pasteText?: string
|
||||
) => void;
|
||||
@property({ type: Boolean }) visible = false;
|
||||
@property({ type: Number }) keyboardHeight = 0;
|
||||
|
|
@ -194,6 +196,16 @@ export class TerminalQuickKeys extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
private handlePasteImmediate(e: Event) {
|
||||
console.log('[QuickKeys] Paste button touched - delegating to paste handler');
|
||||
|
||||
// Always delegate to the main paste handler in direct-keyboard-manager
|
||||
// This preserves user gesture context while keeping all clipboard logic in one place
|
||||
if (this.onKeyPress) {
|
||||
this.onKeyPress('Paste', false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private startKeyRepeat(key: string, isModifier: boolean, isSpecial: boolean) {
|
||||
// Only enable key repeat for arrow keys
|
||||
if (!key.startsWith('Arrow')) return;
|
||||
|
|
@ -560,7 +572,11 @@ export class TerminalQuickKeys extends LitElement {
|
|||
@touchend=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.handleKeyPress(key, modifier || combo, special, toggle, e);
|
||||
if (key === 'Paste') {
|
||||
this.handlePasteImmediate(e);
|
||||
} else {
|
||||
this.handleKeyPress(key, modifier || combo, special, toggle, e);
|
||||
}
|
||||
}}
|
||||
@click=${(e: MouseEvent) => {
|
||||
if (e.detail !== 0) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @vitest-environment happy-dom
|
||||
import { fixture, html } from '@open-wc/testing';
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { resetViewport, waitForElement } from '@/test/utils/component-helpers';
|
||||
import { resetViewport, waitForElement, waitForEvent } from '@/test/utils/component-helpers';
|
||||
import { MockResizeObserver, MockTerminal } from '@/test/utils/terminal-mocks';
|
||||
|
||||
// Mock xterm modules before importing the component
|
||||
|
|
@ -140,7 +140,6 @@ describe('Terminal', () => {
|
|||
it('should handle paste events', async () => {
|
||||
const pasteText = 'pasted content';
|
||||
|
||||
// Create and dispatch paste event
|
||||
const clipboardData = new DataTransfer();
|
||||
clipboardData.setData('text/plain', pasteText);
|
||||
const pasteEvent = new ClipboardEvent('paste', {
|
||||
|
|
@ -149,11 +148,51 @@ describe('Terminal', () => {
|
|||
cancelable: true,
|
||||
});
|
||||
|
||||
// The terminal component listens for paste on the container
|
||||
const container = element.querySelector('.terminal-container');
|
||||
if (container) {
|
||||
container.dispatchEvent(pasteEvent);
|
||||
expect(container).toBeTruthy();
|
||||
|
||||
const detail = await waitForEvent<{ text: string }>(element, 'terminal-paste', () => {
|
||||
container?.dispatchEvent(pasteEvent);
|
||||
});
|
||||
|
||||
expect(pasteEvent.defaultPrevented).toBe(true);
|
||||
expect(detail.text).toBe(pasteText);
|
||||
});
|
||||
|
||||
it('should handle paste events with navigator.clipboard fallback', async () => {
|
||||
const pasteText = 'fallback content';
|
||||
|
||||
// Mock navigator.clipboard for fallback test
|
||||
const originalClipboard = navigator.clipboard;
|
||||
const mockReadText = vi.fn().mockResolvedValue(pasteText);
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { readText: mockReadText },
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
try {
|
||||
// Create paste event without clipboardData (Safari scenario)
|
||||
const pasteEvent = new ClipboardEvent('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
const container = element.querySelector('.terminal-container');
|
||||
expect(container).toBeTruthy();
|
||||
|
||||
const detail = await waitForEvent<{ text: string }>(element, 'terminal-paste', () => {
|
||||
container?.dispatchEvent(pasteEvent);
|
||||
});
|
||||
|
||||
expect(pasteEvent.defaultPrevented).toBe(true);
|
||||
expect(detail.text).toBe(pasteText);
|
||||
expect(mockReadText).toHaveBeenCalled();
|
||||
} finally {
|
||||
// Restore original clipboard
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: originalClipboard,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1587,11 +1587,20 @@ export class Terminal extends LitElement {
|
|||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private handlePaste = (e: ClipboardEvent) => {
|
||||
private handlePaste = async (e: ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const clipboardData = e.clipboardData?.getData('text/plain');
|
||||
let clipboardData = e.clipboardData?.getData('text/plain');
|
||||
|
||||
if (!clipboardData && navigator.clipboard) {
|
||||
try {
|
||||
clipboardData = await navigator.clipboard.readText();
|
||||
} catch (error) {
|
||||
logger.error('Failed to read clipboard via navigator API', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (clipboardData) {
|
||||
// Dispatch a custom event with the pasted text
|
||||
this.dispatchEvent(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@ describe('DirectKeyboardManager', () => {
|
|||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
// Mock secure context for clipboard API to work
|
||||
Object.defineProperty(window, 'isSecureContext', {
|
||||
value: true,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ describe.sequential('Logs API Tests', () => {
|
|||
});
|
||||
|
||||
// Wait and retry for the log file to be written and flushed
|
||||
let info: any;
|
||||
let info: { exists: boolean; size: number };
|
||||
let attempts = 0;
|
||||
const maxAttempts = 10;
|
||||
const retryDelay = 200;
|
||||
|
|
|
|||
Loading…
Reference in a new issue