From 0b45ab7f36e3ad251c97d0f277e07c64412dafe2 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 5 Apr 2026 07:48:54 -0700 Subject: [PATCH] support real-time table column resizing --- .../item-detail-list.module.css | 8 + .../item-detail-list/item-detail-list.tsx | 12 +- .../item-table-list-column.module.css | 8 + .../item-table-list-column.tsx | 35 +++- .../item-table-list-context.tsx | 73 +++++++- .../item-table-list.module.css | 3 +- .../item-table-list/item-table-list.tsx | 176 +++++++++++------- 7 files changed, 238 insertions(+), 77 deletions(-) diff --git a/src/renderer/components/item-list/item-detail-list/item-detail-list.module.css b/src/renderer/components/item-list/item-detail-list/item-detail-list.module.css index 27e262d28..adc6e8d79 100644 --- a/src/renderer/components/item-list/item-detail-list/item-detail-list.module.css +++ b/src/renderer/components/item-list/item-detail-list/item-detail-list.module.css @@ -179,6 +179,14 @@ opacity: 1; } +.resize-handle.resize-handle-disabled { + cursor: not-allowed; +} + +.track-header-cell:hover .resize-handle.resize-handle-disabled { + cursor: not-allowed; +} + .resize-handle:hover { opacity: 1; } diff --git a/src/renderer/components/item-list/item-detail-list/item-detail-list.tsx b/src/renderer/components/item-list/item-detail-list/item-detail-list.tsx index 9c6dbd94c..4f928ed35 100644 --- a/src/renderer/components/item-list/item-detail-list/item-detail-list.tsx +++ b/src/renderer/components/item-list/item-detail-list/item-detail-list.tsx @@ -911,8 +911,7 @@ const DetailListHeaderCell = memo( const percent = col ? (columnWidthPercents[colIndex] ?? 0) : 0; const { fixedWidth, isFixedColumn } = getTrackColumnFixed(columnId); const currentWidth = col?.width ?? (fixedWidth || 100); - const showResizeHandle = - enableColumnResize && !isFixedColumn && !col?.autoSize && onColumnResized; + const showResizeHandle = enableColumnResize && !isFixedColumn; useEffect(() => { if (!containerRef.current || !onColumnReordered) { @@ -1026,6 +1025,7 @@ const DetailListHeaderCell = memo( {showResizeHandle && ( void; side: 'left' | 'right'; @@ -1047,6 +1048,7 @@ interface DetailListColumnResizeHandleProps { const DetailListColumnResizeHandle = ({ columnId, + disabled = false, initialWidth, onResize, side, @@ -1091,6 +1093,11 @@ const DetailListColumnResizeHandle = ({ }, [isDragging, columnId, onResize]); const handleMouseDown = (event: React.MouseEvent) => { + if (disabled) { + event.preventDefault(); + event.stopPropagation(); + return; + } event.preventDefault(); event.stopPropagation(); setIsDragging(true); @@ -1103,6 +1110,7 @@ const DetailListColumnResizeHandle = ({ return (
void; side: 'left' | 'right'; @@ -714,6 +717,8 @@ interface ColumnResizeHandleProps { const ColumnResizeHandle = ({ columnId, + columnIndex, + disabled = false, initialWidth, onResize, side, @@ -723,6 +728,17 @@ const ColumnResizeHandle = ({ const startWidthRef = useRef(initialWidth); const startXRef = useRef(0); const finalWidthRef = useRef(initialWidth); + const columnResizeLive = useItemTableListColumnResizeLive(); + const onResizeRef = useRef(onResize); + const columnResizeLiveRef = useRef(columnResizeLive); + + useEffect(() => { + onResizeRef.current = onResize; + }, [onResize]); + + useEffect(() => { + columnResizeLiveRef.current = columnResizeLive; + }, [columnResizeLive]); // Update the ref when initialWidth changes (but not during drag) useEffect(() => { @@ -738,6 +754,7 @@ const ColumnResizeHandle = ({ const deltaX = event.clientX - startXRef.current; const newWidth = Math.min(Math.max(10, startWidthRef.current + deltaX), 1000); finalWidthRef.current = newWidth; + columnResizeLiveRef.current?.scheduleColumnResizePreview(columnIndex, newWidth); }; const handleMouseUp = () => { @@ -746,7 +763,8 @@ const ColumnResizeHandle = ({ document.body.style.userSelect = ''; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); - onResize(columnId, finalWidthRef.current); + onResizeRef.current(columnId, finalWidthRef.current); + columnResizeLiveRef.current?.clearColumnResizePreview(); }; document.addEventListener('mousemove', handleMouseMove); @@ -755,10 +773,18 @@ const ColumnResizeHandle = ({ return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + columnResizeLiveRef.current?.clearColumnResizePreview(); }; - }, [isDragging, columnId, onResize]); + }, [isDragging, columnId, columnIndex]); const handleMouseDown = (event: React.MouseEvent) => { + if (disabled) { + event.preventDefault(); + event.stopPropagation(); + return; + } event.preventDefault(); event.stopPropagation(); setIsDragging(true); @@ -771,6 +797,7 @@ const ColumnResizeHandle = ({ return (
{columnLabelMap[props.type]} - {!columnConfig.autoSize && props.enableColumnResize && ( + {props.enableColumnResize && ( { return useContext(ItemTableListConfigContext); }; +export type ItemTableListColumnResizeLiveContextValue = { + clearColumnResizePreview: () => void; + scheduleColumnResizePreview: (columnIndex: number, width: number) => void; +}; + +const ItemTableListColumnResizeLiveContext = + createContext(null); + +export const ItemTableListColumnResizeLiveProvider = ({ + children, + value, +}: { + children: React.ReactNode; + value: ItemTableListColumnResizeLiveContextValue; +}) => { + return ( + + {children} + + ); +}; + +export const useItemTableListColumnResizeLive = + (): ItemTableListColumnResizeLiveContextValue | null => { + return useContext(ItemTableListColumnResizeLiveContext); + }; + +export const useItemTableListColumnResizeLiveState = () => { + const [columnResizePreview, setColumnResizePreview] = useState(null); + const previewRafRef = useRef(null); + const pendingPreviewRef = useRef(null); + + const scheduleColumnResizePreview = useCallback((columnIndex: number, width: number) => { + pendingPreviewRef.current = { columnIndex, width }; + if (previewRafRef.current !== null) return; + previewRafRef.current = requestAnimationFrame(() => { + previewRafRef.current = null; + const pending = pendingPreviewRef.current; + if (pending) { + setColumnResizePreview(pending); + } + }); + }, []); + + const clearColumnResizePreview = useCallback(() => { + if (previewRafRef.current !== null) { + cancelAnimationFrame(previewRafRef.current); + previewRafRef.current = null; + } + pendingPreviewRef.current = null; + setColumnResizePreview(null); + }, []); + + return { + clearColumnResizePreview, + columnResizePreview, + scheduleColumnResizePreview, + }; +}; + type ItemTableListStoreContextValue = { activeRowStore: ActiveRowStore; }; diff --git a/src/renderer/components/item-list/item-table-list/item-table-list.module.css b/src/renderer/components/item-list/item-table-list/item-table-list.module.css index 1422f47f7..069583980 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list.module.css +++ b/src/renderer/components/item-list/item-table-list/item-table-list.module.css @@ -72,7 +72,7 @@ z-index: 15; display: flex; flex-direction: row; - overflow: hidden; + overflow: visible; pointer-events: none; background-color: var(--theme-colors-background); border-bottom: 1px solid var(--theme-colors-border); @@ -168,6 +168,7 @@ min-width: 0; height: 100%; min-height: 0; + overflow-x: visible; } .no-scrollbar { diff --git a/src/renderer/components/item-list/item-table-list/item-table-list.tsx b/src/renderer/components/item-list/item-table-list/item-table-list.tsx index eb8a44b44..b88cf3356 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list.tsx @@ -45,9 +45,11 @@ import { useTableRowModel } from '/@/renderer/components/item-list/item-table-li import { useTableScrollToIndex } from '/@/renderer/components/item-list/item-table-list/hooks/use-table-scroll-to-index'; import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; import { + ItemTableListColumnResizeLiveProvider, type ItemTableListConfig, ItemTableListConfigProvider, ItemTableListStoreProvider, + useItemTableListColumnResizeLiveState, } from '/@/renderer/components/item-list/item-table-list/item-table-list-context'; import { MemoizedCellRouter, @@ -541,7 +543,7 @@ const VirtualizedTableGrid = ({ })} style={{ minHeight: `${pinnedRowsMinHeightPx}px`, - overflow: 'hidden', + overflow: 'visible', }} > {parsedColumns @@ -1052,7 +1054,7 @@ const ItemTableListStickyUI = memo( style={{ flex: '0 1 auto', minWidth: `${pinnedRightWidth}px`, - overflow: 'hidden', + overflow: 'visible', }} > {parsedColumns @@ -1288,6 +1290,30 @@ const BaseItemTableList = ({ columns, totalContainerWidth, }); + + const { clearColumnResizePreview, columnResizePreview, scheduleColumnResizePreview } = + useItemTableListColumnResizeLiveState(); + + const columnResizeLiveValue = useMemo( + () => ({ + clearColumnResizePreview, + scheduleColumnResizePreview, + }), + [clearColumnResizePreview, scheduleColumnResizePreview], + ); + + const displayColumnWidths = useMemo(() => { + if (!columnResizePreview) { + return calculatedColumnWidths; + } + const next = calculatedColumnWidths.slice(); + const { columnIndex, width } = columnResizePreview; + if (columnIndex >= 0 && columnIndex < next.length) { + next[columnIndex] = width; + } + return next; + }, [calculatedColumnWidths, columnResizePreview]); + const playerContext = usePlayer(); const { @@ -1505,7 +1531,7 @@ const BaseItemTableList = ({ // Create itemProps for sticky header const stickyHeaderItemProps: TableItemProps = useMemo( () => ({ - calculatedColumnWidths, + calculatedColumnWidths: displayColumnWidths, cellPadding, columns: parsedColumns, controls, @@ -1525,9 +1551,9 @@ const BaseItemTableList = ({ internalState, itemType, pinnedLeftColumnCount, - pinnedLeftColumnWidths: calculatedColumnWidths.slice(0, pinnedLeftColumnCount), + pinnedLeftColumnWidths: displayColumnWidths.slice(0, pinnedLeftColumnCount), pinnedRightColumnCount, - pinnedRightColumnWidths: calculatedColumnWidths.slice( + pinnedRightColumnWidths: displayColumnWidths.slice( pinnedLeftColumnCount + totalColumnCount, ), playerContext, @@ -1536,7 +1562,7 @@ const BaseItemTableList = ({ tableId, }), [ - calculatedColumnWidths, + displayColumnWidths, cellPadding, controls, parsedColumns, @@ -1641,71 +1667,81 @@ const BaseItemTableList = ({ }; }, [CellComponent, columnCellComponents]); + const tableMotion = ( + { + const element = e.currentTarget as HTMLDivElement; + // Focus without scrolling into view + if (element.focus) { + element.focus({ preventScroll: true }); + } + }} + ref={mergedContainerRef} + tabIndex={0} + {...animationProps.fadeIn} + transition={{ duration: enableEntranceAnimation ? 0.3 : 0, ease: 'anticipate' }} + > + + + + ); + return ( - { - const element = e.currentTarget as HTMLDivElement; - // Focus without scrolling into view - if (element.focus) { - element.focus({ preventScroll: true }); - } - }} - ref={mergedContainerRef} - tabIndex={0} - {...animationProps.fadeIn} - transition={{ duration: enableEntranceAnimation ? 0.3 : 0, ease: 'anticipate' }} - > - - - + {onColumnResized ? ( + + {tableMotion} + + ) : ( + tableMotion + )} );