use external store for scroll shadow

This commit is contained in:
jeffvli
2026-04-04 23:32:32 -07:00
parent a868d4d539
commit 573fe5ee35
3 changed files with 145 additions and 53 deletions
@@ -1,3 +1,5 @@
import type { TableScrollShadowStore } from '/@/renderer/components/item-list/item-table-list/table-scroll-shadow-store';
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element'; import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import { useOverlayScrollbars } from 'overlayscrollbars-react'; import { useOverlayScrollbars } from 'overlayscrollbars-react';
@@ -18,9 +20,7 @@ export const useTablePaneSync = ({
pinnedRowRef, pinnedRowRef,
rowRef, rowRef,
scrollContainerRef, scrollContainerRef,
setShowLeftShadow, scrollShadowStore,
setShowRightShadow,
setShowTopShadow,
}: { }: {
enableDrag: boolean | undefined; enableDrag: boolean | undefined;
enableDragScroll: boolean | undefined; enableDragScroll: boolean | undefined;
@@ -36,9 +36,7 @@ export const useTablePaneSync = ({
pinnedRowRef: React.RefObject<HTMLDivElement | null>; pinnedRowRef: React.RefObject<HTMLDivElement | null>;
rowRef: React.RefObject<HTMLDivElement | null>; rowRef: React.RefObject<HTMLDivElement | null>;
scrollContainerRef: React.RefObject<HTMLDivElement | null>; scrollContainerRef: React.RefObject<HTMLDivElement | null>;
setShowLeftShadow: (v: boolean) => void; scrollShadowStore: TableScrollShadowStore;
setShowRightShadow: (v: boolean) => void;
setShowTopShadow: (v: boolean) => void;
}) => { }) => {
// Main grid overlayscrollbars - only handle X-axis if right-pinned columns exist // Main grid overlayscrollbars - only handle X-axis if right-pinned columns exist
const [initialize, osInstance] = useOverlayScrollbars({ const [initialize, osInstance] = useOverlayScrollbars({
@@ -471,8 +469,10 @@ export const useTablePaneSync = ({
if (!row) { if (!row) {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
setShowLeftShadow(false); scrollShadowStore.setSnapshot({
setShowRightShadow(false); showLeftShadow: false,
showRightShadow: false,
});
}, 0); }, 0);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
@@ -482,8 +482,10 @@ export const useTablePaneSync = ({
const scrollLeft = row.scrollLeft; const scrollLeft = row.scrollLeft;
const maxScrollLeft = row.scrollWidth - row.clientWidth; const maxScrollLeft = row.scrollWidth - row.clientWidth;
setShowLeftShadow(pinnedLeftColumnCount > 0 && scrollLeft > 0); scrollShadowStore.setSnapshot({
setShowRightShadow(pinnedRightColumnCount > 0 && scrollLeft < maxScrollLeft); showLeftShadow: pinnedLeftColumnCount > 0 && scrollLeft > 0,
showRightShadow: pinnedRightColumnCount > 0 && scrollLeft < maxScrollLeft,
});
}, 50); }, 50);
checkScrollPosition(); checkScrollPosition();
@@ -494,13 +496,7 @@ export const useTablePaneSync = ({
checkScrollPosition.cancel(); checkScrollPosition.cancel();
row.removeEventListener('scroll', checkScrollPosition); row.removeEventListener('scroll', checkScrollPosition);
}; };
}, [ }, [pinnedLeftColumnCount, pinnedRightColumnCount, rowRef, scrollShadowStore]);
pinnedLeftColumnCount,
pinnedRightColumnCount,
rowRef,
setShowLeftShadow,
setShowRightShadow,
]);
// Handle top shadow visibility based on vertical scroll // Handle top shadow visibility based on vertical scroll
useEffect(() => { useEffect(() => {
@@ -509,7 +505,7 @@ export const useTablePaneSync = ({
if (!row || !enableHeader) { if (!row || !enableHeader) {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
setShowTopShadow(false); scrollShadowStore.setSnapshot({ showTopShadow: false });
}, 0); }, 0);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
@@ -519,7 +515,7 @@ export const useTablePaneSync = ({
const checkScrollPosition = throttle(() => { const checkScrollPosition = throttle(() => {
const currentScrollTop = scrollElement.scrollTop; const currentScrollTop = scrollElement.scrollTop;
setShowTopShadow(currentScrollTop > 0); scrollShadowStore.setSnapshot({ showTopShadow: currentScrollTop > 0 });
}, 50); }, 50);
checkScrollPosition(); checkScrollPosition();
@@ -530,5 +526,5 @@ export const useTablePaneSync = ({
checkScrollPosition.cancel(); checkScrollPosition.cancel();
scrollElement.removeEventListener('scroll', checkScrollPosition); scrollElement.removeEventListener('scroll', checkScrollPosition);
}; };
}, [enableHeader, pinnedRightColumnCount, pinnedRightColumnRef, rowRef, setShowTopShadow]); }, [enableHeader, pinnedRightColumnCount, pinnedRightColumnRef, rowRef, scrollShadowStore]);
}; };
@@ -14,6 +14,7 @@ import React, {
useMemo, useMemo,
useRef, useRef,
useState, useState,
useSyncExternalStore,
} from 'react'; } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { type CellComponentProps, Grid } from 'react-window-v2'; import { type CellComponentProps, Grid } from 'react-window-v2';
@@ -52,6 +53,10 @@ import {
MemoizedCellRouter, MemoizedCellRouter,
useColumnCellComponents, useColumnCellComponents,
} from '/@/renderer/components/item-list/item-table-list/memoized-cell-router'; } from '/@/renderer/components/item-list/item-table-list/memoized-cell-router';
import {
createTableScrollShadowStore,
type TableScrollShadowStore,
} from '/@/renderer/components/item-list/item-table-list/table-scroll-shadow-store';
import { import {
ItemControls, ItemControls,
ItemListHandle, ItemListHandle,
@@ -103,6 +108,63 @@ export enum TableItemSize {
LARGE = 88, LARGE = 88,
} }
const ItemTableScrollShadowTop = memo(function ItemTableScrollShadowTop({
enableHeader,
enableScrollShadow,
scrollShadowStore,
}: {
enableHeader: boolean;
enableScrollShadow: boolean;
scrollShadowStore: TableScrollShadowStore;
}) {
const { showTopShadow } = useSyncExternalStore(
scrollShadowStore.subscribe,
scrollShadowStore.getSnapshot,
);
if (!enableHeader || !enableScrollShadow || !showTopShadow) return null;
return <div className={styles.itemTableTopScrollShadow} />;
});
ItemTableScrollShadowTop.displayName = 'ItemTableScrollShadowTop';
const ItemTableScrollShadowLeft = memo(function ItemTableScrollShadowLeft({
enableScrollShadow,
pinnedLeftColumnCount,
scrollShadowStore,
}: {
enableScrollShadow: boolean;
pinnedLeftColumnCount: number;
scrollShadowStore: TableScrollShadowStore;
}) {
const { showLeftShadow } = useSyncExternalStore(
scrollShadowStore.subscribe,
scrollShadowStore.getSnapshot,
);
if (pinnedLeftColumnCount <= 0 || !enableScrollShadow || !showLeftShadow) return null;
return <div className={styles.itemTableLeftScrollShadow} />;
});
ItemTableScrollShadowLeft.displayName = 'ItemTableScrollShadowLeft';
const ItemTableScrollShadowRight = memo(function ItemTableScrollShadowRight({
enableScrollShadow,
pinnedRightColumnCount,
scrollShadowStore,
}: {
enableScrollShadow: boolean;
pinnedRightColumnCount: number;
scrollShadowStore: TableScrollShadowStore;
}) {
const { showRightShadow } = useSyncExternalStore(
scrollShadowStore.subscribe,
scrollShadowStore.getSnapshot,
);
if (pinnedRightColumnCount <= 0 || !enableScrollShadow || !showRightShadow) return null;
return <div className={styles.itemTableRightScrollShadow} />;
});
ItemTableScrollShadowRight.displayName = 'ItemTableScrollShadowRight';
interface VirtualizedTableGridProps { interface VirtualizedTableGridProps {
calculatedColumnWidths: number[]; calculatedColumnWidths: number[];
CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>; CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
@@ -120,9 +182,7 @@ interface VirtualizedTableGridProps {
pinnedRightColumnRef: React.RefObject<HTMLDivElement | null>; pinnedRightColumnRef: React.RefObject<HTMLDivElement | null>;
pinnedRowCount: number; pinnedRowCount: number;
pinnedRowRef: React.RefObject<HTMLDivElement | null>; pinnedRowRef: React.RefObject<HTMLDivElement | null>;
showLeftShadow: boolean; scrollShadowStore: TableScrollShadowStore;
showRightShadow: boolean;
showTopShadow: boolean;
tableConfig: ItemTableListConfig; tableConfig: ItemTableListConfig;
totalColumnCount: number; totalColumnCount: number;
totalRowCount: number; totalRowCount: number;
@@ -145,9 +205,7 @@ const VirtualizedTableGrid = ({
pinnedRightColumnRef, pinnedRightColumnRef,
pinnedRowCount, pinnedRowCount,
pinnedRowRef, pinnedRowRef,
showLeftShadow, scrollShadowStore,
showRightShadow,
showTopShadow,
tableConfig, tableConfig,
totalColumnCount, totalColumnCount,
totalRowCount, totalRowCount,
@@ -497,9 +555,11 @@ const VirtualizedTableGrid = ({
/> />
</div> </div>
)} )}
{enableHeader && enableScrollShadow && showTopShadow && ( <ItemTableScrollShadowTop
<div className={styles.itemTableTopScrollShadow} /> enableHeader={!!enableHeader}
)} enableScrollShadow={enableScrollShadow}
scrollShadowStore={scrollShadowStore}
/>
{!!pinnedLeftColumnCount && ( {!!pinnedLeftColumnCount && (
<div <div
className={styles.itemTablePinnedColumnsContainer} className={styles.itemTablePinnedColumnsContainer}
@@ -554,9 +614,11 @@ const VirtualizedTableGrid = ({
/> />
</div> </div>
)} )}
{enableHeader && enableScrollShadow && showTopShadow && ( <ItemTableScrollShadowTop
<div className={styles.itemTableTopScrollShadow} /> enableHeader={!!enableHeader}
)} enableScrollShadow={enableScrollShadow}
scrollShadowStore={scrollShadowStore}
/>
<div className={styles.itemTableGridContainer} ref={mergedRowRef}> <div className={styles.itemTableGridContainer} ref={mergedRowRef}>
<Grid <Grid
cellComponent={RowCell} cellComponent={RowCell}
@@ -568,12 +630,16 @@ const VirtualizedTableGrid = ({
rowCount={totalRowCount} rowCount={totalRowCount}
rowHeight={rowHeightMemoized} rowHeight={rowHeightMemoized}
/> />
{pinnedLeftColumnCount > 0 && enableScrollShadow && showLeftShadow && ( <ItemTableScrollShadowLeft
<div className={styles.itemTableLeftScrollShadow} /> enableScrollShadow={enableScrollShadow}
)} pinnedLeftColumnCount={pinnedLeftColumnCount}
{pinnedRightColumnCount > 0 && enableScrollShadow && showRightShadow && ( scrollShadowStore={scrollShadowStore}
<div className={styles.itemTableRightScrollShadow} /> />
)} <ItemTableScrollShadowRight
enableScrollShadow={enableScrollShadow}
pinnedRightColumnCount={pinnedRightColumnCount}
scrollShadowStore={scrollShadowStore}
/>
</div> </div>
</div> </div>
{!!pinnedRightColumnCount && ( {!!pinnedRightColumnCount && (
@@ -611,9 +677,11 @@ const VirtualizedTableGrid = ({
/> />
</div> </div>
)} )}
{enableHeader && enableScrollShadow && showTopShadow && ( <ItemTableScrollShadowTop
<div className={styles.itemTableTopScrollShadow} /> enableHeader={!!enableHeader}
)} enableScrollShadow={enableScrollShadow}
scrollShadowStore={scrollShadowStore}
/>
<div <div
className={styles.itemTablePinnedRightColumnsContainer} className={styles.itemTablePinnedRightColumnsContainer}
ref={pinnedRightColumnRef} ref={pinnedRightColumnRef}
@@ -666,9 +734,7 @@ const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, next
prevProps.pinnedRightColumnRef === nextProps.pinnedRightColumnRef && prevProps.pinnedRightColumnRef === nextProps.pinnedRightColumnRef &&
prevProps.pinnedRowCount === nextProps.pinnedRowCount && prevProps.pinnedRowCount === nextProps.pinnedRowCount &&
prevProps.pinnedRowRef === nextProps.pinnedRowRef && prevProps.pinnedRowRef === nextProps.pinnedRowRef &&
prevProps.showLeftShadow === nextProps.showLeftShadow && prevProps.scrollShadowStore === nextProps.scrollShadowStore &&
prevProps.showRightShadow === nextProps.showRightShadow &&
prevProps.showTopShadow === nextProps.showTopShadow &&
prevProps.totalColumnCount === nextProps.totalColumnCount && prevProps.totalColumnCount === nextProps.totalColumnCount &&
prevProps.totalRowCount === nextProps.totalRowCount && prevProps.totalRowCount === nextProps.totalRowCount &&
prevProps.CellComponent === nextProps.CellComponent prevProps.CellComponent === nextProps.CellComponent
@@ -1257,9 +1323,7 @@ const BaseItemTableList = ({
const pinnedRightColumnRef = useRef<HTMLDivElement>(null); const pinnedRightColumnRef = 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 scrollShadowStore = useMemo(() => createTableScrollShadowStore(), []);
const [showRightShadow, setShowRightShadow] = useState(false);
const [showTopShadow, setShowTopShadow] = useState(false);
const handleRef = useRef<ItemListHandle | null>(null); const handleRef = useRef<ItemListHandle | null>(null);
const { focused, ref: focusRef } = useFocusWithin(); const { focused, ref: focusRef } = useFocusWithin();
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
@@ -1317,9 +1381,7 @@ const BaseItemTableList = ({
pinnedRowRef, pinnedRowRef,
rowRef, rowRef,
scrollContainerRef, scrollContainerRef,
setShowLeftShadow, scrollShadowStore,
setShowRightShadow,
setShowTopShadow,
}); });
const getRowHeight = useCallback( const getRowHeight = useCallback(
@@ -1638,9 +1700,7 @@ const BaseItemTableList = ({
pinnedRightColumnRef={pinnedRightColumnRef} pinnedRightColumnRef={pinnedRightColumnRef}
pinnedRowCount={pinnedRowCount} pinnedRowCount={pinnedRowCount}
pinnedRowRef={pinnedRowRef} pinnedRowRef={pinnedRowRef}
showLeftShadow={showLeftShadow} scrollShadowStore={scrollShadowStore}
showRightShadow={showRightShadow}
showTopShadow={showTopShadow}
tableConfig={tableConfigValue} tableConfig={tableConfigValue}
totalColumnCount={totalColumnCount} totalColumnCount={totalColumnCount}
totalRowCount={totalRowCount} totalRowCount={totalRowCount}
@@ -0,0 +1,36 @@
export interface TableScrollShadowSnapshot {
showLeftShadow: boolean;
showRightShadow: boolean;
showTopShadow: boolean;
}
export type TableScrollShadowStore = ReturnType<typeof createTableScrollShadowStore>;
export function createTableScrollShadowStore() {
let snapshot: TableScrollShadowSnapshot = {
showLeftShadow: false,
showRightShadow: false,
showTopShadow: false,
};
const listeners = new Set<() => void>();
return {
getSnapshot: (): TableScrollShadowSnapshot => snapshot,
setSnapshot: (patch: Partial<TableScrollShadowSnapshot>) => {
const next: TableScrollShadowSnapshot = { ...snapshot, ...patch };
if (
next.showLeftShadow === snapshot.showLeftShadow &&
next.showRightShadow === snapshot.showRightShadow &&
next.showTopShadow === snapshot.showTopShadow
) {
return;
}
snapshot = next;
listeners.forEach((l) => l());
},
subscribe: (listener: () => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}