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 1a349bb..9582fa8 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 @@ -38,6 +38,7 @@ import { IssuePeekOverviewLoader } from "./loader"; import { PeekOverviewProperties } from "./properties"; const SIDE_PEEK_WIDTH_STORAGE_KEY = "nodedc:issue-peek-width"; +const BEAM_VIEWER_CLOSING_CLASS_NAME = "nodedc-beam-viewer-closing"; interface IIssueView { workspaceSlug: string; @@ -94,6 +95,7 @@ export const IssueView = observer(function IssueView(props: IIssueView) { const [isEditIssueModalOpen, setIsEditIssueModalOpen] = useState(false); const [beamPeekViewer, setBeamPeekViewer] = useState(null); const [isBeamPeekFullscreen, setIsBeamPeekFullscreen] = useState(false); + const [isBeamPeekClosing, setIsBeamPeekClosing] = useState(false); const [sidePeekWidth, setSidePeekWidth] = useState(() => { if (typeof window === "undefined") return 720; @@ -106,11 +108,12 @@ export const IssueView = observer(function IssueView(props: IIssueView) { const [isResizingPeek, setIsResizingPeek] = useState(false); // ref const issuePeekOverviewRef = useRef(null); + const beamPeekViewerRef = useRef(null); const editorRef = useRef(null); const initialPeekWidthRef = useRef(0); const initialMouseXRef = useRef(0); const livePeekWidthRef = useRef(sidePeekWidth); - const resizeAnimationFrameRef = useRef(null); + const beamCloseTimeoutRef = useRef(null); // store hooks const { setPeekIssue, @@ -135,19 +138,57 @@ export const IssueView = observer(function IssueView(props: IIssueView) { const isAnyLocalModalOpen = isDeleteIssueModalOpen || isArchiveIssueModalOpen || isDuplicateIssueModalOpen || isEditIssueModalOpen; - const shouldUseBeamPeekShell = !embedIssue && peekMode === "side-peek" && !!beamPeekViewer; + const shouldShowBeamPeekViewer = !embedIssue && peekMode === "side-peek" && !!beamPeekViewer; + + const clearBeamCloseTimeout = useCallback(() => { + if (typeof window === "undefined" || beamCloseTimeoutRef.current === null) return; + + window.clearTimeout(beamCloseTimeoutRef.current); + beamCloseTimeoutRef.current = null; + }, []); + + const closeBeamPeekViewer = useCallback( + (event?: ReactMouseEvent) => { + event?.preventDefault(); + event?.stopPropagation(); + + if (isBeamPeekClosing) return; + + if (typeof document !== "undefined" && document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + + clearBeamCloseTimeout(); + document.documentElement.classList.add(BEAM_VIEWER_CLOSING_CLASS_NAME); + setIsBeamPeekClosing(true); + setIsBeamPeekFullscreen(false); + + beamCloseTimeoutRef.current = window.setTimeout(() => { + beamCloseTimeoutRef.current = null; + setBeamPeekViewer(null); + setIsBeamPeekClosing(false); + document.documentElement.classList.remove(BEAM_VIEWER_CLOSING_CLASS_NAME); + }, 140); + }, + [clearBeamCloseTimeout, isBeamPeekClosing] + ); + + useEffect( + () => () => { + clearBeamCloseTimeout(); + document.documentElement.classList.remove(BEAM_VIEWER_CLOSING_CLASS_NAME); + }, + [clearBeamCloseTimeout] + ); useEffect(() => { - if (typeof window === "undefined" || !shouldUseBeamPeekShell) return; + if (typeof window === "undefined" || !shouldShowBeamPeekViewer) 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 viewerCard = beamPeekViewerRef.current?.querySelector(".nodedc-beam-peek-viewer-card"); const target = event.target instanceof Node ? event.target : null; const path = typeof event.composedPath === "function" ? event.composedPath() : []; @@ -181,30 +222,20 @@ export const IssueView = observer(function IssueView(props: IIssueView) { window.removeEventListener("pointermove", stopViewerGestureLeak, passiveOptions); window.removeEventListener("pointerup", stopViewerGestureLeak, passiveOptions); }; - }, [shouldUseBeamPeekShell]); + }, [shouldShowBeamPeekViewer]); const getMaxSidePeekWidth = useCallback(() => { if (typeof window === "undefined") return 720; const minWidth = 640; - if (shouldUseBeamPeekShell && !isBeamPeekFullscreen) { + if (shouldShowBeamPeekViewer && !isBeamPeekFullscreen) { return Math.max(minWidth, Math.floor((window.innerWidth - 24) / 2)); } return Math.max(720, window.innerWidth - 48); - }, [isBeamPeekFullscreen, shouldUseBeamPeekShell]); + }, [isBeamPeekFullscreen, shouldShowBeamPeekViewer]); const stopPeekResizing = useCallback(() => { - if (resizeAnimationFrameRef.current !== null) { - window.cancelAnimationFrame(resizeAnimationFrameRef.current); - resizeAnimationFrameRef.current = null; - } - - if (shouldUseBeamPeekShell) { - issuePeekOverviewRef.current?.style.setProperty("--nodedc-beam-peek-width", `${livePeekWidthRef.current}px`); - setSidePeekWidth(livePeekWidthRef.current); - } - setIsResizingPeek(false); - }, [shouldUseBeamPeekShell]); + }, []); const handlePeekResize = useCallback( (event: MouseEvent) => { @@ -215,25 +246,10 @@ export const IssueView = observer(function IssueView(props: IIssueView) { const deltaX = event.clientX - initialMouseXRef.current; const nextWidth = Math.min(Math.max(initialPeekWidthRef.current - deltaX, minWidth), maxWidth); - if (shouldUseBeamPeekShell) { - livePeekWidthRef.current = nextWidth; - - if (resizeAnimationFrameRef.current === null) { - resizeAnimationFrameRef.current = window.requestAnimationFrame(() => { - issuePeekOverviewRef.current?.style.setProperty( - "--nodedc-beam-peek-width", - `${livePeekWidthRef.current}px` - ); - resizeAnimationFrameRef.current = null; - }); - } - - return; - } - + livePeekWidthRef.current = nextWidth; setSidePeekWidth(nextWidth); }, - [getMaxSidePeekWidth, isResizingPeek, shouldUseBeamPeekShell] + [getMaxSidePeekWidth, isResizingPeek] ); const startPeekResizing = useCallback( @@ -276,32 +292,29 @@ export const IssueView = observer(function IssueView(props: IIssueView) { livePeekWidthRef.current = sidePeekWidth; }, [sidePeekWidth]); - useEffect( - () => () => { - if (resizeAnimationFrameRef.current !== null) { - window.cancelAnimationFrame(resizeAnimationFrameRef.current); - } - }, - [] - ); - useEffect(() => { + clearBeamCloseTimeout(); setBeamPeekViewer(null); setIsBeamPeekFullscreen(false); - }, [issueId]); + setIsBeamPeekClosing(false); + document.documentElement.classList.remove(BEAM_VIEWER_CLOSING_CLASS_NAME); + }, [clearBeamCloseTimeout, issueId]); useEffect(() => { const handleOpenBeamViewer = (event: Event) => { const detail = (event as CustomEvent).detail; if (!detail?.viewerUrl || detail.issueId !== issueId) return; + clearBeamCloseTimeout(); setPeekMode("side-peek"); + setIsBeamPeekClosing(false); + document.documentElement.classList.remove(BEAM_VIEWER_CLOSING_CLASS_NAME); setBeamPeekViewer(detail); setIsBeamPeekFullscreen(false); }; window.addEventListener(BEAM_VIEWER_OPEN_EVENT, handleOpenBeamViewer); return () => window.removeEventListener(BEAM_VIEWER_OPEN_EVENT, handleOpenBeamViewer); - }, [issueId]); + }, [clearBeamCloseTimeout, issueId]); useEffect(() => { if (!isResizingPeek) return; @@ -342,8 +355,7 @@ export const IssueView = observer(function IssueView(props: IIssueView) { const dropdownElement = document.activeElement?.tagName === "INPUT"; const isAnyDropbarOpen = editorRef.current?.isAnyDropbarOpen(); if (beamPeekViewer) { - setBeamPeekViewer(null); - setIsBeamPeekFullscreen(false); + closeBeamPeekViewer(); return; } if (!isAnyModalOpen && !dropdownElement && !isAnyDropbarOpen && !editorImageFullScreenModalElement) { @@ -365,17 +377,14 @@ export const IssueView = observer(function IssueView(props: IIssueView) { shouldRenderPeekSurface ? "flex flex-col overflow-hidden border border-subtle/70 bg-surface-1/80 backdrop-blur-2xl transition-all duration-300" : "h-full w-full", - !embedIssue && !shouldUseBeamPeekShell && "absolute z-[80]", - !embedIssue && - !shouldUseBeamPeekShell && { - "nodedc-peek-side-bound top-[5.35rem] right-3 w-[calc(100%-1.5rem)] rounded-[28px] border md:max-w-[calc(100vw-1.5rem)] md:min-w-[640px]": - peekMode === "side-peek", - "nodedc-peek-modal-bound rounded-[28px]": peekMode === "modal", - "nodedc-peek-full-bound rounded-[28px]": peekMode === "full-screen", - }, - !embedIssue && - shouldUseBeamPeekShell && - "nodedc-beam-peek-issue-card relative h-full rounded-[28px] border md:min-w-[640px]", + !embedIssue && "absolute z-[80]", + !embedIssue && { + "nodedc-peek-side-bound top-[5.35rem] right-3 w-[calc(100%-1.5rem)] rounded-[28px] border md:max-w-[calc(100vw-1.5rem)] md:min-w-[640px]": + peekMode === "side-peek", + "nodedc-peek-modal-bound rounded-[28px]": peekMode === "modal", + "nodedc-peek-full-bound rounded-[28px]": peekMode === "full-screen", + }, + !embedIssue && isBeamPeekClosing && "nodedc-beam-peek-closing !transition-none !duration-0", shouldUseInteractiveEmbeddedLayout && { "relative ml-auto h-full rounded-[28px]": peekMode === "side-peek", "relative mx-auto my-3 h-[calc(100%-1.5rem)] w-full max-w-[min(1080px,100%)] rounded-[28px]": @@ -389,23 +398,14 @@ export const IssueView = observer(function IssueView(props: IIssueView) { const portalContainer = document.getElementById("full-screen-portal") as HTMLElement; const issuePanelStyle = { - width: shouldUseBeamPeekShell - ? "100%" - : shouldAllowPeekResize && peekMode === "side-peek" - ? `${sidePeekWidth}px` - : undefined, - boxShadow: - shouldRenderPeekSurface && !shouldUseBeamPeekShell - ? "0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12), 0px 1px 16px 0px rgba(16, 24, 40, 0.12)" - : undefined, + width: shouldAllowPeekResize && peekMode === "side-peek" ? `${sidePeekWidth}px` : undefined, + boxShadow: shouldRenderPeekSurface + ? "0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12), 0px 1px 16px 0px rgba(16, 24, 40, 0.12)" + : undefined, }; const issuePanel = issueId ? ( -
+
{shouldAllowPeekResize && peekMode === "side-peek" && (
- {shouldUseBeamPeekShell && beamPeekViewer ? ( + {shouldShowBeamPeekViewer && beamPeekViewer && (
{ - setBeamPeekViewer(null); - setIsBeamPeekFullscreen(false); + onMouseDown={(event) => { + event.preventDefault(); + event.stopPropagation(); }} + onClick={closeBeamPeekViewer} title="Закрыть" > @@ -622,11 +625,9 @@ export const IssueView = observer(function IssueView(props: IIssueView) { /> {isResizingPeek && - ) : ( - issuePanel )} + {issuePanel}
); diff --git a/plane-src/apps/web/styles/globals.css b/plane-src/apps/web/styles/globals.css index e832b48..837c9e9 100644 --- a/plane-src/apps/web/styles/globals.css +++ b/plane-src/apps/web/styles/globals.css @@ -346,25 +346,20 @@ 0 8px 22px rgba(0, 0, 0, 0.24); } - .nodedc-beam-peek-shell { + .nodedc-beam-peek-viewer-layer { --nodedc-beam-peek-width: 720px; position: fixed; top: 5.35rem; left: 0.75rem; - right: 0.75rem; + right: calc(var(--nodedc-beam-peek-width) + 1.5rem); bottom: var(--nodedc-peek-bottom-offset); - z-index: 80; - display: grid; - grid-template-columns: minmax(0, 1fr) var(--nodedc-beam-peek-width); - grid-template-rows: minmax(0, 1fr); - align-items: stretch; - column-gap: 0.75rem; + z-index: 81; + min-width: 0; + min-height: 0; overflow: visible; overscroll-behavior: contain; - transition: - column-gap 260ms ease, - grid-template-columns 260ms ease, - opacity 220ms ease; + pointer-events: auto; + transition: right 260ms ease; } html.nodedc-beam-viewer-interaction-lock, @@ -377,7 +372,12 @@ overscroll-behavior: none; } - .nodedc-beam-peek-shell[data-resizing="true"] { + .nodedc-beam-peek-viewer-layer[data-resizing="true"] { + transition: none; + } + + .nodedc-beam-peek-viewer-layer[data-closing="true"] { + pointer-events: none; transition: none; } @@ -411,9 +411,9 @@ min-height: 0 !important; } - .nodedc-beam-peek-shell[data-fullscreen="true"] { - grid-template-columns: minmax(0, 1fr) 0; - column-gap: 0; + .nodedc-beam-peek-viewer-layer[data-fullscreen="true"] { + right: 0.75rem; + z-index: 82; } .nodedc-beam-peek-viewer-card { @@ -433,30 +433,19 @@ animation: nodedcBeamPeekSlideIn 220ms ease-out; } - .nodedc-beam-peek-shell[data-resizing="true"] .nodedc-beam-peek-viewer-card, - .nodedc-beam-peek-shell[data-resizing="true"] .nodedc-beam-peek-issue-card { + .nodedc-beam-peek-viewer-layer[data-resizing="true"] .nodedc-beam-peek-viewer-card { transition: none; } - .nodedc-beam-peek-shell[data-fullscreen="true"] .nodedc-beam-peek-viewer-card { - border-radius: 28px; - } - - .nodedc-beam-peek-issue-slot { - min-width: 0; - min-height: 0; - height: 100%; - overflow: visible; - transition: - opacity 220ms ease, - transform 260ms ease; - } - - .nodedc-beam-peek-shell[data-fullscreen="true"] .nodedc-beam-peek-issue-slot { - overflow: hidden; + .nodedc-beam-peek-viewer-layer[data-closing="true"] .nodedc-beam-peek-viewer-card { + visibility: hidden; opacity: 0; - transform: translateX(1.5rem); - pointer-events: none; + animation: none; + transition: none; + } + + .nodedc-beam-peek-viewer-layer[data-fullscreen="true"] .nodedc-beam-peek-viewer-card { + border-radius: 28px; } .nodedc-beam-peek-issue-card { @@ -471,6 +460,53 @@ 0px 1px 16px 0px rgba(16, 24, 40, 0.12); } + html.nodedc-beam-viewer-closing .nodedc-peek-side-bound, + html.nodedc-beam-viewer-closing .nodedc-peek-side-bound:focus, + html.nodedc-beam-viewer-closing .nodedc-peek-side-bound:focus-visible, + html.nodedc-beam-viewer-closing .nodedc-peek-side-bound:focus-within, + html.nodedc-beam-viewer-closing .nodedc-beam-peek-closing, + html.nodedc-beam-viewer-closing .nodedc-beam-peek-closing:focus, + html.nodedc-beam-viewer-closing .nodedc-beam-peek-closing:focus-visible, + html.nodedc-beam-viewer-closing .nodedc-beam-peek-closing:focus-within { + outline: none !important; + border-color: rgba(255, 255, 255, 0.08) !important; + box-shadow: + 0px 4px 8px 0px rgba(0, 0, 0, 0.12), + 0px 6px 12px 0px rgba(16, 24, 40, 0.12), + 0px 1px 16px 0px rgba(16, 24, 40, 0.12) !important; + transition: none !important; + } + + html.nodedc-beam-viewer-closing .nodedc-peek-side-bound *, + html.nodedc-beam-viewer-closing .nodedc-peek-side-bound *:focus, + html.nodedc-beam-viewer-closing .nodedc-peek-side-bound *:focus-visible, + html.nodedc-beam-viewer-closing .nodedc-beam-peek-closing *, + html.nodedc-beam-viewer-closing .nodedc-beam-peek-closing *:focus, + html.nodedc-beam-viewer-closing .nodedc-beam-peek-closing *:focus-visible { + outline: none !important; + box-shadow: none !important; + } + + .nodedc-beam-peek-closing, + .nodedc-beam-peek-closing:focus, + .nodedc-beam-peek-closing:focus-visible, + .nodedc-beam-peek-closing:focus-within { + outline: none !important; + border-color: rgba(255, 255, 255, 0.08) !important; + box-shadow: + 0px 4px 8px 0px rgba(0, 0, 0, 0.12), + 0px 6px 12px 0px rgba(16, 24, 40, 0.12), + 0px 1px 16px 0px rgba(16, 24, 40, 0.12) !important; + transition: none !important; + } + + .nodedc-beam-peek-closing *, + .nodedc-beam-peek-closing *:focus, + .nodedc-beam-peek-closing *:focus-visible { + outline: none !important; + box-shadow: none !important; + } + .nodedc-beam-peek-viewer-card iframe, .nodedc-beam-peek-viewer-card iframe:focus, .nodedc-beam-peek-viewer-card iframe:focus-visible { @@ -480,10 +516,15 @@ overscroll-behavior: contain; } - .nodedc-beam-peek-shell[data-resizing="true"] iframe { + .nodedc-beam-peek-viewer-layer[data-resizing="true"] iframe { pointer-events: none; } + .nodedc-beam-peek-viewer-layer[data-closing="true"] iframe { + display: none; + visibility: hidden; + } + .nodedc-beam-peek-resize-shield { position: absolute; inset: -1rem; @@ -505,20 +546,15 @@ } @media (max-width: 900px) { - .nodedc-beam-peek-shell, - .nodedc-beam-peek-shell[data-fullscreen="true"] { - grid-template-columns: minmax(0, 1fr); - column-gap: 0; + .nodedc-beam-peek-viewer-layer, + .nodedc-beam-peek-viewer-layer[data-fullscreen="true"] { + right: 0.75rem; } .nodedc-beam-peek-viewer-card, - .nodedc-beam-peek-shell[data-fullscreen="true"] .nodedc-beam-peek-viewer-card { + .nodedc-beam-peek-viewer-layer[data-fullscreen="true"] .nodedc-beam-peek-viewer-card { border-radius: 24px; } - - .nodedc-beam-peek-issue-slot { - display: none; - } } .nodedc-glass-surface {