From 7ea4dd3744bd87ff9222e7614515acd0efe2ae6e Mon Sep 17 00:00:00 2001 From: oscarkilhed Date: Thu, 8 Jan 2026 09:22:09 +0100 Subject: [PATCH] Fix flickering when dragging AutoGrid items between grids - Add isDroppedElsewhere() method to orchestrator for cross-layout detection - Delay clearing draggingKey until item is moved to prevent source grid flicker - Keep dropPosition/isDropTarget until draggedGridItemInside clears them - Add endExternalDrag() to properly clean up CSS vars and draggingKey together - Track container position to compensate for layout shifts during drag --- .../scene/DashboardLayoutOrchestrator.tsx | 36 +++++++++++++-- .../scene/layout-auto-grid/AutoGridLayout.tsx | 44 +++++++++++++++++-- .../AutoGridLayoutRenderer.tsx | 2 +- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx b/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx index 158a6a1e857..3304364bfeb 100644 --- a/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx @@ -97,6 +97,7 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase { @@ -149,7 +159,16 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase impleme top: number; left: number; } | null = null; + /** Container's initial page position, used to compensate for layout shifts during drag */ + private _initialContainerRect: { top: number; left: number } | null = null; private _lastDropTargetGridItemKey: string | null = null; public constructor(state: Partial) { @@ -150,6 +152,12 @@ export class AutoGridLayout extends SceneObjectBase impleme const { top, left, width, height } = this._draggedGridItem.getBoundingBox(); this._initialGridItemPosition = { pageX: evt.pageX, pageY: evt.pageY, top, left: left }; + + // Capture container's initial page position to compensate for layout shifts + // (e.g., when a grid above expands due to placeholder insertion) + const containerRect = this.containerRef.current?.getBoundingClientRect(); + this._initialContainerRect = containerRect ? { top: containerRect.top, left: containerRect.left } : null; + this._updatePanelSize(width, height); this._updatePanelPosition(top, left); @@ -168,16 +176,33 @@ export class AutoGridLayout extends SceneObjectBase impleme this._draggedGridItem = null; this._initialGridItemPosition = null; + this._initialContainerRect = null; this._lastDropTargetGridItemKey = null; - this._resetPanelPositionAndSize(); - this.setState({ draggingKey: undefined }); + // Only reset position/size and clear draggingKey if not dropping to a different layout. + // For cross-grid drops, the orchestrator will call endExternalDrag() after the item is moved + // to prevent flickering where the item would momentarily appear at wrong position + // (CSS vars cleared but draggingKey still set = absolute positioning with no position). + const orchestrator = getLayoutOrchestratorFor(this); + if (!orchestrator?.isDroppedElsewhere()) { + this._resetPanelPositionAndSize(); + this.setState({ draggingKey: undefined }); + } document.body.removeEventListener('pointermove', this._onDrag); document.body.removeEventListener('pointerup', this._onDragEnd); document.body.classList.remove('dashboard-draggable-transparent-selection'); } + /** + * Called by the orchestrator after a cross-layout drag ends and the item has been moved. + * Cleans up the drag state that was preserved during the cross-layout drop. + */ + public endExternalDrag(): void { + this._resetPanelPositionAndSize(); + this.setState({ draggingKey: undefined }); + } + // Handle inside drag moves private _onDrag(evt: PointerEvent) { if (!this._draggedGridItem || !this._initialGridItemPosition) { @@ -185,9 +210,20 @@ export class AutoGridLayout extends SceneObjectBase impleme return; } + // Calculate how much the container has shifted since drag started + // This can happen when a grid above expands (e.g., placeholder causes row wrap) + let containerShiftY = 0; + let containerShiftX = 0; + if (this._initialContainerRect && this.containerRef.current) { + const currentRect = this.containerRef.current.getBoundingClientRect(); + containerShiftY = currentRect.top - this._initialContainerRect.top; + containerShiftX = currentRect.left - this._initialContainerRect.left; + } + + // Adjust position to compensate for container movement this._updatePanelPosition( - this._initialGridItemPosition.top + (evt.pageY - this._initialGridItemPosition.pageY), - this._initialGridItemPosition.left + (evt.pageX - this._initialGridItemPosition.pageX) + this._initialGridItemPosition.top + (evt.pageY - this._initialGridItemPosition.pageY) - containerShiftY, + this._initialGridItemPosition.left + (evt.pageX - this._initialGridItemPosition.pageX) - containerShiftX ); const dropTargetGridItemKey = document diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutRenderer.tsx index 28e73cb09c1..e4d685c074f 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutRenderer.tsx @@ -101,6 +101,6 @@ const getStyles = (theme: GrafanaTheme2, state: AutoGridLayoutState) => ({ border: `1px dashed ${theme.colors.primary.main}`, borderRadius: theme.shape.radius.default, backgroundColor: theme.colors.primary.transparent, - minHeight: state.autoRows || '320px', + minHeight: '100px', }), });