Merge pull request #16384 from keymanapp/fix/web/cancel-gestures-on-relayout

fix(web): cancel gestures on relayouts and context resets
This commit is contained in:
Joshua Horton 2026-08-18 22:25:28 +07:00 committed by GitHub
commit cafa672d97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 55 additions and 28 deletions

View file

@ -1,8 +1,22 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by jahorton on 2023-10-16.
*
* The GestureHandler interface defines methods common to all OSK
* gesture-handling classes, providing a common abstraction for common-path
* gesture support.
*/
import { ActiveKeyBase, KeyDistribution } from "keyman/engine/keyboard";
export interface GestureHandler {
/**
* Triggers cancellation of any further processing for the gesture being handled.
* Triggers cancellation of any further processing for the gesture being
* handled.
*
* The method should be able to safely handle the `cancel` method being called
* multiple times, much like `clearTimeout` in JS.
*/
cancel(): void;

View file

@ -1,3 +1,13 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by jahorton on 2023-10-04.
*
* The HeldRepeater class models key input that is repeated when its
* corresponding key is held. At this time, the class is mostly used to model a
* repeatable Backspace input.
*/
import { GestureSequence } from "keyman/engine/gesture-processor";
import { KeyDistribution } from "keyman/engine/keyboard";
@ -5,48 +15,47 @@ import { KeyElement } from "../../keyElement.js";
import { GestureHandler } from './gestureHandler.js';
export class HeldRepeater implements GestureHandler {
readonly directlyEmitsKeys = true;
public readonly directlyEmitsKeys = true;
public readonly hasModalVisualization = false;
static readonly INITIAL_DELAY = 500;
static readonly REPEAT_DELAY = 100;
public static readonly INITIAL_DELAY = 500; // msec
public static readonly REPEAT_DELAY = 100; // msec
readonly source: GestureSequence<KeyElement, string>;
readonly hasModalVisualization = false;
readonly repeatClosure: () => void;
private readonly source: GestureSequence<KeyElement, string>;
private readonly baseKey: KeyElement
private readonly actionToRepeat: () => void;
private timerHandle: number;
timerHandle: number;
constructor(source: GestureSequence<KeyElement, string>, closureToRepeat: () => void) {
constructor(source: GestureSequence<KeyElement, string>, actionToRepeat: () => void) {
this.source = source;
const baseKey = source.stageReports[0].item;
baseKey.key.highlight(true);
this.baseKey = source.stageReports[0].item;
this.baseKey.key.highlight(true);
this.actionToRepeat = actionToRepeat;
this.repeatClosure = () => {
closureToRepeat();
// The repeat-closure may cancel key highlighting. This restores it afterward.
baseKey.key.highlight(true);
}
this.timerHandle = window.setTimeout(this.deleteRepeater, HeldRepeater.INITIAL_DELAY);
this.timerHandle = window.setTimeout(() => this.repeatAction(), HeldRepeater.INITIAL_DELAY);
this.source.on('complete', () => {
window.clearTimeout(this.timerHandle);
this.timerHandle = undefined;
baseKey.key.highlight(false);
this.cancel();
});
}
cancel() {
this.deleteRepeater();
if(this.timerHandle !== undefined) {
window.clearTimeout(this.timerHandle);
delete this.timerHandle;
}
this.baseKey.key.highlight(false);
this.source.cancel();
}
readonly deleteRepeater = () => {
this.repeatClosure();
this.timerHandle = window.setTimeout(this.deleteRepeater, HeldRepeater.REPEAT_DELAY);
private repeatAction() {
this.actionToRepeat();
// In case the action to repeat cancels key highlighting, we restore it
// afterward.
this.baseKey.key.highlight(true);
this.timerHandle = window.setTimeout(() => this.repeatAction(), HeldRepeater.REPEAT_DELAY);
}
currentStageKeyDistribution(): KeyDistribution {

View file

@ -1162,6 +1162,10 @@ export class VisualKeyboard extends EventEmitter<EventMap> implements KeyboardVi
return;
}
// A re-layout operation can break an ongoing gesture operation.
// See #15226 - this is surprisingly relevant for backspaces!
this.activeGestures.forEach((g) => g.cancel());
/*
Phase 1: calculations possible at the start without triggering _any_ additional layout reflow.
(A single, initial reflow may happen depending on DOM manipulations before this method...,