Skip to content

fix: prevent mouse event passthrough on floating sidebar#236

Merged
yonaries merged 2 commits intomainfrom
fix/pass-through-sidebar
Mar 14, 2026
Merged

fix: prevent mouse event passthrough on floating sidebar#236
yonaries merged 2 commits intomainfrom
fix/pass-through-sidebar

Conversation

@yonaries
Copy link
Copy Markdown
Contributor

@yonaries yonaries commented Mar 14, 2026

Summary

  • Injects a transparent shield <div> into the web page when the floating sidebar is visible, blocking hover effects and cursor changes on web content behind the sidebar
  • Uses callAsyncJavaScript with parameterized arguments to avoid JS injection risks

@tembo tembo bot added the bug Something isn't working label Mar 14, 2026
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 14, 2026

Greptile Summary

This PR injects a transparent shield <div> into the active tab's web page whenever the floating sidebar is visible, preventing underlying web content from capturing hover/cursor events. It also promotes FloatingSidebarOverlay's fraction constants to static so BrowserView can use the same clamped range without duplication.

Key changes:

  • FloatingSidebarOverlay exposes minFraction/maxFraction as static let constants and a matching clampedSidebarFraction computed property is added to BrowserView.
  • injectSidebarMouseShield(visible:) uses callAsyncJavaScript (parameterized) to insert/remove the shield on the active tab's WKWebView.
  • onChange(of: showFloatingSidebar) injects or removes the shield when the sidebar toggles.
  • onChange(of: tabManager.activeTab) is extended to remove the shield from the old tab and inject it into the new one on a tab switch.
  • Issue: On a tab switch to an uninitialized tab (!isWebViewReady), the shield is injected before restoreTransientState runs — which creates a brand-new WKWebView and discards the one the shield was injected into, leaving the new webView unprotected.
  • Note: The PR description claims an OraWebView WKWebView subclass with a mouseMoved override was added as a secondary layer, but no such class exists in the diff or codebase.
  • Note: The shield is not re-injected on same-tab page navigations, so navigating to a new URL while the sidebar is visible leaves the web content unshielded.

Confidence Score: 2/5

  • The PR has a reliable logic bug where the shield is injected into the wrong (soon-to-be-replaced) WKWebView on tab switches to uninitialized tabs, meaning the core protection is absent in that common scenario.
  • The shield-injection ordering bug (injecting before restoreTransientState replaces the webView) means the feature does not work as intended for any tab that hasn't yet been restored. Additionally, the PR description references an OraWebView secondary defense layer that is entirely absent from the code.
  • ora/Features/Browser/Views/BrowserView.swift — specifically the onChange(of: tabManager.activeTab) handler and the injectSidebarMouseShield function.

Important Files Changed

Filename Overview
ora/Features/Browser/Views/BrowserView.swift Adds shield injection and tab-switch cleanup logic, but the injection races with restoreTransientState (which replaces the webView), leaving freshly-restored tabs unprotected. Also missing re-injection on same-tab navigation.
ora/Features/Browser/Views/FloatingSidebarOverlay.swift Promotes minFraction/maxFraction to static let constants and references them via Self — a clean refactor that enables BrowserView to use the same clamping values with no behavioral change.

Sequence Diagram

sequenceDiagram
    participant User
    participant BrowserView
    participant FloatingSidebarOverlay
    participant TabManager
    participant Tab
    participant WKWebView

    User->>FloatingSidebarOverlay: Mouse enters sidebar strip
    FloatingSidebarOverlay->>BrowserView: showFloatingSidebar = true
    BrowserView->>BrowserView: onChange(showFloatingSidebar)
    BrowserView->>WKWebView: callAsyncJavaScript — inject shield div

    User->>TabManager: Switch to uninitialized tab
    TabManager->>BrowserView: onChange(activeTab)
    BrowserView->>WKWebView: evaluateJavaScript — remove shield from oldTab
    BrowserView->>WKWebView: callAsyncJavaScript — inject shield into placeholder WKWebView ⚠️
    Note over BrowserView,WKWebView: Shield is in the placeholder WebView!
    BrowserView->>Tab: asyncAfter(0.01s) restoreTransientState()
    Tab->>WKWebView: Creates brand-new WKWebView (discards placeholder)
    Note over Tab,WKWebView: Shield is lost — new WKWebView has no shield

    User->>WKWebView: Navigate to new URL (same tab)
    WKWebView->>WKWebView: Page loads, old DOM (with shield) is destroyed
    Note over BrowserView,WKWebView: No re-injection triggered — sidebar still visible but unshielded
Loading

Last reviewed commit: 4934c44

@yonaries yonaries marked this pull request as draft March 14, 2026 08:44
@yonaries yonaries linked an issue Mar 14, 2026 that may be closed by this pull request
@yonaries yonaries marked this pull request as ready for review March 14, 2026 08:58
@yonaries yonaries requested a review from kenenisa as a code owner March 14, 2026 08:58
@yonaries yonaries merged commit ae892cf into main Mar 14, 2026
2 checks passed
@yonaries yonaries deleted the fix/pass-through-sidebar branch March 14, 2026 08:58
Comment on lines +124 to +128
.onChange(of: tabManager.activeTab) { oldTab, newTab in
if showFloatingSidebar {
oldTab?.webView.evaluateJavaScript(Self.removeShieldJS, completionHandler: nil)
injectSidebarMouseShield(visible: true)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shield injected into webView that gets replaced by restoreTransientState

When the active tab switches to one where !tab.isWebViewReady, the injection and the tab restoration race:

  1. injectSidebarMouseShield(visible: true) fires immediately, calling callAsyncJavaScript on tab.webView — which at this point is the uninitialized placeholder WKWebView with no loaded document.
  2. 10 ms later, restoreTransientState creates a brand-new WKWebView, assigns it to tab.webView, and loads the URL.

The injected shield ends up in the discarded placeholder webView; the real webView that the user sees never gets the shield. The floating sidebar therefore has no DOM-level mouse-event protection on any freshly-restored tab.

The fix is to move the shield injection inside the asyncAfter block, after restoreTransientState has run:

.onChange(of: tabManager.activeTab) { oldTab, newTab in
    if showFloatingSidebar {
        oldTab?.webView.evaluateJavaScript(Self.removeShieldJS, completionHandler: nil)
    }
    if let tab = newTab, !tab.isWebViewReady {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
            tab.restoreTransientState(
                historyManager: historyManager,
                downloadManager: downloadManager,
                tabManager: tabManager,
                isPrivate: privacyMode.isPrivate
            )
            if self.showFloatingSidebar {
                self.injectSidebarMouseShield(visible: true)
            }
        }
    } else if showFloatingSidebar {
        injectSidebarMouseShield(visible: true)
    }
}

Comment on lines +106 to +108
.onChange(of: showFloatingSidebar) { _, visible in
injectSidebarMouseShield(visible: visible)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shield lost on same-tab page navigation

The shield is re-injected in two places: when showFloatingSidebar toggles (here) and when tabManager.activeTab changes. However, if the user navigates to a new URL within the same tab while the floating sidebar is visible, the old DOM (including the shield div) is torn down by WebKit, and neither of these observers fires — so the sidebar has no shield for the remainder of that browsing session until the user closes and reopens it.

Consider hooking into WKNavigationDelegate.webView(_:didFinish:) (or a dedicated notification) to re-inject the shield after navigation commits when the sidebar is visible. For example, in WebViewNavigationDelegate, you could post a notification on didFinishNavigation and observe it here:

.onReceive(NotificationCenter.default.publisher(for: .webViewDidFinishNavigation)) { _ in
    if showFloatingSidebar {
        injectSidebarMouseShield(visible: true)
    }
}

Comment on lines +38 to +67
private func injectSidebarMouseShield(visible: Bool) {
guard let webView = tabManager.activeTab?.webView else { return }
if visible {
let side = sidebarManager.sidebarPosition == .primary ? "left" : "right"
let widthVW = clampedSidebarFraction * 100
webView.callAsyncJavaScript(
"""
var e = document.getElementById('ora-sb-shield');
if (e) e.remove();
var d = document.createElement('div');
d.id = 'ora-sb-shield';
d.style.position = 'fixed';
d.style.top = '0';
d.style[side] = '0';
d.style.width = widthVW + 'vw';
d.style.height = '100vh';
d.style.zIndex = '2147483647';
d.style.pointerEvents = 'auto';
d.style.cursor = 'default';
document.documentElement.appendChild(d);
""",
arguments: ["side": side, "widthVW": widthVW],
in: nil,
in: .page,
completionHandler: nil
)
} else {
webView.evaluateJavaScript(Self.removeShieldJS, completionHandler: nil)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OraWebView secondary defense layer described but not implemented

The PR description states:

"Introduces OraWebView (WKWebView subclass) with a mouseMoved override as a secondary defense layer"

However, no OraWebView class exists anywhere in the codebase — Tab.webView is still typed as WKWebView and initialized directly as one. If this secondary layer was intentionally left out, the PR description should be updated. If it was accidentally omitted, the protection is incomplete: the mouseMoved override was expected to serve as a fallback for cases where the DOM shield is not yet present (e.g., immediately after a tab switch).

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mouse cursor is interactive to viewport behind the sidebar

1 participant