From 77476a7754c03e042eb122ec2f81fa5fa2d8e618 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Fri, 1 Dec 2023 12:43:46 +0000 Subject: [PATCH] Re-focus input after modifying model, keep input for 0.5s --- packages/outputarea/src/widget.ts | 39 +++++++++++++++++++++++++++--- packages/outputarea/style/base.css | 31 ++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/outputarea/src/widget.ts b/packages/outputarea/src/widget.ts index 8a588a57520b..76686cd24536 100644 --- a/packages/outputarea/src/widget.ts +++ b/packages/outputarea/src/widget.ts @@ -44,6 +44,8 @@ const OUTPUT_AREA_OUTPUT_CLASS = 'jp-OutputArea-output'; */ const OUTPUT_AREA_PROMPT_CLASS = 'jp-OutputArea-prompt'; +const OUTPUT_AREA_STDIN_HIDING_CLASS = 'jp-OutputArea-stdin-hiding'; + /** * The class name added to OutputPrompt. */ @@ -457,10 +459,11 @@ export class OutputArea extends Widget { if (this.model.length >= this.maxNumberOutputs) { this.maxNumberOutputs = this.model.length; } - this.layout.addWidget(panel); - this._inputRequested.emit(input); + // Get the input node to ensure focus after updating the model upon user reply. + const inputNode = input.node.getElementsByTagName('input')[0]; + /** * Wait for the stdin to complete, add it to the model (so it persists) * and remove the stdin widget. @@ -470,14 +473,37 @@ export class OutputArea extends Widget { if (this.model.length >= this.maxNumberOutputs) { this.maxNumberOutputs = this.model.length + 1; } + panel.addClass(OUTPUT_AREA_STDIN_HIDING_CLASS); // Use stdin as the stream so it does not get combined with stdout. + // Note: because it modifies DOM it may (will) shift focus away from the input node. this.model.add({ output_type: 'stream', name: 'stdin', text: value + '\n' }); - panel.dispose(); + // Refocus the input node after it lost focus due to update of the model. + inputNode.focus(); + + // Keep the input in view for a little while; this (along refocusing) + // ensures that we can avoid the cell editor stealing the focus, and + // leading to user inadvertently modifying editor content when executing + // consecutive commands in short succession. + window.setTimeout(() => { + // Tack currently focused element to ensure that it remains on it + // after disposal of the panel with the old input + // (which modifies DOM and can lead to focus jump). + const focusedElement = document.activeElement; + // Dispose the old panel with no longer needed input box. + panel.dispose(); + // Refocus the element that was focused before. + if (focusedElement && focusedElement instanceof HTMLElement) { + focusedElement.focus(); + } + }, 500); }); + + // Note: the `input.value` promise must be listened to before we attach the panel + this.layout.addWidget(panel); } /** @@ -1093,6 +1119,11 @@ export class Stdin extends Widget implements IStdin { * not be called directly by user code. */ handleEvent(event: KeyboardEvent): void { + if (this._resolved) { + // Do not handle any more key events if the promise was resolved. + event.preventDefault(); + return; + } const input = this._input; if (event.type === 'keydown') { @@ -1112,6 +1143,7 @@ export class Stdin extends Widget implements IStdin { this._value += input.value; Stdin._historyPush(this._historyKey, input.value); } + this._resolved = true; this._promise.resolve(void 0); } else if (event.key === 'Escape') { // currently this gets clobbered by the documentsearch:end command at the notebook level @@ -1227,6 +1259,7 @@ export class Stdin extends Widget implements IStdin { private _trans: TranslationBundle; private _value: string; private _valueCache: string; + private _resolved: boolean = false; } export namespace Stdin { diff --git a/packages/outputarea/style/base.css b/packages/outputarea/style/base.css index 6d59355f4f71..e566d5f2c2a9 100644 --- a/packages/outputarea/style/base.css +++ b/packages/outputarea/style/base.css @@ -249,6 +249,37 @@ body.lm-mod-override-cursor .jp-OutputArea-output.jp-mod-isolated::before { opacity: 1; } +.jp-OutputArea-stdin-hiding:has(~ .jp-OutputArea-stdin-item) { + /* hide the old input immediately if a new input was attached */ + display: none; +} + +.jp-OutputArea-stdin-hiding .jp-Stdin-prompt::after { + content: '✓'; + color: var(--jp-accent-color0); + font-weight: bold; + animation: jp-reveal-check 0.1s forwards; + font-size: 120%; +} + +@keyframes jp-reveal-check { + 0% { + transform: scale(0.5); + opacity: 0; + } + + 100% { + transform: scale(1); + opacity: 1; + } +} + +.jp-OutputArea-stdin-hiding { + opacity: 0; + transition: opacity 0.1s ease-in; + transition-delay: 0.3s; +} + /*----------------------------------------------------------------------------- | Output Area View |----------------------------------------------------------------------------*/