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
This commit is contained in:
@@ -97,6 +97,7 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase<DashboardLayout
|
||||
document.body.removeEventListener('pointerup', this._stopDraggingSync);
|
||||
document.body.removeEventListener('pointerup', this._onRowDragPointerUp);
|
||||
this._clearTabActivationTimer();
|
||||
this._clearDragPreview();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -107,6 +108,14 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase<DashboardLayout
|
||||
return !!(this.state.draggingGridItem || this.state.draggingRow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current drag operation will drop the item to a different layout
|
||||
* than where it started. Used by AutoGridLayout to know whether to clear draggingKey.
|
||||
*/
|
||||
public isDroppedElsewhere(): boolean {
|
||||
return this._lastDropTarget !== null && this._lastDropTarget !== this._sourceDropTarget;
|
||||
}
|
||||
|
||||
public startDraggingSync(evt: ReactPointerEvent, gridItem: SceneGridItemLike): void {
|
||||
this._pointerDistance.set(evt);
|
||||
this._isSelectedObject = false;
|
||||
@@ -137,9 +146,10 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase<DashboardLayout
|
||||
const sourceDropTarget = this._sourceDropTarget;
|
||||
const lastDropTarget = this._lastDropTarget;
|
||||
const dropPosition = this._currentDropPosition;
|
||||
const isCrossLayoutDrop = sourceDropTarget !== lastDropTarget || wasDetached;
|
||||
|
||||
// Handle cross-layout or cross-tab drop
|
||||
if (sourceDropTarget !== lastDropTarget || wasDetached) {
|
||||
if (isCrossLayoutDrop) {
|
||||
// Wrapped in setTimeout to ensure that any event handlers are called
|
||||
// Useful for allowing react-grid-layout to remove placeholders, etc.
|
||||
setTimeout(() => {
|
||||
@@ -149,7 +159,16 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase<DashboardLayout
|
||||
sourceDropTarget?.draggedGridItemOutside?.(gridItem);
|
||||
}
|
||||
// Pass drop position for precise placement (AutoGrid uses this)
|
||||
// Note: draggedGridItemInside also clears isDropTarget and dropPosition
|
||||
lastDropTarget?.draggedGridItemInside?.(gridItem, dropPosition ?? undefined);
|
||||
|
||||
// Clean up source grid's drag state (CSS variables and draggingKey) after item is moved.
|
||||
// This is done here (after movement) to prevent flickering where the item
|
||||
// would momentarily appear at wrong position (CSS vars cleared but draggingKey set
|
||||
// = absolute positioning with no valid position values).
|
||||
if (sourceDropTarget instanceof AutoGridLayoutManager) {
|
||||
sourceDropTarget.state.layout.endExternalDrag();
|
||||
}
|
||||
} else {
|
||||
const warningMessage = 'No grid item to drag';
|
||||
console.warn(warningMessage);
|
||||
@@ -163,8 +182,19 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase<DashboardLayout
|
||||
|
||||
this._clearTabActivationTimer();
|
||||
this._clearDragPreview();
|
||||
this._clearDropPosition();
|
||||
this._lastDropTarget?.setIsDropTarget?.(false);
|
||||
|
||||
// For cross-layout drops, don't clear drop position/target state synchronously.
|
||||
// The placeholder should remain visible until the item is added by draggedGridItemInside,
|
||||
// which also clears isDropTarget and dropPosition. This prevents flickering where the
|
||||
// grid would momentarily shrink (placeholder removed) before expanding again (item added).
|
||||
if (!isCrossLayoutDrop) {
|
||||
this._clearDropPosition();
|
||||
this._lastDropTarget?.setIsDropTarget?.(false);
|
||||
}
|
||||
|
||||
// Clear internal tracking state (but not the visual state on the target for cross-layout drops)
|
||||
this._currentDropPosition = null;
|
||||
this._lastHoveredAutoGridItemKey = null;
|
||||
this._lastDropTarget = null;
|
||||
this._sourceDropTarget = null;
|
||||
this._itemDetachedFromSource = false;
|
||||
|
||||
@@ -65,6 +65,8 @@ export class AutoGridLayout extends SceneObjectBase<AutoGridLayoutState> 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<AutoGridLayoutState>) {
|
||||
@@ -150,6 +152,12 @@ export class AutoGridLayout extends SceneObjectBase<AutoGridLayoutState> 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<AutoGridLayoutState> 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<AutoGridLayoutState> 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
|
||||
|
||||
+1
-1
@@ -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',
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user