diff --git a/plane-src/apps/web/core/components/issues/peek-overview/view.tsx b/plane-src/apps/web/core/components/issues/peek-overview/view.tsx index 5646381..5766e87 100644 --- a/plane-src/apps/web/core/components/issues/peek-overview/view.tsx +++ b/plane-src/apps/web/core/components/issues/peek-overview/view.tsx @@ -138,6 +138,52 @@ export const IssueView = observer(function IssueView(props: IIssueView) { isDeleteIssueModalOpen || isArchiveIssueModalOpen || isDuplicateIssueModalOpen || isEditIssueModalOpen; const shouldUseBeamPeekShell = !embedIssue && peekMode === "side-peek" && !!beamPeekViewer; + useEffect(() => { + if (typeof window === "undefined" || !shouldUseBeamPeekShell) return; + + const root = document.documentElement; + root.classList.add("nodedc-beam-viewer-interaction-lock"); + + const isViewerEvent = (event: Event) => { + const shell = issuePeekOverviewRef.current; + if (!shell) return false; + + const viewerCard = shell.querySelector(".nodedc-beam-peek-viewer-card"); + const target = event.target instanceof Node ? event.target : null; + const path = typeof event.composedPath === "function" ? event.composedPath() : []; + + return !!viewerCard && ((target && viewerCard.contains(target)) || path.includes(viewerCard)); + }; + + const stopViewerGestureLeak = (event: Event) => { + if (!isViewerEvent(event)) return; + + event.stopPropagation(); + + if ((event.type === "wheel" || event.type === "touchmove") && event.cancelable) { + event.preventDefault(); + } + }; + + const passiveOptions: AddEventListenerOptions = { capture: true, passive: true }; + const activeOptions: AddEventListenerOptions = { capture: true, passive: false }; + + window.addEventListener("wheel", stopViewerGestureLeak, activeOptions); + window.addEventListener("touchmove", stopViewerGestureLeak, activeOptions); + window.addEventListener("pointerdown", stopViewerGestureLeak, passiveOptions); + window.addEventListener("pointermove", stopViewerGestureLeak, passiveOptions); + window.addEventListener("pointerup", stopViewerGestureLeak, passiveOptions); + + return () => { + root.classList.remove("nodedc-beam-viewer-interaction-lock"); + window.removeEventListener("wheel", stopViewerGestureLeak, activeOptions); + window.removeEventListener("touchmove", stopViewerGestureLeak, activeOptions); + window.removeEventListener("pointerdown", stopViewerGestureLeak, passiveOptions); + window.removeEventListener("pointermove", stopViewerGestureLeak, passiveOptions); + window.removeEventListener("pointerup", stopViewerGestureLeak, passiveOptions); + }; + }, [shouldUseBeamPeekShell]); + const getMaxSidePeekWidth = useCallback(() => { if (typeof window === "undefined") return 720; const minWidth = 640; diff --git a/plane-src/apps/web/styles/globals.css b/plane-src/apps/web/styles/globals.css index f9abd70..607b87e 100644 --- a/plane-src/apps/web/styles/globals.css +++ b/plane-src/apps/web/styles/globals.css @@ -360,12 +360,23 @@ align-items: stretch; column-gap: 0.75rem; overflow: visible; + overscroll-behavior: contain; transition: column-gap 260ms ease, grid-template-columns 260ms ease, opacity 220ms ease; } + html.nodedc-beam-viewer-interaction-lock, + html.nodedc-beam-viewer-interaction-lock body { + overscroll-behavior: none; + } + + html.nodedc-beam-viewer-interaction-lock main .nodedc-kanban-scroll-container { + overflow: hidden !important; + overscroll-behavior: none; + } + .nodedc-beam-peek-shell[data-resizing="true"] { transition: none; } @@ -386,6 +397,7 @@ min-width: 0; min-height: 0; overflow: hidden; + overscroll-behavior: contain; background: #000; border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 28px; @@ -440,6 +452,7 @@ border: 0 !important; outline: none !important; box-shadow: none !important; + overscroll-behavior: contain; } .nodedc-beam-peek-shell[data-resizing="true"] iframe {