mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
more table list progress
- scroll shadow - header shadow - remove sticky row count - only allow sticky header - support table expansion
This commit is contained in:
@@ -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,10 +331,43 @@ 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();
|
||||||
|
|
||||||
@@ -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,128 +473,121 @@ export const ItemTableList = ({
|
|||||||
ease: 'backInOut',
|
ease: 'backInOut',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
className={styles.itemTableContainer}
|
className={styles.itemTableListContainer}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
>
|
>
|
||||||
<div
|
<div className={styles.itemTableContainer}>
|
||||||
className={styles.itemTableStickyColumnsGridContainer}
|
<div
|
||||||
style={{
|
className={styles.itemTableStickyColumnsGridContainer}
|
||||||
minWidth: `${Array.from({ length: stickyColumnCount ?? 0 }, () => 0).reduce(
|
style={{
|
||||||
(a, _, i) =>
|
minWidth: `${Array.from({ length: stickyColumnCount ?? 0 }, () => 0).reduce(
|
||||||
a +
|
(a, _, i) =>
|
||||||
(typeof columnWidth === 'number'
|
a +
|
||||||
? columnWidth
|
(typeof columnWidth === 'number'
|
||||||
: columnWidth(i, cellProps)),
|
? columnWidth
|
||||||
0,
|
: columnWidth(i, cellProps)),
|
||||||
)}px`,
|
0,
|
||||||
}}
|
)}px`,
|
||||||
>
|
}}
|
||||||
{!!(stickyColumnCount || stickyRowCount) && (
|
>
|
||||||
<div
|
{!!(stickyColumnCount || stickyRowCount) && (
|
||||||
className={styles.itemTableStickyIntersectionGridContainer}
|
<div
|
||||||
style={{
|
className={styles.itemTableStickyIntersectionGridContainer}
|
||||||
minHeight: `${Array.from(
|
style={{
|
||||||
{ length: stickyRowCount ?? 0 },
|
minHeight: `${Array.from(
|
||||||
() => 0,
|
{ length: stickyRowCount ?? 0 },
|
||||||
).reduce(
|
() => 0,
|
||||||
(a, _, i) =>
|
).reduce((a, _, i) => a + getRowHeight(i, cellProps), 0)}px`,
|
||||||
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>
|
<Grid
|
||||||
)}
|
cellComponent={CellComponent as any}
|
||||||
</div>
|
cellProps={cellProps}
|
||||||
<div className={styles.itemTableStickyRowsContainer}>
|
className={styles.noScrollbar}
|
||||||
{!!stickyRowCount && (
|
columnCount={stickyColumnCount}
|
||||||
<div
|
columnWidth={columnWidth}
|
||||||
className={styles.itemTableStickyRowsGridContainer}
|
rowCount={stickyRowCount}
|
||||||
ref={stickyRowRef}
|
rowHeight={getRowHeight}
|
||||||
style={{
|
/>
|
||||||
minHeight: `${Array.from(
|
{enableHeader && <div className={styles.itemTableStickyHeaderShadow} />}
|
||||||
{ length: stickyRowCount ?? 0 },
|
</div>
|
||||||
() => 0,
|
)}
|
||||||
).reduce(
|
{!!stickyColumnCount && (
|
||||||
(a, _, i) =>
|
<div
|
||||||
a +
|
className={styles.itemTableStickyColumnsContainer}
|
||||||
(typeof rowHeight === 'number'
|
ref={stickyColumnRef}
|
||||||
? rowHeight
|
>
|
||||||
: rowHeight(i, cellProps)),
|
<Grid
|
||||||
0,
|
cellComponent={StickyColumnCell}
|
||||||
)}px`,
|
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
|
<Grid
|
||||||
cellComponent={StickyRowCell}
|
cellComponent={RowCell}
|
||||||
cellProps={cellProps}
|
cellProps={cellProps}
|
||||||
className={styles.noScrollbar}
|
className={styles.height100}
|
||||||
columnCount={totalColumnCount}
|
columnCount={totalColumnCount}
|
||||||
columnWidth={(index, cellProps) => {
|
columnWidth={(index, cellProps) => {
|
||||||
return typeof columnWidth === 'number'
|
return typeof columnWidth === 'number'
|
||||||
? columnWidth
|
? columnWidth
|
||||||
: columnWidth(index + (stickyColumnCount ?? 0), cellProps);
|
: columnWidth(index + (stickyColumnCount ?? 0), cellProps);
|
||||||
}}
|
}}
|
||||||
rowCount={Array.from({ length: stickyRowCount ?? 0 }, () => 0).length}
|
onCellsRendered={handleOnCellsRendered}
|
||||||
|
rowCount={totalRowCount}
|
||||||
rowHeight={(index, cellProps) => {
|
rowHeight={(index, cellProps) => {
|
||||||
return typeof rowHeight === 'number'
|
return getRowHeight(index + (stickyRowCount ?? 0), cellProps);
|
||||||
? rowHeight
|
|
||||||
: rowHeight(index, cellProps);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{stickyColumnCount > 0 && showLeftShadow && (
|
||||||
|
<div className={styles.itemTableLeftScrollShadow} />
|
||||||
|
)}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<AnimatePresence>
|
<AnimatePresence initial={false}>
|
||||||
{hasExpanded && (
|
{hasExpanded && (
|
||||||
<motion.div
|
<motion.div
|
||||||
animate="show"
|
animate="show"
|
||||||
|
|||||||
Reference in New Issue
Block a user