From 47a9266a1f592451cf8cc1e92643fcb10b818467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krassowski?= <5832902+krassowski@users.noreply.github.com> Date: Mon, 4 Mar 2024 08:49:53 +0000 Subject: [PATCH] Backport PR #15790: Prevent command shortcuts from preventing user input --- packages/application/src/lab.ts | 81 +++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/packages/application/src/lab.ts b/packages/application/src/lab.ts index d1e205819b67..87b284494a17 100644 --- a/packages/application/src/lab.ts +++ b/packages/application/src/lab.ts @@ -199,6 +199,72 @@ export class JupyterLab extends JupyterFrontEnd { }); } + /** + * Override keydown handling to prevent command shortcuts from preventing user input. + * + * This introduces a slight delay to the command invocation, but no delay to user input. + */ + protected evtKeydown(event: KeyboardEvent): void { + // Process select keys which may call `preventDefault()` immediately + if ( + ['Tab', 'ArrowDown', 'ArrowUp', 'ArrowRight', 'ArrowLeft'].includes( + event.key + ) + ) { + return this.commands.processKeydownEvent(event); + } + // Process remaining events conditionally, depending on whether they would lead to text insertion + const causesInputPromise = Promise.race([ + new Promise(resolve => { + if (!event.target) { + return resolve(false); + } + event.target.addEventListener( + 'beforeinput', + (inputEvent: InputEvent) => { + switch (inputEvent.inputType) { + case 'historyUndo': + case 'historyRedo': { + if ( + inputEvent.target instanceof Element && + inputEvent.target.closest('[data-jp-undoer]') + ) { + // Allow to use custom undo/redo bindings on `jpUndoer`s + inputEvent.preventDefault(); + return resolve(false); + } + break; + } + case 'insertLineBreak': { + if ( + inputEvent.target instanceof Element && + inputEvent.target.closest('.jp-Cell') + ) { + // Allow to override the default action of Shift + Enter on cells as this is used for cell execution + inputEvent.preventDefault(); + return resolve(false); + } + break; + } + } + return resolve(true); + }, + { once: true } + ); + }), + new Promise(resolve => { + setTimeout(() => resolve(false), Private.INPUT_GUARD_TIMEOUT); + }) + ]); + causesInputPromise + .then(willCauseInput => { + if (!willCauseInput) { + this.commands.processKeydownEvent(event); + } + }) + .catch(console.warn); + } + private _info: JupyterLab.IInfo = JupyterLab.defaultInfo; private _paths: JupyterFrontEnd.IPaths; private _allPluginsActivated = new PromiseDelegate(); @@ -373,3 +439,18 @@ export namespace JupyterLab { | JupyterFrontEndPlugin[]; } } + +/** + * A namespace for module-private functionality. + */ +namespace Private { + /** + * The delay for invoking a command introduced by user input guard. + * Decreasing this value may lead to commands incorrectly triggering + * on user input. Increasing this value will lead to longer delay for + * command invocation. Note that user input is never delayed. + * + * The value represents the number in milliseconds. + */ + export const INPUT_GUARD_TIMEOUT = 10; +}