mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 13:00:13 +02:00
support scroll sync on sticky table header and groups
This commit is contained in:
+153
-1
@@ -1,5 +1,5 @@
|
||||
import { useInView } from 'motion/react';
|
||||
import { RefObject, useMemo } from 'react';
|
||||
import { RefObject, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||
import { Platform } from '/@/shared/types/types';
|
||||
@@ -8,12 +8,26 @@ export const useStickyTableHeader = ({
|
||||
containerRef,
|
||||
enabled,
|
||||
headerRef,
|
||||
mainGridRef,
|
||||
pinnedLeftColumnRef,
|
||||
pinnedRightColumnRef,
|
||||
stickyHeaderMainRef,
|
||||
}: {
|
||||
containerRef: RefObject<HTMLDivElement | null>;
|
||||
enabled: boolean;
|
||||
headerRef: RefObject<HTMLDivElement | null>;
|
||||
mainGridRef?: RefObject<HTMLDivElement | null>;
|
||||
pinnedLeftColumnRef?: RefObject<HTMLDivElement | null>;
|
||||
pinnedRightColumnRef?: RefObject<HTMLDivElement | null>;
|
||||
stickyHeaderMainRef?: RefObject<HTMLDivElement | null>;
|
||||
}) => {
|
||||
const { windowBarStyle } = useWindowSettings();
|
||||
const isScrollingRef = useRef({
|
||||
main: false,
|
||||
pinnedLeft: false,
|
||||
pinnedRight: false,
|
||||
stickyHeader: false,
|
||||
});
|
||||
|
||||
const topMargin =
|
||||
windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS
|
||||
@@ -36,6 +50,144 @@ export const useStickyTableHeader = ({
|
||||
return windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS ? 95 : 65;
|
||||
}, [windowBarStyle]);
|
||||
|
||||
// Sync scroll between sticky header and main grid/pinned columns
|
||||
useEffect(() => {
|
||||
if (!shouldShowStickyHeader || !stickyHeaderMainRef?.current || !mainGridRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stickyMainSection = stickyHeaderMainRef.current;
|
||||
const mainGrid = mainGridRef.current.childNodes[0] as HTMLDivElement;
|
||||
const pinnedLeft = pinnedLeftColumnRef?.current?.childNodes[0] as HTMLDivElement | null;
|
||||
const pinnedRight = pinnedRightColumnRef?.current?.childNodes[0] as HTMLDivElement | null;
|
||||
|
||||
if (!mainGrid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync initial scroll position when sticky header becomes visible
|
||||
const syncInitialScroll = () => {
|
||||
const scrollLeft = mainGrid.scrollLeft;
|
||||
const scrollTop = mainGrid.scrollTop;
|
||||
|
||||
// Sync horizontal scroll position
|
||||
stickyMainSection.scrollTo({
|
||||
behavior: 'instant',
|
||||
left: scrollLeft,
|
||||
});
|
||||
|
||||
// Sync vertical scroll position with pinned columns
|
||||
if (pinnedLeft) {
|
||||
pinnedLeft.scrollTo({
|
||||
behavior: 'instant',
|
||||
top: scrollTop,
|
||||
});
|
||||
}
|
||||
if (pinnedRight) {
|
||||
pinnedRight.scrollTo({
|
||||
behavior: 'instant',
|
||||
top: scrollTop,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Sync initial position after a frame to ensure elements are ready
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(syncInitialScroll);
|
||||
});
|
||||
|
||||
const syncScroll = (e: Event) => {
|
||||
const target = e.currentTarget as HTMLDivElement;
|
||||
const scrollLeft = target.scrollLeft;
|
||||
const scrollTop = target.scrollTop;
|
||||
|
||||
// Sync horizontal scroll from main grid to sticky header main section
|
||||
if (target === mainGrid && !isScrollingRef.current.stickyHeader) {
|
||||
isScrollingRef.current.stickyHeader = true;
|
||||
stickyMainSection.scrollTo({
|
||||
behavior: 'instant',
|
||||
left: scrollLeft,
|
||||
});
|
||||
isScrollingRef.current.stickyHeader = false;
|
||||
}
|
||||
|
||||
// Sync horizontal scroll from sticky header to main grid
|
||||
if (target === stickyMainSection && !isScrollingRef.current.main) {
|
||||
isScrollingRef.current.main = true;
|
||||
mainGrid.scrollTo({
|
||||
behavior: 'instant',
|
||||
left: scrollLeft,
|
||||
});
|
||||
isScrollingRef.current.main = false;
|
||||
}
|
||||
|
||||
// Sync vertical scroll from main grid to pinned columns
|
||||
if (target === mainGrid) {
|
||||
if (pinnedLeft && !isScrollingRef.current.pinnedLeft) {
|
||||
isScrollingRef.current.pinnedLeft = true;
|
||||
pinnedLeft.scrollTo({
|
||||
behavior: 'instant',
|
||||
top: scrollTop,
|
||||
});
|
||||
isScrollingRef.current.pinnedLeft = false;
|
||||
}
|
||||
if (pinnedRight && !isScrollingRef.current.pinnedRight) {
|
||||
isScrollingRef.current.pinnedRight = true;
|
||||
pinnedRight.scrollTo({
|
||||
behavior: 'instant',
|
||||
top: scrollTop,
|
||||
});
|
||||
isScrollingRef.current.pinnedRight = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Sync vertical scroll from pinned columns to main grid
|
||||
if (pinnedLeft && target === pinnedLeft && !isScrollingRef.current.main) {
|
||||
isScrollingRef.current.main = true;
|
||||
mainGrid.scrollTo({
|
||||
behavior: 'instant',
|
||||
top: scrollTop,
|
||||
});
|
||||
isScrollingRef.current.main = false;
|
||||
}
|
||||
|
||||
if (pinnedRight && target === pinnedRight && !isScrollingRef.current.main) {
|
||||
isScrollingRef.current.main = true;
|
||||
mainGrid.scrollTo({
|
||||
behavior: 'instant',
|
||||
top: scrollTop,
|
||||
});
|
||||
isScrollingRef.current.main = false;
|
||||
}
|
||||
};
|
||||
|
||||
mainGrid.addEventListener('scroll', syncScroll);
|
||||
stickyMainSection.addEventListener('scroll', syncScroll);
|
||||
if (pinnedLeft) {
|
||||
pinnedLeft.addEventListener('scroll', syncScroll);
|
||||
}
|
||||
if (pinnedRight) {
|
||||
pinnedRight.addEventListener('scroll', syncScroll);
|
||||
}
|
||||
|
||||
return () => {
|
||||
mainGrid.removeEventListener('scroll', syncScroll);
|
||||
stickyMainSection.removeEventListener('scroll', syncScroll);
|
||||
if (pinnedLeft) {
|
||||
pinnedLeft.removeEventListener('scroll', syncScroll);
|
||||
}
|
||||
if (pinnedRight) {
|
||||
pinnedRight.removeEventListener('scroll', syncScroll);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
shouldShowStickyHeader,
|
||||
mainGridRef,
|
||||
pinnedLeftColumnRef,
|
||||
pinnedRightColumnRef,
|
||||
stickyHeaderMainRef,
|
||||
]);
|
||||
|
||||
return {
|
||||
shouldShowStickyHeader,
|
||||
stickyTop,
|
||||
|
||||
@@ -67,12 +67,10 @@
|
||||
z-index: 15;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0 2rem;
|
||||
margin: 0 -2rem;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
background-color: var(--theme-colors-background);
|
||||
border-bottom: 1px solid var(--theme-colors-border);
|
||||
box-shadow: 0 -1px 0 0 var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.sticky-header-row {
|
||||
@@ -93,8 +91,8 @@
|
||||
z-index: 15;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0 2rem;
|
||||
margin: 0 -2rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
background-color: var(--theme-colors-background);
|
||||
border-bottom: 1px solid var(--theme-colors-border);
|
||||
@@ -104,7 +102,6 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
background-color: var(--theme-colors-background);
|
||||
}
|
||||
|
||||
.sticky-group-row-section {
|
||||
@@ -112,7 +109,6 @@
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
background-color: var(--theme-colors-background);
|
||||
}
|
||||
|
||||
.item-table-pinned-rows-grid-container.with-header::after {
|
||||
|
||||
@@ -845,57 +845,45 @@ export const ItemTableList = ({
|
||||
const handleRef = useRef<ItemListHandle | null>(null);
|
||||
const containerFocusRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const stickyHeaderRef = useRef<HTMLDivElement | null>(null);
|
||||
const stickyGroupRowRef = useRef<HTMLDivElement | null>(null);
|
||||
const stickyHeaderLeftRef = useRef<HTMLDivElement | null>(null);
|
||||
const stickyHeaderMainRef = useRef<HTMLDivElement | null>(null);
|
||||
const stickyHeaderRightRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { shouldShowStickyHeader, stickyTop } = useStickyTableHeader({
|
||||
containerRef: containerFocusRef,
|
||||
enabled: enableHeader && enableStickyHeader,
|
||||
headerRef: pinnedRowRef,
|
||||
mainGridRef: rowRef,
|
||||
pinnedLeftColumnRef,
|
||||
pinnedRightColumnRef,
|
||||
stickyHeaderMainRef,
|
||||
});
|
||||
|
||||
const stickyHeaderRef = useRef<HTMLDivElement | null>(null);
|
||||
const stickyGroupRowRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// Sync scroll position and update position of sticky header
|
||||
// Update position and width of sticky header (scroll sync is handled in the hook)
|
||||
useEffect(() => {
|
||||
if (
|
||||
!shouldShowStickyHeader ||
|
||||
!stickyHeaderRef.current ||
|
||||
!rowRef.current ||
|
||||
!containerFocusRef.current
|
||||
) {
|
||||
if (!shouldShowStickyHeader || !stickyHeaderRef.current || !containerFocusRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stickyHeader = stickyHeaderRef.current;
|
||||
const stickyMainSection = stickyHeader.querySelector(
|
||||
`.${styles.stickyHeaderSection}`,
|
||||
) as HTMLDivElement;
|
||||
const mainGrid = rowRef.current.childNodes[0] as HTMLDivElement;
|
||||
const container = containerFocusRef.current;
|
||||
|
||||
if (!stickyMainSection || !mainGrid || !container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatePosition = () => {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
stickyHeader.style.left = `${containerRect.left}px`;
|
||||
};
|
||||
|
||||
const syncScroll = () => {
|
||||
stickyMainSection.scrollLeft = mainGrid.scrollLeft;
|
||||
stickyHeader.style.width = `${containerRect.width}px`;
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
syncScroll();
|
||||
|
||||
window.addEventListener('resize', updatePosition);
|
||||
window.addEventListener('scroll', updatePosition, true);
|
||||
mainGrid.addEventListener('scroll', syncScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updatePosition);
|
||||
window.removeEventListener('scroll', updatePosition, true);
|
||||
mainGrid.removeEventListener('scroll', syncScroll);
|
||||
};
|
||||
}, [shouldShowStickyHeader]);
|
||||
|
||||
@@ -1516,6 +1504,36 @@ export const ItemTableList = ({
|
||||
// Show sticky group row whenever it should be shown
|
||||
const shouldRenderStickyGroupRow = shouldShowStickyGroupRow;
|
||||
|
||||
// Update position and width of sticky group row
|
||||
useEffect(() => {
|
||||
if (
|
||||
!shouldRenderStickyGroupRow ||
|
||||
!stickyGroupRowRef.current ||
|
||||
!containerFocusRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stickyGroupRow = stickyGroupRowRef.current;
|
||||
const container = containerFocusRef.current;
|
||||
|
||||
const updatePosition = () => {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
stickyGroupRow.style.left = `${containerRect.left}px`;
|
||||
stickyGroupRow.style.width = `${containerRect.width}px`;
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
|
||||
window.addEventListener('resize', updatePosition);
|
||||
window.addEventListener('scroll', updatePosition, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updatePosition);
|
||||
window.removeEventListener('scroll', updatePosition, true);
|
||||
};
|
||||
}, [shouldRenderStickyGroupRow]);
|
||||
|
||||
const getDataFn = useCallback(() => {
|
||||
const result: (null | unknown)[] = enableHeader ? [null] : [];
|
||||
|
||||
@@ -1829,8 +1847,16 @@ export const ItemTableList = ({
|
||||
<div className={styles.stickyHeaderRow}>
|
||||
{pinnedLeftColumnCount > 0 && (
|
||||
<div
|
||||
className={styles.stickyHeaderSection}
|
||||
style={{ width: `${pinnedLeftWidth}px` }}
|
||||
className={clsx(
|
||||
styles.stickyHeaderSection,
|
||||
styles.stickyHeaderPinnedLeft,
|
||||
)}
|
||||
ref={stickyHeaderLeftRef}
|
||||
style={{
|
||||
flex: '0 1 auto',
|
||||
minWidth: `${pinnedLeftWidth}px`,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{parsedColumns
|
||||
.filter((col) => col.pinned === 'left')
|
||||
@@ -1855,33 +1881,62 @@ export const ItemTableList = ({
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.stickyHeaderSection} style={{ width: `${mainWidth}px` }}>
|
||||
{parsedColumns
|
||||
.filter((col) => col.pinned === null)
|
||||
.map((col) => {
|
||||
const columnIndex = parsedColumns.findIndex((c) => c === col);
|
||||
return (
|
||||
<CellComponent
|
||||
ariaAttributes={{
|
||||
'aria-colindex': columnIndex + 1,
|
||||
role: 'gridcell',
|
||||
}}
|
||||
columnIndex={columnIndex}
|
||||
key={col.id}
|
||||
rowIndex={0}
|
||||
style={{
|
||||
height: headerHeight,
|
||||
width: calculatedColumnWidths[columnIndex],
|
||||
}}
|
||||
{...stickyHeaderItemProps}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
className={clsx(
|
||||
styles.stickyHeaderSection,
|
||||
styles.stickyHeaderMain,
|
||||
styles.noScrollbar,
|
||||
)}
|
||||
ref={stickyHeaderMainRef}
|
||||
style={{
|
||||
flex: '1 1 auto',
|
||||
minWidth: 0,
|
||||
overflowX: 'auto',
|
||||
overflowY: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
minWidth: `${mainWidth}px`,
|
||||
}}
|
||||
>
|
||||
{parsedColumns
|
||||
.filter((col) => col.pinned === null)
|
||||
.map((col) => {
|
||||
const columnIndex = parsedColumns.findIndex((c) => c === col);
|
||||
return (
|
||||
<CellComponent
|
||||
ariaAttributes={{
|
||||
'aria-colindex': columnIndex + 1,
|
||||
role: 'gridcell',
|
||||
}}
|
||||
columnIndex={columnIndex}
|
||||
key={col.id}
|
||||
rowIndex={0}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
height: headerHeight,
|
||||
width: calculatedColumnWidths[columnIndex],
|
||||
}}
|
||||
{...stickyHeaderItemProps}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{pinnedRightColumnCount > 0 && (
|
||||
<div
|
||||
className={styles.stickyHeaderSection}
|
||||
style={{ width: `${pinnedRightWidth}px` }}
|
||||
className={clsx(
|
||||
styles.stickyHeaderSection,
|
||||
styles.stickyHeaderPinnedRight,
|
||||
)}
|
||||
ref={stickyHeaderRightRef}
|
||||
style={{
|
||||
flex: '0 1 auto',
|
||||
minWidth: `${pinnedRightWidth}px`,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{parsedColumns
|
||||
.filter((col) => col.pinned === 'right')
|
||||
@@ -2003,7 +2058,13 @@ export const ItemTableList = ({
|
||||
)}
|
||||
<div
|
||||
className={styles.stickyGroupRowSection}
|
||||
style={{ width: `${mainWidth}px` }}
|
||||
style={{
|
||||
marginLeft: pinnedLeftColumnCount > 0 ? 0 : '-2rem',
|
||||
marginRight: '-2rem',
|
||||
paddingLeft: pinnedLeftColumnCount > 0 ? 0 : '2rem',
|
||||
paddingRight: '2rem',
|
||||
width: `${mainWidth}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@@ -2025,9 +2086,7 @@ export const ItemTableList = ({
|
||||
height: groupRowHeight,
|
||||
width: `${pinnedRightWidth}px`,
|
||||
}}
|
||||
>
|
||||
{groupContent}
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -552,7 +552,7 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
|
||||
id={`disc-${discGroup.discNumber}`}
|
||||
indeterminate={isSomeSelected}
|
||||
label={
|
||||
<Text component="label" size="sm">
|
||||
<Text component="label" size="sm" truncate>
|
||||
{t('common.disc', { postProcess: 'sentenceCase' })}{' '}
|
||||
{discGroup.discNumber}
|
||||
{discGroup.discSubtitle && ` - ${discGroup.discSubtitle}`}
|
||||
|
||||
Reference in New Issue
Block a user