diff --git a/src/renderer/components/item-list/item-table-list/hooks/use-table-pane-sync.ts b/src/renderer/components/item-list/item-table-list/hooks/use-table-pane-sync.ts index 2089227e2..6597983c8 100644 --- a/src/renderer/components/item-list/item-table-list/hooks/use-table-pane-sync.ts +++ b/src/renderer/components/item-list/item-table-list/hooks/use-table-pane-sync.ts @@ -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 throttle from 'lodash/throttle'; import { useOverlayScrollbars } from 'overlayscrollbars-react'; @@ -18,9 +20,7 @@ export const useTablePaneSync = ({ pinnedRowRef, rowRef, scrollContainerRef, - setShowLeftShadow, - setShowRightShadow, - setShowTopShadow, + scrollShadowStore, }: { enableDrag: boolean | undefined; enableDragScroll: boolean | undefined; @@ -36,9 +36,7 @@ export const useTablePaneSync = ({ pinnedRowRef: React.RefObject; rowRef: React.RefObject; scrollContainerRef: React.RefObject; - setShowLeftShadow: (v: boolean) => void; - setShowRightShadow: (v: boolean) => void; - setShowTopShadow: (v: boolean) => void; + scrollShadowStore: TableScrollShadowStore; }) => { // Main grid overlayscrollbars - only handle X-axis if right-pinned columns exist const [initialize, osInstance] = useOverlayScrollbars({ @@ -471,8 +469,10 @@ export const useTablePaneSync = ({ if (!row) { const timeout = setTimeout(() => { - setShowLeftShadow(false); - setShowRightShadow(false); + scrollShadowStore.setSnapshot({ + showLeftShadow: false, + showRightShadow: false, + }); }, 0); return () => clearTimeout(timeout); @@ -482,8 +482,10 @@ export const useTablePaneSync = ({ const scrollLeft = row.scrollLeft; const maxScrollLeft = row.scrollWidth - row.clientWidth; - setShowLeftShadow(pinnedLeftColumnCount > 0 && scrollLeft > 0); - setShowRightShadow(pinnedRightColumnCount > 0 && scrollLeft < maxScrollLeft); + scrollShadowStore.setSnapshot({ + showLeftShadow: pinnedLeftColumnCount > 0 && scrollLeft > 0, + showRightShadow: pinnedRightColumnCount > 0 && scrollLeft < maxScrollLeft, + }); }, 50); checkScrollPosition(); @@ -494,13 +496,7 @@ export const useTablePaneSync = ({ checkScrollPosition.cancel(); row.removeEventListener('scroll', checkScrollPosition); }; - }, [ - pinnedLeftColumnCount, - pinnedRightColumnCount, - rowRef, - setShowLeftShadow, - setShowRightShadow, - ]); + }, [pinnedLeftColumnCount, pinnedRightColumnCount, rowRef, scrollShadowStore]); // Handle top shadow visibility based on vertical scroll useEffect(() => { @@ -509,7 +505,7 @@ export const useTablePaneSync = ({ if (!row || !enableHeader) { const timeout = setTimeout(() => { - setShowTopShadow(false); + scrollShadowStore.setSnapshot({ showTopShadow: false }); }, 0); return () => clearTimeout(timeout); @@ -519,7 +515,7 @@ export const useTablePaneSync = ({ const checkScrollPosition = throttle(() => { const currentScrollTop = scrollElement.scrollTop; - setShowTopShadow(currentScrollTop > 0); + scrollShadowStore.setSnapshot({ showTopShadow: currentScrollTop > 0 }); }, 50); checkScrollPosition(); @@ -530,5 +526,5 @@ export const useTablePaneSync = ({ checkScrollPosition.cancel(); scrollElement.removeEventListener('scroll', checkScrollPosition); }; - }, [enableHeader, pinnedRightColumnCount, pinnedRightColumnRef, rowRef, setShowTopShadow]); + }, [enableHeader, pinnedRightColumnCount, pinnedRightColumnRef, rowRef, scrollShadowStore]); }; diff --git a/src/renderer/components/item-list/item-table-list/item-table-list.tsx b/src/renderer/components/item-list/item-table-list/item-table-list.tsx index cdcf953c6..eb8a44b44 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list.tsx @@ -14,6 +14,7 @@ import React, { useMemo, useRef, useState, + useSyncExternalStore, } from 'react'; import { useParams } from 'react-router'; import { type CellComponentProps, Grid } from 'react-window-v2'; @@ -52,6 +53,10 @@ import { MemoizedCellRouter, useColumnCellComponents, } 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 { ItemControls, ItemListHandle, @@ -103,6 +108,63 @@ export enum TableItemSize { 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
; +}); + +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
; +}); + +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
; +}); + +ItemTableScrollShadowRight.displayName = 'ItemTableScrollShadowRight'; + interface VirtualizedTableGridProps { calculatedColumnWidths: number[]; CellComponent: JSXElementConstructor>; @@ -120,9 +182,7 @@ interface VirtualizedTableGridProps { pinnedRightColumnRef: React.RefObject; pinnedRowCount: number; pinnedRowRef: React.RefObject; - showLeftShadow: boolean; - showRightShadow: boolean; - showTopShadow: boolean; + scrollShadowStore: TableScrollShadowStore; tableConfig: ItemTableListConfig; totalColumnCount: number; totalRowCount: number; @@ -145,9 +205,7 @@ const VirtualizedTableGrid = ({ pinnedRightColumnRef, pinnedRowCount, pinnedRowRef, - showLeftShadow, - showRightShadow, - showTopShadow, + scrollShadowStore, tableConfig, totalColumnCount, totalRowCount, @@ -497,9 +555,11 @@ const VirtualizedTableGrid = ({ />
)} - {enableHeader && enableScrollShadow && showTopShadow && ( -
- )} + {!!pinnedLeftColumnCount && (
)} - {enableHeader && enableScrollShadow && showTopShadow && ( -
- )} +
- {pinnedLeftColumnCount > 0 && enableScrollShadow && showLeftShadow && ( -
- )} - {pinnedRightColumnCount > 0 && enableScrollShadow && showRightShadow && ( -
- )} + +
{!!pinnedRightColumnCount && ( @@ -611,9 +677,11 @@ const VirtualizedTableGrid = ({ />
)} - {enableHeader && enableScrollShadow && showTopShadow && ( -
- )} +
(null); const scrollContainerRef = useRef(null); const mergedRowRef = useMergedRef(rowRef, scrollContainerRef); - const [showLeftShadow, setShowLeftShadow] = useState(false); - const [showRightShadow, setShowRightShadow] = useState(false); - const [showTopShadow, setShowTopShadow] = useState(false); + const scrollShadowStore = useMemo(() => createTableScrollShadowStore(), []); const handleRef = useRef(null); const { focused, ref: focusRef } = useFocusWithin(); const containerRef = useRef(null); @@ -1317,9 +1381,7 @@ const BaseItemTableList = ({ pinnedRowRef, rowRef, scrollContainerRef, - setShowLeftShadow, - setShowRightShadow, - setShowTopShadow, + scrollShadowStore, }); const getRowHeight = useCallback( @@ -1638,9 +1700,7 @@ const BaseItemTableList = ({ pinnedRightColumnRef={pinnedRightColumnRef} pinnedRowCount={pinnedRowCount} pinnedRowRef={pinnedRowRef} - showLeftShadow={showLeftShadow} - showRightShadow={showRightShadow} - showTopShadow={showTopShadow} + scrollShadowStore={scrollShadowStore} tableConfig={tableConfigValue} totalColumnCount={totalColumnCount} totalRowCount={totalRowCount} diff --git a/src/renderer/components/item-list/item-table-list/table-scroll-shadow-store.ts b/src/renderer/components/item-list/item-table-list/table-scroll-shadow-store.ts new file mode 100644 index 000000000..324986385 --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/table-scroll-shadow-store.ts @@ -0,0 +1,36 @@ +export interface TableScrollShadowSnapshot { + showLeftShadow: boolean; + showRightShadow: boolean; + showTopShadow: boolean; +} + +export type TableScrollShadowStore = ReturnType; + +export function createTableScrollShadowStore() { + let snapshot: TableScrollShadowSnapshot = { + showLeftShadow: false, + showRightShadow: false, + showTopShadow: false, + }; + const listeners = new Set<() => void>(); + + return { + getSnapshot: (): TableScrollShadowSnapshot => snapshot, + setSnapshot: (patch: Partial) => { + 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); + }, + }; +}