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 8e392a9bff
commit f1f3223922
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 {
display: flex;
flex-direction: row;
@@ -8,6 +16,7 @@
}
.item-table-grid-container {
position: relative;
flex: 1 1 auto;
width: 100%;
height: 100%;
@@ -23,6 +32,7 @@
}
.item-table-sticky-rows-grid-container {
position: relative;
flex: 0 1 auto;
min-width: 0;
}
@@ -35,6 +45,7 @@
}
.item-table-sticky-intersection-grid-container {
position: relative;
flex: 0 1 auto;
min-width: 0;
}
@@ -53,3 +64,39 @@
.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,
useEffect,
useRef,
useState,
} from 'react';
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 { 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 {
id: string;
label: string;
width: number;
}
interface CellProps {
columns: ItemTableListColumn[];
data: unknown[];
}
interface ItemTableListProps {
CellComponent: JSXElementConstructor<CellComponentProps<CellProps>>;
columnCount: number;
@@ -39,7 +43,9 @@ interface ItemTableListProps {
columnWidth: ((index: number, cellProps: CellProps) => number) | number;
data: unknown[];
enableExpansion?: boolean;
enableHeader?: boolean;
enableSelection?: boolean;
headerHeight?: number;
initialTopMostItemIndex?:
| number
| {
@@ -60,8 +66,8 @@ interface ItemTableListProps {
onStartReached?: (index: number) => void;
ref?: Ref<GridImperativeAPI>;
rowHeight: ((index: number, cellProps: CellProps) => number) | number;
size?: 'compact' | 'default';
stickyColumnCount: number;
stickyRowCount: number;
totalItemCount: number;
}
@@ -85,6 +91,8 @@ export const ItemTableList = ({
columns,
columnWidth,
data,
enableHeader = true,
headerHeight = 40,
initialTopMostItemIndex,
itemType,
onCellsRendered,
@@ -98,10 +106,11 @@ export const ItemTableList = ({
onStartReached,
ref,
rowHeight,
size = 'default',
stickyColumnCount,
stickyRowCount,
totalItemCount,
}: ItemTableListProps) => {
const stickyRowCount = enableHeader ? 1 : 0;
const totalRowCount = totalItemCount - (stickyRowCount ?? 0);
const totalColumnCount = columnCount - (stickyColumnCount ?? 0);
const stickyRowRef = useRef<HTMLDivElement>(null);
@@ -109,6 +118,7 @@ export const ItemTableList = ({
const stickyColumnRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
const mergedRowRef = useMergedRef(rowRef, scrollContainerRef);
const [showLeftShadow, setShowLeftShadow] = useState(false);
const [initialize] = useOverlayScrollbars({
defer: true,
@@ -321,10 +331,43 @@ export const ItemTableList = ({
return undefined;
}, []);
const cellProps = {
columns,
data,
};
// Handle left shadow visibility based on horizontal scroll
useEffect(() => {
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();
@@ -342,6 +385,7 @@ export const ItemTableList = ({
},
[internalState],
);
const handleOnCellsRendered = useCallback(
(cells: {
columnStartIndex: number;
@@ -349,6 +393,11 @@ export const ItemTableList = ({
rowStartIndex: number;
rowStopIndex: number;
}) => {
onRangeChanged?.({
endIndex: cells.rowStopIndex,
startIndex: cells.rowStartIndex,
});
return onCellsRendered
? ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }) => {
return onCellsRendered!(
@@ -363,7 +412,7 @@ export const ItemTableList = ({
}
: undefined;
},
[onCellsRendered, stickyColumnCount, stickyRowCount],
[onCellsRendered, onRangeChanged, stickyColumnCount, stickyRowCount],
);
const StickyRowCell = useCallback(
@@ -406,6 +455,14 @@ export const ItemTableList = ({
[stickyColumnCount, stickyRowCount, CellComponent],
);
const cellProps = {
columns,
data,
handleExpand,
itemType,
size,
};
return (
<motion.div
animate={{
@@ -416,128 +473,121 @@ export const ItemTableList = ({
ease: 'backInOut',
},
}}
className={styles.itemTableContainer}
className={styles.itemTableListContainer}
initial={{ opacity: 0 }}
>
<div
className={styles.itemTableStickyColumnsGridContainer}
style={{
minWidth: `${Array.from({ length: stickyColumnCount ?? 0 }, () => 0).reduce(
(a, _, i) =>
a +
(typeof columnWidth === 'number'
? columnWidth
: columnWidth(i, cellProps)),
0,
)}px`,
}}
>
{!!(stickyColumnCount || stickyRowCount) && (
<div
className={styles.itemTableStickyIntersectionGridContainer}
style={{
minHeight: `${Array.from(
{ length: stickyRowCount ?? 0 },
() => 0,
).reduce(
(a, _, i) =>
a +
(typeof rowHeight === 'number'
? rowHeight
: rowHeight(i, cellProps)),
0,
)}px`,
}}
>
<Grid
cellComponent={CellComponent as any}
cellProps={cellProps}
className={styles.noScrollbar}
columnCount={stickyColumnCount}
columnWidth={columnWidth}
rowCount={stickyRowCount}
rowHeight={rowHeight}
/>
</div>
)}
{!!stickyColumnCount && (
<div className={styles.itemTableStickyColumnsContainer} ref={stickyColumnRef}>
<Grid
cellComponent={StickyColumnCell}
cellProps={cellProps}
className={clsx(styles.noScrollbar, styles.height100)}
columnCount={stickyColumnCount}
columnWidth={columnWidth}
rowCount={totalRowCount}
rowHeight={(index, cellProps) => {
return typeof rowHeight === 'number'
? rowHeight
: rowHeight(index + (stickyRowCount ?? 0), cellProps);
<div className={styles.itemTableContainer}>
<div
className={styles.itemTableStickyColumnsGridContainer}
style={{
minWidth: `${Array.from({ length: stickyColumnCount ?? 0 }, () => 0).reduce(
(a, _, i) =>
a +
(typeof columnWidth === 'number'
? columnWidth
: columnWidth(i, cellProps)),
0,
)}px`,
}}
>
{!!(stickyColumnCount || stickyRowCount) && (
<div
className={styles.itemTableStickyIntersectionGridContainer}
style={{
minHeight: `${Array.from(
{ length: stickyRowCount ?? 0 },
() => 0,
).reduce((a, _, i) => a + getRowHeight(i, cellProps), 0)}px`,
}}
/>
</div>
)}
</div>
<div className={styles.itemTableStickyRowsContainer}>
{!!stickyRowCount && (
<div
className={styles.itemTableStickyRowsGridContainer}
ref={stickyRowRef}
style={{
minHeight: `${Array.from(
{ length: stickyRowCount ?? 0 },
() => 0,
).reduce(
(a, _, i) =>
a +
(typeof rowHeight === 'number'
? rowHeight
: rowHeight(i, cellProps)),
0,
)}px`,
}}
>
>
<Grid
cellComponent={CellComponent as any}
cellProps={cellProps}
className={styles.noScrollbar}
columnCount={stickyColumnCount}
columnWidth={columnWidth}
rowCount={stickyRowCount}
rowHeight={getRowHeight}
/>
{enableHeader && <div className={styles.itemTableStickyHeaderShadow} />}
</div>
)}
{!!stickyColumnCount && (
<div
className={styles.itemTableStickyColumnsContainer}
ref={stickyColumnRef}
>
<Grid
cellComponent={StickyColumnCell}
cellProps={cellProps}
className={clsx(styles.noScrollbar, styles.height100)}
columnCount={stickyColumnCount}
columnWidth={columnWidth}
rowCount={totalRowCount}
rowHeight={(index, cellProps) => {
return getRowHeight(index + (stickyRowCount ?? 0), cellProps);
}}
/>
</div>
)}
</div>
<div className={styles.itemTableStickyRowsContainer}>
{!!stickyRowCount && (
<div
className={styles.itemTableStickyRowsGridContainer}
ref={stickyRowRef}
style={
{
'--header-height': `${headerHeight}px`,
minHeight: `${Array.from(
{ length: stickyRowCount ?? 0 },
() => 0,
).reduce((a, _, i) => a + getRowHeight(i, cellProps), 0)}px`,
} as React.CSSProperties
}
>
<Grid
cellComponent={StickyRowCell}
cellProps={cellProps}
className={styles.noScrollbar}
columnCount={totalColumnCount}
columnWidth={(index, cellProps) => {
return typeof columnWidth === 'number'
? columnWidth
: columnWidth(index + (stickyColumnCount ?? 0), cellProps);
}}
rowCount={
Array.from({ length: stickyRowCount ?? 0 }, () => 0).length
}
rowHeight={getRowHeight}
/>
{enableHeader && <div className={styles.itemTableStickyHeaderShadow} />}
</div>
)}
<div className={styles.itemTableGridContainer} ref={mergedRowRef}>
<Grid
cellComponent={StickyRowCell}
cellComponent={RowCell}
cellProps={cellProps}
className={styles.noScrollbar}
className={styles.height100}
columnCount={totalColumnCount}
columnWidth={(index, cellProps) => {
return typeof columnWidth === 'number'
? columnWidth
: columnWidth(index + (stickyColumnCount ?? 0), cellProps);
}}
rowCount={Array.from({ length: stickyRowCount ?? 0 }, () => 0).length}
onCellsRendered={handleOnCellsRendered}
rowCount={totalRowCount}
rowHeight={(index, cellProps) => {
return typeof rowHeight === 'number'
? rowHeight
: rowHeight(index, cellProps);
return getRowHeight(index + (stickyRowCount ?? 0), cellProps);
}}
/>
{stickyColumnCount > 0 && showLeftShadow && (
<div className={styles.itemTableLeftScrollShadow} />
)}
</div>
)}
<div className={styles.itemTableGridContainer} ref={mergedRowRef}>
<Grid
cellComponent={RowCell}
cellProps={cellProps}
className={styles.height100}
columnCount={totalColumnCount}
columnWidth={(index, cellProps) => {
return typeof columnWidth === 'number'
? columnWidth
: columnWidth(index + (stickyColumnCount ?? 0), cellProps);
}}
onCellsRendered={handleOnCellsRendered}
rowCount={totalRowCount}
rowHeight={(index, cellProps) => {
return typeof rowHeight === 'number'
? rowHeight
: rowHeight(index + (stickyRowCount ?? 0), cellProps);
}}
/>
</div>
</div>
<AnimatePresence>
<AnimatePresence initial={false}>
{hasExpanded && (
<motion.div
animate="show"