Skip to content

[Feature Reqiest] Add "Auto-focus tab when content matches" toggle for separate sidebar tabs (Mobile drawer ergonomics) #69

Description

@bobrock-git

[Feature Request] Add "Auto-focus tab when content matches" toggle for separate sidebar tabs (Mobile drawer ergonomics)

Problem / Motivation

When using Virtual Content with renderLocation: "sidebar" and showInSeparateTab: true", the content is rendered into a dedicated sidebar tab (e.g., virtual-content-separate-view-X).

This works great, but introduces a UX friction point—especially on Obsidian Mobile:

  • Obsidian's right sidebar / mobile drawer remembers the last active tab (e.g. Outline, Backlinks, Tags, or another plugin view).
  • When a user opens a note that triggers a specific Virtual Content sidebar rule (such as a Dashboard, Project note, or CRM entity) and swipes left to open the drawer expecting that tab, Obsidian displays whichever unrelated tab was active previously.
  • The user is then forced to perform a second, precise tap on the small tab icon in the mobile header.

Proposed Solution

Add an optional toggle under sidebar rule settings:

  • "Auto-focus tab when content matches"
  • When enabled, whenever the active note matches the rule and generates content, the plugin automatically ensures that this tab is selected as the active tab in its parent sidebar container.

Key UX Detail: Calling leaf.parent.selectTab(leaf) switches the active tab inside the mobile drawer / sidebar without calling revealLeaf(). This means the drawer does not jump open or disrupt the user while reading or typing—it simply guarantees that when the user does swipe the drawer open, the correct tab is already active and waiting.


Implementation Details & Code

Here is the implementation that can be added to the plugin:

1. Extend the Rule interface & defaults

In rule type definitions:

export interface Rule {
    // ... existing fields ...
    showInSeparateTab?: boolean;
    sidebarTabName?: string;
    autoFocusSidebarTab?: boolean; // NEW: Auto-select tab when content matches
}

In default settings / rule sanitization:

autoFocusSidebarTab: typeof rule.autoFocusSidebarTab === "boolean" ? rule.autoFocusSidebarTab : false,

2. Add Setting Toggle in RuleModal

Inside renderLocationSpecificControls(containerEl: HTMLElement):

if (this.workingRule.renderLocation === "sidebar") {
    new Setting(containerEl)
        .setName("Show in separate tab")
        .setDesc("If enabled, this content appears in its own sidebar tab.")
        .addToggle(toggle => toggle
            .setValue(this.workingRule.showInSeparateTab ?? false)
            .onChange(value => {
                this.workingRule.showInSeparateTab = value;
                this.render();
            })
        );

    if (this.workingRule.showInSeparateTab) {
        new Setting(containerEl)
            .setName("Sidebar tab name")
            .setDesc("If empty, a default name is used.")
            .addText(text => text
                .setPlaceholder("e.g., Related notes")
                .setValue(this.workingRule.sidebarTabName || "")
                .onChange(value => {
                    this.workingRule.sidebarTabName = value;
                })
            );

        // NEW: Auto-focus toggle
        new Setting(containerEl)
            .setName("Auto-focus tab when content matches")
            .setDesc("Automatically select this tab in the sidebar/drawer when the active note matches this rule. On mobile, this ensures the tab is immediately visible upon swiping open the drawer.")
            .addToggle(toggle => toggle
                .setValue(this.workingRule.autoFocusSidebarTab ?? false)
                .onChange(value => {
                    this.workingRule.autoFocusSidebarTab = value;
                })
            );
    }
}

3. Update updateAllSidebarViews() in main.ts

When refreshing sidebar views, check if auto-focus is requested and activate the tab:

async updateAllSidebarViews(): Promise<void> {
    const mainLeaves = this.app.workspace.getLeavesOfType(VIRTUAL_CONTENT_VIEW);
    for (const leaf of mainLeaves) {
        if (leaf.view instanceof VirtualContentView) {
            leaf.view.update();
        }
    }

    for (let index = 0; index < this.settings.rules.length; index++) {
        const rule = this.settings.rules[index];
        if (rule.renderLocation === "sidebar" && rule.showInSeparateTab) {
            const viewId = this.getSeparateViewId(index);
            const leaves = this.app.workspace.getLeavesOfType(viewId);
            for (const leaf of leaves) {
                if (leaf.view instanceof VirtualContentView) {
                    leaf.view.update();
                }
            }

            // Auto-focus logic:
            if (rule.autoFocusSidebarTab) {
                const contentObj = this.getSeparateTabContent(viewId);
                if (contentObj && contentObj.content && contentObj.content.trim()) {
                    let targetLeaf = leaves.length > 0 ? leaves[0] : null;

                    // Lazily create the leaf in the right sidebar if it doesn't exist yet
                    if (!targetLeaf && this.app.workspace.getRightLeaf) {
                        const newLeaf = this.app.workspace.getRightLeaf(false);
                        if (newLeaf) {
                            await newLeaf.setViewState({ type: viewId, active: false });
                            targetLeaf = newLeaf;
                        }
                    }

                    // Select tab in container without forcing drawer open
                    if (targetLeaf && targetLeaf.parent) {
                        if (typeof (targetLeaf.parent as any).selectTab === "function") {
                            (targetLeaf.parent as any).selectTab(targetLeaf);
                        } else if (Array.isArray((targetLeaf.parent as any).children)) {
                            const tabIdx = (targetLeaf.parent as any).children.indexOf(targetLeaf);
                            if (tabIdx !== -1) {
                                (targetLeaf.parent as any).currentTab = tabIdx;
                            }
                        }
                    }
                }
            }
        }
    }
}

Why this is safe

  1. Zero UI flicker: selectTab() only mutates the active index of the parent tab container (WorkspaceTabs / WorkspaceMobileDrawer). It does not trigger unprompted popovers or viewport shifts.
  2. Opt-in behavior: By defaulting autoFocusSidebarTab to false, existing workflows and users' tab states remain completely unaffected unless explicitly turned on.
  3. No performance penalty: The check only runs on file switch (updateAllSidebarViews), which already iterates through active sidebar rules.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions