From 22f3035df078b8076f283dc154dc1bd24bfab0b4 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Mon, 27 Apr 2026 15:30:51 -0700 Subject: [PATCH 1/4] Add a test for global keypress with shadow-dom elements This is a regression test for https://github.com/rust-lang/mdBook/issues/2507. --- tests/gui/books/shadow-dom/book.toml | 6 ++++ tests/gui/books/shadow-dom/shadow-dom.js | 38 +++++++++++++++++++++ tests/gui/books/shadow-dom/src/SUMMARY.md | 3 ++ tests/gui/books/shadow-dom/src/chapter_1.md | 1 + tests/gui/shadow-dom.goml | 18 ++++++++++ 5 files changed, 66 insertions(+) create mode 100644 tests/gui/books/shadow-dom/book.toml create mode 100644 tests/gui/books/shadow-dom/shadow-dom.js create mode 100644 tests/gui/books/shadow-dom/src/SUMMARY.md create mode 100644 tests/gui/books/shadow-dom/src/chapter_1.md create mode 100644 tests/gui/shadow-dom.goml diff --git a/tests/gui/books/shadow-dom/book.toml b/tests/gui/books/shadow-dom/book.toml new file mode 100644 index 00000000..edba3344 --- /dev/null +++ b/tests/gui/books/shadow-dom/book.toml @@ -0,0 +1,6 @@ +[book] +title = "shadow-dom" +language = "en" + +[output.html] +additional-js = ["shadow-dom.js"] diff --git a/tests/gui/books/shadow-dom/shadow-dom.js b/tests/gui/books/shadow-dom/shadow-dom.js new file mode 100644 index 00000000..950442e2 --- /dev/null +++ b/tests/gui/books/shadow-dom/shadow-dom.js @@ -0,0 +1,38 @@ +(function() { + let shadowHost = null; + let shadowInput = null; + + document.addEventListener('keypress', function(e) { + if (e.key === 'x' || e.key === 'X') { + if (shadowHost && shadowHost.isConnected) { + shadowInput.focus(); + return; + } + + shadowHost = document.createElement('div'); + shadowHost.id = 'shadow-input-host'; + shadowHost.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:9999;'; + + document.body.appendChild(shadowHost); + + const shadowRoot = shadowHost.attachShadow({ mode: 'open' }); + + shadowInput = document.createElement('input'); + shadowInput.type = 'text'; + shadowInput.id = 'shadow-input'; + shadowInput.placeholder = 'Shadow DOM input (press Escape to close)'; + shadowInput.style.cssText = 'font-size:1.2em;padding:8px;width:300px;'; + + shadowRoot.appendChild(shadowInput); + shadowInput.focus(); + + shadowInput.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + shadowHost.remove(); + shadowHost = null; + shadowInput = null; + } + }); + } + }); +})(); diff --git a/tests/gui/books/shadow-dom/src/SUMMARY.md b/tests/gui/books/shadow-dom/src/SUMMARY.md new file mode 100644 index 00000000..7390c828 --- /dev/null +++ b/tests/gui/books/shadow-dom/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Chapter 1](./chapter_1.md) diff --git a/tests/gui/books/shadow-dom/src/chapter_1.md b/tests/gui/books/shadow-dom/src/chapter_1.md new file mode 100644 index 00000000..b743fda3 --- /dev/null +++ b/tests/gui/books/shadow-dom/src/chapter_1.md @@ -0,0 +1 @@ +# Chapter 1 diff --git a/tests/gui/shadow-dom.goml b/tests/gui/shadow-dom.goml new file mode 100644 index 00000000..b1f11210 --- /dev/null +++ b/tests/gui/shadow-dom.goml @@ -0,0 +1,18 @@ +// Tests the keypress handler when there is a shadow-dom element. +// See https://github.com/rust-lang/mdBook/issues/2507 + +go-to: |DOC_PATH| + "shadow-dom/chapter_1.html" + +// Open the shadow-dom generated element. +press-key: 'x' +wait-for: '#shadow-input-host' +// See what happens when the s key is pressed. +press-key: 's' +wait-for-css-false: ("#mdbook-search-wrapper", {"display": "none"}) + +// Also try the global key handlers. +reload: +press-key: 'x' +wait-for: '#shadow-input-host' +press-key: '?' +wait-for-css: ("#mdbook-help-container", {"display": "flex"}) From 4adc4b08ba0a892d9369fad83fb52bc36f8109ba Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Mon, 27 Apr 2026 15:32:09 -0700 Subject: [PATCH 2/4] Add a test for global keypress with ACE editor focused This is a regression test for https://github.com/rust-lang/mdBook/issues/3064. --- tests/gui/books/editor/book.toml | 6 ++++++ tests/gui/books/editor/src/SUMMARY.md | 4 ++++ tests/gui/books/editor/src/chapter_1.md | 7 +++++++ tests/gui/books/editor/src/chapter_2.md | 1 + tests/gui/editor-keypress.goml | 23 +++++++++++++++++++++++ 5 files changed, 41 insertions(+) create mode 100644 tests/gui/books/editor/book.toml create mode 100644 tests/gui/books/editor/src/SUMMARY.md create mode 100644 tests/gui/books/editor/src/chapter_1.md create mode 100644 tests/gui/books/editor/src/chapter_2.md create mode 100644 tests/gui/editor-keypress.goml diff --git a/tests/gui/books/editor/book.toml b/tests/gui/books/editor/book.toml new file mode 100644 index 00000000..2af92d4c --- /dev/null +++ b/tests/gui/books/editor/book.toml @@ -0,0 +1,6 @@ +[book] +title = "editor" +language = "en" + +[output.html.playground] +editable = true diff --git a/tests/gui/books/editor/src/SUMMARY.md b/tests/gui/books/editor/src/SUMMARY.md new file mode 100644 index 00000000..fa67cf3e --- /dev/null +++ b/tests/gui/books/editor/src/SUMMARY.md @@ -0,0 +1,4 @@ +# Summary + +- [Chapter 1](./chapter_1.md) +- [Chapter 2](./chapter_2.md) diff --git a/tests/gui/books/editor/src/chapter_1.md b/tests/gui/books/editor/src/chapter_1.md new file mode 100644 index 00000000..8982f7d3 --- /dev/null +++ b/tests/gui/books/editor/src/chapter_1.md @@ -0,0 +1,7 @@ +# Chapter 1 + +```rust,editable +fn main() { + println!("Hello, world!"); +} +``` diff --git a/tests/gui/books/editor/src/chapter_2.md b/tests/gui/books/editor/src/chapter_2.md new file mode 100644 index 00000000..7ebb5961 --- /dev/null +++ b/tests/gui/books/editor/src/chapter_2.md @@ -0,0 +1 @@ +# Chapter 2 diff --git a/tests/gui/editor-keypress.goml b/tests/gui/editor-keypress.goml new file mode 100644 index 00000000..6b1a96fc --- /dev/null +++ b/tests/gui/editor-keypress.goml @@ -0,0 +1,23 @@ +// Tests for global keypress handlers when ACE editor is in focus. +// See https://github.com/rust-lang/mdBook/issues/3064 + +go-to: |DOC_PATH| + "editor/chapter_1.html" + +click: ".ace_editor" +press-key: "s" +// Wait briefly to allow any event handlers triggered by the keypress to run. +// Otherwise there is a race here since the wrapper is already display:none. +wait-for: 200 +wait-for-css: ("#mdbook-search-wrapper", {"display": "none"}) + +press-key: "?" +wait-for: 200 +wait-for-css: ("#mdbook-help-container", {"display": "flex"}) + +// Make sure arrow keys don"t navigate. +press-key: "ArrowRight" +wait-for: 200 +assert-window-property: ({"location": |DOC_PATH| + "editor/chapter_1.html"}) +press-key: "ArrowLeft" +wait-for: 200 +assert-window-property: ({"location": |DOC_PATH| + "editor/chapter_1.html"}) From c5c31bb9f5dfb10d66f6b6478395e2068d593949 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Mon, 4 May 2026 11:48:41 -0700 Subject: [PATCH 3/4] Fix global keypress handler with the ACE editor This fixes an issue where pressing `?` inside the ACE editor was opening the help popup. The solution here is to ensure that the global keypress handler works the same as the one used in the search handler. Fortunately other keypresses like left and right were working OK because ACE was doing something like stopPropagation (I'm not sure exactly). Fixes https://github.com/rust-lang/mdBook/issues/3064 --- crates/mdbook-html/front-end/js/book.js | 19 +++++++++++++++---- .../front-end/searcher/searcher.js | 2 +- tests/gui/editor-keypress.goml | 3 ++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/mdbook-html/front-end/js/book.js b/crates/mdbook-html/front-end/js/book.js index 62d7c4cc..ed1dfca4 100644 --- a/crates/mdbook-html/front-end/js/book.js +++ b/crates/mdbook-html/front-end/js/book.js @@ -19,6 +19,14 @@ function playground_text(playground, hidden = true) { } } +/** + * Helper for global keypress handlers so they don't trigger when certain elements are active. + * @returns {boolean} True if the keypress handler should be skipped. + */ +function mdbook_something_else_has_focus(e) { + return /^(?:input|select|textarea)$/i.test(e.target.nodeName); +} + (function codeSnippets() { function fetch_with_timeout(url, options, timeout = 6000) { return Promise.race([ @@ -648,12 +656,15 @@ aria-label="Show hidden lines">'; (function chapterNavigation() { document.addEventListener('keydown', function(e) { - if (e.altKey || e.ctrlKey || e.metaKey) { - return; - } - if (window.search && window.search.hasFocus()) { + if (e.altKey || + e.ctrlKey || + e.metaKey || + window.search && window.search.hasFocus() || + mdbook_something_else_has_focus(e) + ) { return; } + const html = document.querySelector('html'); function next() { diff --git a/crates/mdbook-html/front-end/searcher/searcher.js b/crates/mdbook-html/front-end/searcher/searcher.js index 5c4bab65..ea17ae38 100644 --- a/crates/mdbook-html/front-end/searcher/searcher.js +++ b/crates/mdbook-html/front-end/searcher/searcher.js @@ -357,7 +357,7 @@ window.search = window.search || {}; e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text' || - !hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName) + !hasFocus() && mdbook_something_else_has_focus(e) ) { return; } diff --git a/tests/gui/editor-keypress.goml b/tests/gui/editor-keypress.goml index 6b1a96fc..f2cfe6f2 100644 --- a/tests/gui/editor-keypress.goml +++ b/tests/gui/editor-keypress.goml @@ -10,9 +10,10 @@ press-key: "s" wait-for: 200 wait-for-css: ("#mdbook-search-wrapper", {"display": "none"}) +// ? inside ACE editor shouldn't show global help popup. press-key: "?" wait-for: 200 -wait-for-css: ("#mdbook-help-container", {"display": "flex"}) +wait-for-css: ("#mdbook-help-container", {"display": "none"}) // Make sure arrow keys don"t navigate. press-key: "ArrowRight" From 4fb647c760ec10a5c4a7882cc44527b21ac7615d Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Mon, 4 May 2026 11:51:32 -0700 Subject: [PATCH 4/4] Fix global keypress handling with shadow DOM elements This fixes an issue where global keypresses were being caught when they shouldn't when there is a shadow DOM element with focus. This can happen if the user has included their own extensions. Arguably the extension should do a stopPropagation, but they don't always do that. I think this is relatively safe way to approach this. Fixes https://github.com/rust-lang/mdBook/issues/2507 --- crates/mdbook-html/front-end/js/book.js | 5 ++++- tests/gui/shadow-dom.goml | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/mdbook-html/front-end/js/book.js b/crates/mdbook-html/front-end/js/book.js index ed1dfca4..fe74b710 100644 --- a/crates/mdbook-html/front-end/js/book.js +++ b/crates/mdbook-html/front-end/js/book.js @@ -24,7 +24,10 @@ function playground_text(playground, hidden = true) { * @returns {boolean} True if the keypress handler should be skipped. */ function mdbook_something_else_has_focus(e) { - return /^(?:input|select|textarea)$/i.test(e.target.nodeName); + // Check composedPath in case the event happened from something generated + // from the shadowDOM. + const target = e.composedPath()[0] || e.target; + return /^(?:input|select|textarea)$/i.test(target.nodeName); } (function codeSnippets() { diff --git a/tests/gui/shadow-dom.goml b/tests/gui/shadow-dom.goml index b1f11210..2b401f20 100644 --- a/tests/gui/shadow-dom.goml +++ b/tests/gui/shadow-dom.goml @@ -8,11 +8,13 @@ press-key: 'x' wait-for: '#shadow-input-host' // See what happens when the s key is pressed. press-key: 's' -wait-for-css-false: ("#mdbook-search-wrapper", {"display": "none"}) +wait-for: 200 +wait-for-css: ("#mdbook-search-wrapper", {"display": "none"}) // Also try the global key handlers. reload: press-key: 'x' wait-for: '#shadow-input-host' press-key: '?' -wait-for-css: ("#mdbook-help-container", {"display": "flex"}) +wait-for: 200 +wait-for-css: ("#mdbook-help-container", {"display": "none"})