Skip to content

Commit 1c7d161

Browse files
committed
Reduce touch latency on the virtual keyboard
- Drop the non-passive touchstart listener; pointerdown.preventDefault (scoped to skip the topbar) still blocks focus theft without paying the iOS scroll-resolution penalty on every tap. - Move the alt-key pointermove handler into the press lifecycle via a per-press AbortController; no idle listeners on every char button. - Emit alt-capable char keys on pointerdown when no modifiers are armed, matching native iOS behaviour. A popover selection corrects the base char via backspace + alternate, using the shift state captured at press start. - Isolate the keyboard's layout/style with contain: layout style (paint would reparent the fixed popover). - Drop the transform scale on key press; the background-color flash is enough and avoids a composite per tap.
1 parent b55b44d commit 1c7d161

2 files changed

Lines changed: 42 additions & 12 deletions

File tree

src/keyboard/virtual-keyboard.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
display: flex;
3939
flex-direction: column;
4040
background: var(--vk-bg);
41+
contain: layout style;
4142
}
4243

4344
.keyboard {
@@ -185,12 +186,11 @@
185186
padding: 0 4px;
186187
border: 0;
187188
cursor: pointer;
188-
transition: background-color 80ms ease, transform 80ms ease;
189+
transition: background-color 80ms ease;
189190
-webkit-tap-highlight-color: transparent;
190191

191192
&:active {
192193
background: color-mix(in srgb, var(--vk-key-bg) 60%, var(--vk-accent));
193-
transform: scale(0.97);
194194
}
195195

196196
&.mod {

src/keyboard/virtual-keyboard.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ type Press = {
3030
alts: HTMLButtonElement[];
3131
selectedIndex: number;
3232
committed: boolean;
33+
committedChar: string | null;
34+
upperAtDown: boolean;
35+
controller: AbortController;
3336
};
3437

3538
const DEFAULT_LOCALE: Locale = "en";
@@ -182,16 +185,14 @@ export class VirtualKeyboard extends HTMLElement {
182185

183186
this.#controller = new AbortController();
184187
const { signal } = this.#controller;
185-
const swallowFocus = (e: Event): void => e.preventDefault();
186188
const swallowFocusUnlessTopbar = (e: Event): void => {
187189
const target = e.target as HTMLElement | null;
188190
if (target?.closest(".topbar")) return;
189191
e.preventDefault();
190192
};
191193
const host = this.#root.querySelector(".vk");
192-
host?.addEventListener("pointerdown", swallowFocus, { signal });
193-
host?.addEventListener("mousedown", swallowFocus, { signal });
194-
host?.addEventListener("touchstart", swallowFocusUnlessTopbar, { passive: false, signal });
194+
host?.addEventListener("pointerdown", swallowFocusUnlessTopbar, { signal });
195+
host?.addEventListener("mousedown", swallowFocusUnlessTopbar, { signal });
195196
this.#renderTopbar();
196197
this.#attachDragScroll(this.#root.querySelector(".topbar") as HTMLElement);
197198
this.#render();
@@ -466,7 +467,6 @@ export class VirtualKeyboard extends HTMLElement {
466467
const signal = this.#controller!.signal;
467468
if (hasAlts) {
468469
btn.addEventListener("pointerdown", (e) => this.#onPointerDown(e, key, btn), { signal });
469-
btn.addEventListener("pointermove", (e) => this.#onPointerMove(e), { signal });
470470
btn.addEventListener("pointerup", (e) => this.#onPointerUp(e), { signal });
471471
btn.addEventListener("pointercancel", () => this.#cancelPress(), { signal });
472472
} else if (isRepeatableKey(key)) {
@@ -505,6 +505,22 @@ export class VirtualKeyboard extends HTMLElement {
505505
return;
506506
}
507507
btn.setPointerCapture(e.pointerId);
508+
509+
const upperAtDown = this.#state.shift !== "off" && this.#state.layer === "letters";
510+
let committedChar: string | null = null;
511+
if (key.action.kind === "char" && this.#state.modifiers.size === 0) {
512+
committedChar = upperAtDown ? key.action.value.toUpperCase() : key.action.value;
513+
this.#emit(committedChar);
514+
if (this.#state.shift === "on") {
515+
this.#state.shift = "off";
516+
this.#applyShiftState();
517+
}
518+
}
519+
520+
const controller = new AbortController();
521+
btn.addEventListener("pointermove", (ev) => this.#onPointerMove(ev), {
522+
signal: controller.signal,
523+
});
508524
const press: Press = {
509525
key,
510526
button: btn,
@@ -513,7 +529,10 @@ export class VirtualKeyboard extends HTMLElement {
513529
popover: null,
514530
alts: [],
515531
selectedIndex: 0,
516-
committed: false,
532+
committed: committedChar !== null,
533+
committedChar,
534+
upperAtDown,
535+
controller,
517536
};
518537
press.timer = window.setTimeout(() => this.#openPopover(), this.#longPressMs);
519538
this.#press = press;
@@ -539,13 +558,23 @@ export class VirtualKeyboard extends HTMLElement {
539558
p.timer = null;
540559
}
541560
if (p.popover) {
542-
const alts = p.key.alternates ?? [];
543-
const choice = alts[p.selectedIndex] ?? p.key.label;
544-
this.#emitChar(choice);
561+
const chosen = p.key.alternates?.[p.selectedIndex];
562+
if (chosen !== undefined) {
563+
const alternative = p.upperAtDown ? chosen.toUpperCase() : chosen;
564+
if (p.committed) {
565+
if (alternative !== p.committedChar) {
566+
this.#adapter.execute({ kind: "backspace" });
567+
this.#emit(alternative);
568+
}
569+
} else {
570+
this.#emitChar(chosen);
571+
}
572+
}
545573
this.#closePopover();
546574
} else if (!p.committed) {
547575
this.#handle(p.key);
548576
}
577+
p.controller.abort();
549578
this.#press = null;
550579
}
551580

@@ -554,14 +583,15 @@ export class VirtualKeyboard extends HTMLElement {
554583
if (!p) return;
555584
if (p.timer !== null) clearTimeout(p.timer);
556585
if (p.popover) this.#closePopover();
586+
p.controller.abort();
557587
this.#press = null;
558588
}
559589

560590
#openPopover(): void {
561591
const p = this.#press;
562592
if (!p || !p.key.alternates) return;
563593
const alts = p.key.alternates;
564-
const upper = this.#state.shift !== "off" && this.#state.layer === "letters";
594+
const upper = p.upperAtDown;
565595

566596
const popover = document.createElement("div");
567597
popover.className = "popover";

0 commit comments

Comments
 (0)