diff --git a/frontend/src/App.css b/frontend/src/App.css index 6606b942..9a6a0fda 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -2528,6 +2528,8 @@ body[data-ui-version] .ant-layout-sider[data-sidebar-panel='true'] { body[data-ui-version] .ant-layout-sider[data-sidebar-panel='true'][data-sidebar-resizing='true'], body[data-sidebar-resizing='true'] .ant-layout-sider[data-sidebar-panel='true'] { transition: none !important; + width: var(--gonavi-sidebar-resize-width) !important; + flex: 0 0 var(--gonavi-sidebar-resize-width) !important; } body[data-ui-version] .ant-layout-sider[data-sidebar-collapsed='true'] { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 61ec4151..3b57b57c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4243,7 +4243,6 @@ function App() { }, [t]); const { - ghostRef, handleSidebarMouseDown, sidebarResizeHandleWidth, siderRef, @@ -9028,22 +9027,6 @@ function App() { )} - {/* Ghost Resize Line for Sidebar */} -
- {/* Ghost Resize Line for Log Panel */}
(); + + setProperty(name: string, value: string) { + this.properties.set(name, value); + } + + removeProperty(name: string) { + const previous = this.properties.get(name) || ''; + this.properties.delete(name); + return previous; + } + + getPropertyValue(name: string) { + return this.properties.get(name) || ''; + } +} + class FakeHTMLElement extends FakeAttributeHost { + style = new FakeStyle(); + getBoundingClientRect() { return { right: 240, width: 240 }; } @@ -81,7 +101,6 @@ describe('useAppSidebarResize interaction cleanup', () => { let resize: ReturnType | null = null; let fakeWindow: FakeEventTarget & { getComputedStyle: () => { minWidth: string; maxWidth: string }; innerWidth: number }; let fakeDocument: FakeEventTarget & { body: FakeBody }; - let ghost: { style: { display: string; left: string } }; let scheduledFrames: Map; let nextFrameId: number; let setSidebarWidth: ReturnType; @@ -117,8 +136,6 @@ describe('useAppSidebarResize interaction cleanup', () => { fakeDocument = Object.assign(new FakeEventTarget(), { body: new FakeBody(), }); - ghost = { style: { display: 'none', left: '' } }; - Object.defineProperty(globalThis, 'window', { configurable: true, value: fakeWindow }); Object.defineProperty(globalThis, 'document', { configurable: true, value: fakeDocument }); Object.defineProperty(globalThis, 'HTMLElement', { configurable: true, value: FakeHTMLElement }); @@ -139,7 +156,6 @@ describe('useAppSidebarResize interaction cleanup', () => { renderer = create(); }); (resize!.siderRef as React.MutableRefObject).current = new FakeHTMLElement(); - (resize!.ghostRef as React.MutableRefObject).current = ghost; }); afterEach(() => { @@ -170,7 +186,6 @@ describe('useAppSidebarResize interaction cleanup', () => { userSelect: 'none', webkitUserSelect: 'none', }); - expect(ghost.style.display).toBe('block'); expect(fakeWindow.listenerCount('blur')).toBe(1); act(() => fakeWindow.dispatch('blur')); @@ -180,7 +195,6 @@ describe('useAppSidebarResize interaction cleanup', () => { userSelect: 'text', webkitUserSelect: 'auto', }); - expect(ghost.style.display).toBe('none'); expect(fakeDocument.listenerCount('mousemove')).toBe(0); expect(fakeDocument.listenerCount('mouseup')).toBe(0); expect(fakeWindow.listenerCount('blur')).toBe(0); @@ -193,14 +207,36 @@ describe('useAppSidebarResize interaction cleanup', () => { act(() => fakeDocument.dispatch('mousemove', { buttons: 0, clientX: 260 })); expect(setSidebarWidth).toHaveBeenCalledWith(300); - expect(ghost.style.display).toBe('none'); + expect((resize?.siderRef.current as unknown as FakeHTMLElement).style.getPropertyValue('--gonavi-sidebar-resize-width')).toBe('300px'); expect(fakeDocument.body.style.cursor).toBe('wait'); expect(fakeDocument.body.style.userSelect).toBe('text'); expect(fakeDocument.listenerCount('mousemove')).toBe(0); expect(fakeWindow.listenerCount('blur')).toBe(0); }); + it('previews the sidebar width while dragging and persists it only after release', () => { + const sider = resize!.siderRef.current as unknown as FakeHTMLElement; + + beginResize(); + act(() => fakeDocument.dispatch('mousemove', { buttons: 1, clientX: 260 })); + + expect(setSidebarWidth).not.toHaveBeenCalled(); + expect(sider.style.getPropertyValue('--gonavi-sidebar-resize-width')).toBe('240px'); + + act(() => flushAnimationFrames(scheduledFrames, 1)); + + expect(sider.style.getPropertyValue('--gonavi-sidebar-resize-width')).toBe('300px'); + expect(setSidebarWidth).not.toHaveBeenCalled(); + + act(() => fakeDocument.dispatch('mouseup', { clientX: 260 })); + + expect(setSidebarWidth).toHaveBeenCalledTimes(1); + expect(setSidebarWidth).toHaveBeenCalledWith(300); + }); + it('cancels pending work and restores interaction state when unmounted mid-resize', () => { + const sider = resize!.siderRef.current as unknown as FakeHTMLElement; + beginResize(); act(() => fakeDocument.dispatch('mousemove', { buttons: 1, clientX: 250 })); expect(scheduledFrames.size).toBe(1); @@ -210,7 +246,7 @@ describe('useAppSidebarResize interaction cleanup', () => { expect(scheduledFrames.size).toBe(0); expect(cancelAnimationFrame).toHaveBeenCalledTimes(1); - expect(ghost.style.display).toBe('none'); + expect(sider.style.getPropertyValue('--gonavi-sidebar-resize-width')).toBe(''); expect(fakeDocument.body.style).toEqual({ cursor: 'wait', userSelect: 'text', @@ -239,5 +275,6 @@ describe('useAppSidebarResize interaction cleanup', () => { expect(sider.getAttribute('data-sidebar-resizing')).toBe(null); expect(fakeDocument.body.getAttribute('data-sidebar-resizing')).toBe(null); + expect(sider.style.getPropertyValue('--gonavi-sidebar-resize-width')).toBe(''); }); }); diff --git a/frontend/src/hooks/useAppSidebarResize.ts b/frontend/src/hooks/useAppSidebarResize.ts index 8972fc5d..20d4d35e 100644 --- a/frontend/src/hooks/useAppSidebarResize.ts +++ b/frontend/src/hooks/useAppSidebarResize.ts @@ -9,7 +9,6 @@ type SidebarResizeBounds = { minWidth: number; maxWidth: number }; type SidebarResizeDragState = SidebarResizeBounds & { startX: number; startWidth: number; - startGuideLeft: number; }; type SidebarResizeListeners = { blur: () => void; @@ -39,6 +38,8 @@ const clampSidebarResizeWidth = (width: number, bounds: SidebarResizeBounds): nu Math.max(bounds.minWidth, Math.min(bounds.maxWidth, width)) ); +const SIDEBAR_RESIZE_WIDTH_CSS_VARIABLE = '--gonavi-sidebar-resize-width'; + type UseAppSidebarResizeOptions = { effectiveUiScale: number; setSidebarWidth: (width: number) => void; @@ -53,7 +54,6 @@ export const useAppSidebarResize = ({ const sidebarDragRef = useRef(null); const rafRef = useRef(null); const clearResizingFrameRef = useRef(null); - const ghostRef = useRef(null); const siderRef = useRef(null); const sidebarDragBodyStyleRef = useRef<{ cursor: string; userSelect: string; webkitUserSelect: string } | null>(null); const sidebarResizeListenersRef = useRef(null); @@ -80,6 +80,7 @@ export const useAppSidebarResize = ({ sider.setAttribute('data-sidebar-resizing', 'true'); } else { sider.removeAttribute('data-sidebar-resizing'); + sider.style.removeProperty(SIDEBAR_RESIZE_WIDTH_CSS_VARIABLE); } } if (typeof document !== 'undefined') { @@ -91,6 +92,12 @@ export const useAppSidebarResize = ({ } }, []); + const previewSidebarWidth = useCallback((width: number) => { + const sider = siderRef.current; + if (!(sider instanceof HTMLElement)) return; + sider.style.setProperty(SIDEBAR_RESIZE_WIDTH_CSS_VARIABLE, `${width}px`); + }, []); + const scheduleClearSidebarResizing = useCallback(() => { cancelClearResizingFrame(); if (typeof window === 'undefined') { @@ -142,21 +149,20 @@ export const useAppSidebarResize = ({ rafRef.current = null; } - if (ghostRef.current) { - ghostRef.current.style.display = 'none'; - } detachSidebarResizeListeners(); restoreSidebarDragBodyStyles(); if (commit && dragState) { const finalMouseX = Number.isFinite(clientX) ? clientX as number : latestMouseX.current; const delta = finalMouseX - dragState.startX; - // Keep transition disabled across the state commit + first paint. - setSidebarResizing(true); - setSidebarWidthRef.current(clampSidebarResizeWidth( + const finalWidth = clampSidebarResizeWidth( dragState.startWidth + delta, dragState, - )); + ); + // Keep transition disabled across the state commit + first paint. + previewSidebarWidth(finalWidth); + setSidebarResizing(true); + setSidebarWidthRef.current(finalWidth); scheduleClearSidebarResizing(); return; } @@ -166,6 +172,7 @@ export const useAppSidebarResize = ({ }, [ cancelClearResizingFrame, detachSidebarResizeListeners, + previewSidebarWidth, restoreSidebarDragBodyStyles, scheduleClearSidebarResizing, setSidebarResizing, @@ -183,7 +190,6 @@ export const useAppSidebarResize = ({ finishSidebarResize(undefined, false); cancelClearResizingFrame(); - setSidebarResizing(true); if (typeof document !== 'undefined') { sidebarDragBodyStyleRef.current = { @@ -197,19 +203,15 @@ export const useAppSidebarResize = ({ } const siderRect = siderRef.current?.getBoundingClientRect(); - const startGuideLeft = siderRect?.right ?? sidebarWidth; const startWidth = siderRect?.width ?? sidebarWidth; const resizeBounds = resolveSidebarResizeBounds(siderRef.current); - if (ghostRef.current) { - ghostRef.current.style.left = `${startGuideLeft}px`; - ghostRef.current.style.display = 'block'; - } + previewSidebarWidth(startWidth); + setSidebarResizing(true); sidebarDragRef.current = { startX: e.clientX, startWidth, - startGuideLeft, ...resizeBounds, }; latestMouseX.current = e.clientX; @@ -225,11 +227,11 @@ export const useAppSidebarResize = ({ rafRef.current = requestAnimationFrame(() => { rafRef.current = null; - if (!sidebarDragRef.current || !ghostRef.current) return; - const { startX, startWidth, startGuideLeft, minWidth, maxWidth } = sidebarDragRef.current; + if (!sidebarDragRef.current) return; + const { startX, startWidth, minWidth, maxWidth } = sidebarDragRef.current; const delta = latestMouseX.current - startX; const newWidth = clampSidebarResizeWidth(startWidth + delta, { minWidth, maxWidth }); - ghostRef.current.style.left = `${startGuideLeft + (newWidth - startWidth)}px`; + previewSidebarWidth(newWidth); }); }; const handleUp = (event: MouseEvent) => finishSidebarResize(event.clientX); @@ -243,7 +245,7 @@ export const useAppSidebarResize = ({ document.addEventListener('mousemove', handleMove); document.addEventListener('mouseup', handleUp); window.addEventListener('blur', handleBlur); - }, [cancelClearResizingFrame, finishSidebarResize, setSidebarResizing, sidebarWidth]); + }, [cancelClearResizingFrame, finishSidebarResize, previewSidebarWidth, setSidebarResizing, sidebarWidth]); useEffect(() => () => { finishSidebarResize(undefined, false); @@ -251,7 +253,6 @@ export const useAppSidebarResize = ({ }, [cancelClearResizingFrame, finishSidebarResize]); return { - ghostRef, handleSidebarMouseDown, sidebarResizeHandleWidth, siderRef,