From 0fa126e7be0cf3bbbe5dbc38b15704daabd069fb Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:51:24 -0500 Subject: [PATCH 01/14] Removing some complexity from animations --- src/components/animate/animation-utils.ts | 70 ++------------------- src/components/simple-table/SimpleTable.tsx | 2 +- 2 files changed, 5 insertions(+), 67 deletions(-) diff --git a/src/components/animate/animation-utils.ts b/src/components/animate/animation-utils.ts index 2bfad155..05039dc0 100644 --- a/src/components/animate/animation-utils.ts +++ b/src/components/animate/animation-utils.ts @@ -1,39 +1,17 @@ import CellValue from "../../types/CellValue"; import { AnimationConfig, FlipAnimationOptions, CustomAnimationOptions } from "./types"; -/** - * Check if user prefers reduced motion - */ -export const prefersReducedMotion = (): boolean => { - if (typeof window === "undefined") return false; - return window.matchMedia("(prefers-reduced-motion: reduce)").matches; -}; - /** * Animation configs for different types of movements */ export const ANIMATION_CONFIGS = { - // For column reordering (horizontal movement) - COLUMN_REORDER: { - // duration: 3000, - duration: 180, - easing: "cubic-bezier(0.2, 0.0, 0.2, 1)", - delay: 0, - }, // For row reordering (vertical movement) ROW_REORDER: { // duration: 3000, - duration: 200, + duration: 500, easing: "cubic-bezier(0.2, 0.0, 0.2, 1)", delay: 0, }, - // For reduced motion users - REDUCED_MOTION: { - // duration: 3000, - duration: 150, // Even faster for reduced motion - easing: "ease-out", - delay: 0, - }, } as const; /** @@ -42,11 +20,7 @@ export const ANIMATION_CONFIGS = { export const createAnimationConfig = ( overrides: Partial = {} ): AnimationConfig => { - const baseConfig = prefersReducedMotion() - ? ANIMATION_CONFIGS.REDUCED_MOTION - : ANIMATION_CONFIGS.ROW_REORDER; // Default to row reorder as it's more common in tables - - return { ...baseConfig, ...overrides }; + return { ...ANIMATION_CONFIGS.ROW_REORDER, ...overrides }; }; /** @@ -142,23 +116,7 @@ const animateToFinalPosition = ( /** * Get appropriate animation config based on movement type and user preferences */ -export const getAnimationConfig = ( - options: FlipAnimationOptions = {}, - movementType?: "column" | "row" -): AnimationConfig => { - // Check for user's motion preferences first - if (prefersReducedMotion()) { - return { ...ANIMATION_CONFIGS.REDUCED_MOTION, ...options }; - } - - // Use specific config based on movement type - if (movementType === "column") { - return { ...ANIMATION_CONFIGS.COLUMN_REORDER, ...options }; - } - if (movementType === "row") { - return { ...ANIMATION_CONFIGS.ROW_REORDER, ...options }; - } - +export const getAnimationConfig = (options: FlipAnimationOptions = {}): AnimationConfig => { // Fall back to default config return { ...ANIMATION_CONFIGS.ROW_REORDER, ...options }; }; @@ -186,17 +144,8 @@ export const flipElement = async ({ return; } - // Skip animation entirely if user prefers reduced motion and no explicit override - if (prefersReducedMotion() && finalConfig.respectReducedMotion !== false) { - return; - } - - // Determine movement type based on the invert values - const isColumnMovement = Math.abs(invert.x) > Math.abs(invert.y); - const movementType = isColumnMovement ? "column" : "row"; - // Get appropriate config based on movement type and user preferences - const config = getAnimationConfig(finalConfig, movementType); + const config = getAnimationConfig(finalConfig); // Clean up any existing animation before starting a new one cleanupAnimation(element); @@ -231,17 +180,6 @@ export const animateWithCustomCoordinates = async ({ respectReducedMotion = true, } = options; - // Skip animation entirely if user prefers reduced motion and no explicit override - if (prefersReducedMotion() && respectReducedMotion) { - // Jump directly to final position if specified - if (finalY !== undefined) { - element.style.transform = ""; - element.style.top = `${finalY}px`; - } - if (onComplete) onComplete(); - return; - } - // Get element's current position const rect = element.getBoundingClientRect(); const currentY = rect.top; diff --git a/src/components/simple-table/SimpleTable.tsx b/src/components/simple-table/SimpleTable.tsx index 2e6498f6..bb0bb34b 100644 --- a/src/components/simple-table/SimpleTable.tsx +++ b/src/components/simple-table/SimpleTable.tsx @@ -116,7 +116,7 @@ const SimpleTable = (props: SimpleTableProps) => { }; const SimpleTableComp = ({ - allowAnimations = false, + allowAnimations = true, cellUpdateFlash = false, className, columnBorders = false, From dcf78b059c0115b580d75d6810e51581c6055947 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Tue, 30 Sep 2025 19:17:15 -0500 Subject: [PATCH 02/14] Linter clean up --- src/components/animate/Animate.tsx | 5 +++++ src/components/animate/animation-utils.ts | 5 ++--- src/components/animate/types.ts | 2 -- src/components/simple-table/TableHeaderCell.tsx | 7 ------- src/utils/columnUtils.ts | 1 - 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/components/animate/Animate.tsx b/src/components/animate/Animate.tsx index fa9d0cc9..7930ff6d 100644 --- a/src/components/animate/Animate.tsx +++ b/src/components/animate/Animate.tsx @@ -51,6 +51,11 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate const toBounds = elementRef.current.getBoundingClientRect(); const fromBounds = fromBoundsRef.current; + if (id === "1-name") { + console.log(elementRef.current.getBoundingClientRect()); + console.log(fromBoundsRef.current); + } + // If we're currently scrolling, don't animate and don't update bounds if (isScrolling) { return; diff --git a/src/components/animate/animation-utils.ts b/src/components/animate/animation-utils.ts index 05039dc0..e7de9908 100644 --- a/src/components/animate/animation-utils.ts +++ b/src/components/animate/animation-utils.ts @@ -7,8 +7,8 @@ import { AnimationConfig, FlipAnimationOptions, CustomAnimationOptions } from ". export const ANIMATION_CONFIGS = { // For row reordering (vertical movement) ROW_REORDER: { - // duration: 3000, - duration: 500, + duration: 3000, + // duration: 500, easing: "cubic-bezier(0.2, 0.0, 0.2, 1)", delay: 0, }, @@ -177,7 +177,6 @@ export const animateWithCustomCoordinates = async ({ easing = "cubic-bezier(0.2, 0.0, 0.2, 1)", delay = 0, onComplete, - respectReducedMotion = true, } = options; // Get element's current position diff --git a/src/components/animate/types.ts b/src/components/animate/types.ts index 64ecd66f..062fbedd 100644 --- a/src/components/animate/types.ts +++ b/src/components/animate/types.ts @@ -13,7 +13,6 @@ export interface FlipAnimationOptions { maxYLeavingRatio?: number; maxYEnteringRatio?: number; onComplete?: () => void; - respectReducedMotion?: boolean; // Whether to respect user's reduced motion preference (default: true) } export interface CustomAnimationOptions { @@ -24,5 +23,4 @@ export interface CustomAnimationOptions { easing?: string; delay?: number; onComplete?: () => void; - respectReducedMotion?: boolean; } diff --git a/src/components/simple-table/TableHeaderCell.tsx b/src/components/simple-table/TableHeaderCell.tsx index 630c9ca3..a9372a42 100644 --- a/src/components/simple-table/TableHeaderCell.tsx +++ b/src/components/simple-table/TableHeaderCell.tsx @@ -249,13 +249,6 @@ const TableHeaderCell = ({ [headers, setHeaders, onHeaderEdit, header] ); - // Handle header dropdown toggle - const handleHeaderDropdownToggle = useCallback(() => { - if (setActiveHeaderDropdown) { - setActiveHeaderDropdown(isDropdownOpen ? null : header); - } - }, [setActiveHeaderDropdown, isDropdownOpen, header]); - // Close header dropdown const handleHeaderDropdownClose = useCallback(() => { if (setActiveHeaderDropdown) { diff --git a/src/utils/columnUtils.ts b/src/utils/columnUtils.ts index 6cc5464e..441dd8aa 100644 --- a/src/utils/columnUtils.ts +++ b/src/utils/columnUtils.ts @@ -1,5 +1,4 @@ import HeaderObject, { Accessor } from "../types/HeaderObject"; -import { findLeafHeaders } from "./headerWidthUtils"; const getColumnWidth = (header: HeaderObject) => { let { minWidth, width } = header; From 812ad53b40a1aa5ef80122b9c93065212477ec34 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Thu, 2 Oct 2025 21:31:41 -0500 Subject: [PATCH 03/14] Animation improvements --- src/components/animate/Animate.tsx | 75 ++++++++- src/components/animate/animation-utils.ts | 173 +++++++++++++++++--- src/components/simple-table/SimpleTable.tsx | 30 +++- src/context/TableContext.tsx | 1 + src/hooks/useSortableData.ts | 7 +- 5 files changed, 257 insertions(+), 29 deletions(-) diff --git a/src/components/animate/Animate.tsx b/src/components/animate/Animate.tsx index 7930ff6d..7618fde4 100644 --- a/src/components/animate/Animate.tsx +++ b/src/components/animate/Animate.tsx @@ -31,11 +31,13 @@ interface AnimateProps extends Omit, "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(null); const fromBoundsRef = useRef(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 @@ -49,11 +51,22 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate } const toBounds = elementRef.current.getBoundingClientRect(); - const fromBounds = fromBoundsRef.current; + // 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; + + // Debug logging for one specific cell if (id === "1-name") { - console.log(elementRef.current.getBoundingClientRect()); - console.log(fromBoundsRef.current); + console.log("πŸ“ [Animate useLayoutEffect] 1-name:", { + hasCapturedPosition: !!capturedPosition, + capturedY: capturedPosition?.y, + fromBoundsRefY: fromBoundsRef.current?.y, + fromBoundsY: fromBounds?.y, + toBoundsY: toBounds.y, + delta: fromBounds ? toBounds.y - fromBounds.y : null, + }); } // If we're currently scrolling, don't animate and don't update bounds @@ -70,12 +83,21 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate // If resizing just ended, update the previous bounds without animating if (previousResizingState && !isResizing) { fromBoundsRef.current = toBounds; + capturedPositionsRef.current.delete(id); // Clear captured position 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 (id === "1-name") { + console.log("🧹 [Animate] Cleared captured position for 1-name"); + } + } + // If there's no previous bound data, don't animate (prevents first render animations) if (!fromBounds) { return; @@ -101,6 +123,48 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate hasPositionChanged = hasDOMPositionChanged; if (hasPositionChanged) { + if (id === "1-name") { + console.log("🎬 [Animate] Starting animation for 1-name", { + deltaX, + deltaY, + fromY: fromBounds.y, + toY: toBounds.y, + currentTransform: elementRef.current.style.transform, + currentTransition: elementRef.current.style.transition, + }); + } + + // 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 (id === "1-name") { + console.log("🚫 [Animate] Cancelling old animation cleanup for 1-name"); + } + 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 (id === "1-name") { + console.log("⏸️ [Animate] Stopping in-progress animation for 1-name", { + beforeStopVisualY: elementRef.current.getBoundingClientRect().y, + currentTransform: elementRef.current.style.transform, + }); + } + + // Force stop the animation by removing transition and keeping current transform + elementRef.current.style.transition = "none"; + + if (id === "1-name") { + console.log("⏸️ [Animate] After stopping animation for 1-name", { + afterStopVisualY: elementRef.current.getBoundingClientRect().y, + transform: elementRef.current.style.transform, + }); + } + } + // Merge animation config with defaults const finalConfig = { ...ANIMATION_CONFIGS.ROW_REORDER, @@ -289,11 +353,14 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate } } + // Start the animation and store the cleanup function flipElement({ element: elementRef.current, fromBounds, toBounds, finalConfig, + }).then((cleanup) => { + cleanupCallbackRef.current = cleanup; }); } else { } diff --git a/src/components/animate/animation-utils.ts b/src/components/animate/animation-utils.ts index e7de9908..312c4776 100644 --- a/src/components/animate/animation-utils.ts +++ b/src/components/animate/animation-utils.ts @@ -46,6 +46,9 @@ export const calculateInvert = ( * Applies initial transform to element for FLIP animation */ export const applyInitialTransform = (element: HTMLElement, invert: { x: number; y: number }) => { + const elementId = element.getAttribute("data-animate-id"); + const beforeY = element.getBoundingClientRect().y; + element.style.transform = `translate3d(${invert.x}px, ${invert.y}px, 0)`; element.style.transition = "none"; // Performance optimizations for smoother animations @@ -53,12 +56,30 @@ export const applyInitialTransform = (element: HTMLElement, invert: { x: number; element.style.backfaceVisibility = "hidden"; // Prevent flickering during animation // Add animating class to ensure proper z-index during animation element.classList.add("st-animating"); + + if (elementId === "1-name") { + console.log("🎨 [applyInitialTransform] 1-name", { + invertY: invert.y, + beforeY, + afterY: element.getBoundingClientRect().y, + transform: element.style.transform, + }); + } }; /** * Cleans up animation styles from element */ const cleanupAnimation = (element: HTMLElement) => { + const elementId = element.getAttribute("data-animate-id"); + if (elementId === "1-name") { + console.log("🧼 [cleanupAnimation] Cleaning up 1-name", { + currentTransform: element.style.transform, + currentTransition: element.style.transition, + visualY: element.getBoundingClientRect().y, + }); + } + element.style.transition = ""; element.style.transitionDelay = ""; element.style.transform = ""; @@ -68,18 +89,27 @@ const cleanupAnimation = (element: HTMLElement) => { element.style.backfaceVisibility = ""; // Remove animating class to restore normal z-index element.classList.remove("st-animating"); + + if (elementId === "1-name") { + console.log("🧼 [cleanupAnimation] After cleanup 1-name", { + visualY: element.getBoundingClientRect().y, + }); + } }; /** * Animates element to its final position + * Returns a Promise that resolves to a cleanup function that can cancel the animation */ const animateToFinalPosition = ( element: HTMLElement, config: AnimationConfig, options: FlipAnimationOptions = {}, id?: CellValue -): Promise => { +): Promise<() => void> => { return new Promise((resolve) => { + const elementId = element.getAttribute("data-animate-id"); + // Force a reflow to ensure the initial transform is applied // eslint-disable-next-line @typescript-eslint/no-unused-expressions element.offsetHeight; @@ -92,24 +122,74 @@ const animateToFinalPosition = ( element.style.transitionDelay = `${config.delay}ms`; } + if (elementId === "1-name") { + console.log("🎭 [animateToFinalPosition] Before animating 1-name", { + visualY: element.getBoundingClientRect().y, + currentTransform: element.style.transform, + willAnimateTo: "translate3d(0, 0, 0)", + duration: config.duration, + }); + } + // Animate to final position element.style.transform = "translate3d(0, 0, 0)"; + if (elementId === "1-name") { + console.log("🎭 [animateToFinalPosition] After setting final transform 1-name", { + visualY: element.getBoundingClientRect().y, + transform: element.style.transform, + }); + } + + let isCleanedUp = false; + let timeoutId: ReturnType | null = null; + // Clean up after animation - const cleanup = () => { + const doCleanup = () => { + if (isCleanedUp) return; + isCleanedUp = true; + + if (elementId === "1-name") { + console.log("βœ… [animateToFinalPosition] Animation complete for 1-name", { + visualY: element.getBoundingClientRect().y, + }); + } + cleanupAnimation(element); - element.removeEventListener("transitionend", cleanup); + element.removeEventListener("transitionend", doCleanup); + + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } if (options.onComplete) { options.onComplete(); } - resolve(); }; - element.addEventListener("transitionend", cleanup); + element.addEventListener("transitionend", doCleanup); // Fallback timeout in case transitionend doesn't fire - setTimeout(cleanup, config.duration + (config.delay || 0) + 50); + timeoutId = setTimeout(doCleanup, config.duration + (config.delay || 0) + 50); + + // Return cleanup function that can cancel the animation + const cancelCleanup = () => { + if (isCleanedUp) return; + isCleanedUp = true; + + if (elementId === "1-name") { + console.log("🚫 [animateToFinalPosition] Animation cleanup cancelled for 1-name"); + } + + element.removeEventListener("transitionend", doCleanup); + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + }; + + resolve(cancelCleanup); }); }; @@ -125,6 +205,7 @@ export const getAnimationConfig = (options: FlipAnimationOptions = {}): Animatio * Performs FLIP animation on a single element * This function can be called multiple times on the same element - it will automatically * interrupt any ongoing animation and start a new one. + * Returns a cleanup function that can be called to cancel the animation. */ export const flipElement = async ({ element, @@ -136,25 +217,75 @@ export const flipElement = async ({ finalConfig: FlipAnimationOptions; fromBounds: DOMRect; toBounds: DOMRect; -}): Promise => { +}): Promise<() => void> => { const invert = calculateInvert(fromBounds, toBounds); + // Debug logging for one specific cell + const elementId = element.getAttribute("data-animate-id"); + if (elementId === "1-name") { + console.log("⚑ [flipElement] Called for 1-name", { + fromY: fromBounds.y, + toY: toBounds.y, + invertY: invert.y, + }); + } + // Skip animation if element hasn't moved if (invert.x === 0 && invert.y === 0) { - return; + if (elementId === "1-name") { + console.log("⏭️ [flipElement] No movement needed for 1-name, cleaning up"); + } + // Still need to clean up if there was an animation in progress + cleanupAnimation(element); + // Return no-op cleanup function + return () => {}; } // Get appropriate config based on movement type and user preferences const config = getAnimationConfig(finalConfig); - // Clean up any existing animation before starting a new one - cleanupAnimation(element); + if (elementId === "1-name") { + console.log("πŸ”„ [flipElement] Starting FLIP sequence for 1-name", { + step: "1-before-apply-transform", + currentVisualY: element.getBoundingClientRect().y, + currentTransform: element.style.transform, + currentTransition: element.style.transition, + }); + } - // Apply initial transform with limited values + // CRITICAL: Apply new transform BEFORE cleaning up old one to prevent flickering + // This ensures there's no gap where the element snaps to its DOM position + // First, apply the new initial transform (this will override the existing transform) applyInitialTransform(element, invert); - // Animate to final position - await animateToFinalPosition(element, config, finalConfig); + if (elementId === "1-name") { + console.log("πŸ”„ [flipElement] After applyInitialTransform for 1-name", { + step: "2-after-apply-transform", + currentVisualY: element.getBoundingClientRect().y, + currentTransform: element.style.transform, + }); + } + + // Now safe to clean up transition properties (but transform is already set above) + // Only clean transition-related properties, not transform + element.style.transition = ""; + element.style.transitionDelay = ""; + + if (elementId === "1-name") { + console.log("πŸ”„ [flipElement] After clearing transitions for 1-name", { + step: "3-after-clear-transitions", + currentVisualY: element.getBoundingClientRect().y, + }); + } + + // Animate to final position and get cleanup function + const cleanup = await animateToFinalPosition(element, config, finalConfig); + + if (elementId === "1-name") { + console.log("πŸ”„ [flipElement] FLIP sequence complete for 1-name"); + } + + return cleanup; }; /** @@ -188,21 +319,17 @@ export const animateWithCustomCoordinates = async ({ const endTransformY = endY - currentY; return new Promise((resolve) => { - // Clean up any existing animation - element.style.transition = ""; - element.style.transitionDelay = ""; - element.style.transform = ""; - element.style.willChange = ""; - element.style.backfaceVisibility = ""; - element.classList.remove("st-animating"); - - // Set initial position (startY) + // CRITICAL: Set initial position BEFORE cleaning up to prevent flickering + // Apply new transform first (overrides any existing transform) element.style.transform = `translate3d(0, ${startTransformY}px, 0)`; - element.style.transition = "none"; element.style.willChange = "transform"; element.style.backfaceVisibility = "hidden"; element.classList.add("st-animating"); + // Now clean up transition properties (transform already set above) + element.style.transition = "none"; + element.style.transitionDelay = ""; + // Force reflow // eslint-disable-next-line @typescript-eslint/no-unused-expressions element.offsetHeight; diff --git a/src/components/simple-table/SimpleTable.tsx b/src/components/simple-table/SimpleTable.tsx index bb0bb34b..1dfdd43d 100644 --- a/src/components/simple-table/SimpleTable.tsx +++ b/src/components/simple-table/SimpleTable.tsx @@ -179,6 +179,7 @@ const SimpleTableComp = ({ // Refs const draggedHeaderRef = useRef(null); const hoveredHeaderRef = useRef(null); + const capturedPositionsRef = useRef>(new Map()); const mainBodyRef = useRef(null); const pinnedLeftRef = useRef(null); @@ -297,6 +298,27 @@ const SimpleTableComp = ({ onFilterChange, }); + // Function to capture all element positions (called right before sort state change) + const captureAllPositions = useCallback(() => { + const allElements = document.querySelectorAll("[data-animate-id]"); + capturedPositionsRef.current.clear(); + allElements.forEach((el) => { + const id = el.getAttribute("data-animate-id"); + if (id && el instanceof HTMLElement) { + const rect = el.getBoundingClientRect(); + capturedPositionsRef.current.set(id, rect); + + // Debug logging for one specific cell + if (id === "1-name") { + console.log("🎯 [captureAllPositions] Capturing position for 1-name:", { + y: rect.y, + top: rect.top, + }); + } + } + }); + }, []); + // Use custom hook for sorting (now operates on filtered rows) const { sort, sortedRows, updateSort, computeSortedRowsPreview } = useSortableData({ headers, @@ -304,6 +326,7 @@ const SimpleTableComp = ({ externalSortHandling, onSortChange, rowGrouping, + onBeforeSort: captureAllPositions, }); // Process rows through pagination, grouping, and virtualization @@ -370,6 +393,7 @@ const SimpleTableComp = ({ prepareForSortChange(accessor); // STAGE 2: Apply sort after Stage 1 is rendered (next frame) + // Note: Position capture happens in updateSort via onBeforeSort callback setTimeout(() => { updateSort(accessor); }, 0); @@ -409,11 +433,14 @@ const SimpleTableComp = ({ // STAGE 2: Apply filter after Stage 1 is rendered (next frame) setTimeout(() => { + // Capture positions right before filter state change + captureAllPositions(); + // Update internal state and call external handler if provided internalHandleApplyFilter(filter); }, 0); }, - [prepareForFilterChange, internalHandleApplyFilter] + [prepareForFilterChange, internalHandleApplyFilter, captureAllPositions] ); return ( @@ -421,6 +448,7 @@ const SimpleTableComp = ({ value={{ allowAnimations, areAllRowsSelected, + capturedPositionsRef, cellRegistry: cellRegistryRef.current, cellUpdateFlash, clearSelection, diff --git a/src/context/TableContext.tsx b/src/context/TableContext.tsx index 94b26bfb..25a7befa 100644 --- a/src/context/TableContext.tsx +++ b/src/context/TableContext.tsx @@ -33,6 +33,7 @@ interface TableContextType { activeHeaderDropdown?: HeaderObject | null; allowAnimations?: boolean; areAllRowsSelected?: () => boolean; + capturedPositionsRef: MutableRefObject>; cellRegistry?: Map; cellUpdateFlash?: boolean; clearSelection?: () => void; diff --git a/src/hooks/useSortableData.ts b/src/hooks/useSortableData.ts index f331c246..09bce26a 100644 --- a/src/hooks/useSortableData.ts +++ b/src/hooks/useSortableData.ts @@ -48,12 +48,14 @@ const useSortableData = ({ externalSortHandling, onSortChange, rowGrouping, + onBeforeSort, }: { headers: HeaderObject[]; tableRows: Row[]; externalSortHandling: boolean; onSortChange?: (sort: SortColumn | null) => void; rowGrouping?: string[]; + onBeforeSort?: () => void; }) => { // Single sort state instead of complex 3-state system const [sort, setSort] = useState(null); @@ -154,10 +156,13 @@ const useSortableData = ({ }; } + // CRITICAL: Capture positions right before state change (react-flip-move pattern) + onBeforeSort?.(); + setSort(newSortColumn); onSortChange?.(newSortColumn); }, - [sort, headers, onSortChange] + [sort, headers, onSortChange, onBeforeSort] ); // Function to preview what rows would be after applying a sort From 3c07b3eafbff50ff8d16f2c6cf4f53fd427bd024 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Thu, 2 Oct 2025 21:55:02 -0500 Subject: [PATCH 04/14] Tables with few rows, animation works --- src/components/animate/Animate.tsx | 42 ++++++++++++++++++--- src/components/animate/animation-utils.ts | 45 +++++++++++++++++++++-- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/components/animate/Animate.tsx b/src/components/animate/Animate.tsx index 7618fde4..45e089c6 100644 --- a/src/components/animate/Animate.tsx +++ b/src/components/animate/Animate.tsx @@ -50,7 +50,7 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate return; } - const toBounds = elementRef.current.getBoundingClientRect(); + let toBounds = elementRef.current.getBoundingClientRect(); // 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 @@ -147,20 +147,52 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate // 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) { + // Get current visual position (with transform applied) + const currentVisualY = elementRef.current.getBoundingClientRect().y; + const oldTransform = elementRef.current.style.transform; + const oldTransition = elementRef.current.style.transition; + if (id === "1-name") { console.log("⏸️ [Animate] Stopping in-progress animation for 1-name", { - beforeStopVisualY: elementRef.current.getBoundingClientRect().y, - currentTransform: elementRef.current.style.transform, + beforeStopVisualY: currentVisualY, + currentTransform: oldTransform, }); } - // Force stop the animation by removing transition and keeping current transform + // CRITICAL: 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; + + // 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; if (id === "1-name") { console.log("⏸️ [Animate] After stopping animation for 1-name", { afterStopVisualY: elementRef.current.getBoundingClientRect().y, - transform: elementRef.current.style.transform, + frozenTransform: elementRef.current.style.transform, + pureDOMY, + currentVisualY, + offsetY, + }); + } + + // 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 (id === "1-name") { + console.log("πŸ”„ [Animate] Recaptured toBounds after freeze", { + newToBoundsY: toBounds.y, + shouldMatch: currentVisualY, }); } } diff --git a/src/components/animate/animation-utils.ts b/src/components/animate/animation-utils.ts index 312c4776..556edf88 100644 --- a/src/components/animate/animation-utils.ts +++ b/src/components/animate/animation-utils.ts @@ -7,7 +7,7 @@ import { AnimationConfig, FlipAnimationOptions, CustomAnimationOptions } from ". export const ANIMATION_CONFIGS = { // For row reordering (vertical movement) ROW_REORDER: { - duration: 3000, + duration: 9000, // duration: 500, easing: "cubic-bezier(0.2, 0.0, 0.2, 1)", delay: 0, @@ -47,9 +47,45 @@ export const calculateInvert = ( */ export const applyInitialTransform = (element: HTMLElement, invert: { x: number; y: number }) => { const elementId = element.getAttribute("data-animate-id"); - const beforeY = element.getBoundingClientRect().y; + const currentVisualY = element.getBoundingClientRect().y; + const hasExistingTransform = element.style.transform && element.style.transform !== "none"; - element.style.transform = `translate3d(${invert.x}px, ${invert.y}px, 0)`; + // CRITICAL: If element has a frozen transform, we need to recalculate invert from pure DOM position + let adjustedInvertX = invert.x; + let adjustedInvertY = invert.y; + + if (hasExistingTransform) { + const oldTransform = element.style.transform; + + // Temporarily remove transform to get pure DOM position + element.style.transform = "none"; + const pureDOMY = element.getBoundingClientRect().y; + const pureDOMX = element.getBoundingClientRect().x; + + // Recalculate invert: we want to go from current visual position to where fromBounds expects + // The invert passed in assumes we're at pureDOMY, but we need to adjust for frozen position + // Original calculation: fromBoundsY - toBoundsY = invert.y + // But toBounds is the frozen visual position, not DOM position + // So we need: fromBoundsY - pureDOMY = adjusted invert + const fromBoundsY = currentVisualY + invert.y; // Reverse engineer fromBounds + const fromBoundsX = element.getBoundingClientRect().x + invert.x; + + adjustedInvertY = fromBoundsY - pureDOMY; + adjustedInvertX = fromBoundsX - pureDOMX; + + if (elementId === "1-name") { + console.log("πŸ”§ [applyInitialTransform] Adjusting for frozen transform", { + oldTransform, + currentVisualY, + pureDOMY, + originalInvertY: invert.y, + adjustedInvertY, + fromBoundsY, + }); + } + } + + element.style.transform = `translate3d(${adjustedInvertX}px, ${adjustedInvertY}px, 0)`; element.style.transition = "none"; // Performance optimizations for smoother animations element.style.willChange = "transform"; // Hint to browser for optimization @@ -60,7 +96,8 @@ export const applyInitialTransform = (element: HTMLElement, invert: { x: number; if (elementId === "1-name") { console.log("🎨 [applyInitialTransform] 1-name", { invertY: invert.y, - beforeY, + adjustedInvertY, + beforeY: currentVisualY, afterY: element.getBoundingClientRect().y, transform: element.style.transform, }); From ff0c3270e3bdef6246a149575f4ccbdcd5dc0b8b Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Thu, 2 Oct 2025 22:00:21 -0500 Subject: [PATCH 05/14] Removing debugging logs --- src/components/animate/Animate.tsx | 59 +-------- src/components/animate/animation-utils.ts | 135 ++------------------ src/components/simple-table/SimpleTable.tsx | 8 -- 3 files changed, 12 insertions(+), 190 deletions(-) diff --git a/src/components/animate/Animate.tsx b/src/components/animate/Animate.tsx index 45e089c6..469c6f38 100644 --- a/src/components/animate/Animate.tsx +++ b/src/components/animate/Animate.tsx @@ -57,18 +57,6 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate const capturedPosition = capturedPositionsRef.current.get(id); const fromBounds = capturedPosition || fromBoundsRef.current; - // Debug logging for one specific cell - if (id === "1-name") { - console.log("πŸ“ [Animate useLayoutEffect] 1-name:", { - hasCapturedPosition: !!capturedPosition, - capturedY: capturedPosition?.y, - fromBoundsRefY: fromBoundsRef.current?.y, - fromBoundsY: fromBounds?.y, - toBoundsY: toBounds.y, - delta: fromBounds ? toBounds.y - fromBounds.y : null, - }); - } - // If we're currently scrolling, don't animate and don't update bounds if (isScrolling) { return; @@ -83,7 +71,7 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate // If resizing just ended, update the previous bounds without animating if (previousResizingState && !isResizing) { fromBoundsRef.current = toBounds; - capturedPositionsRef.current.delete(id); // Clear captured position + capturedPositionsRef.current.delete(id); return; } @@ -93,9 +81,6 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate // Clear captured position after using it (it's been consumed) if (capturedPosition) { capturedPositionsRef.current.delete(id); - if (id === "1-name") { - console.log("🧹 [Animate] Cleared captured position for 1-name"); - } } // If there's no previous bound data, don't animate (prevents first render animations) @@ -123,23 +108,9 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate hasPositionChanged = hasDOMPositionChanged; if (hasPositionChanged) { - if (id === "1-name") { - console.log("🎬 [Animate] Starting animation for 1-name", { - deltaX, - deltaY, - fromY: fromBounds.y, - toY: toBounds.y, - currentTransform: elementRef.current.style.transform, - currentTransition: elementRef.current.style.transition, - }); - } - // 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 (id === "1-name") { - console.log("🚫 [Animate] Cancelling old animation cleanup for 1-name"); - } cleanupCallbackRef.current(); cleanupCallbackRef.current = null; } @@ -149,17 +120,8 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate if (elementRef.current.style.transition) { // Get current visual position (with transform applied) const currentVisualY = elementRef.current.getBoundingClientRect().y; - const oldTransform = elementRef.current.style.transform; - const oldTransition = elementRef.current.style.transition; - if (id === "1-name") { - console.log("⏸️ [Animate] Stopping in-progress animation for 1-name", { - beforeStopVisualY: currentVisualY, - currentTransform: oldTransform, - }); - } - - // CRITICAL: Get the pure DOM position without any transforms + // 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"; @@ -175,26 +137,9 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate // eslint-disable-next-line @typescript-eslint/no-unused-expressions elementRef.current.offsetHeight; - if (id === "1-name") { - console.log("⏸️ [Animate] After stopping animation for 1-name", { - afterStopVisualY: elementRef.current.getBoundingClientRect().y, - frozenTransform: elementRef.current.style.transform, - pureDOMY, - currentVisualY, - offsetY, - }); - } - // 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 (id === "1-name") { - console.log("πŸ”„ [Animate] Recaptured toBounds after freeze", { - newToBoundsY: toBounds.y, - shouldMatch: currentVisualY, - }); - } } // Merge animation config with defaults diff --git a/src/components/animate/animation-utils.ts b/src/components/animate/animation-utils.ts index 556edf88..dd06bf1f 100644 --- a/src/components/animate/animation-utils.ts +++ b/src/components/animate/animation-utils.ts @@ -7,8 +7,7 @@ import { AnimationConfig, FlipAnimationOptions, CustomAnimationOptions } from ". export const ANIMATION_CONFIGS = { // For row reordering (vertical movement) ROW_REORDER: { - duration: 9000, - // duration: 500, + duration: 500, easing: "cubic-bezier(0.2, 0.0, 0.2, 1)", delay: 0, }, @@ -44,94 +43,50 @@ export const calculateInvert = ( /** * Applies initial transform to element for FLIP animation + * Handles interrupting in-progress animations by adjusting the transform calculation */ export const applyInitialTransform = (element: HTMLElement, invert: { x: number; y: number }) => { - const elementId = element.getAttribute("data-animate-id"); const currentVisualY = element.getBoundingClientRect().y; const hasExistingTransform = element.style.transform && element.style.transform !== "none"; - // CRITICAL: If element has a frozen transform, we need to recalculate invert from pure DOM position + // If element has a frozen transform from an interrupted animation, + // we need to recalculate invert from pure DOM position let adjustedInvertX = invert.x; let adjustedInvertY = invert.y; if (hasExistingTransform) { - const oldTransform = element.style.transform; - // Temporarily remove transform to get pure DOM position element.style.transform = "none"; const pureDOMY = element.getBoundingClientRect().y; const pureDOMX = element.getBoundingClientRect().x; - // Recalculate invert: we want to go from current visual position to where fromBounds expects - // The invert passed in assumes we're at pureDOMY, but we need to adjust for frozen position - // Original calculation: fromBoundsY - toBoundsY = invert.y - // But toBounds is the frozen visual position, not DOM position - // So we need: fromBoundsY - pureDOMY = adjusted invert - const fromBoundsY = currentVisualY + invert.y; // Reverse engineer fromBounds + // Recalculate invert from pure DOM position + // fromBounds represents where we want to visually appear to come from + const fromBoundsY = currentVisualY + invert.y; const fromBoundsX = element.getBoundingClientRect().x + invert.x; adjustedInvertY = fromBoundsY - pureDOMY; adjustedInvertX = fromBoundsX - pureDOMX; - - if (elementId === "1-name") { - console.log("πŸ”§ [applyInitialTransform] Adjusting for frozen transform", { - oldTransform, - currentVisualY, - pureDOMY, - originalInvertY: invert.y, - adjustedInvertY, - fromBoundsY, - }); - } } element.style.transform = `translate3d(${adjustedInvertX}px, ${adjustedInvertY}px, 0)`; element.style.transition = "none"; - // Performance optimizations for smoother animations - element.style.willChange = "transform"; // Hint to browser for optimization - element.style.backfaceVisibility = "hidden"; // Prevent flickering during animation - // Add animating class to ensure proper z-index during animation + element.style.willChange = "transform"; + element.style.backfaceVisibility = "hidden"; element.classList.add("st-animating"); - - if (elementId === "1-name") { - console.log("🎨 [applyInitialTransform] 1-name", { - invertY: invert.y, - adjustedInvertY, - beforeY: currentVisualY, - afterY: element.getBoundingClientRect().y, - transform: element.style.transform, - }); - } }; /** * Cleans up animation styles from element */ const cleanupAnimation = (element: HTMLElement) => { - const elementId = element.getAttribute("data-animate-id"); - if (elementId === "1-name") { - console.log("🧼 [cleanupAnimation] Cleaning up 1-name", { - currentTransform: element.style.transform, - currentTransition: element.style.transition, - visualY: element.getBoundingClientRect().y, - }); - } - element.style.transition = ""; element.style.transitionDelay = ""; element.style.transform = ""; element.style.top = ""; - // Clean up performance optimization styles element.style.willChange = ""; element.style.backfaceVisibility = ""; - // Remove animating class to restore normal z-index element.classList.remove("st-animating"); - - if (elementId === "1-name") { - console.log("🧼 [cleanupAnimation] After cleanup 1-name", { - visualY: element.getBoundingClientRect().y, - }); - } }; /** @@ -145,8 +100,6 @@ const animateToFinalPosition = ( id?: CellValue ): Promise<() => void> => { return new Promise((resolve) => { - const elementId = element.getAttribute("data-animate-id"); - // Force a reflow to ensure the initial transform is applied // eslint-disable-next-line @typescript-eslint/no-unused-expressions element.offsetHeight; @@ -159,25 +112,9 @@ const animateToFinalPosition = ( element.style.transitionDelay = `${config.delay}ms`; } - if (elementId === "1-name") { - console.log("🎭 [animateToFinalPosition] Before animating 1-name", { - visualY: element.getBoundingClientRect().y, - currentTransform: element.style.transform, - willAnimateTo: "translate3d(0, 0, 0)", - duration: config.duration, - }); - } - // Animate to final position element.style.transform = "translate3d(0, 0, 0)"; - if (elementId === "1-name") { - console.log("🎭 [animateToFinalPosition] After setting final transform 1-name", { - visualY: element.getBoundingClientRect().y, - transform: element.style.transform, - }); - } - let isCleanedUp = false; let timeoutId: ReturnType | null = null; @@ -186,12 +123,6 @@ const animateToFinalPosition = ( if (isCleanedUp) return; isCleanedUp = true; - if (elementId === "1-name") { - console.log("βœ… [animateToFinalPosition] Animation complete for 1-name", { - visualY: element.getBoundingClientRect().y, - }); - } - cleanupAnimation(element); element.removeEventListener("transitionend", doCleanup); @@ -215,10 +146,6 @@ const animateToFinalPosition = ( if (isCleanedUp) return; isCleanedUp = true; - if (elementId === "1-name") { - console.log("🚫 [animateToFinalPosition] Animation cleanup cancelled for 1-name"); - } - element.removeEventListener("transitionend", doCleanup); if (timeoutId) { clearTimeout(timeoutId); @@ -257,21 +184,8 @@ export const flipElement = async ({ }): Promise<() => void> => { const invert = calculateInvert(fromBounds, toBounds); - // Debug logging for one specific cell - const elementId = element.getAttribute("data-animate-id"); - if (elementId === "1-name") { - console.log("⚑ [flipElement] Called for 1-name", { - fromY: fromBounds.y, - toY: toBounds.y, - invertY: invert.y, - }); - } - // Skip animation if element hasn't moved if (invert.x === 0 && invert.y === 0) { - if (elementId === "1-name") { - console.log("⏭️ [flipElement] No movement needed for 1-name, cleaning up"); - } // Still need to clean up if there was an animation in progress cleanupAnimation(element); // Return no-op cleanup function @@ -281,47 +195,18 @@ export const flipElement = async ({ // Get appropriate config based on movement type and user preferences const config = getAnimationConfig(finalConfig); - if (elementId === "1-name") { - console.log("πŸ”„ [flipElement] Starting FLIP sequence for 1-name", { - step: "1-before-apply-transform", - currentVisualY: element.getBoundingClientRect().y, - currentTransform: element.style.transform, - currentTransition: element.style.transition, - }); - } - - // CRITICAL: Apply new transform BEFORE cleaning up old one to prevent flickering + // Apply new transform BEFORE cleaning up old one to prevent flickering // This ensures there's no gap where the element snaps to its DOM position - // First, apply the new initial transform (this will override the existing transform) applyInitialTransform(element, invert); - if (elementId === "1-name") { - console.log("πŸ”„ [flipElement] After applyInitialTransform for 1-name", { - step: "2-after-apply-transform", - currentVisualY: element.getBoundingClientRect().y, - currentTransform: element.style.transform, - }); - } - // Now safe to clean up transition properties (but transform is already set above) // Only clean transition-related properties, not transform element.style.transition = ""; element.style.transitionDelay = ""; - if (elementId === "1-name") { - console.log("πŸ”„ [flipElement] After clearing transitions for 1-name", { - step: "3-after-clear-transitions", - currentVisualY: element.getBoundingClientRect().y, - }); - } - // Animate to final position and get cleanup function const cleanup = await animateToFinalPosition(element, config, finalConfig); - if (elementId === "1-name") { - console.log("πŸ”„ [flipElement] FLIP sequence complete for 1-name"); - } - return cleanup; }; diff --git a/src/components/simple-table/SimpleTable.tsx b/src/components/simple-table/SimpleTable.tsx index 1dfdd43d..f1a9997a 100644 --- a/src/components/simple-table/SimpleTable.tsx +++ b/src/components/simple-table/SimpleTable.tsx @@ -307,14 +307,6 @@ const SimpleTableComp = ({ if (id && el instanceof HTMLElement) { const rect = el.getBoundingClientRect(); capturedPositionsRef.current.set(id, rect); - - // Debug logging for one specific cell - if (id === "1-name") { - console.log("🎯 [captureAllPositions] Capturing position for 1-name:", { - y: rect.y, - top: rect.top, - }); - } } }); }, []); From 32692557d48a1956628184de54b1882a356cc223 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sat, 4 Oct 2025 12:21:00 -0500 Subject: [PATCH 06/14] Testing larger tables --- src/components/animate/animation-utils.ts | 2 +- src/stories/examples/finance-example/finance-headers.tsx | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/animate/animation-utils.ts b/src/components/animate/animation-utils.ts index dd06bf1f..9d15c34f 100644 --- a/src/components/animate/animation-utils.ts +++ b/src/components/animate/animation-utils.ts @@ -7,7 +7,7 @@ import { AnimationConfig, FlipAnimationOptions, CustomAnimationOptions } from ". export const ANIMATION_CONFIGS = { // For row reordering (vertical movement) ROW_REORDER: { - duration: 500, + duration: 9000, easing: "cubic-bezier(0.2, 0.0, 0.2, 1)", delay: 0, }, diff --git a/src/stories/examples/finance-example/finance-headers.tsx b/src/stories/examples/finance-example/finance-headers.tsx index ece80915..58f3b004 100644 --- a/src/stories/examples/finance-example/finance-headers.tsx +++ b/src/stories/examples/finance-example/finance-headers.tsx @@ -38,7 +38,6 @@ export const HEADERS: HeaderObject[] = [ isSortable: true, filterable: true, isEditable: true, - summaryColumn: true, align: "right", type: "number", cellRenderer: ({ row }) => { @@ -50,7 +49,6 @@ export const HEADERS: HeaderObject[] = [ }, }, { - summaryColumn: true, accessor: "priceChangePercent", label: "Change %", width: 160, @@ -81,7 +79,6 @@ export const HEADERS: HeaderObject[] = [ label: "Fundamentals", width: 380, isSortable: false, - collapsible: true, children: [ { accessor: "marketCap", @@ -90,7 +87,6 @@ export const HEADERS: HeaderObject[] = [ isSortable: true, filterable: true, isEditable: true, - summaryColumn: true, align: "right", type: "number", }, @@ -138,7 +134,6 @@ export const HEADERS: HeaderObject[] = [ isSortable: true, isEditable: true, filterable: true, - summaryColumn: true, align: "center", type: "enum", enumOptions: [ From 3eb923452fb48a70fd16af55f7cd9cb877e9de46 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:34:33 -0500 Subject: [PATCH 07/14] AI Changes --- src/components/animate/Animate.tsx | 2 + src/components/simple-table/SimpleTable.tsx | 30 ++++++--- src/components/simple-table/TableBody.tsx | 4 ++ src/components/simple-table/TableCell.tsx | 4 +- src/components/simple-table/TableContent.tsx | 6 ++ src/components/simple-table/TableSection.tsx | 40 +++++++++++- src/hooks/useTableRowProcessing.ts | 66 +++++++++++++++++++- src/types/TableBodyProps.ts | 2 + 8 files changed, 140 insertions(+), 14 deletions(-) diff --git a/src/components/animate/Animate.tsx b/src/components/animate/Animate.tsx index 469c6f38..bb29c89f 100644 --- a/src/components/animate/Animate.tsx +++ b/src/components/animate/Animate.tsx @@ -342,7 +342,9 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate } else { } }, [ + id, allowAnimations, + capturedPositionsRef, isResizing, isScrolling, parentRef, diff --git a/src/components/simple-table/SimpleTable.tsx b/src/components/simple-table/SimpleTable.tsx index f1a9997a..c25a2748 100644 --- a/src/components/simple-table/SimpleTable.tsx +++ b/src/components/simple-table/SimpleTable.tsx @@ -325,6 +325,8 @@ const SimpleTableComp = ({ const { currentTableRows, rowsToRender, + alreadyRenderedRows, + enteringDomRows, prepareForFilterChange, prepareForSortChange, isAnimating, @@ -386,9 +388,12 @@ const SimpleTableComp = ({ // STAGE 2: Apply sort after Stage 1 is rendered (next frame) // Note: Position capture happens in updateSort via onBeforeSort callback - setTimeout(() => { - updateSort(accessor); - }, 0); + // Use double RAF to ensure browser has completed layout before capturing positions + requestAnimationFrame(() => { + requestAnimationFrame(() => { + updateSort(accessor); + }); + }); }, [prepareForSortChange, updateSort] ); @@ -424,13 +429,16 @@ const SimpleTableComp = ({ prepareForFilterChange(filter); // STAGE 2: Apply filter after Stage 1 is rendered (next frame) - setTimeout(() => { - // Capture positions right before filter state change - captureAllPositions(); - - // Update internal state and call external handler if provided - internalHandleApplyFilter(filter); - }, 0); + // Use double RAF to ensure browser has completed layout before capturing positions + requestAnimationFrame(() => { + requestAnimationFrame(() => { + // Capture positions right before filter state change + captureAllPositions(); + + // Update internal state and call external handler if provided + internalHandleApplyFilter(filter); + }); + }); }, [prepareForFilterChange, internalHandleApplyFilter, captureAllPositions] ); @@ -549,6 +557,8 @@ const SimpleTableComp = ({ sort={sort} tableRows={currentTableRows} rowsToRender={rowsToRender} + alreadyRenderedRows={alreadyRenderedRows} + enteringDomRows={enteringDomRows} /> { @@ -161,6 +163,8 @@ const TableBody = ({ rowHeight, rowIndices, rowsToRender, + alreadyRenderedRows, + enteringDomRows, setHoveredIndex, }; diff --git a/src/components/simple-table/TableCell.tsx b/src/components/simple-table/TableCell.tsx index a0efbaf0..d24763f4 100644 --- a/src/components/simple-table/TableCell.tsx +++ b/src/components/simple-table/TableCell.tsx @@ -95,6 +95,8 @@ const TableCell = ({ // Get row ID and check if row has children const rowId = getRowId({ row, rowIdAccessor }); + const cellId = getCellId({ accessor: header.accessor, rowId }); + const currentGroupingKey = rowGrouping && rowGrouping[depth]; const cellHasChildren = currentGroupingKey ? hasNestedRows(row, currentGroupingKey) : false; const isRowExpanded = !unexpandedRows.has(String(rowId)); @@ -132,7 +134,7 @@ const TableCell = ({ const throttle = useThrottle(); // Cell focus id (used for keyboard navigation) - const cellId = getCellId({ accessor: header.accessor, rowId }); + // const cellId = getCellId({ accessor: header.accessor, rowId }); // Generate a unique key that includes the content value to force re-render when it changes const cellKey = getCellKey({ rowId, accessor: header.accessor }); diff --git a/src/components/simple-table/TableContent.tsx b/src/components/simple-table/TableContent.tsx index f2964b19..a4c2c0af 100644 --- a/src/components/simple-table/TableContent.tsx +++ b/src/components/simple-table/TableContent.tsx @@ -16,6 +16,8 @@ interface TableContentLocalProps { sort: SortColumn | null; tableRows: TableRow[]; rowsToRender: TableRow[]; + alreadyRenderedRows: TableRow[]; + enteringDomRows: TableRow[]; } const TableContent = ({ @@ -25,6 +27,8 @@ const TableContent = ({ sort, tableRows, rowsToRender, + alreadyRenderedRows, + enteringDomRows, }: TableContentLocalProps) => { // Get stable props from context const { columnResizing, editColumns, headers, collapsedHeaders } = useTableContext(); @@ -72,6 +76,8 @@ const TableContent = ({ pinnedRightWidth, setScrollTop, rowsToRender, + alreadyRenderedRows, + enteringDomRows, }; return ( diff --git a/src/components/simple-table/TableSection.tsx b/src/components/simple-table/TableSection.tsx index e4f5507a..67bf561c 100644 --- a/src/components/simple-table/TableSection.tsx +++ b/src/components/simple-table/TableSection.tsx @@ -28,6 +28,8 @@ interface TableSectionProps { rowHeight: number; rowIndices: RowIndices; rowsToRender: TableRowType[]; + alreadyRenderedRows: TableRowType[]; + enteringDomRows: TableRowType[]; setHoveredIndex: (index: number | null) => void; templateColumns: string; totalHeight: number; @@ -48,6 +50,8 @@ const TableSection = forwardRef( templateColumns, totalHeight, rowsToRender, + alreadyRenderedRows, + enteringDomRows, width, }, ref @@ -80,7 +84,8 @@ const TableSection = forwardRef( ...(!pinned && { flexGrow: 1 }), }} > - {rowsToRender.map((tableRow, index) => { + {/* Render already rendered rows first - keeps array indices stable */} + {alreadyRenderedRows.map((tableRow, index) => { const rowId = getRowId({ row: tableRow.row, rowIdAccessor }); return ( @@ -111,6 +116,39 @@ const TableSection = forwardRef( ); })} + {/* Render entering rows after - appends at end without affecting existing indices */} + {enteringDomRows.map((tableRow, index) => { + const rowId = getRowId({ row: tableRow.row, rowIdAccessor }); + const actualIndex = alreadyRenderedRows.length + index; + return ( + + {actualIndex !== 0 && ( + + )} + + + ); + })} ); diff --git a/src/hooks/useTableRowProcessing.ts b/src/hooks/useTableRowProcessing.ts index b981735b..1e360403 100644 --- a/src/hooks/useTableRowProcessing.ts +++ b/src/hooks/useTableRowProcessing.ts @@ -1,4 +1,4 @@ -import { useMemo, useState, useLayoutEffect, useCallback, useRef } from "react"; +import { useMemo, useState, useLayoutEffect, useCallback, useRef, useEffect } from "react"; import { BUFFER_ROW_COUNT } from "../consts/general-consts"; import { getVisibleRows } from "../utils/infiniteScrollUtils"; import { flattenRowsWithGrouping, getRowId } from "../utils/rowUtils"; @@ -49,6 +49,13 @@ const useTableRowProcessing = ({ const previousTableRowsRef = useRef([]); // Track ALL processed rows, not just visible const previousVisibleRowsRef = useRef([]); // Track only visible rows for animation + // Debug: Monitor when extendedRows state changes + useEffect(() => { + if (extendedRows.length > 0) { + // Animation state is now active + } + }, [extendedRows]); + // Track original positions of all rows (before any sort/filter applied) const originalPositionsRef = useRef>(new Map()); @@ -375,7 +382,13 @@ const useTableRowProcessing = ({ .filter(Boolean); if (enteringFromCurrentState.length > 0) { - setExtendedRows([...targetVisibleRows, ...enteringFromCurrentState]); + const newExtendedRows = [...targetVisibleRows, ...enteringFromCurrentState]; + setExtendedRows(newExtendedRows); + + // CRITICAL: Update previousVisibleRowsRef to reflect what's NOW on screen + // This ensures if another sort comes in mid-animation, we split based on + // the rows that are ACTUALLY rendered, not stale rows from before the first sort + previousVisibleRowsRef.current = targetVisibleRows; } }, [ @@ -393,6 +406,53 @@ const useTableRowProcessing = ({ ] ); + // Split rows into already rendered (stable) and entering (new) for separate rendering + const { alreadyRenderedRows, enteringDomRows } = useMemo(() => { + if (!allowAnimations || shouldPaginate || extendedRows.length === 0) { + // No animation or no extended rows: all rows are "already rendered" + return { + alreadyRenderedRows: rowsToRender, + enteringDomRows: [], + }; + } + + // During animation with extended rows: + // CRITICAL: Maintain ORIGINAL array order for already rendered rows + // Create a map of current rows by ID for quick lookup + const currentRowsById = new Map( + rowsToRender.map((row) => [String(getRowId({ row: row.row, rowIdAccessor })), row]) + ); + + // Start with previousVisibleRows in their ORIGINAL order + const alreadyRendered: any[] = []; + const previousRowIds = new Set(); + + previousVisibleRowsRef.current.forEach((prevRow) => { + const id = String(getRowId({ row: prevRow.row, rowIdAccessor })); + previousRowIds.add(id); + + // If this row still exists in current rowsToRender, add it (with updated position) + const currentRow = currentRowsById.get(id); + if (currentRow) { + alreadyRendered.push(currentRow); + } + }); + + // Find entering rows (rows in current but not in previous) + const entering: any[] = []; + rowsToRender.forEach((row) => { + const id = String(getRowId({ row: row.row, rowIdAccessor })); + if (!previousRowIds.has(id)) { + entering.push(row); + } + }); + + return { + alreadyRenderedRows: alreadyRendered, + enteringDomRows: entering, + }; + }, [rowsToRender, extendedRows.length, allowAnimations, shouldPaginate, rowIdAccessor]); + return { currentTableRows, currentVisibleRows: targetVisibleRows, @@ -400,6 +460,8 @@ const useTableRowProcessing = ({ prepareForFilterChange, prepareForSortChange, rowsToRender, + alreadyRenderedRows, + enteringDomRows, }; }; diff --git a/src/types/TableBodyProps.ts b/src/types/TableBodyProps.ts index e8fb166d..0a8be8df 100644 --- a/src/types/TableBodyProps.ts +++ b/src/types/TableBodyProps.ts @@ -13,6 +13,8 @@ interface TableBodyProps { pinnedRightTemplateColumns: string; pinnedRightWidth: number; rowsToRender: TableRow[]; + alreadyRenderedRows: TableRow[]; + enteringDomRows: TableRow[]; setScrollTop: Dispatch>; tableRows: TableRow[]; } From 49584a9603ef869d7941a4d45849ec0cb0299138 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sun, 5 Oct 2025 13:23:59 -0500 Subject: [PATCH 08/14] Removing unused code --- src/components/simple-table/SimpleTable.tsx | 1 - src/hooks/useTableRowProcessing.ts | 127 +------------------- 2 files changed, 1 insertion(+), 127 deletions(-) diff --git a/src/components/simple-table/SimpleTable.tsx b/src/components/simple-table/SimpleTable.tsx index c25a2748..a47e4cea 100644 --- a/src/components/simple-table/SimpleTable.tsx +++ b/src/components/simple-table/SimpleTable.tsx @@ -333,7 +333,6 @@ const SimpleTableComp = ({ } = useTableRowProcessing({ allowAnimations, sortedRows, - originalRows: aggregatedRows, currentPage, rowsPerPage, shouldPaginate, diff --git a/src/hooks/useTableRowProcessing.ts b/src/hooks/useTableRowProcessing.ts index 1e360403..bf501999 100644 --- a/src/hooks/useTableRowProcessing.ts +++ b/src/hooks/useTableRowProcessing.ts @@ -1,8 +1,7 @@ -import { useMemo, useState, useLayoutEffect, useCallback, useRef, useEffect } from "react"; +import { useMemo, useState, useCallback, useRef } from "react"; import { BUFFER_ROW_COUNT } from "../consts/general-consts"; import { getVisibleRows } from "../utils/infiniteScrollUtils"; import { flattenRowsWithGrouping, getRowId } from "../utils/rowUtils"; -import { ANIMATION_CONFIGS } from "../components/animate/animation-utils"; import Row from "../types/Row"; import { Accessor } from "../types/HeaderObject"; import { FilterCondition } from "../types/FilterTypes"; @@ -10,8 +9,6 @@ import { FilterCondition } from "../types/FilterTypes"; interface UseTableRowProcessingProps { allowAnimations: boolean; sortedRows: Row[]; - // Original unfiltered rows for establishing baseline positions - originalRows: Row[]; currentPage: number; rowsPerPage: number; shouldPaginate: boolean; @@ -30,7 +27,6 @@ interface UseTableRowProcessingProps { const useTableRowProcessing = ({ allowAnimations, sortedRows, - originalRows, currentPage, rowsPerPage, shouldPaginate, @@ -46,25 +42,8 @@ const useTableRowProcessing = ({ }: UseTableRowProcessingProps) => { const [isAnimating, setIsAnimating] = useState(false); const [extendedRows, setExtendedRows] = useState([]); - const previousTableRowsRef = useRef([]); // Track ALL processed rows, not just visible const previousVisibleRowsRef = useRef([]); // Track only visible rows for animation - // Debug: Monitor when extendedRows state changes - useEffect(() => { - if (extendedRows.length > 0) { - // Animation state is now active - } - }, [extendedRows]); - - // Track original positions of all rows (before any sort/filter applied) - const originalPositionsRef = useRef>(new Map()); - - // Capture values when animation starts to avoid dependency issues in timeout effect - const animationCaptureRef = useRef<{ - tableRows: any[]; - visibleRows: any[]; - } | null>(null); - // Process rows through pagination and grouping const processRowSet = useCallback( (rows: Row[]) => { @@ -103,21 +82,6 @@ const useTableRowProcessing = ({ ] ); - // Establish original positions from unfiltered/unsorted data - useMemo(() => { - // Only set original positions once when component first loads - if (originalPositionsRef.current.size === 0) { - const originalProcessedRows = processRowSet(originalRows); - const newOriginalPositions = new Map(); - originalProcessedRows.forEach((tableRow) => { - const id = String(getRowId({ row: tableRow.row, rowIdAccessor })); - newOriginalPositions.set(id, tableRow.position); - }); - - originalPositionsRef.current = newOriginalPositions; - } - }, [originalRows, processRowSet, rowIdAccessor]); - // Current table rows (processed for display) const currentTableRows = useMemo(() => { // Use sortedRows which already have filters applied @@ -165,95 +129,6 @@ const useTableRowProcessing = ({ [rowIdAccessor] ); - // Check if there are actual row changes (comparing all rows, not just visible) - const hasRowChanges = useMemo(() => { - if (previousTableRowsRef.current.length === 0) { - return false; - } - - const currentIds = currentTableRows.map((row) => - String(getRowId({ row: row.row, rowIdAccessor })) - ); - const previousIds = previousTableRowsRef.current.map((row) => - String(getRowId({ row: row.row, rowIdAccessor })) - ); - - const hasChanges = - currentIds.length !== previousIds.length || - !currentIds.every((id, index) => id === previousIds[index]); - - return hasChanges; - }, [currentTableRows, rowIdAccessor]); - - // Animation effect - useLayoutEffect(() => { - // Don't re-run effect while animation is in progress - if (isAnimating) { - return; - } - - // Always sync when not animating - if (!allowAnimations || shouldPaginate) { - setExtendedRows([]); // Clear extended rows to use normal virtualization - previousTableRowsRef.current = currentTableRows; - previousVisibleRowsRef.current = targetVisibleRows; - return; - } - - // Initialize on first render - if (previousTableRowsRef.current.length === 0) { - setExtendedRows([]); // Clear extended rows to use normal virtualization - previousTableRowsRef.current = currentTableRows; - previousVisibleRowsRef.current = targetVisibleRows; - return; - } - - // Check if rows actually changed - this detects STAGE 2 (after sort/filter applied) - if (!hasRowChanges) { - setExtendedRows([]); // Clear extended rows to use normal virtualization - previousTableRowsRef.current = currentTableRows; - previousVisibleRowsRef.current = targetVisibleRows; - return; - } - - // STAGE 2: Rows have new positions, trigger animation - // extendedRows already contains current + entering rows (from STAGE 1) - // Now the positions will update automatically when the component re-renders - - // Capture current values before starting animation - animationCaptureRef.current = { - tableRows: currentTableRows, - visibleRows: targetVisibleRows, - }; - - // Start animation - setIsAnimating(true); - }, [ - allowAnimations, - currentTableRows, - hasRowChanges, - isAnimating, - shouldPaginate, - targetVisibleRows, - ]); - - // Separate effect to handle animation timeout - only runs when we have extended rows to animate - useLayoutEffect(() => { - if (isAnimating && animationCaptureRef.current && extendedRows.length > 0) { - // STAGE 3: After animation completes, remove leaving rows - const timeout = setTimeout(() => { - const captured = animationCaptureRef.current!; - setIsAnimating(false); - setExtendedRows([]); // Clear extended rows to use normal virtualization - previousTableRowsRef.current = captured.tableRows; - previousVisibleRowsRef.current = captured.visibleRows; - animationCaptureRef.current = null; // Clean up - }, ANIMATION_CONFIGS.ROW_REORDER.duration + 100); - - return () => clearTimeout(timeout); - } - }, [isAnimating, extendedRows.length]); // Depend on isAnimating AND extendedRows length - // Final rows to render - handles 3-stage animation const rowsToRender = useMemo(() => { // If animations are disabled, always use normal virtualization From fbd956d76232084b9af60f9fba0d8465d0e84ea7 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:10:51 -0500 Subject: [PATCH 09/14] New animation state vars added but not working --- src/components/simple-table/SimpleTable.tsx | 16 +- src/components/simple-table/TableBody.tsx | 16 +- src/components/simple-table/TableContent.tsx | 20 +- src/components/simple-table/TableSection.tsx | 56 +++- src/hooks/useTableRowProcessing.ts | 273 ++++++++----------- src/types/TableBodyProps.ts | 7 +- 6 files changed, 181 insertions(+), 207 deletions(-) diff --git a/src/components/simple-table/SimpleTable.tsx b/src/components/simple-table/SimpleTable.tsx index a47e4cea..3a75e5c5 100644 --- a/src/components/simple-table/SimpleTable.tsx +++ b/src/components/simple-table/SimpleTable.tsx @@ -324,9 +324,9 @@ const SimpleTableComp = ({ // Process rows through pagination, grouping, and virtualization const { currentTableRows, - rowsToRender, - alreadyRenderedRows, - enteringDomRows, + currentVisibleRows, + rowsEnteringTheDom, + rowsLeavingTheDom, prepareForFilterChange, prepareForSortChange, isAnimating, @@ -383,7 +383,7 @@ const SimpleTableComp = ({ const onSort = useCallback( (accessor: Accessor) => { // STAGE 1: Prepare animation by adding entering rows before applying sort - prepareForSortChange(accessor); + prepareForSortChange(accessor, currentVisibleRows); // STAGE 2: Apply sort after Stage 1 is rendered (next frame) // Note: Position capture happens in updateSort via onBeforeSort callback @@ -394,7 +394,7 @@ const SimpleTableComp = ({ }); }); }, - [prepareForSortChange, updateSort] + [prepareForSortChange, updateSort, currentVisibleRows] ); const onTableHeaderDragEnd = useCallback((newHeaders: HeaderObject[]) => { @@ -550,14 +550,14 @@ const SimpleTableComp = ({ onMouseLeave={handleMouseUp} > { @@ -90,13 +90,13 @@ const TableBody = ({ const indices: RowIndices = {}; // Map each row's ID to its index in the visible rows array - rowsToRender.forEach((rowsToRender, index) => { + currentVisibleRows.forEach((rowsToRender, index) => { const rowId = String(getRowId({ row: rowsToRender.row, rowIdAccessor })); indices[rowId] = index; }); return indices; - }, [rowsToRender, rowIdAccessor]); + }, [currentVisibleRows, rowIdAccessor]); // Check if we should load more data const checkForLoadMore = useCallback( @@ -157,14 +157,14 @@ const TableBody = ({ // Create all props needed for TableSection const commonProps = { columnIndices, + currentVisibleRows, + rowsEnteringTheDom, + rowsLeavingTheDom, headerContainerRef, headers, hoveredIndex, rowHeight, rowIndices, - rowsToRender, - alreadyRenderedRows, - enteringDomRows, setHoveredIndex, }; diff --git a/src/components/simple-table/TableContent.tsx b/src/components/simple-table/TableContent.tsx index a4c2c0af..75d8f78c 100644 --- a/src/components/simple-table/TableContent.tsx +++ b/src/components/simple-table/TableContent.tsx @@ -10,25 +10,25 @@ import TableRow from "../../types/TableRow"; // Define props for the frequently changing values not in context interface TableContentLocalProps { + currentVisibleRows: TableRow[]; + rowsEnteringTheDom: TableRow[]; + rowsLeavingTheDom: TableRow[]; pinnedLeftWidth: number; pinnedRightWidth: number; setScrollTop: Dispatch>; sort: SortColumn | null; tableRows: TableRow[]; - rowsToRender: TableRow[]; - alreadyRenderedRows: TableRow[]; - enteringDomRows: TableRow[]; } const TableContent = ({ + currentVisibleRows, + rowsEnteringTheDom, + rowsLeavingTheDom, pinnedLeftWidth, pinnedRightWidth, setScrollTop, sort, tableRows, - rowsToRender, - alreadyRenderedRows, - enteringDomRows, }: TableContentLocalProps) => { // Get stable props from context const { columnResizing, editColumns, headers, collapsedHeaders } = useTableContext(); @@ -66,7 +66,9 @@ const TableContent = ({ }; const tableBodyProps: TableBodyProps = { - tableRows, + currentVisibleRows, + rowsEnteringTheDom, + rowsLeavingTheDom, mainTemplateColumns, pinnedLeftColumns, pinnedLeftTemplateColumns, @@ -75,9 +77,7 @@ const TableContent = ({ pinnedRightTemplateColumns, pinnedRightWidth, setScrollTop, - rowsToRender, - alreadyRenderedRows, - enteringDomRows, + tableRows, }; return ( diff --git a/src/components/simple-table/TableSection.tsx b/src/components/simple-table/TableSection.tsx index 67bf561c..e9ea8829 100644 --- a/src/components/simple-table/TableSection.tsx +++ b/src/components/simple-table/TableSection.tsx @@ -22,14 +22,14 @@ import { useTableContext } from "../../context/TableContext"; interface TableSectionProps { columnIndexStart?: number; // This is to know how many columns there were before this section to see if the columns are odd or even columnIndices: ColumnIndices; + currentVisibleRows: TableRowType[]; + rowsEnteringTheDom: TableRowType[]; + rowsLeavingTheDom: TableRowType[]; headers: HeaderObject[]; hoveredIndex: number | null; pinned?: Pinned; rowHeight: number; rowIndices: RowIndices; - rowsToRender: TableRowType[]; - alreadyRenderedRows: TableRowType[]; - enteringDomRows: TableRowType[]; setHoveredIndex: (index: number | null) => void; templateColumns: string; totalHeight: number; @@ -41,6 +41,9 @@ const TableSection = forwardRef( { columnIndexStart, columnIndices, + currentVisibleRows, + rowsEnteringTheDom, + rowsLeavingTheDom, headers, hoveredIndex, pinned, @@ -49,9 +52,6 @@ const TableSection = forwardRef( setHoveredIndex, templateColumns, totalHeight, - rowsToRender, - alreadyRenderedRows, - enteringDomRows, width, }, ref @@ -84,8 +84,7 @@ const TableSection = forwardRef( ...(!pinned && { flexGrow: 1 }), }} > - {/* Render already rendered rows first - keeps array indices stable */} - {alreadyRenderedRows.map((tableRow, index) => { + {currentVisibleRows.map((tableRow, index) => { const rowId = getRowId({ row: tableRow.row, rowIdAccessor }); return ( @@ -116,20 +115,18 @@ const TableSection = forwardRef( ); })} - {/* Render entering rows after - appends at end without affecting existing indices */} - {enteringDomRows.map((tableRow, index) => { + {rowsEnteringTheDom.map((tableRow, index) => { const rowId = getRowId({ row: tableRow.row, rowIdAccessor }); - const actualIndex = alreadyRenderedRows.length + index; return ( - {actualIndex !== 0 && ( + {index !== 0 && ( )} ( gridTemplateColumns={templateColumns} headers={headers} hoveredIndex={hoveredIndex} - index={actualIndex} + index={index} + key={rowId} + pinned={pinned} + rowHeight={rowHeight} + rowIndices={rowIndices} + setHoveredIndex={setHoveredIndex} + tableRow={tableRow} + /> + + ); + })} + {rowsLeavingTheDom.map((tableRow, index) => { + const rowId = getRowId({ row: tableRow.row, rowIdAccessor }); + return ( + + {index !== 0 && ( + + )} + { const [isAnimating, setIsAnimating] = useState(false); - const [extendedRows, setExtendedRows] = useState([]); - const previousVisibleRowsRef = useRef([]); // Track only visible rows for animation + const [rowsEnteringTheDom, setRowsEnteringTheDom] = useState([]); + const [rowsLeavingTheDom, setRowsLeavingTheDom] = useState([]); // Process rows through pagination and grouping const processRowSet = useCallback( @@ -99,81 +100,6 @@ const useTableRowProcessing = ({ }); }, [currentTableRows, contentHeight, rowHeight, scrollTop]); - // Categorize rows based on ID changes - const categorizeRows = useCallback( - (previousRows: any[], currentRows: any[]) => { - const previousIds = new Set( - previousRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) - ); - const currentIds = new Set( - currentRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) - ); - - const staying = currentRows.filter((row) => { - const id = String(getRowId({ row: row.row, rowIdAccessor })); - return previousIds.has(id); - }); - - const entering = currentRows.filter((row) => { - const id = String(getRowId({ row: row.row, rowIdAccessor })); - return !previousIds.has(id); - }); - - const leaving = previousRows.filter((row) => { - const id = String(getRowId({ row: row.row, rowIdAccessor })); - return !currentIds.has(id); - }); - - return { staying, entering, leaving }; - }, - [rowIdAccessor] - ); - - // Final rows to render - handles 3-stage animation - const rowsToRender = useMemo(() => { - // If animations are disabled, always use normal virtualization - if (!allowAnimations || shouldPaginate) { - return targetVisibleRows; - } - - // If we have extended rows (from STAGE 1), we need to dynamically update their positions - // to reflect the current sort/filter state (STAGE 2) - if (extendedRows.length > 0) { - // Create a map of ALL positions from currentTableRows (not just visible ones) - // This ensures we have positions for existing rows AND entering rows - const positionMap = new Map(); - currentTableRows.forEach((row) => { - const id = String(getRowId({ row: row.row, rowIdAccessor })); - positionMap.set(id, row.position); - }); - - // Update ALL rows in extendedRows with their new positions - const updatedExtendedRows = extendedRows.map((row) => { - const id = String(getRowId({ row: row.row, rowIdAccessor })); - const newPosition = positionMap.get(id); - - // If this row exists in the new sorted state, use its new position - // Otherwise keep the original position (for leaving rows that are no longer in the sorted data) - if (newPosition !== undefined) { - return { ...row, position: newPosition }; - } - return row; - }); - - return updatedExtendedRows; - } - - // Default: use normal visible rows (STAGE 3 after animation completes) - return targetVisibleRows; - }, [ - targetVisibleRows, - extendedRows, - currentTableRows, - allowAnimations, - shouldPaginate, - rowIdAccessor, - ]); - // Animation handlers for filter/sort changes const prepareForFilterChange = useCallback( (filter: any) => { @@ -190,25 +116,64 @@ const useTableRowProcessing = ({ scrollTop, }); - // CRITICAL: Compare VISIBLE rows (before filter) vs what WILL BE visible (after filter) - // This identifies rows that are entering the visible area - const { entering: visibleEntering } = categorizeRows(targetVisibleRows, newVisibleRows); - - // Find these entering rows in the CURRENT table state (before filter) to get original positions - const enteringFromCurrentState = visibleEntering - .map((enteringRow) => { - const id = String(getRowId({ row: enteringRow.row, rowIdAccessor })); - // Find this row in the current table state to get its original position + // Find all rows that WILL BE visible after the filter, but with their CURRENT positions (before filter) + // This gives us the starting point for animation + const rowsInCurrentPosition = newVisibleRows + .map((newVisibleRow) => { + const id = String(getRowId({ row: newVisibleRow.row, rowIdAccessor })); + // Find this row in the CURRENT table state (before filter) to get its current position const currentStateRow = currentTableRows.find( (currentRow) => String(getRowId({ row: currentRow.row, rowIdAccessor })) === id ); - return currentStateRow || enteringRow; // Fallback to enteringRow if not found + return currentStateRow || newVisibleRow; // Fallback to newVisibleRow if not found in current state }) .filter(Boolean); - if (enteringFromCurrentState.length > 0) { - setExtendedRows([...targetVisibleRows, ...enteringFromCurrentState]); - } + setIsAnimating(true); + + // Add unique rows to rowsEnteringTheDom (don't add duplicates from targetVisibleRows or existing rowsEnteringTheDom) + setRowsEnteringTheDom((existingRows) => { + // Create set of IDs already in targetVisibleRows + const targetVisibleIds = new Set( + targetVisibleRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) + ); + + // Create set of IDs already in existingRows + const existingRowIds = new Set( + existingRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) + ); + + // Filter to only include rows that aren't already in targetVisibleRows or existingRows + const uniqueNewRows = rowsInCurrentPosition.filter((row) => { + const id = String(getRowId({ row: row.row, rowIdAccessor })); + return !targetVisibleIds.has(id) && !existingRowIds.has(id); + }); + + // Add unique rows to existing rows + return [...existingRows, ...uniqueNewRows]; + }); + + // Track rows that are leaving the DOM (currently visible but won't be visible after filter) + setRowsLeavingTheDom((existingRows) => { + // Create set of IDs that will be visible after filter + const newVisibleIds = new Set( + newVisibleRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) + ); + + // Create set of IDs already in existingRows + const existingRowIds = new Set( + existingRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) + ); + + // Find rows currently visible but won't be visible after filter + const leavingRows = targetVisibleRows.filter((row) => { + const id = String(getRowId({ row: row.row, rowIdAccessor })); + return !newVisibleIds.has(id) && !existingRowIds.has(id); + }); + + // Add unique leaving rows to existing rows + return [...existingRows, ...leavingRows]; + }); }, [ allowAnimations, @@ -218,7 +183,6 @@ const useTableRowProcessing = ({ contentHeight, rowHeight, scrollTop, - categorizeRows, currentTableRows, targetVisibleRows, rowIdAccessor, @@ -226,7 +190,7 @@ const useTableRowProcessing = ({ ); const prepareForSortChange = useCallback( - (accessor: Accessor) => { + (accessor: Accessor, targetVisibleRows: TableRow[]) => { if (!allowAnimations || shouldPaginate) return; // Calculate what rows would be after sort @@ -240,31 +204,64 @@ const useTableRowProcessing = ({ scrollTop, }); - // CRITICAL: Compare VISIBLE rows (before sort) vs what WILL BE visible (after sort) - // This identifies rows that are entering the visible area - const { entering: visibleEntering } = categorizeRows(targetVisibleRows, newVisibleRows); - - // Find these entering rows in the CURRENT table state (before sort) to get original positions - const enteringFromCurrentState = visibleEntering - .map((enteringRow) => { - const id = String(getRowId({ row: enteringRow.row, rowIdAccessor })); - // Find this row in the current table state to get its original position + // Find all rows that WILL BE visible after the sort, but with their CURRENT positions (before sort) + // This gives us the starting point for animation + const rowsInCurrentPosition = newVisibleRows + .map((newVisibleRow) => { + const id = String(getRowId({ row: newVisibleRow.row, rowIdAccessor })); + // Find this row in the CURRENT table state (before sort) to get its current position const currentStateRow = currentTableRows.find( (currentRow) => String(getRowId({ row: currentRow.row, rowIdAccessor })) === id ); - return currentStateRow || enteringRow; // Fallback to enteringRow if not found + return currentStateRow || newVisibleRow; // Fallback to newVisibleRow if not found in current state }) .filter(Boolean); - if (enteringFromCurrentState.length > 0) { - const newExtendedRows = [...targetVisibleRows, ...enteringFromCurrentState]; - setExtendedRows(newExtendedRows); + setIsAnimating(true); - // CRITICAL: Update previousVisibleRowsRef to reflect what's NOW on screen - // This ensures if another sort comes in mid-animation, we split based on - // the rows that are ACTUALLY rendered, not stale rows from before the first sort - previousVisibleRowsRef.current = targetVisibleRows; - } + // Add unique rows to rowsEnteringTheDom (don't add duplicates from targetVisibleRows or existing rowsEnteringTheDom) + setRowsEnteringTheDom((existingRows) => { + // Create set of IDs already in targetVisibleRows + const targetVisibleIds = new Set( + targetVisibleRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) + ); + + // Create set of IDs already in existingRows + const existingRowIds = new Set( + existingRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) + ); + + // Filter to only include rows that aren't already in targetVisibleRows or existingRows + const uniqueNewRows = rowsInCurrentPosition.filter((row) => { + const id = String(getRowId({ row: row.row, rowIdAccessor })); + return !targetVisibleIds.has(id) && !existingRowIds.has(id); + }); + + // Add unique rows to existing rows + return [...existingRows, ...uniqueNewRows]; + }); + + // Track rows that are leaving the DOM (currently visible but won't be visible after sort) + setRowsLeavingTheDom((existingRows) => { + // Create set of IDs that will be visible after sort + const newVisibleIds = new Set( + newVisibleRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) + ); + + // Create set of IDs already in existingRows + const existingRowIds = new Set( + existingRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) + ); + + // Find rows currently visible but won't be visible after sort + const leavingRows = targetVisibleRows.filter((row) => { + const id = String(getRowId({ row: row.row, rowIdAccessor })); + return !newVisibleIds.has(id) && !existingRowIds.has(id); + }); + + // Add unique leaving rows to existing rows + return [...existingRows, ...leavingRows]; + }); }, [ allowAnimations, @@ -274,69 +271,19 @@ const useTableRowProcessing = ({ contentHeight, rowHeight, scrollTop, - categorizeRows, currentTableRows, - targetVisibleRows, rowIdAccessor, ] ); - // Split rows into already rendered (stable) and entering (new) for separate rendering - const { alreadyRenderedRows, enteringDomRows } = useMemo(() => { - if (!allowAnimations || shouldPaginate || extendedRows.length === 0) { - // No animation or no extended rows: all rows are "already rendered" - return { - alreadyRenderedRows: rowsToRender, - enteringDomRows: [], - }; - } - - // During animation with extended rows: - // CRITICAL: Maintain ORIGINAL array order for already rendered rows - // Create a map of current rows by ID for quick lookup - const currentRowsById = new Map( - rowsToRender.map((row) => [String(getRowId({ row: row.row, rowIdAccessor })), row]) - ); - - // Start with previousVisibleRows in their ORIGINAL order - const alreadyRendered: any[] = []; - const previousRowIds = new Set(); - - previousVisibleRowsRef.current.forEach((prevRow) => { - const id = String(getRowId({ row: prevRow.row, rowIdAccessor })); - previousRowIds.add(id); - - // If this row still exists in current rowsToRender, add it (with updated position) - const currentRow = currentRowsById.get(id); - if (currentRow) { - alreadyRendered.push(currentRow); - } - }); - - // Find entering rows (rows in current but not in previous) - const entering: any[] = []; - rowsToRender.forEach((row) => { - const id = String(getRowId({ row: row.row, rowIdAccessor })); - if (!previousRowIds.has(id)) { - entering.push(row); - } - }); - - return { - alreadyRenderedRows: alreadyRendered, - enteringDomRows: entering, - }; - }, [rowsToRender, extendedRows.length, allowAnimations, shouldPaginate, rowIdAccessor]); - return { currentTableRows, currentVisibleRows: targetVisibleRows, isAnimating, prepareForFilterChange, prepareForSortChange, - rowsToRender, - alreadyRenderedRows, - enteringDomRows, + rowsEnteringTheDom, + rowsLeavingTheDom, }; }; diff --git a/src/types/TableBodyProps.ts b/src/types/TableBodyProps.ts index 0a8be8df..9e3a6363 100644 --- a/src/types/TableBodyProps.ts +++ b/src/types/TableBodyProps.ts @@ -1,10 +1,12 @@ -import { RefObject } from "react"; import { HeaderObject } from ".."; import { Dispatch } from "react"; import { SetStateAction } from "react"; import TableRow from "./TableRow"; interface TableBodyProps { + currentVisibleRows: TableRow[]; + rowsEnteringTheDom: TableRow[]; + rowsLeavingTheDom: TableRow[]; mainTemplateColumns: string; pinnedLeftColumns: HeaderObject[]; pinnedLeftTemplateColumns: string; @@ -12,9 +14,6 @@ interface TableBodyProps { pinnedRightColumns: HeaderObject[]; pinnedRightTemplateColumns: string; pinnedRightWidth: number; - rowsToRender: TableRow[]; - alreadyRenderedRows: TableRow[]; - enteringDomRows: TableRow[]; setScrollTop: Dispatch>; tableRows: TableRow[]; } From 8f5ef413d0b7e4e2bbbf770dcaedd073505dc17f Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:35:17 -0500 Subject: [PATCH 10/14] Animations working like they were previously but with new system and vars --- src/components/simple-table/SimpleTable.tsx | 2 - src/components/simple-table/TableBody.tsx | 2 - src/components/simple-table/TableContent.tsx | 3 - src/components/simple-table/TableSection.tsx | 33 ------- src/hooks/useTableRowProcessing.ts | 90 +++++++++++++++++--- src/types/TableBodyProps.ts | 1 - 6 files changed, 76 insertions(+), 55 deletions(-) diff --git a/src/components/simple-table/SimpleTable.tsx b/src/components/simple-table/SimpleTable.tsx index 3a75e5c5..4582db32 100644 --- a/src/components/simple-table/SimpleTable.tsx +++ b/src/components/simple-table/SimpleTable.tsx @@ -326,7 +326,6 @@ const SimpleTableComp = ({ currentTableRows, currentVisibleRows, rowsEnteringTheDom, - rowsLeavingTheDom, prepareForFilterChange, prepareForSortChange, isAnimating, @@ -552,7 +551,6 @@ const SimpleTableComp = ({ >; @@ -23,7 +22,6 @@ interface TableContentLocalProps { const TableContent = ({ currentVisibleRows, rowsEnteringTheDom, - rowsLeavingTheDom, pinnedLeftWidth, pinnedRightWidth, setScrollTop, @@ -68,7 +66,6 @@ const TableContent = ({ const tableBodyProps: TableBodyProps = { currentVisibleRows, rowsEnteringTheDom, - rowsLeavingTheDom, mainTemplateColumns, pinnedLeftColumns, pinnedLeftTemplateColumns, diff --git a/src/components/simple-table/TableSection.tsx b/src/components/simple-table/TableSection.tsx index e9ea8829..d8ff28ef 100644 --- a/src/components/simple-table/TableSection.tsx +++ b/src/components/simple-table/TableSection.tsx @@ -24,7 +24,6 @@ interface TableSectionProps { columnIndices: ColumnIndices; currentVisibleRows: TableRowType[]; rowsEnteringTheDom: TableRowType[]; - rowsLeavingTheDom: TableRowType[]; headers: HeaderObject[]; hoveredIndex: number | null; pinned?: Pinned; @@ -43,7 +42,6 @@ const TableSection = forwardRef( columnIndices, currentVisibleRows, rowsEnteringTheDom, - rowsLeavingTheDom, headers, hoveredIndex, pinned, @@ -146,37 +144,6 @@ const TableSection = forwardRef( ); })} - {rowsLeavingTheDom.map((tableRow, index) => { - const rowId = getRowId({ row: tableRow.row, rowIdAccessor }); - return ( - - {index !== 0 && ( - - )} - - - ); - })} ); diff --git a/src/hooks/useTableRowProcessing.ts b/src/hooks/useTableRowProcessing.ts index 52a01bd6..fb432d5d 100644 --- a/src/hooks/useTableRowProcessing.ts +++ b/src/hooks/useTableRowProcessing.ts @@ -100,6 +100,23 @@ const useTableRowProcessing = ({ }); }, [currentTableRows, contentHeight, rowHeight, scrollTop]); + // Combine target visible rows with leaving rows for rendering + const visibleRowsWithLeaving = useMemo(() => { + // Create a set of IDs from rowsLeavingTheDom + const leavingRowIds = new Set( + rowsLeavingTheDom.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) + ); + + // Filter out any rows from targetVisibleRows that are already in rowsLeavingTheDom + const uniqueTargetRows = targetVisibleRows.filter((row) => { + const id = String(getRowId({ row: row.row, rowIdAccessor })); + return !leavingRowIds.has(id); + }); + + // Combine unique target rows with leaving rows (leaving rows take precedence) + return [...uniqueTargetRows, ...rowsLeavingTheDom]; + }, [targetVisibleRows, rowsLeavingTheDom, rowIdAccessor]); + // Animation handlers for filter/sort changes const prepareForFilterChange = useCallback( (filter: any) => { @@ -166,12 +183,22 @@ const useTableRowProcessing = ({ ); // Find rows currently visible but won't be visible after filter - const leavingRows = targetVisibleRows.filter((row) => { - const id = String(getRowId({ row: row.row, rowIdAccessor })); - return !newVisibleIds.has(id) && !existingRowIds.has(id); - }); - - // Add unique leaving rows to existing rows + const leavingRows = targetVisibleRows + .filter((row) => { + const id = String(getRowId({ row: row.row, rowIdAccessor })); + return !newVisibleIds.has(id) && !existingRowIds.has(id); + }) + .map((leavingRow) => { + const id = String(getRowId({ row: leavingRow.row, rowIdAccessor })); + // Find this row in the NEW processed rows to get its NEW position (after filter) + const rowInNewState = newProcessedRows.find( + (newRow) => String(getRowId({ row: newRow.row, rowIdAccessor })) === id + ); + // Use the new position if found, otherwise keep current position + return rowInNewState || leavingRow; + }); + + // Add unique leaving rows with their new positions return [...existingRows, ...leavingRows]; }); }, @@ -254,12 +281,22 @@ const useTableRowProcessing = ({ ); // Find rows currently visible but won't be visible after sort - const leavingRows = targetVisibleRows.filter((row) => { - const id = String(getRowId({ row: row.row, rowIdAccessor })); - return !newVisibleIds.has(id) && !existingRowIds.has(id); - }); - - // Add unique leaving rows to existing rows + const leavingRows = targetVisibleRows + .filter((row) => { + const id = String(getRowId({ row: row.row, rowIdAccessor })); + return !newVisibleIds.has(id) && !existingRowIds.has(id); + }) + .map((leavingRow) => { + const id = String(getRowId({ row: leavingRow.row, rowIdAccessor })); + // Find this row in the NEW processed rows to get its NEW position (after sort) + const rowInNewState = newProcessedRows.find( + (newRow) => String(getRowId({ row: newRow.row, rowIdAccessor })) === id + ); + // Use the new position if found, otherwise keep current position + return rowInNewState || leavingRow; + }); + + // Add unique leaving rows with their new positions return [...existingRows, ...leavingRows]; }); }, @@ -276,14 +313,39 @@ const useTableRowProcessing = ({ ] ); + console.log("\n"); + console.log( + "rowsLeavingTheDom", + rowsLeavingTheDom.map((row) => ({ + companyName: row.row.companyName, + id: row.row.id, + position: row.position, + })) + ); + console.log( + "targetVisibleRows", + targetVisibleRows.map((row) => ({ + companyName: row.row.companyName, + id: row.row.id, + position: row.position, + })) + ); + console.log( + "rowsEnteringTheDom", + rowsEnteringTheDom.map((row) => ({ + companyName: row.row.companyName, + id: row.row.id, + position: row.position, + })) + ); + return { currentTableRows, - currentVisibleRows: targetVisibleRows, + currentVisibleRows: visibleRowsWithLeaving, isAnimating, prepareForFilterChange, prepareForSortChange, rowsEnteringTheDom, - rowsLeavingTheDom, }; }; diff --git a/src/types/TableBodyProps.ts b/src/types/TableBodyProps.ts index 9e3a6363..b0422731 100644 --- a/src/types/TableBodyProps.ts +++ b/src/types/TableBodyProps.ts @@ -6,7 +6,6 @@ import TableRow from "./TableRow"; interface TableBodyProps { currentVisibleRows: TableRow[]; rowsEnteringTheDom: TableRow[]; - rowsLeavingTheDom: TableRow[]; mainTemplateColumns: string; pinnedLeftColumns: HeaderObject[]; pinnedLeftTemplateColumns: string; From 374f91dcc6cab85610dca336616ed32498606785 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:54:42 -0500 Subject: [PATCH 11/14] Small improvements --- src/components/simple-table/SimpleTable.tsx | 15 +++++++ src/hooks/useTableRowProcessing.ts | 48 ++++++++++++++------- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/components/simple-table/SimpleTable.tsx b/src/components/simple-table/SimpleTable.tsx index 4582db32..d81218ef 100644 --- a/src/components/simple-table/SimpleTable.tsx +++ b/src/components/simple-table/SimpleTable.tsx @@ -50,6 +50,7 @@ import RowSelectionChangeProps from "../../types/RowSelectionChangeProps"; import CellClickProps from "../../types/CellClickProps"; import { RowButton } from "../../types/RowButton"; import { HeaderDropdown } from "../../types/HeaderDropdownProps"; +import { ANIMATION_CONFIGS } from "../animate/animation-utils"; interface SimpleTableProps { allowAnimations?: boolean; // Flag for allowing animations @@ -328,7 +329,9 @@ const SimpleTableComp = ({ rowsEnteringTheDom, prepareForFilterChange, prepareForSortChange, + cleanupAnimationRows, isAnimating, + animationStartTime, } = useTableRowProcessing({ allowAnimations, sortedRows, @@ -378,6 +381,18 @@ const SimpleTableComp = ({ collapsedHeaders, }); + // Cleanup animation rows after animation completes + useEffect(() => { + if (isAnimating && animationStartTime > 0) { + // Animation duration is 9000ms, add buffer for safety + const timeoutId = setTimeout(() => { + cleanupAnimationRows(); + }, ANIMATION_CONFIGS.ROW_REORDER.duration + 50); + + return () => clearTimeout(timeoutId); + } + }, [isAnimating, animationStartTime, cleanupAnimationRows]); + // Memoize handlers const onSort = useCallback( (accessor: Accessor) => { diff --git a/src/hooks/useTableRowProcessing.ts b/src/hooks/useTableRowProcessing.ts index fb432d5d..02ac8b77 100644 --- a/src/hooks/useTableRowProcessing.ts +++ b/src/hooks/useTableRowProcessing.ts @@ -42,9 +42,17 @@ const useTableRowProcessing = ({ computeSortedRowsPreview, }: UseTableRowProcessingProps) => { const [isAnimating, setIsAnimating] = useState(false); + const [animationStartTime, setAnimationStartTime] = useState(0); const [rowsEnteringTheDom, setRowsEnteringTheDom] = useState([]); const [rowsLeavingTheDom, setRowsLeavingTheDom] = useState([]); + // Cleanup function to reset animation states + const cleanupAnimationRows = useCallback(() => { + setRowsEnteringTheDom([]); + setRowsLeavingTheDom([]); + setIsAnimating(false); + }, []); + // Process rows through pagination and grouping const processRowSet = useCallback( (rows: Row[]) => { @@ -147,6 +155,7 @@ const useTableRowProcessing = ({ .filter(Boolean); setIsAnimating(true); + setAnimationStartTime(Date.now()); // Add unique rows to rowsEnteringTheDom (don't add duplicates from targetVisibleRows or existing rowsEnteringTheDom) setRowsEnteringTheDom((existingRows) => { @@ -245,6 +254,7 @@ const useTableRowProcessing = ({ .filter(Boolean); setIsAnimating(true); + setAnimationStartTime(Date.now()); // Add unique rows to rowsEnteringTheDom (don't add duplicates from targetVisibleRows or existing rowsEnteringTheDom) setRowsEnteringTheDom((existingRows) => { @@ -316,35 +326,43 @@ const useTableRowProcessing = ({ console.log("\n"); console.log( "rowsLeavingTheDom", - rowsLeavingTheDom.map((row) => ({ - companyName: row.row.companyName, - id: row.row.id, - position: row.position, - })) + JSON.stringify( + rowsLeavingTheDom.map((row) => ({ + companyName: row.row.companyName, + id: row.row.id, + position: row.position, + })) + ) ); console.log( "targetVisibleRows", - targetVisibleRows.map((row) => ({ - companyName: row.row.companyName, - id: row.row.id, - position: row.position, - })) + JSON.stringify( + targetVisibleRows.map((row) => ({ + companyName: row.row.companyName, + id: row.row.id, + position: row.position, + })) + ) ); console.log( "rowsEnteringTheDom", - rowsEnteringTheDom.map((row) => ({ - companyName: row.row.companyName, - id: row.row.id, - position: row.position, - })) + JSON.stringify( + rowsEnteringTheDom.map((row) => ({ + companyName: row.row.companyName, + id: row.row.id, + position: row.position, + })) + ) ); return { currentTableRows, currentVisibleRows: visibleRowsWithLeaving, isAnimating, + animationStartTime, prepareForFilterChange, prepareForSortChange, + cleanupAnimationRows, rowsEnteringTheDom, }; }; From 2156011720ff7280519f0c17ba6f572b6d3abb9f Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sun, 5 Oct 2025 21:50:51 -0500 Subject: [PATCH 12/14] Animations are broken --- src/components/animate/Animate.tsx | 137 ++++++++++++++++ src/components/simple-table/SimpleTable.tsx | 8 +- src/hooks/useTableRowProcessing.ts | 172 +++++++++++--------- 3 files changed, 238 insertions(+), 79 deletions(-) diff --git a/src/components/animate/Animate.tsx b/src/components/animate/Animate.tsx index bb29c89f..de546a4c 100644 --- a/src/components/animate/Animate.tsx +++ b/src/components/animate/Animate.tsx @@ -52,24 +52,58 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate 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; @@ -85,6 +119,9 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate // 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; } @@ -93,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; } @@ -108,9 +158,20 @@ 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; } @@ -118,6 +179,10 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate // 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; @@ -130,6 +195,14 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate // 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)`; @@ -140,6 +213,12 @@ export const Animate = ({ children, id, parentRef, tableRow, ...props }: Animate // 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 @@ -225,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: { @@ -257,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: { @@ -289,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: { @@ -315,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: { @@ -330,6 +459,14 @@ 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, diff --git a/src/components/simple-table/SimpleTable.tsx b/src/components/simple-table/SimpleTable.tsx index d81218ef..42535dec 100644 --- a/src/components/simple-table/SimpleTable.tsx +++ b/src/components/simple-table/SimpleTable.tsx @@ -397,7 +397,8 @@ const SimpleTableComp = ({ const onSort = useCallback( (accessor: Accessor) => { // STAGE 1: Prepare animation by adding entering rows before applying sort - prepareForSortChange(accessor, currentVisibleRows); + // Pass captureAllPositions so it can capture leaving rows before updating their positions + prepareForSortChange(accessor, currentVisibleRows, captureAllPositions); // STAGE 2: Apply sort after Stage 1 is rendered (next frame) // Note: Position capture happens in updateSort via onBeforeSort callback @@ -408,7 +409,7 @@ const SimpleTableComp = ({ }); }); }, - [prepareForSortChange, updateSort, currentVisibleRows] + [prepareForSortChange, updateSort, currentVisibleRows, captureAllPositions] ); const onTableHeaderDragEnd = useCallback((newHeaders: HeaderObject[]) => { @@ -439,7 +440,8 @@ const SimpleTableComp = ({ const handleApplyFilter = useCallback( (filter: FilterCondition) => { // STAGE 1: Prepare animation by adding entering rows before applying filter - prepareForFilterChange(filter); + // Pass captureAllPositions so it can capture leaving rows before updating their positions + prepareForFilterChange(filter, captureAllPositions); // STAGE 2: Apply filter after Stage 1 is rendered (next frame) // Use double RAF to ensure browser has completed layout before capturing positions diff --git a/src/hooks/useTableRowProcessing.ts b/src/hooks/useTableRowProcessing.ts index 02ac8b77..ff327903 100644 --- a/src/hooks/useTableRowProcessing.ts +++ b/src/hooks/useTableRowProcessing.ts @@ -127,9 +127,15 @@ const useTableRowProcessing = ({ // Animation handlers for filter/sort changes const prepareForFilterChange = useCallback( - (filter: any) => { + (filter: any, capturePositions?: () => void) => { if (!allowAnimations || shouldPaginate) return; + // CRITICAL: Capture positions of existing leaving rows BEFORE updating them + // This prevents teleporting when their positions change + if (capturePositions) { + capturePositions(); + } + // Calculate what rows would be after filter const newFilteredRows = computeFilteredRowsPreview(filter); const newProcessedRows = processRowSet(newFilteredRows); @@ -186,29 +192,49 @@ const useTableRowProcessing = ({ newVisibleRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) ); - // Create set of IDs already in existingRows - const existingRowIds = new Set( - existingRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) + // Create map of existing leaving rows for quick lookup + const existingRowsMap = new Map( + existingRows.map((row) => [String(getRowId({ row: row.row, rowIdAccessor })), row]) ); - // Find rows currently visible but won't be visible after filter - const leavingRows = targetVisibleRows - .filter((row) => { - const id = String(getRowId({ row: row.row, rowIdAccessor })); - return !newVisibleIds.has(id) && !existingRowIds.has(id); - }) - .map((leavingRow) => { - const id = String(getRowId({ row: leavingRow.row, rowIdAccessor })); - // Find this row in the NEW processed rows to get its NEW position (after filter) - const rowInNewState = newProcessedRows.find( - (newRow) => String(getRowId({ row: newRow.row, rowIdAccessor })) === id - ); - // Use the new position if found, otherwise keep current position - return rowInNewState || leavingRow; - }); - - // Add unique leaving rows with their new positions - return [...existingRows, ...leavingRows]; + // Find rows from targetVisibleRows that won't be visible after filter + const candidateLeavingRows = targetVisibleRows.filter((row) => { + const id = String(getRowId({ row: row.row, rowIdAccessor })); + return !newVisibleIds.has(id); + }); + + // Separate into rows that need position updates vs truly new leaving rows + const rowsToUpdate: TableRow[] = []; + const newLeavingRows: TableRow[] = []; + + candidateLeavingRows.forEach((leavingRow) => { + const id = String(getRowId({ row: leavingRow.row, rowIdAccessor })); + // Find this row in the NEW processed rows to get its NEW position (after filter) + const rowInNewState = newProcessedRows.find( + (newRow) => String(getRowId({ row: newRow.row, rowIdAccessor })) === id + ); + const rowWithNewPosition = rowInNewState || leavingRow; + + if (existingRowsMap.has(id)) { + // Row is already leaving, update its position + rowsToUpdate.push(rowWithNewPosition); + } else { + // Row is newly leaving + newLeavingRows.push(rowWithNewPosition); + } + }); + + // Update existing rows with new positions, keep rows not in update list unchanged + const updatedExistingRows = existingRows.map((row) => { + const id = String(getRowId({ row: row.row, rowIdAccessor })); + const updatedRow = rowsToUpdate.find( + (r) => String(getRowId({ row: r.row, rowIdAccessor })) === id + ); + return updatedRow || row; + }); + + // Combine updated existing rows with new leaving rows + return [...updatedExistingRows, ...newLeavingRows]; }); }, [ @@ -226,9 +252,15 @@ const useTableRowProcessing = ({ ); const prepareForSortChange = useCallback( - (accessor: Accessor, targetVisibleRows: TableRow[]) => { + (accessor: Accessor, targetVisibleRows: TableRow[], capturePositions?: () => void) => { if (!allowAnimations || shouldPaginate) return; + // CRITICAL: Capture positions of existing leaving rows BEFORE updating them + // This prevents teleporting when their positions change + if (capturePositions) { + capturePositions(); + } + // Calculate what rows would be after sort const newSortedRows = computeSortedRowsPreview(accessor); const newProcessedRows = processRowSet(newSortedRows); @@ -285,29 +317,49 @@ const useTableRowProcessing = ({ newVisibleRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) ); - // Create set of IDs already in existingRows - const existingRowIds = new Set( - existingRows.map((row) => String(getRowId({ row: row.row, rowIdAccessor }))) + // Create map of existing leaving rows for quick lookup + const existingRowsMap = new Map( + existingRows.map((row) => [String(getRowId({ row: row.row, rowIdAccessor })), row]) ); - // Find rows currently visible but won't be visible after sort - const leavingRows = targetVisibleRows - .filter((row) => { - const id = String(getRowId({ row: row.row, rowIdAccessor })); - return !newVisibleIds.has(id) && !existingRowIds.has(id); - }) - .map((leavingRow) => { - const id = String(getRowId({ row: leavingRow.row, rowIdAccessor })); - // Find this row in the NEW processed rows to get its NEW position (after sort) - const rowInNewState = newProcessedRows.find( - (newRow) => String(getRowId({ row: newRow.row, rowIdAccessor })) === id - ); - // Use the new position if found, otherwise keep current position - return rowInNewState || leavingRow; - }); - - // Add unique leaving rows with their new positions - return [...existingRows, ...leavingRows]; + // Find rows from targetVisibleRows that won't be visible after sort + const candidateLeavingRows = targetVisibleRows.filter((row) => { + const id = String(getRowId({ row: row.row, rowIdAccessor })); + return !newVisibleIds.has(id); + }); + + // Separate into rows that need position updates vs truly new leaving rows + const rowsToUpdate: TableRow[] = []; + const newLeavingRows: TableRow[] = []; + + candidateLeavingRows.forEach((leavingRow) => { + const id = String(getRowId({ row: leavingRow.row, rowIdAccessor })); + // Find this row in the NEW processed rows to get its NEW position (after sort) + const rowInNewState = newProcessedRows.find( + (newRow) => String(getRowId({ row: newRow.row, rowIdAccessor })) === id + ); + const rowWithNewPosition = rowInNewState || leavingRow; + + if (existingRowsMap.has(id)) { + // Row is already leaving, update its position + rowsToUpdate.push(rowWithNewPosition); + } else { + // Row is newly leaving + newLeavingRows.push(rowWithNewPosition); + } + }); + + // Update existing rows with new positions, keep rows not in update list unchanged + const updatedExistingRows = existingRows.map((row) => { + const id = String(getRowId({ row: row.row, rowIdAccessor })); + const updatedRow = rowsToUpdate.find( + (r) => String(getRowId({ row: r.row, rowIdAccessor })) === id + ); + return updatedRow || row; + }); + + // Combine updated existing rows with new leaving rows + return [...updatedExistingRows, ...newLeavingRows]; }); }, [ @@ -323,38 +375,6 @@ const useTableRowProcessing = ({ ] ); - console.log("\n"); - console.log( - "rowsLeavingTheDom", - JSON.stringify( - rowsLeavingTheDom.map((row) => ({ - companyName: row.row.companyName, - id: row.row.id, - position: row.position, - })) - ) - ); - console.log( - "targetVisibleRows", - JSON.stringify( - targetVisibleRows.map((row) => ({ - companyName: row.row.companyName, - id: row.row.id, - position: row.position, - })) - ) - ); - console.log( - "rowsEnteringTheDom", - JSON.stringify( - rowsEnteringTheDom.map((row) => ({ - companyName: row.row.companyName, - id: row.row.id, - position: row.position, - })) - ) - ); - return { currentTableRows, currentVisibleRows: visibleRowsWithLeaving, From 14e4817dde11c28e62453e17d804cd502f335275 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:02:30 -0500 Subject: [PATCH 13/14] Test --- src/components/{Dropdown => dropdown2}/Dropdown.tsx | 0 src/components/{Dropdown => dropdown2}/DropdownItem.tsx | 0 src/components/filters/DateFilter.tsx | 2 +- src/components/filters/shared/CustomSelect.tsx | 2 +- src/components/simple-table/TableHeaderCell.tsx | 2 +- .../simple-table/editable-cells/BooleanDropdownEdit.tsx | 4 ++-- .../simple-table/editable-cells/DateDropdownEdit.tsx | 2 +- .../simple-table/editable-cells/EnumDropdownEdit.tsx | 4 ++-- 8 files changed, 8 insertions(+), 8 deletions(-) rename src/components/{Dropdown => dropdown2}/Dropdown.tsx (100%) rename src/components/{Dropdown => dropdown2}/DropdownItem.tsx (100%) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/dropdown2/Dropdown.tsx similarity index 100% rename from src/components/Dropdown/Dropdown.tsx rename to src/components/dropdown2/Dropdown.tsx diff --git a/src/components/Dropdown/DropdownItem.tsx b/src/components/dropdown2/DropdownItem.tsx similarity index 100% rename from src/components/Dropdown/DropdownItem.tsx rename to src/components/dropdown2/DropdownItem.tsx diff --git a/src/components/filters/DateFilter.tsx b/src/components/filters/DateFilter.tsx index 0658144c..443bbefe 100644 --- a/src/components/filters/DateFilter.tsx +++ b/src/components/filters/DateFilter.tsx @@ -14,7 +14,7 @@ import FilterSection from "./shared/FilterSection"; import FilterActions from "./shared/FilterActions"; import DatePicker from "../date-picker/DatePicker"; import { createSafeDate } from "../../utils/dateUtils"; -import Dropdown from "../dropdown/Dropdown"; +import Dropdown from "../dropdown2/Dropdown"; interface DateFilterProps { header: HeaderObject; diff --git a/src/components/filters/shared/CustomSelect.tsx b/src/components/filters/shared/CustomSelect.tsx index 6f33ce50..f56ef022 100644 --- a/src/components/filters/shared/CustomSelect.tsx +++ b/src/components/filters/shared/CustomSelect.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect } from "react"; import SelectIcon from "../../../icons/SelectIcon"; -import Dropdown from "../../dropdown/Dropdown"; +import Dropdown from "../../dropdown2/Dropdown"; export interface CustomSelectOption { value: string; diff --git a/src/components/simple-table/TableHeaderCell.tsx b/src/components/simple-table/TableHeaderCell.tsx index 2b5e4b27..c78d3a25 100644 --- a/src/components/simple-table/TableHeaderCell.tsx +++ b/src/components/simple-table/TableHeaderCell.tsx @@ -18,7 +18,7 @@ import { useTableContext } from "../../context/TableContext"; import { HandleResizeStartProps } from "../../types/HandleResizeStartProps"; import { handleResizeStart } from "../../utils/resizeUtils"; import FilterIcon from "../../icons/FilterIcon"; -import Dropdown from "../dropdown/Dropdown"; +import Dropdown from "../dropdown2/Dropdown"; import FilterDropdown from "../filters/FilterDropdown"; import { FilterCondition } from "../../types/FilterTypes"; import Animate from "../animate/Animate"; diff --git a/src/components/simple-table/editable-cells/BooleanDropdownEdit.tsx b/src/components/simple-table/editable-cells/BooleanDropdownEdit.tsx index e01971ce..22720576 100644 --- a/src/components/simple-table/editable-cells/BooleanDropdownEdit.tsx +++ b/src/components/simple-table/editable-cells/BooleanDropdownEdit.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -import Dropdown from "../../dropdown/Dropdown"; -import DropdownItem from "../../dropdown/DropdownItem"; +import Dropdown from "../../dropdown2/Dropdown"; +import DropdownItem from "../../dropdown2/DropdownItem"; interface BooleanDropdownEditProps { onBlur: () => void; diff --git a/src/components/simple-table/editable-cells/DateDropdownEdit.tsx b/src/components/simple-table/editable-cells/DateDropdownEdit.tsx index 4f4b987e..e949df75 100644 --- a/src/components/simple-table/editable-cells/DateDropdownEdit.tsx +++ b/src/components/simple-table/editable-cells/DateDropdownEdit.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import Dropdown from "../../dropdown/Dropdown"; +import Dropdown from "../../dropdown2/Dropdown"; import DatePicker from "../../date-picker/DatePicker"; import CellValue from "../../../types/CellValue"; import { parseDateString } from "../../../utils/dateUtils"; diff --git a/src/components/simple-table/editable-cells/EnumDropdownEdit.tsx b/src/components/simple-table/editable-cells/EnumDropdownEdit.tsx index c6bfe1e6..fc05a62a 100644 --- a/src/components/simple-table/editable-cells/EnumDropdownEdit.tsx +++ b/src/components/simple-table/editable-cells/EnumDropdownEdit.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -import Dropdown from "../../dropdown/Dropdown"; -import DropdownItem from "../../dropdown/DropdownItem"; +import Dropdown from "../../dropdown2/Dropdown"; +import DropdownItem from "../../dropdown2/DropdownItem"; import EnumOption from "../../../types/EnumOption"; interface EnumDropdownEditProps { From 5e935bd601eb44a34357e7d936b58ec4c3cc9dc7 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:02:41 -0500 Subject: [PATCH 14/14] Test --- src/components/{dropdown2 => dropdown}/Dropdown.tsx | 0 src/components/{dropdown2 => dropdown}/DropdownItem.tsx | 0 src/components/filters/DateFilter.tsx | 2 +- src/components/filters/shared/CustomSelect.tsx | 2 +- src/components/simple-table/TableHeaderCell.tsx | 2 +- .../simple-table/editable-cells/BooleanDropdownEdit.tsx | 4 ++-- .../simple-table/editable-cells/DateDropdownEdit.tsx | 2 +- .../simple-table/editable-cells/EnumDropdownEdit.tsx | 4 ++-- src/stories/examples/music/MusicExample.tsx | 1 - 9 files changed, 8 insertions(+), 9 deletions(-) rename src/components/{dropdown2 => dropdown}/Dropdown.tsx (100%) rename src/components/{dropdown2 => dropdown}/DropdownItem.tsx (100%) diff --git a/src/components/dropdown2/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx similarity index 100% rename from src/components/dropdown2/Dropdown.tsx rename to src/components/dropdown/Dropdown.tsx diff --git a/src/components/dropdown2/DropdownItem.tsx b/src/components/dropdown/DropdownItem.tsx similarity index 100% rename from src/components/dropdown2/DropdownItem.tsx rename to src/components/dropdown/DropdownItem.tsx diff --git a/src/components/filters/DateFilter.tsx b/src/components/filters/DateFilter.tsx index 443bbefe..0658144c 100644 --- a/src/components/filters/DateFilter.tsx +++ b/src/components/filters/DateFilter.tsx @@ -14,7 +14,7 @@ import FilterSection from "./shared/FilterSection"; import FilterActions from "./shared/FilterActions"; import DatePicker from "../date-picker/DatePicker"; import { createSafeDate } from "../../utils/dateUtils"; -import Dropdown from "../dropdown2/Dropdown"; +import Dropdown from "../dropdown/Dropdown"; interface DateFilterProps { header: HeaderObject; diff --git a/src/components/filters/shared/CustomSelect.tsx b/src/components/filters/shared/CustomSelect.tsx index f56ef022..6f33ce50 100644 --- a/src/components/filters/shared/CustomSelect.tsx +++ b/src/components/filters/shared/CustomSelect.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect } from "react"; import SelectIcon from "../../../icons/SelectIcon"; -import Dropdown from "../../dropdown2/Dropdown"; +import Dropdown from "../../dropdown/Dropdown"; export interface CustomSelectOption { value: string; diff --git a/src/components/simple-table/TableHeaderCell.tsx b/src/components/simple-table/TableHeaderCell.tsx index c78d3a25..2b5e4b27 100644 --- a/src/components/simple-table/TableHeaderCell.tsx +++ b/src/components/simple-table/TableHeaderCell.tsx @@ -18,7 +18,7 @@ import { useTableContext } from "../../context/TableContext"; import { HandleResizeStartProps } from "../../types/HandleResizeStartProps"; import { handleResizeStart } from "../../utils/resizeUtils"; import FilterIcon from "../../icons/FilterIcon"; -import Dropdown from "../dropdown2/Dropdown"; +import Dropdown from "../dropdown/Dropdown"; import FilterDropdown from "../filters/FilterDropdown"; import { FilterCondition } from "../../types/FilterTypes"; import Animate from "../animate/Animate"; diff --git a/src/components/simple-table/editable-cells/BooleanDropdownEdit.tsx b/src/components/simple-table/editable-cells/BooleanDropdownEdit.tsx index 22720576..e01971ce 100644 --- a/src/components/simple-table/editable-cells/BooleanDropdownEdit.tsx +++ b/src/components/simple-table/editable-cells/BooleanDropdownEdit.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -import Dropdown from "../../dropdown2/Dropdown"; -import DropdownItem from "../../dropdown2/DropdownItem"; +import Dropdown from "../../dropdown/Dropdown"; +import DropdownItem from "../../dropdown/DropdownItem"; interface BooleanDropdownEditProps { onBlur: () => void; diff --git a/src/components/simple-table/editable-cells/DateDropdownEdit.tsx b/src/components/simple-table/editable-cells/DateDropdownEdit.tsx index e949df75..4f4b987e 100644 --- a/src/components/simple-table/editable-cells/DateDropdownEdit.tsx +++ b/src/components/simple-table/editable-cells/DateDropdownEdit.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import Dropdown from "../../dropdown2/Dropdown"; +import Dropdown from "../../dropdown/Dropdown"; import DatePicker from "../../date-picker/DatePicker"; import CellValue from "../../../types/CellValue"; import { parseDateString } from "../../../utils/dateUtils"; diff --git a/src/components/simple-table/editable-cells/EnumDropdownEdit.tsx b/src/components/simple-table/editable-cells/EnumDropdownEdit.tsx index fc05a62a..c6bfe1e6 100644 --- a/src/components/simple-table/editable-cells/EnumDropdownEdit.tsx +++ b/src/components/simple-table/editable-cells/EnumDropdownEdit.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -import Dropdown from "../../dropdown2/Dropdown"; -import DropdownItem from "../../dropdown2/DropdownItem"; +import Dropdown from "../../dropdown/Dropdown"; +import DropdownItem from "../../dropdown/DropdownItem"; import EnumOption from "../../../types/EnumOption"; interface EnumDropdownEditProps { diff --git a/src/stories/examples/music/MusicExample.tsx b/src/stories/examples/music/MusicExample.tsx index d8898de5..2181407d 100644 --- a/src/stories/examples/music/MusicExample.tsx +++ b/src/stories/examples/music/MusicExample.tsx @@ -10,7 +10,6 @@ import data from "./music-data.json"; export const musicExampleDefaults = { columnResizing: true, height: "70dvh", - theme: "frost", }; export default function MusicExample({