From eed671fcca84690982ff714c15f92bc5380219df Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 6 Nov 2026 02:02:33 -0800 Subject: [PATCH] fix(web): tagging a thread by drag no longer scrolls the sidebar Dragging a sidebar thread into the composer crosses dnd-kit's autoscroll edge zone on the way out, so the sidebar scrolled, often to the bottom. Remember the sidebar scroll position at drag start and restore it once the drag leaves the list. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/Sidebar.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 2d904e1ec6f70..d45ba6a3a3686 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1055,6 +1055,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve } | null>(null); const dragSensorRef = useRef(null); const contextDragKeyRef = useRef(null); + const dragScrollRef = useRef<{ readonly viewport: Element; readonly top: number } | null>(null); const [actionSweep, setActionSweep] = useState<{ action: SidebarSweepAction; keys: ReadonlySet; @@ -1713,6 +1714,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve const finishThreadDrag = (started: boolean) => { dragSensorRef.current = null; contextDragKeyRef.current = null; + dragScrollRef.current = null; endThreadContextDrag(); if (started) { listMotionRef.current?.release(); @@ -1739,6 +1741,11 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve const bounds = threadListRef.current?.getBoundingClientRect(); return bounds !== undefined && (point.x < bounds.left || point.x > bounds.right); }; + // The path out of the list often crosses dnd-kit's edge zone, which autoscrolls the sidebar. + const restoreDragScroll = () => { + const scroll = dragScrollRef.current; + if (scroll && scroll.viewport.scrollTop !== scroll.top) scroll.viewport.scrollTop = scroll.top; + }; const moveThreadContextDrag = (point: { x: number; y: number }) => { const contextDrag = pointerOutsideThreadList(point); setDragState((current) => @@ -1750,6 +1757,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve endThreadContextDrag(); return false; } + restoreDragScroll(); const threads = contextDragThreads(); const title = threadByKeyRef.current.get(contextDragKeyRef.current ?? "")?.title.trim() || "Thread"; @@ -1758,6 +1766,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve }; const dropThreadContextDrag = (point: { x: number; y: number }) => { if (!pointerOutsideThreadList(point)) return false; + restoreDragScroll(); dropThreadContext(point, contextDragThreads()); return true; }; @@ -1884,6 +1893,8 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve if (activeSection === undefined) return; listMotionRef.current?.suspend(); const list = threadListRef.current; + const viewport = list?.closest('[data-slot="scroll-area-viewport"]'); + dragScrollRef.current = viewport ? { viewport, top: viewport.scrollTop } : null; const header = list?.querySelector('[data-testid="sidebar-pinned-header"]'); if (list && header) { const listRect = list.getBoundingClientRect();