Scrolling with arrow keys doesn't work correctly after changing focus through "Focus on pane"

Sorry for the lengthy title couldn’t come up with anything better.

Steps to reproduce

  1. Focus on a pane (with a note that is in preview) by using the hotkeys “Focus on pane (above/below/to the left/to the right)”
  2. Use the arrow keys to scroll on that note.

Expected result

The arrow keys allow for scrolling the note that is in focus.

Actual result

The note in focus doesn’t scroll, and if the last note that I clicked while in preview is still in preview it’ll scroll on that one.

Environment

  • Operating system: Windows 10
  • Obsidian version: v0.7.6 (No custom CSS)

Additional information

What I’m doing:

  1. Clicking on the middle note (after this I move the mouse to the left most pane to show that everything else is being done through the keyboard)
  2. Changing focus to the note on the right through the hotkey (look at the underlining in the title)
  3. Trying to scroll on the right-most note (that is in focus) through arrow keys but instead it scrolls on the middle one

Thanks for the report. I can reproduce.

1 Like

Do you still have this problem in 0.8.5?

Steps to reproduce

  1. have (at least) 2 note panes open
  2. use keyboard shortcuts to switch between the panes
  3. use arrow keys to scroll up/down (with the active pane in preview mode)

Expected result

the arrow keys should navigate the active pane

Actual result

When the current active pane is in preview mode, the arrow keys navigate the pane I last clicked the mouse on. In edit mode, the arrow keys do navigate the active pane

Environment

  • Operating system: windows 10
  • Obsidian version: 0.8.12

Steps to reproduce

  1. Open a file and go to edit mode
  2. edit somthing and go back to preview mode to read the file

Expected result

As i can go up and down to the file via arrow keys

Actual result

Arrow keys don’t work after coming back from edit mode without clicking the file

Environment

  • Operating system: Linux
  • Obsidian version: Obsidian v0.7.6

Additional information

1 Like

(Sorry for the late response)
0.8.15 and it still happens.

still there in 0.9.10

Do you still have this issue in v0.10.1?

Yes, it still happens.

Steps to reproduce

  1. Open multiple panes, some in edit, some in preview (preview content and/or screen sized so as to have enough content to scroll)
  2. Use the “focus pane to the right/left/up/down” commands to move from a source pane to a preview pane next to it
  3. Use the page up/down keys or arrow keys to try to scroll the preview content

Expected result

Preview content should scroll in response to the arrows or page up/down keys

Actual result

Content does not scroll; instead, the content of the last-accessed pane containing a source mode note responds to the page up/down keystrokes. (Or sometimes, another preview pane, if that pane has been clicked on, and the target pane has not.)

Environment

  • Operating system: Windows
  • Obsidian version: 0.10.1

Additional information

The scrollbars work normally if you click into the pane rather than using keyboard commands. They also work normally if the pane is in source mode. They only fail if the active pane is in preview mode, and you activated the pane using keyboard commands.

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.)

I’ve ran into using tabIndex+focus during my research as well. I think it’s pretty hacky (that it messed with tabindex), but it also adds a “focused” state on an element that shouldn’t receive focus state:
image

While we could hide the outline using CSS, I still don’t think it’s a good way to go, unless there’s absolutely no proper web API that does this.

I’m trying to find the W3C spec on how arrow/space/pageup/down navigation works, but I can’t seem to find it. Anyone has an idea?

As far as I can tell, this is the standard, proper web API for doing this. That is, focus() is the proper API for focusing an element, and tabIndex is the standard way to make an otherwise not-focusable element focus()able.

In essence, the problem is that the pseudo-focusing behavior that browsers do when you click a non-focusable scroll container, is actually nonstandard. That is, IIUC, the standards do not say what should happen in such a case, and browsers just do the relatively sane thing. (See CSS 3 scroll containers.)

Ironically, what this means is that the fact Obsidian works when clicking in scroll containers is the part that isn’t following web standards! :wink:

TBH, I see no problem with that element having the focused state, since it is focused from all practical perspectives. Keyboard events are going there, it’s the active pane… in what sense is it not the focused element, when working correctly?

I guess that’s what I was looking for from a standards perspective. Shame there isn’t one :joy:

Fair enough. I’ll look into a clean way to have this done.

FWIW, after reading over the tabindex specs I switched from using tabindex=0 to tabindex=-1, since the latter means “allow focusing but don’t put it in the actual tab sequence”. This should avoid it affecting the current tabbing order.

And for the appearance issue, div.markdown-preview-view:focus { outline: none; } was sufficient for my plugin to turn off the focus outlines. The combination effectively works around the overall issue without changing either the visible appearance or the current tab order within Obsidian.

will be fixed 0.10.3

2 Likes