Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions demo/bin/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,33 @@ const HTML_TEMPLATE = `<!doctype html>
window.addEventListener('resize', () => {
fitAddon.fit();
});
// Handle mobile keyboard showing/hiding using visualViewport API
if (window.visualViewport) {
const terminalContent = document.querySelector('.terminal-content');
const terminalWindow = document.querySelector('.terminal-window');
const originalHeight = terminalContent.style.height;
const body = document.body;
window.visualViewport.addEventListener('resize', () => {
const keyboardHeight = window.innerHeight - window.visualViewport.height;
if (keyboardHeight > 100) {
body.style.padding = '0';
body.style.alignItems = 'flex-start';
terminalWindow.style.borderRadius = '0';
terminalWindow.style.maxWidth = '100%';
terminalContent.style.height = (window.visualViewport.height - 60) + 'px';
window.scrollTo(0, 0);
} else {
body.style.padding = '40px 20px';
body.style.alignItems = 'center';
terminalWindow.style.borderRadius = '12px';
terminalWindow.style.maxWidth = '1000px';
terminalContent.style.height = originalHeight || '600px';
}
fitAddon.fit();
});
}
</script>
</body>
</html>`;
Expand Down
33 changes: 23 additions & 10 deletions lib/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,29 +345,42 @@ export class Terminal implements ITerminalCore {
this.canvas.style.display = 'block';
parent.appendChild(this.canvas);

// Create hidden textarea for clipboard operations (xterm.js pattern)
// This textarea will be positioned under the mouse cursor during right-clicks
// to enable the browser's native context menu with Copy/Paste options
// Create hidden textarea for keyboard input (must be inside parent for event bubbling)
this.textarea = document.createElement('textarea');
this.textarea.setAttribute('autocorrect', 'off');
this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false');
this.textarea.setAttribute('tabindex', '-1'); // Don't interfere with tab navigation
this.textarea.setAttribute('tabindex', '0'); // Allow focus for mobile keyboard
this.textarea.setAttribute('aria-label', 'Terminal input');
// Use clip-path to completely hide the textarea and its caret
this.textarea.style.position = 'absolute';
this.textarea.style.left = '0';
this.textarea.style.top = '0';
this.textarea.style.width = '0';
this.textarea.style.height = '0';
this.textarea.style.zIndex = '-10';
this.textarea.style.width = '1px';
this.textarea.style.height = '1px';
this.textarea.style.padding = '0';
this.textarea.style.border = 'none';
this.textarea.style.margin = '0';
this.textarea.style.opacity = '0';
this.textarea.style.clipPath = 'inset(50%)'; // Clip everything including caret
this.textarea.style.overflow = 'hidden';
this.textarea.style.pointerEvents = 'none'; // Don't interfere with mouse events normally
this.textarea.style.whiteSpace = 'nowrap';
this.textarea.style.resize = 'none';
this.textarea.style.border = 'none';
this.textarea.style.outline = 'none';
parent.appendChild(this.textarea);

// Focus textarea on interaction - preventDefault before focus
const textarea = this.textarea;
// Desktop: mousedown
this.canvas.addEventListener('mousedown', (ev) => {
ev.preventDefault();
textarea.focus();
});
// Mobile: touchend with preventDefault to suppress iOS caret
this.canvas.addEventListener('touchend', (ev) => {
ev.preventDefault();
textarea.focus();
});

// Create renderer
this.renderer = new CanvasRenderer(this.canvas, {
fontSize: this.options.fontSize,
Expand Down