Skip to content

Commit fd6b90f

Browse files
committed
fix: md viewer skips auto-refocus for parent-side picker shortcuts
When the md viewer iframe forwards an unhandled shortcut to Phoenix, it re-focuses its own viewer-content 100ms later so commands like Save return focus to the editor. That timer also fired for shortcuts that open parent UIs (Quick Open, Find in Files) — pulling focus out of the picker and making the dropdown vanish on the first keystroke. Send the set of "skip refocus" key strings from the parent to the iframe at runtime. MarkdownSync reads the bindings for the relevant commands from KeyBindingManager and posts MDVIEWR_SKIP_REFOCUS_KEYS on iframe ready and whenever a binding changes. The iframe stores the set and builds the same canonical key string KBM uses from each keydown to decide whether to skip the refocus. Adding more shortcuts is now just appending a command id to SKIP_REFOCUS_COMMANDS in MarkdownSync — no hardcoded keys in the iframe. Also remove the QuickOpen design-mode exit branch added earlier: the floating picker variant replaces that workaround entirely.
1 parent 49a1647 commit fd6b90f

3 files changed

Lines changed: 114 additions & 14 deletions

File tree

src-mdviewer/src/bridge.js

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,35 @@ let _baseURL = "";
2020
let _cursorPosBeforeEdit = null; // cursor position before current edit batch
2121
let _cursorPosDirty = false; // true after content changes, reset when emitted
2222
let _pendingReloadScroll = null; // { filePath, scrollSourceLine } for scroll restore after reload
23+
// Key strings ("Ctrl-Shift-O", "Ctrl-Shift-F", …) for shortcuts Phoenix
24+
// handles by opening a UI that needs focus — we must not auto-refocus our
25+
// own editor after forwarding those. Populated from the parent via the
26+
// MDVIEWR_SKIP_REFOCUS_KEYS message; starts empty so bugs in the parent
27+
// can't break the default refocus behavior.
28+
let _skipRefocusKeys = new Set();
29+
const _isMacForKey = /Mac|iPhone|iPad/.test(navigator.platform);
30+
31+
/**
32+
* Mirror Phoenix's KeyBindingManager _buildKeyDescriptor to produce the
33+
* same canonical key string from a KeyboardEvent so we can look it up in
34+
* _skipRefocusKeys. Non-mac: "Ctrl-Shift-O"; mac: "Shift-Cmd-O".
35+
*/
36+
function _eventToKeyString(e) {
37+
const parts = [];
38+
if (_isMacForKey && e.ctrlKey) { parts.push("Ctrl"); }
39+
if (e.altKey) { parts.push("Alt"); }
40+
if (e.shiftKey) { parts.push("Shift"); }
41+
if (!_isMacForKey && e.ctrlKey) { parts.unshift("Ctrl"); }
42+
if (_isMacForKey && e.metaKey) { parts.push("Cmd"); }
43+
let key = e.key || "";
44+
if (key.length === 1) { key = key.toUpperCase(); }
45+
parts.push(key);
46+
return parts.join("-");
47+
}
48+
49+
function _isSkipRefocusShortcut(e) {
50+
return _skipRefocusKeys.has(_eventToKeyString(e));
51+
}
2352

2453
/**
2554
* Check if a URL is absolute (not relative to the document).
@@ -317,6 +346,14 @@ export function initBridge() {
317346
window.getSelection().removeAllRanges();
318347
document.body.click();
319348
break;
349+
case "MDVIEWR_SKIP_REFOCUS_KEYS":
350+
// Phoenix tells us which forwarded shortcuts should NOT
351+
// trigger our auto-refocus (e.g. Quick Open, Find in Files
352+
// — shortcuts that open parent-side UI which needs focus).
353+
if (Array.isArray(data.keys)) {
354+
_skipRefocusKeys = new Set(data.keys);
355+
}
356+
break;
320357
}
321358
});
322359

@@ -416,13 +453,18 @@ export function initBridge() {
416453
altKey: e.altKey
417454
});
418455
// Refocus md editor after Phoenix handles the shortcut
419-
// (some commands like Save focus the CM editor)
420-
setTimeout(() => {
421-
const content = document.getElementById("viewer-content");
422-
if (content && getState().editMode) {
423-
content.focus({ preventScroll: true });
424-
}
425-
}, 100);
456+
// (e.g. Save focuses the CM editor). Phoenix sends us the
457+
// list of shortcuts for which focus should stay with the
458+
// parent (Quick Open, Find in Files …) via
459+
// MDVIEWR_SKIP_REFOCUS_KEYS. Don't refocus for those.
460+
if (!_isSkipRefocusShortcut(e)) {
461+
setTimeout(() => {
462+
const content = document.getElementById("viewer-content");
463+
if (content && getState().editMode) {
464+
content.focus({ preventScroll: true });
465+
}
466+
}, 100);
467+
}
426468
}
427469
}
428470
}, true);

src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,23 @@ define(function (require, exports, module) {
2525
const ThemeManager = require("view/ThemeManager"),
2626
NativeApp = require("utils/NativeApp"),
2727
EditorManager = require("editor/EditorManager"),
28+
AppInit = require("utils/AppInit"),
29+
CommandManager = require("command/CommandManager"),
30+
Commands = require("command/Commands"),
31+
KeyBindingManager = require("command/KeyBindingManager"),
2832
utils = require("./utils");
2933

34+
// Commands whose shortcuts, when forwarded from the md viewer iframe,
35+
// open a parent-side UI that needs to keep keyboard focus. The iframe's
36+
// 100ms auto-refocus must skip these shortcuts — otherwise it yanks
37+
// focus out of the picker immediately after it opens. We send the raw
38+
// key strings (resolved at runtime from KeyBindingManager) so the
39+
// iframe stays theme/rebind-agnostic.
40+
const SKIP_REFOCUS_COMMANDS = [
41+
Commands.NAVIGATE_QUICK_OPEN,
42+
Commands.CMD_FIND_IN_FILES
43+
];
44+
3045
let _active = false;
3146
let _doc = null;
3247
let _$iframe = null;
@@ -348,16 +363,52 @@ define(function (require, exports, module) {
348363

349364
// --- iframe ready ---
350365

366+
/**
367+
* Collect the current `key` strings bound to each SKIP_REFOCUS_COMMANDS
368+
* command and send them to the iframe so its keyboard-shortcut forward
369+
* path can skip its own 100ms auto-refocus for these shortcuts.
370+
*/
371+
function _sendSkipRefocusShortcuts() {
372+
const iframeWindow = _getIframeWindow();
373+
if (!iframeWindow) { return; }
374+
const keys = [];
375+
SKIP_REFOCUS_COMMANDS.forEach(function (cmdID) {
376+
const bindings = KeyBindingManager.getKeyBindings(cmdID) || [];
377+
bindings.forEach(function (b) {
378+
if (b && b.key) { keys.push(b.key); }
379+
});
380+
});
381+
iframeWindow.postMessage({
382+
type: "MDVIEWR_SKIP_REFOCUS_KEYS",
383+
keys: keys
384+
}, "*");
385+
}
386+
351387
function _onIframeReady() {
352388
_iframeReady = true;
353389
_sendContent();
354390
_sendTheme();
355391
_sendLocale();
392+
_sendSkipRefocusShortcuts();
356393
if (_onIframeReadyCallback) {
357394
_onIframeReadyCallback();
358395
}
359396
}
360397

398+
// Re-send shortcuts if the user rebinds Quick Open / Find in Files.
399+
// Deferred to appReady so the commands have been registered before we
400+
// try to hook their key-binding-change events.
401+
AppInit.appReady(function () {
402+
SKIP_REFOCUS_COMMANDS.forEach(function (cmdID) {
403+
const cmd = CommandManager.get(cmdID);
404+
if (cmd && cmd.on) {
405+
cmd.on(KeyBindingManager.EVENT_KEY_BINDING_ADDED, function () {
406+
if (_iframeReady) { _sendSkipRefocusShortcuts(); }
407+
});
408+
}
409+
});
410+
});
411+
361412
// --- Phoenix → iframe ---
362413

363414
/**

src/search/QuickOpen.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,22 +108,29 @@ define(function (require, exports, module) {
108108
if (isInPicker(e.target)) { return; }
109109
close();
110110
}
111-
window.document.addEventListener("mousedown", onMousedown, true);
112-
// Clicks inside the live-preview iframe never surface as mousedown
113-
// events on the parent document — the iframe consumes them. Detect
114-
// that case via the window blur event (focus moves to the iframe's
115-
// content window) and a slight delay to let focus settle before
116-
// deciding whether we really moved out of the picker.
111+
// Defer attaching the dismiss listeners by a tick so any transient
112+
// focus / blur events fired while the picker is opening (common when
113+
// the shortcut was forwarded from an iframe like the md viewer —
114+
// focus transfers between iframe and parent input can fire spurious
115+
// window.blur) don't close the picker before it's ready.
116+
let dismissArmed = false;
117117
function onWindowBlur() {
118+
if (!dismissArmed) { return; }
118119
window.setTimeout(function () {
119120
if (closed) { return; }
120121
const active = window.document.activeElement;
121122
if (active && isInPicker(active)) { return; }
122123
close();
123124
}, 0);
124125
}
125-
window.addEventListener("blur", onWindowBlur);
126+
const armTimer = window.setTimeout(function () {
127+
if (closed) { return; }
128+
dismissArmed = true;
129+
window.document.addEventListener("mousedown", onMousedown, true);
130+
window.addEventListener("blur", onWindowBlur);
131+
}, 120);
126132
closeHandlers.push(function () {
133+
window.clearTimeout(armTimer);
127134
window.document.removeEventListener("mousedown", onMousedown, true);
128135
window.removeEventListener("blur", onWindowBlur);
129136
});

0 commit comments

Comments
 (0)