diff --git a/skrub/_reporting/_data/templates/report.js b/skrub/_reporting/_data/templates/report.js index a5c7fc9fe..aa446a3e4 100644 --- a/skrub/_reporting/_data/templates/report.js +++ b/skrub/_reporting/_data/templates/report.js @@ -227,6 +227,9 @@ if (customElements.get('skrub-table-report') === undefined) { this.activate(); event.preventDefault(); }); + // See forwardKeyboardEvent for details about captureKeys + this.elem.dataset.captureKeys = + "ArrowRight ArrowLeft ArrowUp ArrowDown Escape"; this.elem.setAttribute("tabindex", -1); this.elem.oncopy = (event) => this.copyCell(event); } @@ -318,6 +321,8 @@ if (customElements.get('skrub-table-report') === undefined) { this.nTailRows = this.elem.dataset.nTailRows; this.nCols = this.elem.dataset.nCols; this.elem.addEventListener('keydown', (e) => this.onKeyDown(e)); + this.elem.addEventListener('skrub-keydown', (e) => this.onKeyDown( + unwrapSkrubKeyDown(e))); } onKeyDown(event) { @@ -493,8 +498,13 @@ if (customElements.get('skrub-table-report') === undefined) { } this.lastTab = tab; tab.addEventListener("click", () => this.selectTab(tab)); + // See forwardKeyboardEvent for details about captureKeys + tab.dataset.captureKeys = "ArrowRight ArrowLeft"; tab.addEventListener("keydown", (event) => this.onKeyDown( event)); + tab.addEventListener("skrub-keydown", (event) => this + .onKeyDown( + unwrapSkrubKeyDown(event))); }); this.selectTab(this.firstTab, false); } @@ -671,4 +681,72 @@ if (customElements.get('skrub-table-report') === undefined) { function hasModifier(event) { return event.ctrlKey || event.metaKey || event.shiftKey || event.altKey; } + + /* Jupyter notebooks and vscode stop the propagation of some keyboard events + during the capture phase to implement their keyboard shortcuts etc. When an + element inside the TableReport has the keyboard focus and wants to react on + that event (eg in the sample table arrow keys allow selecting a neighbouring + cell) we need to override that behavior. + + We do that by adding an event listener on the window that is triggered + during the capture phase. If it can make sure the key press is for a report + element that will react to it, we stop its propagation (to avoid eg the + notebook jumping to the next jupyter code cell) and dispatch an event on the + targeted element. To make sure it does not get handled again by the listener + on the window and cause infinite recursion, we dispatch a custom event + instead of a KeyDown event. + + This capture is only enabled if we detect the report is inserted in a page + where it is needed, by checking if there are elements in the page with class + names that are used by jupyter or vscode. + */ + function forwardKeyboardEvent(e) { + if (e.eventPhase !== 1) { + return; + } + if (e.target.tagName !== "SKRUB-TABLE-REPORT") { + return; + } + if (hasModifier(e)) { + return; + } + const target = e.target.shadowRoot.activeElement; + // only capture the event if the element lists the key in the keys it + // wants to capture ie in captureKeys + const wantsKey = target?.dataset.captureKeys?.split(/\s+/).includes(e.key); + if (!wantsKey) { + return; + } + const newEvent = new CustomEvent('skrub-keydown', { + bubbles: true, + cancelable: true, + detail: { + key: e.key, + code: e.code, + shiftKey: e.shiftKey, + altKey: e.altKey, + ctrlKey: e.ctrlKey, + metaKey: e.metaKey, + } + }); + target.dispatchEvent(newEvent); + e.stopImmediatePropagation(); + e.preventDefault(); + } + + /* Helper to unpack the custom event (see forwardKeyboardEvent above) and + make it look like a regular KeyDown event. */ + function unwrapSkrubKeyDown(e) { + return { + preventDefault: () => {}, + stopPropagation: () => {}, + target: e.target, + ...e.detail + }; + } + + if (document.querySelector(".jp-Cell, .widgetarea")) { + window.addEventListener("keydown", forwardKeyboardEvent, true); + } + }