Skip to content

Commit 59bb045

Browse files
authored
iOS support (#76)
* Add mobile keyboard support Focus textarea on touch/click to trigger mobile keyboard. Use clip-path to hide iOS caret. * Add mobile keyboard viewport resize handling Resize terminal when mobile keyboard shows/hides using visualViewport API.
1 parent e297e6c commit 59bb045

File tree

2 files changed

+50
-10
lines changed

2 files changed

+50
-10
lines changed

demo/bin/demo.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,33 @@ const HTML_TEMPLATE = `<!doctype html>
286286
window.addEventListener('resize', () => {
287287
fitAddon.fit();
288288
});
289+
290+
// Handle mobile keyboard showing/hiding using visualViewport API
291+
if (window.visualViewport) {
292+
const terminalContent = document.querySelector('.terminal-content');
293+
const terminalWindow = document.querySelector('.terminal-window');
294+
const originalHeight = terminalContent.style.height;
295+
const body = document.body;
296+
297+
window.visualViewport.addEventListener('resize', () => {
298+
const keyboardHeight = window.innerHeight - window.visualViewport.height;
299+
if (keyboardHeight > 100) {
300+
body.style.padding = '0';
301+
body.style.alignItems = 'flex-start';
302+
terminalWindow.style.borderRadius = '0';
303+
terminalWindow.style.maxWidth = '100%';
304+
terminalContent.style.height = (window.visualViewport.height - 60) + 'px';
305+
window.scrollTo(0, 0);
306+
} else {
307+
body.style.padding = '40px 20px';
308+
body.style.alignItems = 'center';
309+
terminalWindow.style.borderRadius = '12px';
310+
terminalWindow.style.maxWidth = '1000px';
311+
terminalContent.style.height = originalHeight || '600px';
312+
}
313+
fitAddon.fit();
314+
});
315+
}
289316
</script>
290317
</body>
291318
</html>`;

lib/terminal.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -345,29 +345,42 @@ export class Terminal implements ITerminalCore {
345345
this.canvas.style.display = 'block';
346346
parent.appendChild(this.canvas);
347347

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

371+
// Focus textarea on interaction - preventDefault before focus
372+
const textarea = this.textarea;
373+
// Desktop: mousedown
374+
this.canvas.addEventListener('mousedown', (ev) => {
375+
ev.preventDefault();
376+
textarea.focus();
377+
});
378+
// Mobile: touchend with preventDefault to suppress iOS caret
379+
this.canvas.addEventListener('touchend', (ev) => {
380+
ev.preventDefault();
381+
textarea.focus();
382+
});
383+
371384
// Create renderer
372385
this.renderer = new CanvasRenderer(this.canvas, {
373386
fontSize: this.options.fontSize,

0 commit comments

Comments
 (0)