Skip to content
194 changes: 191 additions & 3 deletions src/components/animate/Animate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ interface AnimateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "id">
tableRow?: TableRow;
}
export const Animate = ({ children, id, parentRef, tableRow, ...props }: AnimateProps) => {
const { allowAnimations, isResizing, isScrolling, rowHeight } = useTableContext();
const { allowAnimations, capturedPositionsRef, isResizing, isScrolling, rowHeight } =
useTableContext();
const elementRef = useRef<HTMLDivElement>(null);
const fromBoundsRef = useRef<DOMRect | null>(null);
const previousScrollingState = usePrevious(isScrolling);
const previousResizingState = usePrevious(isResizing);
const cleanupCallbackRef = useRef<(() => void) | null>(null);

useLayoutEffect(() => {
// Early exit if animations are disabled - don't do any work at all
Expand All @@ -48,31 +50,78 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate
return;
}

const toBounds = elementRef.current.getBoundingClientRect();
const fromBounds = fromBoundsRef.current;
let toBounds = elementRef.current.getBoundingClientRect();

// DEBUG: Track specific cell
const isDebugCell = id === "3-companyName";
if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - Start of useLayoutEffect", {
id,
toBounds: { x: toBounds.x, y: toBounds.y, top: toBounds.top, left: toBounds.left },
currentTransform: elementRef.current.style.transform,
currentTransition: elementRef.current.style.transition,
});
}

// CRITICAL: Check if we have a captured position for this element (react-flip-move pattern)
// This allows animations to continue smoothly even when interrupted by rapid clicks
const capturedPosition = capturedPositionsRef.current.get(id);
const fromBounds = capturedPosition || fromBoundsRef.current;

if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - Position sources", {
hasCapturedPosition: !!capturedPosition,
capturedPosition: capturedPosition
? { x: capturedPosition.x, y: capturedPosition.y }
: null,
hasFromBoundsRef: !!fromBoundsRef.current,
fromBoundsRef: fromBoundsRef.current
? { x: fromBoundsRef.current.x, y: fromBoundsRef.current.y }
: null,
fromBounds: fromBounds ? { x: fromBounds.x, y: fromBounds.y } : null,
});
}

// If we're currently scrolling, don't animate and don't update bounds
if (isScrolling) {
if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - Scrolling, skipping");
}
return;
}

// If scrolling just ended, update the previous bounds without animating
if (previousScrollingState && !isScrolling) {
if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - Scrolling just ended, updating bounds");
}
fromBoundsRef.current = toBounds;
return;
}

// If resizing just ended, update the previous bounds without animating
if (previousResizingState && !isResizing) {
if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - Resizing just ended, updating bounds");
}
fromBoundsRef.current = toBounds;
capturedPositionsRef.current.delete(id);
return;
}

// Store current bounds for next render
fromBoundsRef.current = toBounds;

// Clear captured position after using it (it's been consumed)
if (capturedPosition) {
capturedPositionsRef.current.delete(id);
}

// If there's no previous bound data, don't animate (prevents first render animations)
if (!fromBounds) {
if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - No fromBounds, skipping (first render)");
}
return;
}

Expand All @@ -81,8 +130,21 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate
const deltaY = toBounds.y - fromBounds.y;
const positionDelta = Math.abs(deltaX);

if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - Position deltas", {
deltaX,
deltaY,
positionDelta,
fromBounds: { x: fromBounds.x, y: fromBounds.y },
toBounds: { x: toBounds.x, y: toBounds.y },
});
}

// Only animate if position change is significant (indicates column/row reordering)
if (positionDelta < COLUMN_REORDER_THRESHOLD && Math.abs(deltaY) <= ROW_REORDER_THRESHOLD) {
if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - Position change too small, skipping");
}
return;
}

Expand All @@ -96,6 +158,69 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate
hasPositionChanged = hasDOMPositionChanged;

if (hasPositionChanged) {
if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - Position changed, starting animation", {
hasDOMPositionChanged,
deltaX,
deltaY,
});
}

// CRITICAL: Cancel any pending cleanup from the previous animation
// This prevents the old animation's cleanup from interfering with the new one
if (cleanupCallbackRef.current) {
if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - Canceling previous cleanup");
}
cleanupCallbackRef.current();
cleanupCallbackRef.current = null;
}

// CRITICAL: Immediately stop any in-progress animation before starting a new one
// This prevents the old animation from interfering with position calculations
if (elementRef.current.style.transition) {
if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - Animation in progress, freezing element");
}

// Get current visual position (with transform applied)
const currentVisualY = elementRef.current.getBoundingClientRect().y;

// Get the pure DOM position without any transforms
// Temporarily remove transform to get true DOM position
elementRef.current.style.transform = "none";
elementRef.current.style.transition = "none";
const pureDOMY = elementRef.current.getBoundingClientRect().y;

// Calculate offset needed to keep element at current visual position
const offsetY = currentVisualY - pureDOMY;

if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - Freeze details", {
currentVisualY,
pureDOMY,
offsetY,
});
}

// Set the frozen transform to keep element at current visual position
elementRef.current.style.transform = `translate3d(0px, ${offsetY}px, 0px)`;

// Force reflow to ensure the freeze is applied
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
elementRef.current.offsetHeight;

// CRITICAL: Recapture toBounds after freezing the element
// The DOM position has changed (it's now at pureDOMY), so we need to update toBounds
toBounds = elementRef.current.getBoundingClientRect();

if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - toBounds after freeze", {
toBounds: { x: toBounds.x, y: toBounds.y },
});
}
}

// Merge animation config with defaults
const finalConfig = {
...ANIMATION_CONFIGS.ROW_REORDER,
Expand Down Expand Up @@ -179,6 +304,18 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate
const animationEndY =
parentScrollTop + clientHeight + dynamicDistance + leavingStagger + positionVariance;

if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - ANIMATING: Moving below viewport", {
startY: fromBounds.y,
endY: animationEndY,
finalY: toBounds.y,
dynamicDistance,
leavingStagger,
positionVariance,
elementPosition,
});
}

animateWithCustomCoordinates({
element: elementRef.current,
options: {
Expand Down Expand Up @@ -211,6 +348,18 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate
const animationEndY =
parentScrollTop - dynamicDistance - leavingStagger - positionVariance;

if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - ANIMATING: Moving above viewport", {
startY: fromBounds.y,
endY: animationEndY,
finalY: toBounds.y,
dynamicDistance,
leavingStagger,
positionVariance,
elementPosition,
});
}

animateWithCustomCoordinates({
element: elementRef.current,
options: {
Expand Down Expand Up @@ -243,6 +392,19 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate
const animationStartY =
parentScrollTop + clientHeight + dynamicDistance + enteringStagger;

if (isDebugCell) {
console.log(
"🔍 [Animate] Cell 3-companyName - ANIMATING: Entering from below viewport",
{
startY: animationStartY,
endY: toBounds.y,
dynamicDistance,
enteringStagger,
elementPosition,
}
);
}

animateWithCustomCoordinates({
element: elementRef.current,
options: {
Expand All @@ -269,6 +431,19 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate
(elementPosition % ENTERING_STAGGER_CYCLE) * baseStagger * ENTERING_STAGGER_MULTIPLIER;
const animationStartY = parentScrollTop - dynamicDistance - enteringStagger;

if (isDebugCell) {
console.log(
"🔍 [Animate] Cell 3-companyName - ANIMATING: Entering from above viewport",
{
startY: animationStartY,
endY: toBounds.y,
dynamicDistance,
enteringStagger,
elementPosition,
}
);
}

animateWithCustomCoordinates({
element: elementRef.current,
options: {
Expand All @@ -284,16 +459,29 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate
}
}

if (isDebugCell) {
console.log("🔍 [Animate] Cell 3-companyName - ANIMATING: Regular flip animation", {
fromBounds: { x: fromBounds.x, y: fromBounds.y },
toBounds: { x: toBounds.x, y: toBounds.y },
deltaY: toBounds.y - fromBounds.y,
});
}

// Start the animation and store the cleanup function
flipElement({
element: elementRef.current,
fromBounds,
toBounds,
finalConfig,
}).then((cleanup) => {
cleanupCallbackRef.current = cleanup;
});
} else {
}
}, [
id,
allowAnimations,
capturedPositionsRef,
isResizing,
isScrolling,
parentRef,
Expand Down
Loading