Overview
Mobile devices present unique challenges: virtual keyboards, touch interactions, different IME behavior, and viewport constraints. This guide covers mobile-specific considerations.
Key challenges:
- Virtual keyboard resizes viewport
- Touch selection is less precise than mouse
- IME composition events don't fire on iOS Safari for Korean
- Text prediction and autocorrect interfere with input
- Different keyboard apps have different behavior
Virtual Keyboard
Handle virtual keyboard appearance and viewport changes.
Keyboard Detection
class VirtualKeyboardDetector {
#viewportHeight = window.visualViewport?.height || window.innerHeight;
#isKeyboardVisible = false;
constructor(editor: Editor) {
this.#editor = editor;
this.#setupDetection();
}
#setupDetection() {
if (!window.visualViewport) {
// Fallback for browsers without visualViewport
window.addEventListener('resize', () => this.#detectKeyboard());
return;
}
window.visualViewport.addEventListener('resize', () => {
const newHeight = window.visualViewport.height;
const heightDiff = this.#viewportHeight - newHeight;
if (heightDiff > 150) {
// Keyboard appeared
this.#onKeyboardShow();
} else if (heightDiff < -150) {
// Keyboard hidden
this.#onKeyboardHide();
}
this.#viewportHeight = newHeight;
});
}
#onKeyboardShow() {
this.#isKeyboardVisible = true;
this.#scrollToCursor();
this.#adjustLayout();
}
#onKeyboardHide() {
this.#isKeyboardVisible = false;
this.#restoreLayout();
}
#scrollToCursor() {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
const viewportHeight = window.visualViewport?.height || window.innerHeight;
if (rect.bottom > viewportHeight) {
range.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
}Viewport Adjustment
// Adjust editor height when keyboard appears
function adjustEditorForKeyboard(editor: HTMLElement) {
const viewport = window.visualViewport;
if (!viewport) return;
const keyboardHeight = window.innerHeight - viewport.height;
if (keyboardHeight > 0) {
// Keyboard is visible
editor.style.maxHeight = (viewport.height - 100) + 'px';
editor.style.overflowY = 'auto';
} else {
// Keyboard is hidden
editor.style.maxHeight = '';
editor.style.overflowY = '';
}
}Touch Selection
Handle touch-based text selection.
// Improve touch selection accuracy
class TouchSelectionHandler {
constructor(editor: Editor) {
this.#editor = editor;
this.#setupTouchHandlers();
}
#setupTouchHandlers() {
let touchStart: Touch | null = null;
this.#editor.element.addEventListener('touchstart', (e) => {
touchStart = e.touches[0];
});
this.#editor.element.addEventListener('touchend', (e) => {
if (!touchStart) return;
// Wait for selection to update
setTimeout(() => {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
this.#adjustSelection(selection.getRangeAt(0));
}
}, 100);
});
}
#adjustSelection(range: Range) {
// Adjust selection boundaries if needed
// Touch selection can be imprecise
}
}Mobile Input Handling
Handle mobile-specific input behavior.
Text Prediction
// Handle mobile text prediction
element.addEventListener('beforeinput', (e) => {
if (e.inputType === 'insertFromPredictiveText') {
// Mobile keyboard text prediction
e.preventDefault();
this.#handlePredictiveText(e.data);
}
});
// Disable prediction for code blocks
function disablePrediction(element: HTMLElement) {
element.setAttribute('autocorrect', 'off');
element.setAttribute('autocapitalize', 'off');
element.setAttribute('spellcheck', 'false');
element.setAttribute('inputmode', 'text');
}Autocorrect
// Detect and handle autocorrect
class AutocorrectHandler {
#lastInput = '';
handleInput(e: InputEvent) {
if (e.inputType === 'insertText') {
// Check if this might be autocorrect
if (this.#isLikelyAutocorrect(e.data)) {
// Handle autocorrect
this.#handleAutocorrect(e.data);
} else {
this.#lastInput = e.data;
}
}
}
#isLikelyAutocorrect(text: string): boolean {
// Heuristic: If text is very different from what was typed
// This is difficult to detect reliably
return false;
}
}iOS-Specific Issues
iOS Safari has unique behaviors.
iOS Safari Issues:
- Composition events don't fire for Korean IME
- Selection can be lost when editor loses focus
- Virtual keyboard behavior differs from Android
// iOS Safari workaround for composition
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent);
}
function isSafari() {
return /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
}
// Always allow default for certain keys on iOS Safari
if (isIOS() && isSafari()) {
element.addEventListener('keydown', (e) => {
if (['Enter', 'Backspace', 'Delete'].includes(e.key)) {
// Allow browser default
return;
}
});
}Android-Specific Issues
Android browsers have their own quirks.
// Android Chrome specific handling
function isAndroid() {
return /Android/.test(navigator.userAgent);
}
// Handle different keyboard apps
// Gboard, SwiftKey, Samsung Keyboard have different behaviors
function handleAndroidKeyboard() {
// Some keyboards may interfere with input handling
// Test with different keyboard apps
}Best Practices
- Test on real devices, not just emulators
- Handle viewport changes when keyboard appears
- Scroll to keep cursor visible
- Disable autocorrect for code blocks
- Test with different keyboard apps
- Handle touch selection imprecision
- Consider mobile performance constraints