From f921eb30f7791b752c25c75a91fb90f50bb6c25f Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sat, 18 Apr 2026 09:35:46 -0500 Subject: [PATCH 1/4] Drag scroll fix --- packages/core/src/core/SimpleTableVanilla.ts | 10 +++++- .../src/core/rendering/SectionRenderer.ts | 26 +++++++++++--- .../core/src/core/rendering/TableRenderer.ts | 4 +-- .../SelectionManager/SelectionManager.ts | 35 ++++++++++++------- .../managers/SelectionManager/mouseUtils.ts | 12 +++++-- packages/core/src/utils/bodyCell/styling.ts | 5 +-- packages/core/src/utils/bodyCell/types.ts | 2 +- 7 files changed, 67 insertions(+), 27 deletions(-) diff --git a/packages/core/src/core/SimpleTableVanilla.ts b/packages/core/src/core/SimpleTableVanilla.ts index 36f7751de..dc294f3c1 100644 --- a/packages/core/src/core/SimpleTableVanilla.ts +++ b/packages/core/src/core/SimpleTableVanilla.ts @@ -270,7 +270,15 @@ export class SimpleTableVanilla { const refs = this.domManager.getRefs(); const header = refs.mainHeaderRef.current; const body = refs.mainBodyRef.current; - (header as any)?.__renderHeaderCells?.(scrollLeft); + const sel = this.selectionManager; + const liveSelection = + sel && (this.config.selectableCells || this.config.selectableColumns) + ? { + columnsWithSelectedCells: sel.getColumnsWithSelectedCells(), + selectedColumns: sel.getSelectedColumns(), + } + : undefined; + (header as any)?.__renderHeaderCells?.(scrollLeft, liveSelection); (body as any)?.__renderBodyCells?.(scrollLeft); }, }); diff --git a/packages/core/src/core/rendering/SectionRenderer.ts b/packages/core/src/core/rendering/SectionRenderer.ts index 994f8dd9c..5278e5644 100644 --- a/packages/core/src/core/rendering/SectionRenderer.ts +++ b/packages/core/src/core/rendering/SectionRenderer.ts @@ -185,12 +185,28 @@ export class SectionRenderer { section.scrollLeft = currentScrollLeft; } - // For main section (not pinned), attach render function for scroll updates + // For main section (not pinned), attach render function for scroll updates. + // cachedContext is from the last full header render; during vertical drag-scroll the header + // may not re-render while selection changes. Callers should pass live selection sets so + // calculateHeaderCellClasses does not overwrite st-header-* with stale data (flicker). if (!pinned && section) { - (section as any).__renderHeaderCells = (scrollLeft: number) => { - if (section) { - renderHeaderCells(section, absoluteCells, cachedContext, scrollLeft); - } + (section as any).__renderHeaderCells = ( + scrollLeft: number, + liveSelection?: { + columnsWithSelectedCells: Set; + selectedColumns: Set; + }, + ) => { + if (!section) return; + const ctx = + liveSelection !== undefined + ? { + ...cachedContext, + columnsWithSelectedCells: liveSelection.columnsWithSelectedCells, + selectedColumns: liveSelection.selectedColumns, + } + : cachedContext; + renderHeaderCells(section, absoluteCells, ctx, scrollLeft); }; } diff --git a/packages/core/src/core/rendering/TableRenderer.ts b/packages/core/src/core/rendering/TableRenderer.ts index afdde9504..5708ec701 100644 --- a/packages/core/src/core/rendering/TableRenderer.ts +++ b/packages/core/src/core/rendering/TableRenderer.ts @@ -496,8 +496,8 @@ export class TableRenderer { deps.selectionManager?.isWarningFlashing(cell) || false, handleMouseDown: (cell: any) => deps.selectionManager?.handleMouseDown(cell), - handleMouseOver: (cell: any) => - deps.selectionManager?.handleMouseOver(cell), + handleMouseOver: (cell: any, clientX: number, clientY: number) => + deps.selectionManager?.handleMouseOver(cell, clientX, clientY), isRowSelected: (rowId: string) => deps.rowSelectionManager?.isRowSelected(rowId) ?? false, canExpandRowGroup: deps.config.canExpandRowGroup, diff --git a/packages/core/src/managers/SelectionManager/SelectionManager.ts b/packages/core/src/managers/SelectionManager/SelectionManager.ts index 2a44e3cf5..20d9ac153 100644 --- a/packages/core/src/managers/SelectionManager/SelectionManager.ts +++ b/packages/core/src/managers/SelectionManager/SelectionManager.ts @@ -15,7 +15,6 @@ import { computeSelectionRange } from "./selectionRangeUtils"; import { getCellFromMousePosition as getCellFromMousePositionUtil, handleAutoScroll as handleAutoScrollUtil, - calculateNearestCell as calculateNearestCellUtil, } from "./mouseUtils"; import { getHeaderLeafIndices, flattenAllHeaders } from "../../utils/headerUtils"; @@ -959,7 +958,12 @@ export class SelectionManager { if (colIndex < 0 || Number.isNaN(colIndex)) continue; const header = byAccessor.get(accessor); - if (!header) continue; + if (!header) { + for (const cls of SelectionManager.HEADER_SELECTION_CLASSES) { + el.classList.remove(cls); + } + continue; + } const isSelectionColumn = Boolean(header.isSelectionColumn) && Boolean(this.config.enableRowSelection); @@ -1408,13 +1412,6 @@ export class SelectionManager { this.updateAllCellClasses(); } - /** - * Calculate the nearest cell to a given mouse position - */ - private calculateNearestCell(clientX: number, clientY: number): Cell | null { - return calculateNearestCellUtil(clientX, clientY); - } - /** * Get cell from mouse position */ @@ -1422,7 +1419,11 @@ export class SelectionManager { clientX: number, clientY: number, ): Cell | null { - return getCellFromMousePositionUtil(clientX, clientY); + return getCellFromMousePositionUtil( + clientX, + clientY, + this.config.tableRoot ?? document, + ); } /** @@ -1533,12 +1534,20 @@ export class SelectionManager { } /** - * Handle mouse over a cell during selection drag + * Handle mouse over a cell during selection drag. + * Uses the pointer position to resolve the cell under the cursor (same as continuousScroll) + * so virtualization/recycled DOM does not apply stale cellData from the firing element. */ - handleMouseOver({ colIndex, rowIndex, rowId }: Cell): void { + handleMouseOver( + cellFromElement: Cell, + clientX: number, + clientY: number, + ): void { if (!this.config.selectableCells) return; if (this.isSelecting && this.startCell) { - this.updateSelectionRange(this.startCell, { colIndex, rowIndex, rowId }); + const resolved = + this.getCellFromMousePosition(clientX, clientY) ?? cellFromElement; + this.updateSelectionRange(this.startCell, resolved); } } } diff --git a/packages/core/src/managers/SelectionManager/mouseUtils.ts b/packages/core/src/managers/SelectionManager/mouseUtils.ts index 762ca3a76..410198e89 100644 --- a/packages/core/src/managers/SelectionManager/mouseUtils.ts +++ b/packages/core/src/managers/SelectionManager/mouseUtils.ts @@ -8,15 +8,17 @@ import type Cell from "../../types/Cell"; export function calculateNearestCell( clientX: number, clientY: number, + root: Document | HTMLElement = document, ): Cell | null { - const tableContainer = document.querySelector(".st-body-container"); + const searchRoot: ParentNode = root; + const tableContainer = searchRoot.querySelector(".st-body-container"); if (!tableContainer) return null; const rect = tableContainer.getBoundingClientRect(); const clampedX = Math.max(rect.left, Math.min(rect.right, clientX)); const clampedY = Math.max(rect.top, Math.min(rect.bottom, clientY)); - const cellElements = document.querySelectorAll( + const cellElements = searchRoot.querySelectorAll( ".st-cell[data-row-index][data-col-index][data-row-id]:not(.st-selection-cell)", ); if (cellElements.length === 0) return null; @@ -112,6 +114,7 @@ export function calculateNearestCell( export function getCellFromMousePosition( clientX: number, clientY: number, + root: Document | HTMLElement = document, ): Cell | null { const element = document.elementFromPoint(clientX, clientY); if (!element) return null; @@ -119,6 +122,9 @@ export function getCellFromMousePosition( const cellElement = element.closest(".st-cell"); if (cellElement instanceof HTMLElement) { + if (!root.contains(cellElement)) { + return calculateNearestCell(clientX, clientY, root); + } const rowIndex = parseInt( cellElement.getAttribute("data-row-index") || "-1", 10, @@ -134,7 +140,7 @@ export function getCellFromMousePosition( } } - return calculateNearestCell(clientX, clientY); + return calculateNearestCell(clientX, clientY, root); } /** diff --git a/packages/core/src/utils/bodyCell/styling.ts b/packages/core/src/utils/bodyCell/styling.ts index 58e3b63fc..78ad06482 100644 --- a/packages/core/src/utils/bodyCell/styling.ts +++ b/packages/core/src/utils/bodyCell/styling.ts @@ -302,8 +302,9 @@ export const createBodyCellElement = ( context.handleMouseDown(cellData); }; - const handleMouseOver = () => { - context.handleMouseOver(cellData); + const handleMouseOver = (event: Event) => { + const e = event as MouseEvent; + context.handleMouseOver(cellData, e.clientX, e.clientY); }; addTrackedEventListener(cellElement, "mousedown", handleMouseDown); diff --git a/packages/core/src/utils/bodyCell/types.ts b/packages/core/src/utils/bodyCell/types.ts index 1c4189f04..3afa927ca 100644 --- a/packages/core/src/utils/bodyCell/types.ts +++ b/packages/core/src/utils/bodyCell/types.ts @@ -99,7 +99,7 @@ export interface CellRenderContext { onRowGroupExpand?: (props: OnRowGroupExpandProps) => void | Promise; handleRowSelect?: (rowId: string, checked: boolean) => void; handleMouseDown: (cell: CellData) => void; - handleMouseOver: (cell: CellData) => void; + handleMouseOver: (cell: CellData, clientX: number, clientY: number) => void; // Refs and state setters cellRegistry?: Map; From 96692a42f68e76003c2b703942a01b7ac0a49cf4 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sat, 18 Apr 2026 09:37:37 -0500 Subject: [PATCH 2/4] Cell type fixes --- packages/core/src/core/rendering/TableRenderer.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/src/core/rendering/TableRenderer.ts b/packages/core/src/core/rendering/TableRenderer.ts index 5708ec701..33fdfdaf0 100644 --- a/packages/core/src/core/rendering/TableRenderer.ts +++ b/packages/core/src/core/rendering/TableRenderer.ts @@ -484,19 +484,19 @@ export class TableRenderer { loadingStateRenderer: deps.config.loadingStateRenderer, errorStateRenderer: deps.config.errorStateRenderer, emptyStateRenderer: deps.config.emptyStateRenderer, - getBorderClass: (cell: any) => + getBorderClass: (cell) => deps.selectionManager?.getBorderClass(cell) || "", - isSelected: (cell: any) => + isSelected: (cell) => deps.selectionManager?.isSelected(cell) || false, - isInitialFocusedCell: (cell: any) => + isInitialFocusedCell: (cell) => deps.selectionManager?.isInitialFocusedCell(cell) || false, - isCopyFlashing: (cell: any) => + isCopyFlashing: (cell) => deps.selectionManager?.isCopyFlashing(cell) || false, - isWarningFlashing: (cell: any) => + isWarningFlashing: (cell) => deps.selectionManager?.isWarningFlashing(cell) || false, - handleMouseDown: (cell: any) => + handleMouseDown: (cell) => deps.selectionManager?.handleMouseDown(cell), - handleMouseOver: (cell: any, clientX: number, clientY: number) => + handleMouseOver: (cell, clientX: number, clientY: number) => deps.selectionManager?.handleMouseOver(cell, clientX, clientY), isRowSelected: (rowId: string) => deps.rowSelectionManager?.isRowSelected(rowId) ?? false, From bd1072a657b37d6820e4be4a9627919675c75926 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sat, 18 Apr 2026 09:44:30 -0500 Subject: [PATCH 3/4] Mobile footer renderer improvements --- apps/marketing/src/examples/crm/CRMFooter.tsx | 176 ++++++++++++------ .../src/examples/crm/CustomTheme.css | 38 ++++ 2 files changed, 159 insertions(+), 55 deletions(-) diff --git a/apps/marketing/src/examples/crm/CRMFooter.tsx b/apps/marketing/src/examples/crm/CRMFooter.tsx index ba48aa3e2..2ed1cf065 100644 --- a/apps/marketing/src/examples/crm/CRMFooter.tsx +++ b/apps/marketing/src/examples/crm/CRMFooter.tsx @@ -1,5 +1,28 @@ +import { useSyncExternalStore } from "react"; import type { FooterRendererProps } from "@simple-table/react"; +const MOBILE_MQ = "(max-width: 740px)"; + +function getMobileMq(): MediaQueryList | null { + if (typeof window === "undefined") return null; + return window.matchMedia(MOBILE_MQ); +} + +function subscribeMobile(cb: () => void) { + const mq = getMobileMq(); + if (!mq) return () => {}; + mq.addEventListener("change", cb); + return () => mq.removeEventListener("change", cb); +} + +function getMobileSnapshot(): boolean { + return getMobileMq()?.matches ?? false; +} + +function useFooterIsMobile(): boolean { + return useSyncExternalStore(subscribeMobile, getMobileSnapshot, () => false); +} + const CRMCustomFooter = ({ currentPage, totalPages, @@ -15,26 +38,25 @@ const CRMCustomFooter = ({ isDark, setRowsPerPage, }: FooterRendererProps & { isDark?: boolean; setRowsPerPage: (rowsPerPage: number) => void }) => { - // Generate visible page numbers with current page centered + const isMobile = useFooterIsMobile(); + const generateVisiblePages = (currentPage: number, totalPages: number): number[] => { const maxVisible = 5; if (totalPages <= maxVisible) { - // Show all pages if we have fewer than maxVisible return Array.from({ length: totalPages }, (_, i) => i + 1); } - // Calculate start and end to center current page - let start = currentPage - 2; - let end = currentPage + 2; + const halfLeft = Math.floor((maxVisible - 1) / 2); + const halfRight = Math.ceil((maxVisible - 1) / 2); + let start = currentPage - halfLeft; + let end = currentPage + halfRight; - // Adjust if we're near the beginning if (start < 1) { start = 1; end = Math.min(maxVisible, totalPages); } - // Adjust if we're near the end if (end > totalPages) { end = totalPages; start = Math.max(1, totalPages - maxVisible + 1); @@ -43,7 +65,7 @@ const CRMCustomFooter = ({ return Array.from({ length: end - start + 1 }, (_, i) => start + i); }; - const visiblePages = generateVisiblePages(currentPage, totalPages); + const visiblePages = isMobile ? [] : generateVisiblePages(currentPage, totalPages); const colors = isDark ? { @@ -73,47 +95,69 @@ const CRMCustomFooter = ({ activeText: "#ea580c", }; + const summaryFontSize = isMobile ? "12px" : "14px"; + const controlFontSize = isMobile ? "12px" : "14px"; + const pageBtnPadding = isMobile ? "6px 10px" : "8px 16px"; + const arrowPadding = isMobile ? "6px" : "8px"; + + const selectStyle = { + border: `1px solid ${colors.inputBorder}`, + borderRadius: "6px", + padding: isMobile ? "2px 6px" : "4px 8px", + fontSize: controlFontSize, + backgroundColor: colors.inputBg, + color: colors.text, + cursor: "pointer" as const, + maxWidth: isMobile ? "4.5rem" : undefined, + }; + return (
- {/* Results text */} -

- Showing {startRow} to{" "} - {endRow} of{" "} - {totalRows} results +

+ {isMobile ? ( + <> + {startRow}– + {endRow} + {" / "} + {totalRows} + + ) : ( + <> + Showing {startRow} to{" "} + {endRow} of{" "} + {totalRows} results + + )}

- {/* Controls */} -
- {/* Page size selector */} -
- +
+
+ {!isMobile && ( + + )} - per page + {!isMobile && per page}
- {/* Pagination */}
- {showFrameworksCallout ? ( + {/* Introduction */} +
+ + {introText} + +
+ + {/* AI Disclaimer */} +
- {SIMPLE_TABLE_MULTI_FRAMEWORK_TAGLINE}{" "} - - Browse framework setup - - . + This comparison guide was created with AI assistance. While we strive for accuracy, + if you notice any incorrect information, please{" "} + + contact us + {" "} + so we can correct it promptly. } type="info" showIcon - className="mb-8 max-w-3xl mx-auto" /> - ) : null} - - {/* Introduction */} -
- - {introText} -
- {/* AI Disclaimer */} - - This comparison guide was created with AI assistance. While we strive for accuracy, if - you notice any incorrect information, please{" "} - - contact us - {" "} - so we can correct it promptly. - - } - type="info" - showIcon - className="mb-8" - /> - {/* Comparison Table */}