support real-time table column resizing

This commit is contained in:
jeffvli
2026-04-05 07:48:54 -07:00
parent 031d365262
commit 0b45ab7f36
7 changed files with 238 additions and 77 deletions
@@ -179,6 +179,14 @@
opacity: 1; 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 { .resize-handle:hover {
opacity: 1; opacity: 1;
} }
@@ -911,8 +911,7 @@ const DetailListHeaderCell = memo(
const percent = col ? (columnWidthPercents[colIndex] ?? 0) : 0; const percent = col ? (columnWidthPercents[colIndex] ?? 0) : 0;
const { fixedWidth, isFixedColumn } = getTrackColumnFixed(columnId); const { fixedWidth, isFixedColumn } = getTrackColumnFixed(columnId);
const currentWidth = col?.width ?? (fixedWidth || 100); const currentWidth = col?.width ?? (fixedWidth || 100);
const showResizeHandle = const showResizeHandle = enableColumnResize && !isFixedColumn;
enableColumnResize && !isFixedColumn && !col?.autoSize && onColumnResized;
useEffect(() => { useEffect(() => {
if (!containerRef.current || !onColumnReordered) { if (!containerRef.current || !onColumnReordered) {
@@ -1026,6 +1025,7 @@ const DetailListHeaderCell = memo(
{showResizeHandle && ( {showResizeHandle && (
<DetailListColumnResizeHandle <DetailListColumnResizeHandle
columnId={columnId} columnId={columnId}
disabled={!!col?.autoSize}
initialWidth={currentWidth} initialWidth={currentWidth}
onResize={handleResize} onResize={handleResize}
side="right" side="right"
@@ -1040,6 +1040,7 @@ DetailListHeaderCell.displayName = 'DetailListHeaderCell';
interface DetailListColumnResizeHandleProps { interface DetailListColumnResizeHandleProps {
columnId: TableColumn; columnId: TableColumn;
disabled?: boolean;
initialWidth: number; initialWidth: number;
onResize: (columnId: TableColumn, width: number) => void; onResize: (columnId: TableColumn, width: number) => void;
side: 'left' | 'right'; side: 'left' | 'right';
@@ -1047,6 +1048,7 @@ interface DetailListColumnResizeHandleProps {
const DetailListColumnResizeHandle = ({ const DetailListColumnResizeHandle = ({
columnId, columnId,
disabled = false,
initialWidth, initialWidth,
onResize, onResize,
side, side,
@@ -1091,6 +1093,11 @@ const DetailListColumnResizeHandle = ({
}, [isDragging, columnId, onResize]); }, [isDragging, columnId, onResize]);
const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => { const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
if (disabled) {
event.preventDefault();
event.stopPropagation();
return;
}
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
setIsDragging(true); setIsDragging(true);
@@ -1103,6 +1110,7 @@ const DetailListColumnResizeHandle = ({
return ( return (
<div <div
className={clsx(styles.resizeHandle, { className={clsx(styles.resizeHandle, {
[styles.resizeHandleDisabled]: disabled,
[styles.resizeHandleDragging]: isDragging, [styles.resizeHandleDragging]: isDragging,
[styles.resizeHandleLeft]: side === 'left', [styles.resizeHandleLeft]: side === 'left',
[styles.resizeHandleRight]: side === 'right', [styles.resizeHandleRight]: side === 'right',
@@ -366,6 +366,14 @@
opacity: 1; opacity: 1;
} }
.resize-handle.resize-handle-disabled {
cursor: not-allowed;
}
.header-container:hover .resize-handle.resize-handle-disabled {
cursor: not-allowed;
}
.resize-handle:hover { .resize-handle:hover {
opacity: 1; opacity: 1;
} }
@@ -57,6 +57,7 @@ import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table
import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column'; import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column';
import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state'; import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state';
import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list'; import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list';
import { useItemTableListColumnResizeLive } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
import { ItemControls, ItemListItem } from '/@/renderer/components/item-list/types'; import { ItemControls, ItemListItem } from '/@/renderer/components/item-list/types';
import { Flex } from '/@/shared/components/flex/flex'; import { Flex } from '/@/shared/components/flex/flex';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
@@ -707,6 +708,8 @@ export const TableColumnContainer = (
interface ColumnResizeHandleProps { interface ColumnResizeHandleProps {
columnId: TableColumn; columnId: TableColumn;
columnIndex: number;
disabled?: boolean;
initialWidth: number; initialWidth: number;
onResize: (columnId: TableColumn, width: number) => void; onResize: (columnId: TableColumn, width: number) => void;
side: 'left' | 'right'; side: 'left' | 'right';
@@ -714,6 +717,8 @@ interface ColumnResizeHandleProps {
const ColumnResizeHandle = ({ const ColumnResizeHandle = ({
columnId, columnId,
columnIndex,
disabled = false,
initialWidth, initialWidth,
onResize, onResize,
side, side,
@@ -723,6 +728,17 @@ const ColumnResizeHandle = ({
const startWidthRef = useRef<number>(initialWidth); const startWidthRef = useRef<number>(initialWidth);
const startXRef = useRef<number>(0); const startXRef = useRef<number>(0);
const finalWidthRef = useRef<number>(initialWidth); const finalWidthRef = useRef<number>(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) // Update the ref when initialWidth changes (but not during drag)
useEffect(() => { useEffect(() => {
@@ -738,6 +754,7 @@ const ColumnResizeHandle = ({
const deltaX = event.clientX - startXRef.current; const deltaX = event.clientX - startXRef.current;
const newWidth = Math.min(Math.max(10, startWidthRef.current + deltaX), 1000); const newWidth = Math.min(Math.max(10, startWidthRef.current + deltaX), 1000);
finalWidthRef.current = newWidth; finalWidthRef.current = newWidth;
columnResizeLiveRef.current?.scheduleColumnResizePreview(columnIndex, newWidth);
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
@@ -746,7 +763,8 @@ const ColumnResizeHandle = ({
document.body.style.userSelect = ''; document.body.style.userSelect = '';
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseup', handleMouseUp);
onResize(columnId, finalWidthRef.current); onResizeRef.current(columnId, finalWidthRef.current);
columnResizeLiveRef.current?.clearColumnResizePreview();
}; };
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
@@ -755,10 +773,18 @@ const ColumnResizeHandle = ({
return () => { return () => {
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp); 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<HTMLDivElement>) => { const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
if (disabled) {
event.preventDefault();
event.stopPropagation();
return;
}
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
setIsDragging(true); setIsDragging(true);
@@ -771,6 +797,7 @@ const ColumnResizeHandle = ({
return ( return (
<div <div
className={clsx(styles.resizeHandle, { className={clsx(styles.resizeHandle, {
[styles.resizeHandleDisabled]: disabled,
[styles.resizeHandleDragging]: isDragging, [styles.resizeHandleDragging]: isDragging,
[styles.resizeHandleLeft]: side === 'left', [styles.resizeHandleLeft]: side === 'left',
[styles.resizeHandleRight]: side === 'right', [styles.resizeHandleRight]: side === 'right',
@@ -917,9 +944,11 @@ export const TableColumnHeaderContainer = (
> >
{columnLabelMap[props.type]} {columnLabelMap[props.type]}
</Text> </Text>
{!columnConfig.autoSize && props.enableColumnResize && ( {props.enableColumnResize && (
<ColumnResizeHandle <ColumnResizeHandle
columnId={props.type} columnId={props.type}
columnIndex={props.columnIndex}
disabled={!!columnConfig.autoSize}
initialWidth={currentWidth} initialWidth={currentWidth}
onResize={handleResize} onResize={handleResize}
side="right" side="right"
@@ -1,6 +1,14 @@
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useSyncExternalStore } from 'react'; import { useSyncExternalStore } from 'react';
import type { TableItemProps } from './item-table-list'; import type { TableItemProps } from './item-table-list';
@@ -68,6 +76,69 @@ export const useItemTableListConfig = (): ItemTableListConfig | null => {
return useContext(ItemTableListConfigContext); return useContext(ItemTableListConfigContext);
}; };
export type ItemTableListColumnResizeLiveContextValue = {
clearColumnResizePreview: () => void;
scheduleColumnResizePreview: (columnIndex: number, width: number) => void;
};
const ItemTableListColumnResizeLiveContext =
createContext<ItemTableListColumnResizeLiveContextValue | null>(null);
export const ItemTableListColumnResizeLiveProvider = ({
children,
value,
}: {
children: React.ReactNode;
value: ItemTableListColumnResizeLiveContextValue;
}) => {
return (
<ItemTableListColumnResizeLiveContext.Provider value={value}>
{children}
</ItemTableListColumnResizeLiveContext.Provider>
);
};
export const useItemTableListColumnResizeLive =
(): ItemTableListColumnResizeLiveContextValue | null => {
return useContext(ItemTableListColumnResizeLiveContext);
};
export const useItemTableListColumnResizeLiveState = () => {
const [columnResizePreview, setColumnResizePreview] = useState<null | {
columnIndex: number;
width: number;
}>(null);
const previewRafRef = useRef<null | number>(null);
const pendingPreviewRef = useRef<null | { columnIndex: number; width: number }>(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 = { type ItemTableListStoreContextValue = {
activeRowStore: ActiveRowStore; activeRowStore: ActiveRowStore;
}; };
@@ -72,7 +72,7 @@
z-index: 15; z-index: 15;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
overflow: hidden; overflow: visible;
pointer-events: none; pointer-events: none;
background-color: var(--theme-colors-background); background-color: var(--theme-colors-background);
border-bottom: 1px solid var(--theme-colors-border); border-bottom: 1px solid var(--theme-colors-border);
@@ -168,6 +168,7 @@
min-width: 0; min-width: 0;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
overflow-x: visible;
} }
.no-scrollbar { .no-scrollbar {
@@ -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 { 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 { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { import {
ItemTableListColumnResizeLiveProvider,
type ItemTableListConfig, type ItemTableListConfig,
ItemTableListConfigProvider, ItemTableListConfigProvider,
ItemTableListStoreProvider, ItemTableListStoreProvider,
useItemTableListColumnResizeLiveState,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-context'; } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
import { import {
MemoizedCellRouter, MemoizedCellRouter,
@@ -541,7 +543,7 @@ const VirtualizedTableGrid = ({
})} })}
style={{ style={{
minHeight: `${pinnedRowsMinHeightPx}px`, minHeight: `${pinnedRowsMinHeightPx}px`,
overflow: 'hidden', overflow: 'visible',
}} }}
> >
<Grid <Grid
@@ -659,7 +661,7 @@ const VirtualizedTableGrid = ({
})} })}
style={{ style={{
minHeight: `${pinnedRowsMinHeightPx}px`, minHeight: `${pinnedRowsMinHeightPx}px`,
overflow: 'hidden', overflow: 'visible',
}} }}
> >
<Grid <Grid
@@ -968,7 +970,7 @@ const ItemTableListStickyUI = memo(
style={{ style={{
flex: '0 1 auto', flex: '0 1 auto',
minWidth: `${pinnedLeftWidth}px`, minWidth: `${pinnedLeftWidth}px`,
overflow: 'hidden', overflow: 'visible',
}} }}
> >
{parsedColumns {parsedColumns
@@ -1052,7 +1054,7 @@ const ItemTableListStickyUI = memo(
style={{ style={{
flex: '0 1 auto', flex: '0 1 auto',
minWidth: `${pinnedRightWidth}px`, minWidth: `${pinnedRightWidth}px`,
overflow: 'hidden', overflow: 'visible',
}} }}
> >
{parsedColumns {parsedColumns
@@ -1288,6 +1290,30 @@ const BaseItemTableList = ({
columns, columns,
totalContainerWidth, 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 playerContext = usePlayer();
const { const {
@@ -1505,7 +1531,7 @@ const BaseItemTableList = ({
// Create itemProps for sticky header // Create itemProps for sticky header
const stickyHeaderItemProps: TableItemProps = useMemo( const stickyHeaderItemProps: TableItemProps = useMemo(
() => ({ () => ({
calculatedColumnWidths, calculatedColumnWidths: displayColumnWidths,
cellPadding, cellPadding,
columns: parsedColumns, columns: parsedColumns,
controls, controls,
@@ -1525,9 +1551,9 @@ const BaseItemTableList = ({
internalState, internalState,
itemType, itemType,
pinnedLeftColumnCount, pinnedLeftColumnCount,
pinnedLeftColumnWidths: calculatedColumnWidths.slice(0, pinnedLeftColumnCount), pinnedLeftColumnWidths: displayColumnWidths.slice(0, pinnedLeftColumnCount),
pinnedRightColumnCount, pinnedRightColumnCount,
pinnedRightColumnWidths: calculatedColumnWidths.slice( pinnedRightColumnWidths: displayColumnWidths.slice(
pinnedLeftColumnCount + totalColumnCount, pinnedLeftColumnCount + totalColumnCount,
), ),
playerContext, playerContext,
@@ -1536,7 +1562,7 @@ const BaseItemTableList = ({
tableId, tableId,
}), }),
[ [
calculatedColumnWidths, displayColumnWidths,
cellPadding, cellPadding,
controls, controls,
parsedColumns, parsedColumns,
@@ -1641,9 +1667,7 @@ const BaseItemTableList = ({
}; };
}, [CellComponent, columnCellComponents]); }, [CellComponent, columnCellComponents]);
return ( const tableMotion = (
<ItemTableListStoreProvider activeRowId={activeRowId}>
<ItemTableListConfigProvider value={tableConfigValue}>
<motion.div <motion.div
className={styles.itemTableListContainer} className={styles.itemTableListContainer}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@@ -1660,7 +1684,7 @@ const BaseItemTableList = ({
transition={{ duration: enableEntranceAnimation ? 0.3 : 0, ease: 'anticipate' }} transition={{ duration: enableEntranceAnimation ? 0.3 : 0, ease: 'anticipate' }}
> >
<ItemTableListStickyUI <ItemTableListStickyUI
calculatedColumnWidths={calculatedColumnWidths} calculatedColumnWidths={displayColumnWidths}
CellComponent={optimizedCellComponent} CellComponent={optimizedCellComponent}
containerRef={containerRef} containerRef={containerRef}
data={data} data={data}
@@ -1684,7 +1708,7 @@ const BaseItemTableList = ({
totalColumnCount={totalColumnCount} totalColumnCount={totalColumnCount}
/> />
<MemoizedVirtualizedTableGrid <MemoizedVirtualizedTableGrid
calculatedColumnWidths={calculatedColumnWidths} calculatedColumnWidths={displayColumnWidths}
CellComponent={optimizedCellComponent} CellComponent={optimizedCellComponent}
data={data} data={data}
dataWithGroups={dataWithGroups} dataWithGroups={dataWithGroups}
@@ -1706,6 +1730,18 @@ const BaseItemTableList = ({
totalRowCount={totalRowCount} totalRowCount={totalRowCount}
/> />
</motion.div> </motion.div>
);
return (
<ItemTableListStoreProvider activeRowId={activeRowId}>
<ItemTableListConfigProvider value={tableConfigValue}>
{onColumnResized ? (
<ItemTableListColumnResizeLiveProvider value={columnResizeLiveValue}>
{tableMotion}
</ItemTableListColumnResizeLiveProvider>
) : (
tableMotion
)}
</ItemTableListConfigProvider> </ItemTableListConfigProvider>
</ItemTableListStoreProvider> </ItemTableListStoreProvider>
); );