more table list progress

- scroll shadow
- header shadow
- remove sticky row count - only allow sticky header
- support table expansion
This commit is contained in:
jeffvli
2025-10-03 17:50:23 -07:00
parent 6e73b08663
commit 3f0536e780
2 changed files with 214 additions and 117 deletions
@@ -1,3 +1,11 @@
.item-table-list-container {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
}
.item-table-container { .item-table-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -8,6 +16,7 @@
} }
.item-table-grid-container { .item-table-grid-container {
position: relative;
flex: 1 1 auto; flex: 1 1 auto;
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -23,6 +32,7 @@
} }
.item-table-sticky-rows-grid-container { .item-table-sticky-rows-grid-container {
position: relative;
flex: 0 1 auto; flex: 0 1 auto;
min-width: 0; min-width: 0;
} }
@@ -35,6 +45,7 @@
} }
.item-table-sticky-intersection-grid-container { .item-table-sticky-intersection-grid-container {
position: relative;
flex: 0 1 auto; flex: 0 1 auto;
min-width: 0; min-width: 0;
} }
@@ -53,3 +64,39 @@
.height-100 { .height-100 {
height: 100%; height: 100%;
} }
.item-table-sticky-header-shadow {
position: absolute;
top: 100%;
right: 0;
left: 0;
z-index: 1;
height: 8px;
pointer-events: none;
background: linear-gradient(
to bottom,
rgb(0 0 0 / 50%) 0%,
rgb(0 0 0 / 5%) 50%,
transparent 100%
);
}
.item-table-left-scroll-shadow {
position: absolute;
top: 0;
bottom: 0;
left: 0;
z-index: 1;
width: 8px;
pointer-events: none;
background: linear-gradient(
to right,
rgb(0 0 0 / 50%) 0%,
rgb(0 0 0 / 5%) 50%,
transparent 100%
);
}
.list-expanded-container {
height: 500px;
}
@@ -12,6 +12,7 @@ import {
useCallback, useCallback,
useEffect, useEffect,
useRef, useRef,
useState,
} from 'react'; } from 'react';
import { type CellComponentProps, Grid, GridImperativeAPI, type GridProps } from 'react-window-v2'; import { type CellComponentProps, Grid, GridImperativeAPI, type GridProps } from 'react-window-v2';
@@ -21,17 +22,20 @@ import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list
import { useItemListState } from '/@/renderer/components/item-list/helpers/item-list-state'; import { useItemListState } from '/@/renderer/components/item-list/helpers/item-list-state';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
export interface CellProps {
columns: ItemTableListColumn[];
data: unknown[];
handleExpand: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
itemType: LibraryItem;
size?: 'compact' | 'default';
}
export interface ItemTableListColumn { export interface ItemTableListColumn {
id: string; id: string;
label: string; label: string;
width: number; width: number;
} }
interface CellProps {
columns: ItemTableListColumn[];
data: unknown[];
}
interface ItemTableListProps { interface ItemTableListProps {
CellComponent: JSXElementConstructor<CellComponentProps<CellProps>>; CellComponent: JSXElementConstructor<CellComponentProps<CellProps>>;
columnCount: number; columnCount: number;
@@ -39,7 +43,9 @@ interface ItemTableListProps {
columnWidth: ((index: number, cellProps: CellProps) => number) | number; columnWidth: ((index: number, cellProps: CellProps) => number) | number;
data: unknown[]; data: unknown[];
enableExpansion?: boolean; enableExpansion?: boolean;
enableHeader?: boolean;
enableSelection?: boolean; enableSelection?: boolean;
headerHeight?: number;
initialTopMostItemIndex?: initialTopMostItemIndex?:
| number | number
| { | {
@@ -60,8 +66,8 @@ interface ItemTableListProps {
onStartReached?: (index: number) => void; onStartReached?: (index: number) => void;
ref?: Ref<GridImperativeAPI>; ref?: Ref<GridImperativeAPI>;
rowHeight: ((index: number, cellProps: CellProps) => number) | number; rowHeight: ((index: number, cellProps: CellProps) => number) | number;
size?: 'compact' | 'default';
stickyColumnCount: number; stickyColumnCount: number;
stickyRowCount: number;
totalItemCount: number; totalItemCount: number;
} }
@@ -85,6 +91,8 @@ export const ItemTableList = ({
columns, columns,
columnWidth, columnWidth,
data, data,
enableHeader = true,
headerHeight = 40,
initialTopMostItemIndex, initialTopMostItemIndex,
itemType, itemType,
onCellsRendered, onCellsRendered,
@@ -98,10 +106,11 @@ export const ItemTableList = ({
onStartReached, onStartReached,
ref, ref,
rowHeight, rowHeight,
size = 'default',
stickyColumnCount, stickyColumnCount,
stickyRowCount,
totalItemCount, totalItemCount,
}: ItemTableListProps) => { }: ItemTableListProps) => {
const stickyRowCount = enableHeader ? 1 : 0;
const totalRowCount = totalItemCount - (stickyRowCount ?? 0); const totalRowCount = totalItemCount - (stickyRowCount ?? 0);
const totalColumnCount = columnCount - (stickyColumnCount ?? 0); const totalColumnCount = columnCount - (stickyColumnCount ?? 0);
const stickyRowRef = useRef<HTMLDivElement>(null); const stickyRowRef = useRef<HTMLDivElement>(null);
@@ -109,6 +118,7 @@ export const ItemTableList = ({
const stickyColumnRef = useRef<HTMLDivElement>(null); const stickyColumnRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement | null>(null); const scrollContainerRef = useRef<HTMLDivElement | null>(null);
const mergedRowRef = useMergedRef(rowRef, scrollContainerRef); const mergedRowRef = useMergedRef(rowRef, scrollContainerRef);
const [showLeftShadow, setShowLeftShadow] = useState(false);
const [initialize] = useOverlayScrollbars({ const [initialize] = useOverlayScrollbars({
defer: true, defer: true,
@@ -321,11 +331,44 @@ export const ItemTableList = ({
return undefined; return undefined;
}, []); }, []);
const cellProps = { // Handle left shadow visibility based on horizontal scroll
columns, useEffect(() => {
data, const row = rowRef.current?.childNodes[0] as HTMLDivElement;
if (!row || !stickyColumnCount) {
setShowLeftShadow(false);
return;
}
const checkScrollPosition = () => {
const scrollLeft = row.scrollLeft;
setShowLeftShadow(scrollLeft > 0);
}; };
checkScrollPosition();
row.addEventListener('scroll', checkScrollPosition);
return () => {
row.removeEventListener('scroll', checkScrollPosition);
};
}, [stickyColumnCount]);
const getRowHeight = useCallback(
(index: number, cellProps: CellProps) => {
const baseHeight =
typeof rowHeight === 'number' ? rowHeight : rowHeight(index, cellProps);
// If enableHeader is true and this is the first sticky row, use fixed header height
if (enableHeader && index === 0 && stickyRowCount > 0) {
return headerHeight;
}
return baseHeight;
},
[enableHeader, headerHeight, rowHeight, stickyRowCount],
);
const internalState = useItemListState(); const internalState = useItemListState();
const hasExpanded = internalState.hasExpanded(); const hasExpanded = internalState.hasExpanded();
@@ -342,6 +385,7 @@ export const ItemTableList = ({
}, },
[internalState], [internalState],
); );
const handleOnCellsRendered = useCallback( const handleOnCellsRendered = useCallback(
(cells: { (cells: {
columnStartIndex: number; columnStartIndex: number;
@@ -349,6 +393,11 @@ export const ItemTableList = ({
rowStartIndex: number; rowStartIndex: number;
rowStopIndex: number; rowStopIndex: number;
}) => { }) => {
onRangeChanged?.({
endIndex: cells.rowStopIndex,
startIndex: cells.rowStartIndex,
});
return onCellsRendered return onCellsRendered
? ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }) => { ? ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }) => {
return onCellsRendered!( return onCellsRendered!(
@@ -363,7 +412,7 @@ export const ItemTableList = ({
} }
: undefined; : undefined;
}, },
[onCellsRendered, stickyColumnCount, stickyRowCount], [onCellsRendered, onRangeChanged, stickyColumnCount, stickyRowCount],
); );
const StickyRowCell = useCallback( const StickyRowCell = useCallback(
@@ -406,6 +455,14 @@ export const ItemTableList = ({
[stickyColumnCount, stickyRowCount, CellComponent], [stickyColumnCount, stickyRowCount, CellComponent],
); );
const cellProps = {
columns,
data,
handleExpand,
itemType,
size,
};
return ( return (
<motion.div <motion.div
animate={{ animate={{
@@ -416,9 +473,10 @@ export const ItemTableList = ({
ease: 'backInOut', ease: 'backInOut',
}, },
}} }}
className={styles.itemTableContainer} className={styles.itemTableListContainer}
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
> >
<div className={styles.itemTableContainer}>
<div <div
className={styles.itemTableStickyColumnsGridContainer} className={styles.itemTableStickyColumnsGridContainer}
style={{ style={{
@@ -439,14 +497,7 @@ export const ItemTableList = ({
minHeight: `${Array.from( minHeight: `${Array.from(
{ length: stickyRowCount ?? 0 }, { length: stickyRowCount ?? 0 },
() => 0, () => 0,
).reduce( ).reduce((a, _, i) => a + getRowHeight(i, cellProps), 0)}px`,
(a, _, i) =>
a +
(typeof rowHeight === 'number'
? rowHeight
: rowHeight(i, cellProps)),
0,
)}px`,
}} }}
> >
<Grid <Grid
@@ -456,12 +507,16 @@ export const ItemTableList = ({
columnCount={stickyColumnCount} columnCount={stickyColumnCount}
columnWidth={columnWidth} columnWidth={columnWidth}
rowCount={stickyRowCount} rowCount={stickyRowCount}
rowHeight={rowHeight} rowHeight={getRowHeight}
/> />
{enableHeader && <div className={styles.itemTableStickyHeaderShadow} />}
</div> </div>
)} )}
{!!stickyColumnCount && ( {!!stickyColumnCount && (
<div className={styles.itemTableStickyColumnsContainer} ref={stickyColumnRef}> <div
className={styles.itemTableStickyColumnsContainer}
ref={stickyColumnRef}
>
<Grid <Grid
cellComponent={StickyColumnCell} cellComponent={StickyColumnCell}
cellProps={cellProps} cellProps={cellProps}
@@ -470,9 +525,7 @@ export const ItemTableList = ({
columnWidth={columnWidth} columnWidth={columnWidth}
rowCount={totalRowCount} rowCount={totalRowCount}
rowHeight={(index, cellProps) => { rowHeight={(index, cellProps) => {
return typeof rowHeight === 'number' return getRowHeight(index + (stickyRowCount ?? 0), cellProps);
? rowHeight
: rowHeight(index + (stickyRowCount ?? 0), cellProps);
}} }}
/> />
</div> </div>
@@ -483,19 +536,15 @@ export const ItemTableList = ({
<div <div
className={styles.itemTableStickyRowsGridContainer} className={styles.itemTableStickyRowsGridContainer}
ref={stickyRowRef} ref={stickyRowRef}
style={{ style={
{
'--header-height': `${headerHeight}px`,
minHeight: `${Array.from( minHeight: `${Array.from(
{ length: stickyRowCount ?? 0 }, { length: stickyRowCount ?? 0 },
() => 0, () => 0,
).reduce( ).reduce((a, _, i) => a + getRowHeight(i, cellProps), 0)}px`,
(a, _, i) => } as React.CSSProperties
a + }
(typeof rowHeight === 'number'
? rowHeight
: rowHeight(i, cellProps)),
0,
)}px`,
}}
> >
<Grid <Grid
cellComponent={StickyRowCell} cellComponent={StickyRowCell}
@@ -507,13 +556,12 @@ export const ItemTableList = ({
? columnWidth ? columnWidth
: columnWidth(index + (stickyColumnCount ?? 0), cellProps); : columnWidth(index + (stickyColumnCount ?? 0), cellProps);
}} }}
rowCount={Array.from({ length: stickyRowCount ?? 0 }, () => 0).length} rowCount={
rowHeight={(index, cellProps) => { Array.from({ length: stickyRowCount ?? 0 }, () => 0).length
return typeof rowHeight === 'number' }
? rowHeight rowHeight={getRowHeight}
: rowHeight(index, cellProps);
}}
/> />
{enableHeader && <div className={styles.itemTableStickyHeaderShadow} />}
</div> </div>
)} )}
<div className={styles.itemTableGridContainer} ref={mergedRowRef}> <div className={styles.itemTableGridContainer} ref={mergedRowRef}>
@@ -530,14 +578,16 @@ export const ItemTableList = ({
onCellsRendered={handleOnCellsRendered} onCellsRendered={handleOnCellsRendered}
rowCount={totalRowCount} rowCount={totalRowCount}
rowHeight={(index, cellProps) => { rowHeight={(index, cellProps) => {
return typeof rowHeight === 'number' return getRowHeight(index + (stickyRowCount ?? 0), cellProps);
? rowHeight
: rowHeight(index + (stickyRowCount ?? 0), cellProps);
}} }}
/> />
{stickyColumnCount > 0 && showLeftShadow && (
<div className={styles.itemTableLeftScrollShadow} />
)}
</div> </div>
</div> </div>
<AnimatePresence> </div>
<AnimatePresence initial={false}>
{hasExpanded && ( {hasExpanded && (
<motion.div <motion.div
animate="show" animate="show"