diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useBrowserShellInteractionPassthrough/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useBrowserShellInteractionPassthrough/index.ts new file mode 100644 index 00000000000..d0fa34260cd --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useBrowserShellInteractionPassthrough/index.ts @@ -0,0 +1 @@ +export { useBrowserShellInteractionPassthrough } from "./useBrowserShellInteractionPassthrough"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useBrowserShellInteractionPassthrough/useBrowserShellInteractionPassthrough.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useBrowserShellInteractionPassthrough/useBrowserShellInteractionPassthrough.ts new file mode 100644 index 00000000000..ca2fdb0c855 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useBrowserShellInteractionPassthrough/useBrowserShellInteractionPassthrough.ts @@ -0,0 +1,64 @@ +import type { WorkspaceInteractionState } from "@superset/panes"; +import { useCallback, useEffect, useRef } from "react"; +import { browserRuntimeRegistry } from "../usePaneRegistry/components/BrowserPane"; + +interface UseBrowserShellInteractionPassthroughArgs { + sidebarOpen: boolean; +} + +export function useBrowserShellInteractionPassthrough({ + sidebarOpen, +}: UseBrowserShellInteractionPassthroughArgs) { + const workspaceResizeActiveRef = useRef(false); + const sidebarResizeActiveRef = useRef(false); + + const syncBrowserShellInteractionPassthrough = useCallback(() => { + browserRuntimeRegistry.setShellInteractionPassthrough( + workspaceResizeActiveRef.current || sidebarResizeActiveRef.current, + ); + }, []); + + const onWorkspaceInteractionStateChange = useCallback( + (state: WorkspaceInteractionState) => { + workspaceResizeActiveRef.current = state.resizeActive; + syncBrowserShellInteractionPassthrough(); + }, + [syncBrowserShellInteractionPassthrough], + ); + + const onSidebarResizeDragging = useCallback( + (isDragging: boolean) => { + sidebarResizeActiveRef.current = isDragging; + syncBrowserShellInteractionPassthrough(); + }, + [syncBrowserShellInteractionPassthrough], + ); + + const clearBrowserShellInteractionPassthrough = useCallback(() => { + workspaceResizeActiveRef.current = false; + sidebarResizeActiveRef.current = false; + browserRuntimeRegistry.setShellInteractionPassthrough(false); + }, []); + + useEffect(() => { + window.addEventListener("blur", clearBrowserShellInteractionPassthrough); + return () => { + window.removeEventListener( + "blur", + clearBrowserShellInteractionPassthrough, + ); + clearBrowserShellInteractionPassthrough(); + }; + }, [clearBrowserShellInteractionPassthrough]); + + useEffect(() => { + if (sidebarOpen || !sidebarResizeActiveRef.current) return; + sidebarResizeActiveRef.current = false; + syncBrowserShellInteractionPassthrough(); + }, [sidebarOpen, syncBrowserShellInteractionPassthrough]); + + return { + onSidebarResizeDragging, + onWorkspaceInteractionStateChange, + }; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts index bba11c1868f..31ebb1cc4a6 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/browserRuntimeRegistry.ts @@ -46,6 +46,8 @@ class BrowserRuntimeRegistryImpl { private listenersByPaneId = new Map void>>(); private rootContainer: HTMLDivElement | null = null; private globalListenersInstalled = false; + private windowDragPassthrough = false; + private shellInteractionPassthrough = false; private getListeners(paneId: string): Set<() => void> { let set = this.listenersByPaneId.get(paneId); @@ -57,12 +59,16 @@ class BrowserRuntimeRegistryImpl { } private ensureRootContainer(): HTMLDivElement { - if (this.rootContainer?.isConnected) return this.rootContainer; + if (this.rootContainer?.isConnected) { + this.installGlobalListeners(); + return this.rootContainer; + } const existing = document.getElementById( ROOT_CONTAINER_ID, ) as HTMLDivElement | null; if (existing) { this.rootContainer = existing; + this.installGlobalListeners(); return existing; } const root = document.createElement("div"); @@ -84,15 +90,22 @@ class BrowserRuntimeRegistryImpl { if (this.globalListenersInstalled) return; this.globalListenersInstalled = true; - const setPassthrough = (passthrough: boolean) => { - for (const entry of this.entries.values()) { - if (!entry.visible) continue; - entry.webview.style.pointerEvents = passthrough ? "none" : "auto"; - } - }; - window.addEventListener("dragstart", () => setPassthrough(true), true); - window.addEventListener("dragend", () => setPassthrough(false), true); - window.addEventListener("drop", () => setPassthrough(false), true); + window.addEventListener( + "dragstart", + () => this.setWindowDragPassthrough(true), + true, + ); + window.addEventListener( + "dragend", + () => this.setWindowDragPassthrough(false), + true, + ); + window.addEventListener( + "drop", + () => this.setWindowDragPassthrough(false), + true, + ); + window.addEventListener("blur", () => this.setWindowDragPassthrough(false)); window.addEventListener("resize", () => { for (const entry of this.entries.values()) { @@ -101,6 +114,35 @@ class BrowserRuntimeRegistryImpl { }); } + private setWindowDragPassthrough(passthrough: boolean) { + const wasActive = this.isPointerPassthroughActive(); + this.windowDragPassthrough = passthrough; + this.applyPointerPassthroughIfChanged(wasActive); + } + + setShellInteractionPassthrough(passthrough: boolean): void { + const wasActive = this.isPointerPassthroughActive(); + this.shellInteractionPassthrough = passthrough; + this.applyPointerPassthroughIfChanged(wasActive); + } + + private isPointerPassthroughActive() { + return this.windowDragPassthrough || this.shellInteractionPassthrough; + } + + private applyPointerPassthroughIfChanged(wasActive: boolean) { + const isActive = this.isPointerPassthroughActive(); + if (wasActive !== isActive) this.applyPointerPassthrough(); + } + + private applyPointerPassthrough() { + const passthrough = this.isPointerPassthroughActive(); + for (const entry of this.entries.values()) { + if (!entry.visible) continue; + entry.webview.style.pointerEvents = passthrough ? "none" : "auto"; + } + } + private updateLayout(entry: RegistryEntry) { if (!entry.placeholder) return; const rect = entry.placeholder.getBoundingClientRect(); @@ -349,6 +391,7 @@ class BrowserRuntimeRegistryImpl { this.updateLayout(entry); entry.webview.style.visibility = "visible"; + this.applyPointerPassthrough(); } detach(paneId: string): void { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx index 6784db3ec01..b2ab81d0229 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx @@ -39,6 +39,7 @@ import { V2NotificationStatusIndicator } from "./components/V2NotificationStatus import { V2PresetsBar } from "./components/V2PresetsBar"; import { WorkspaceEmptyState } from "./components/WorkspaceEmptyState"; import { WorkspaceSidebar } from "./components/WorkspaceSidebar"; +import { useBrowserShellInteractionPassthrough } from "./hooks/useBrowserShellInteractionPassthrough"; import { useConsumeAutomationRunLink } from "./hooks/useConsumeAutomationRunLink"; import { useConsumeOpenUrlRequest } from "./hooks/useConsumeOpenUrlRequest"; import { useConsumePendingLaunch } from "./hooks/useConsumePendingLaunch"; @@ -482,6 +483,8 @@ function WorkspaceContent({ ); const sidebarOpen = v2UserPreferences.rightSidebarOpen; + const { onSidebarResizeDragging, onWorkspaceInteractionStateChange } = + useBrowserShellInteractionPassthrough({ sidebarOpen }); useWorkspaceHotkeys({ store, @@ -593,13 +596,14 @@ function WorkspaceContent({ }); }); }} + onInteractionStateChange={onWorkspaceInteractionStateChange} store={store} /> {sidebarOpen && ( <> - + ({ @@ -17,12 +18,16 @@ export function Workspace({ renderAddTabMenu, renderBelowTabBar, onBeforeCloseTab, + onInteractionStateChange, paneActions, contextMenuActions, }: WorkspaceProps) { const tabs = useStore(store, (s) => s.tabs); const activeTabId = useStore(store, (s) => s.activeTabId); const activeTab = tabs.find((t) => t.id === activeTabId) ?? null; + const { onSplitResizeDragging } = useWorkspaceInteractionState({ + onInteractionStateChange, + }); const previousPanesRef = useRef>>(new Map()); useEffect(() => { @@ -91,6 +96,7 @@ export function Workspace({ registry={registry} paneActions={paneActions} contextMenuActions={contextMenuActions} + onSplitResizeDragging={onSplitResizeDragging} /> ) : (
diff --git a/packages/panes/src/react/components/Workspace/components/Tab/Tab.tsx b/packages/panes/src/react/components/Workspace/components/Tab/Tab.tsx index d4e52a2eeaf..7a8065bd18e 100644 --- a/packages/panes/src/react/components/Workspace/components/Tab/Tab.tsx +++ b/packages/panes/src/react/components/Workspace/components/Tab/Tab.tsx @@ -3,7 +3,7 @@ import { ResizablePanel, ResizablePanelGroup, } from "@superset/ui/resizable"; -import { useRef } from "react"; +import { useEffect, useRef } from "react"; import type { StoreApi } from "zustand/vanilla"; import type { WorkspaceStore } from "../../../../../core/store"; import type { @@ -30,6 +30,7 @@ interface TabProps { contextMenuActions?: | ContextMenuActionConfig[] | ((context: RendererContext) => ContextMenuActionConfig[]); + onSplitResizeDragging?: (sourceId: string, isDragging: boolean) => void; } function SplitView({ @@ -40,6 +41,7 @@ function SplitView({ registry, paneActions, contextMenuActions, + onSplitResizeDragging, }: { store: StoreApi>; tab: TabType; @@ -48,10 +50,18 @@ function SplitView({ registry: PaneRegistry; paneActions?: TabProps["paneActions"]; contextMenuActions?: TabProps["contextMenuActions"]; + onSplitResizeDragging?: TabProps["onSplitResizeDragging"]; }) { const groupRef = useRef>(null); const firstSize = node.splitPercentage ?? 50; const secondSize = 100 - firstSize; + const resizeSourceId = `${tab.id}:${path.join(".") || "root"}`; + + useEffect(() => { + return () => { + onSplitResizeDragging?.(resizeSourceId, false); + }; + }, [onSplitResizeDragging, resizeSourceId]); return ( ({ registry={registry} paneActions={paneActions} contextMenuActions={contextMenuActions} + onSplitResizeDragging={onSplitResizeDragging} parentDirection={node.direction} /> - + + onSplitResizeDragging?.(resizeSourceId, isDragging) + } + /> ({ registry={registry} paneActions={paneActions} contextMenuActions={contextMenuActions} + onSplitResizeDragging={onSplitResizeDragging} parentDirection={node.direction} /> @@ -115,6 +131,7 @@ function LayoutNodeView({ registry, paneActions, contextMenuActions, + onSplitResizeDragging, parentDirection = null, }: { store: StoreApi>; @@ -124,6 +141,7 @@ function LayoutNodeView({ registry: PaneRegistry; paneActions?: TabProps["paneActions"]; contextMenuActions?: TabProps["contextMenuActions"]; + onSplitResizeDragging?: TabProps["onSplitResizeDragging"]; parentDirection?: "horizontal" | "vertical" | null; }) { if (node.type === "pane") { @@ -153,6 +171,7 @@ function LayoutNodeView({ registry={registry} paneActions={paneActions} contextMenuActions={contextMenuActions} + onSplitResizeDragging={onSplitResizeDragging} /> ); } @@ -163,6 +182,7 @@ export function Tab({ registry, paneActions, contextMenuActions, + onSplitResizeDragging, }: TabProps) { if (!tab.layout) { return ( @@ -182,6 +202,7 @@ export function Tab({ registry={registry} paneActions={paneActions} contextMenuActions={contextMenuActions} + onSplitResizeDragging={onSplitResizeDragging} />
); diff --git a/packages/panes/src/react/components/Workspace/hooks/useWorkspaceInteractionState/index.ts b/packages/panes/src/react/components/Workspace/hooks/useWorkspaceInteractionState/index.ts new file mode 100644 index 00000000000..0fb538d4c1f --- /dev/null +++ b/packages/panes/src/react/components/Workspace/hooks/useWorkspaceInteractionState/index.ts @@ -0,0 +1 @@ +export { useWorkspaceInteractionState } from "./useWorkspaceInteractionState"; diff --git a/packages/panes/src/react/components/Workspace/hooks/useWorkspaceInteractionState/useWorkspaceInteractionState.ts b/packages/panes/src/react/components/Workspace/hooks/useWorkspaceInteractionState/useWorkspaceInteractionState.ts new file mode 100644 index 00000000000..2693732f6f6 --- /dev/null +++ b/packages/panes/src/react/components/Workspace/hooks/useWorkspaceInteractionState/useWorkspaceInteractionState.ts @@ -0,0 +1,48 @@ +import { useCallback, useEffect, useRef } from "react"; +import type { WorkspaceInteractionState } from "../../../../types"; + +interface UseWorkspaceInteractionStateOptions { + onInteractionStateChange?: (state: WorkspaceInteractionState) => void; +} + +export function useWorkspaceInteractionState({ + onInteractionStateChange, +}: UseWorkspaceInteractionStateOptions) { + const splitResizeSourcesRef = useRef>(new Set()); + const resizeActiveRef = useRef(false); + const onInteractionStateChangeRef = useRef(onInteractionStateChange); + onInteractionStateChangeRef.current = onInteractionStateChange; + + const emitResizeActive = useCallback((resizeActive: boolean) => { + if (resizeActiveRef.current === resizeActive) return; + resizeActiveRef.current = resizeActive; + onInteractionStateChangeRef.current?.({ resizeActive }); + }, []); + + const clearResizeSources = useCallback(() => { + splitResizeSourcesRef.current.clear(); + emitResizeActive(false); + }, [emitResizeActive]); + + const onSplitResizeDragging = useCallback( + (sourceId: string, isDragging: boolean) => { + if (isDragging) { + splitResizeSourcesRef.current.add(sourceId); + } else { + splitResizeSourcesRef.current.delete(sourceId); + } + emitResizeActive(splitResizeSourcesRef.current.size > 0); + }, + [emitResizeActive], + ); + + useEffect(() => { + window.addEventListener("blur", clearResizeSources); + return () => { + window.removeEventListener("blur", clearResizeSources); + clearResizeSources(); + }; + }, [clearResizeSources]); + + return { onSplitResizeDragging }; +} diff --git a/packages/panes/src/react/index.ts b/packages/panes/src/react/index.ts index 4d825812559..a6fdb1547b4 100644 --- a/packages/panes/src/react/index.ts +++ b/packages/panes/src/react/index.ts @@ -7,5 +7,6 @@ export type { PaneRegistry, RendererContext, TabContext, + WorkspaceInteractionState, WorkspaceProps, } from "./types"; diff --git a/packages/panes/src/react/types.ts b/packages/panes/src/react/types.ts index ab4b3058de0..f91ccd0fc0c 100644 --- a/packages/panes/src/react/types.ts +++ b/packages/panes/src/react/types.ts @@ -82,6 +82,10 @@ export interface PaneDefinition { export type PaneRegistry = Record>; +export interface WorkspaceInteractionState { + resizeActive: boolean; +} + export interface WorkspaceProps { store: StoreApi>; registry: PaneRegistry; @@ -96,6 +100,7 @@ export interface WorkspaceProps { tab: Tab, ) => boolean | Promise; onBeforeCloseTab?: (tab: Tab) => boolean | Promise; + onInteractionStateChange?: (state: WorkspaceInteractionState) => void; paneActions?: | PaneActionConfig[] | ((context: RendererContext) => PaneActionConfig[]);