I have determined the root cause, and identified a fix.
The issue is that in order for Chromium to allow keyboard control of scrolling, the element that owns the scrollbars must be focused. When you click on a child of such an element, Chromium internally focuses it (in the sense of directing keyboard events there). But you cannot programmatically focus()
the element if it’s a div or other non-input element.
However, if you explicitly set a tabIndex
for the element in question (specifically, the markdown-preview-view
div, aka the .view.previewMode.containerEl.children[0]
of the active leaf), then it can be programmatically focus()
ed. Doing so as part of pane activation fixes the problem, as long as the focusing takes place after the document.activeElement.blur()
that’s done by the “focus on pane” commands. (Otherwise, the just-applied focus is removed.)
ISTM that this needs to be factored into some type of interface for telling a leaf and its view that they have become active, since the workspace can’t know (and shouldn’t have to) what element needs a tabindex or should be programmatically focused. Arguably, setActiveLeaf()
should also be in charge of some of the logic being done by the “Focus on pane” commands, e.g., blurring the active element and dropping selections before activating the new pane and telling it that it should become focused.
So rather than directly manipulating the leaf contents, setActiveLeaf()
would simply tell the leaf to activate itself through a method that would in turn tell the view to activate itself as well. The default behavior to trigger file-open
events, record history, set the active time, and so on would then belong to the WorkspaceLeaf and View/FileView classes, not the Workspace. (Otherwise, plugins will not be able to properly handle focus issues in custom leaf and view types.)
Anyway, adding a tabIndex and calling focus() on the correct element from setActiveLeaf()
fixes this problem if I use the “Cycle through panes” plugin to do the keyboard navigation. (The “Focus on pane” commands still don’t work because they do their blurring and deselecting after calling setActiveLeaf()
instead of before; ideally, the fix would just move that logic to setActiveLeaf()
and drop it from the focusing commands.)
For clarity, here is the code I’m currently using to work around this issue (excerpted from a plugin’s onload()
), that hooks the file-open
event to add the behavior to setActiveLeaf()
:
// Whenever a pane is activated, reveal it and ensure focus is given
this.registerEvent( this.app.workspace.on('file-open', () => {
const ws = this.app.workspace,
leaf = ws.activeLeaf,
view = leaf.view;
// Ensure the activated leaf is visible (needed for sidebar documents)
ws.revealLeaf(leaf);
// Drop old leaf's focus, give it to the new one
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
window.getSelection().removeAllRanges();
leaf.setEphemeralState({ focus: true })
if (view instanceof MarkdownView) {
if (view.getMode() === "source") {
// Codemirror allows programmatic focus
view.sourceMode.cmEditor.focus();
} else {
// Preview needs the scrollbar owner focused, but it can't without tabIndex
view.previewMode.containerEl.children[0].tabIndex = -1;
view.previewMode.containerEl.children[0].focus();
}
}
}));
In actual practice, the later part of this code would presumably be done by the view itself. (And the blur/removeranges/setEphemeral stuff would be removed from the “Focus on pane” commands.)
Anyway, this snippet works for me to get focus in the panes, as long as I use the “Cycle through panes” plugin’s commands rather than the built-in ones (which blur away the focusing done by the above code after setActiveLeaf()
returns.)
(It does not, however, handle focusing the scroll region when you toggle in and out of edit mode. Presumably, adding the same focus()
call to the activation code for preview mode would fix that as well.)