@wallarm-org/design-system 0.52.0-rc-feature-shell.1 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/components/NavRail/NavRail.d.ts +0 -1
  2. package/dist/components/NavRail/NavRail.js +2 -2
  3. package/dist/components/NavRail/classes.js +1 -1
  4. package/dist/components/Table/TableBody/TableBody.js +6 -1
  5. package/dist/components/Table/TableBody/TableBodyVirtualizedContainer.js +7 -2
  6. package/dist/components/Table/TableBody/TableBodyVirtualizedCore.js +6 -1
  7. package/dist/components/Table/TableBody/TableBodyVirtualizedWindow.js +7 -2
  8. package/dist/components/Table/TableBody/lib/measureRowElement.d.ts +10 -0
  9. package/dist/components/Table/TableBody/lib/measureRowElement.js +7 -0
  10. package/dist/components/Table/TableBody/useResetVirtualizerOnDataChange.d.ts +3 -3
  11. package/dist/components/Table/TableBody/useResetVirtualizerOnDataChange.js +8 -6
  12. package/dist/components/Table/TableContext/TableProvider.js +9 -3
  13. package/dist/components/Table/TableContext/types.d.ts +4 -1
  14. package/dist/components/Table/TableInner/TableInnerContainer.js +10 -4
  15. package/dist/components/Table/TableInner/TableInnerWindow.js +10 -4
  16. package/dist/components/Table/TableLoadingState.d.ts +12 -1
  17. package/dist/components/Table/TableLoadingState.js +4 -3
  18. package/dist/components/Table/hooks/index.d.ts +1 -1
  19. package/dist/components/Table/hooks/index.js +2 -2
  20. package/dist/components/Table/hooks/infiniteScroll/index.d.ts +1 -0
  21. package/dist/components/Table/hooks/infiniteScroll/index.js +2 -0
  22. package/dist/components/Table/hooks/infiniteScroll/useInfiniteScroll.d.ts +19 -0
  23. package/dist/components/Table/hooks/infiniteScroll/useInfiniteScroll.js +36 -0
  24. package/dist/components/Table/hooks/infiniteScroll/useInitialAnchor.d.ts +16 -0
  25. package/dist/components/Table/hooks/infiniteScroll/useInitialAnchor.js +28 -0
  26. package/dist/components/Table/hooks/infiniteScroll/usePrependScrollAnchor.d.ts +27 -0
  27. package/dist/components/Table/hooks/infiniteScroll/usePrependScrollAnchor.js +70 -0
  28. package/dist/components/Table/hooks/infiniteScroll/useScrollEdge.d.ts +20 -0
  29. package/dist/components/Table/hooks/{useEndReached.js → infiniteScroll/useScrollEdge.js} +15 -11
  30. package/dist/components/Table/lib/constants.d.ts +5 -0
  31. package/dist/components/Table/lib/constants.js +4 -1
  32. package/dist/components/Table/lib/detectDataChange.d.ts +4 -0
  33. package/dist/components/Table/lib/detectDataChange.js +7 -0
  34. package/dist/components/Table/lib/getRowKey.d.ts +4 -0
  35. package/dist/components/Table/lib/getRowKey.js +2 -0
  36. package/dist/components/Table/lib/index.d.ts +3 -1
  37. package/dist/components/Table/lib/index.js +4 -2
  38. package/dist/components/Table/mocks.d.ts +14 -0
  39. package/dist/components/Table/mocks.js +61 -1
  40. package/dist/components/Table/types.d.ts +17 -1
  41. package/dist/components/TopHeader/TopHeader.d.ts +0 -1
  42. package/dist/components/TopHeader/TopHeader.js +2 -2
  43. package/dist/hooks/useOverflowItems.d.ts +6 -0
  44. package/dist/hooks/useOverflowItems.js +44 -14
  45. package/dist/hooks/useOverflowItems.scheduler.d.ts +16 -0
  46. package/dist/hooks/useOverflowItems.scheduler.js +25 -0
  47. package/dist/metadata/components.json +22 -12
  48. package/package.json +1 -1
  49. package/dist/components/Table/hooks/useEndReached.d.ts +0 -19
@@ -4,6 +4,5 @@ export interface NavRailProps extends HTMLAttributes<HTMLElement>, TestableProps
4
4
  ref?: Ref<HTMLElement>;
5
5
  children?: ReactNode;
6
6
  collapsed?: boolean;
7
- appeared?: boolean;
8
7
  }
9
8
  export declare const NavRail: FC<NavRailProps>;
@@ -6,7 +6,7 @@ import { cn } from "../../utils/cn.js";
6
6
  import { TestIdProvider } from "../../utils/testId.js";
7
7
  import { navRailVariants } from "./classes.js";
8
8
  import { NavRailContextProvider } from "./NavRailContext.js";
9
- const NavRail = ({ ref, collapsed = false, appeared, className, children, 'data-testid': testId, ...props })=>{
9
+ const NavRail = ({ ref, collapsed = false, className, children, 'data-testid': testId, ...props })=>{
10
10
  const internalRef = useRef(null);
11
11
  const focusPanel = useCallback(()=>{
12
12
  const panel = document.querySelector('[data-slot="nav-panel"]');
@@ -47,7 +47,7 @@ const NavRail = ({ ref, collapsed = false, appeared, className, children, 'data-
47
47
  "data-testid": testId,
48
48
  className: cn(navRailVariants({
49
49
  collapsed
50
- }), false === appeared && 'opacity-0', className),
50
+ }), className),
51
51
  children: children
52
52
  })
53
53
  })
@@ -1,5 +1,5 @@
1
1
  import { cva } from "class-variance-authority";
2
- const navRailVariants = cva('flex h-full shrink-0 flex-col overflow-hidden px-8 pt-6 pb-12 transition-[width] duration-200 ease-in-out transition-[width,opacity] duration-200 ease-in-out', {
2
+ const navRailVariants = cva('flex h-full shrink-0 flex-col overflow-hidden px-8 pt-6 pb-12 transition-[width] duration-200 ease-in-out', {
3
3
  variants: {
4
4
  collapsed: {
5
5
  true: 'w-[48px]',
@@ -1,5 +1,6 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useTestId } from "../../../utils/testId.js";
3
+ import { TABLE_PREPEND_SKELETON_ROWS } from "../lib/index.js";
3
4
  import { TBody } from "../primitives/index.js";
4
5
  import { useTableContext } from "../TableContext/index.js";
5
6
  import { TableLoadingState } from "../TableLoadingState.js";
@@ -7,7 +8,7 @@ import { TableRow } from "../TableRow.js";
7
8
  import { TableBodyVirtualizedContainer } from "./TableBodyVirtualizedContainer.js";
8
9
  import { TableBodyVirtualizedWindow } from "./TableBodyVirtualizedWindow.js";
9
10
  const TableBody = ()=>{
10
- const { table, isLoading, virtualized, tbodyRef, virtualizerRef } = useTableContext();
11
+ const { table, isLoading, isLoadingPrevious, virtualized, tbodyRef, virtualizerRef } = useTableContext();
11
12
  const testId = useTestId('body');
12
13
  const rows = table.getRowModel().rows;
13
14
  const hasData = rows.length > 0;
@@ -20,6 +21,10 @@ const TableBody = ()=>{
20
21
  ref: tbodyRef,
21
22
  "data-testid": testId,
22
23
  children: [
24
+ isLoadingPrevious && /*#__PURE__*/ jsx(TableLoadingState, {
25
+ position: "start",
26
+ count: TABLE_PREPEND_SKELETON_ROWS
27
+ }),
23
28
  rows.map((row)=>/*#__PURE__*/ jsx(TableRow, {
24
29
  row: row
25
30
  }, row.id)),
@@ -1,8 +1,9 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect } from "react";
3
3
  import { useVirtualizer } from "@tanstack/react-virtual";
4
- import { TABLE_VIRTUALIZATION_OVERSCAN } from "../lib/index.js";
4
+ import { TABLE_VIRTUALIZATION_OVERSCAN, getRowKey } from "../lib/index.js";
5
5
  import { useTableContext } from "../TableContext/index.js";
6
+ import { measureRowElement } from "./lib/measureRowElement.js";
6
7
  import { TableBodyVirtualizedCore } from "./TableBodyVirtualizedCore.js";
7
8
  import { useResetVirtualizerOnDataChange } from "./useResetVirtualizerOnDataChange.js";
8
9
  import { useSmoothScrollOnSort } from "./useSmoothScrollOnSort.js";
@@ -15,7 +16,11 @@ const TableBodyVirtualizedContainer = ()=>{
15
16
  count: table.getRowModel().rows.length,
16
17
  getScrollElement,
17
18
  estimateSize: estimateRowHeight ?? (()=>40),
18
- overscan: overscan ?? TABLE_VIRTUALIZATION_OVERSCAN
19
+ overscan: overscan ?? TABLE_VIRTUALIZATION_OVERSCAN,
20
+ getItemKey: useCallback((index)=>getRowKey(table.getRowModel().rows, index), [
21
+ table
22
+ ]),
23
+ measureElement: measureRowElement
19
24
  });
20
25
  virtualizerRef.current = virtualizer;
21
26
  useEffect(()=>()=>{
@@ -1,11 +1,12 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useTestId } from "../../../utils/testId.js";
3
+ import { TABLE_PREPEND_SKELETON_ROWS } from "../lib/index.js";
3
4
  import { TBody, Td, Tr } from "../primitives/index.js";
4
5
  import { useTableContext } from "../TableContext/index.js";
5
6
  import { TableLoadingState } from "../TableLoadingState.js";
6
7
  import { TableRow } from "../TableRow.js";
7
8
  const TableBodyVirtualizedCore = ({ tbodyRef, virtualizer })=>{
8
- const { table, isLoading } = useTableContext();
9
+ const { table, isLoading, isLoadingPrevious } = useTableContext();
9
10
  const testId = useTestId('body');
10
11
  const virtualRows = virtualizer.getVirtualItems();
11
12
  const totalSize = virtualizer.getTotalSize();
@@ -17,6 +18,10 @@ const TableBodyVirtualizedCore = ({ tbodyRef, virtualizer })=>{
17
18
  ref: tbodyRef,
18
19
  "data-testid": testId,
19
20
  children: [
21
+ isLoadingPrevious && /*#__PURE__*/ jsx(TableLoadingState, {
22
+ position: "start",
23
+ count: TABLE_PREPEND_SKELETON_ROWS
24
+ }),
20
25
  virtualRows.length > 0 && /*#__PURE__*/ jsx(Tr, {
21
26
  children: /*#__PURE__*/ jsx(Td, {
22
27
  style: {
@@ -1,9 +1,10 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect } from "react";
3
3
  import { useWindowVirtualizer } from "@tanstack/react-virtual";
4
- import { TABLE_VIRTUALIZATION_OVERSCAN } from "../lib/index.js";
4
+ import { TABLE_VIRTUALIZATION_OVERSCAN, getRowKey } from "../lib/index.js";
5
5
  import { useTableContext } from "../TableContext/index.js";
6
6
  import { getDocumentOffsetTop } from "./lib/getDocumentOffsetTop.js";
7
+ import { measureRowElement } from "./lib/measureRowElement.js";
7
8
  import { TableBodyVirtualizedCore } from "./TableBodyVirtualizedCore.js";
8
9
  import { useResetVirtualizerOnDataChange } from "./useResetVirtualizerOnDataChange.js";
9
10
  import { useSmoothScrollOnSort } from "./useSmoothScrollOnSort.js";
@@ -13,7 +14,11 @@ const TableBodyVirtualizedWindow = ()=>{
13
14
  count: table.getRowModel().rows.length,
14
15
  estimateSize: estimateRowHeight ?? (()=>40),
15
16
  overscan: overscan ?? TABLE_VIRTUALIZATION_OVERSCAN,
16
- scrollMargin: tbodyRef.current ? getDocumentOffsetTop(tbodyRef.current) : 0
17
+ scrollMargin: tbodyRef.current ? getDocumentOffsetTop(tbodyRef.current) : 0,
18
+ getItemKey: useCallback((index)=>getRowKey(table.getRowModel().rows, index), [
19
+ table
20
+ ]),
21
+ measureElement: measureRowElement
17
22
  });
18
23
  virtualizerRef.current = virtualizer;
19
24
  useEffect(()=>()=>{
@@ -0,0 +1,10 @@
1
+ import { type Virtualizer } from '@tanstack/react-virtual';
2
+ /**
3
+ * Row-measurement for the table virtualizers. TanStack's default reads
4
+ * `offsetHeight` when called without a ResizeObserver entry — i.e. on row
5
+ * mount mid-commit, forcing a reflow per new row while scrolling. That read is
6
+ * redundant: `observe()` delivers an initial `borderBoxSize` entry the same
7
+ * frame, post-layout. So entry-less we return the assumed size (cache/estimate)
8
+ * — a no-op — and let the observer entry supply the real one.
9
+ */
10
+ export declare const measureRowElement: <TScrollElement extends Element | Window>(element: Element, entry: ResizeObserverEntry | undefined, instance: Virtualizer<TScrollElement, Element>) => number;
@@ -0,0 +1,7 @@
1
+ import { measureElement } from "@tanstack/react-virtual";
2
+ const measureRowElement = (element, entry, instance)=>{
3
+ if (entry) return measureElement(element, entry, instance);
4
+ const index = instance.indexFromElement(element);
5
+ return instance.measurementsCache[index]?.size ?? instance.options.estimateSize(index);
6
+ };
7
+ export { measureRowElement };
@@ -1,8 +1,8 @@
1
1
  import type { Table } from '@tanstack/react-table';
2
2
  import type { Virtualizer } from '@tanstack/react-virtual';
3
3
  /**
4
- * Reset cached measurements when the data set changes so the virtualizer
5
- * does not retain stale heights from a previous data set.
6
- * Tracks first row ID to distinguish "new data" from "appended rows" (infinite scroll).
4
+ * Reset cached measurements only on a full dataset replacement. On a prepend
5
+ * (infinite scroll up) measurements are kept usePrependScrollAnchor handles
6
+ * the position so the virtualizer does not re-measure and jump.
7
7
  */
8
8
  export declare const useResetVirtualizerOnDataChange: (table: Table<unknown>, virtualizer: Virtualizer<Element, Element> | Virtualizer<Window, Element> | Virtualizer<HTMLElement, Element>) => void;
@@ -1,15 +1,17 @@
1
1
  import { useEffect, useRef } from "react";
2
+ import { detectDataChange } from "../lib/index.js";
2
3
  const useResetVirtualizerOnDataChange = (table, virtualizer)=>{
3
- const rows = table.getRowModel().rows;
4
- const firstRowId = rows[0]?.id;
4
+ const firstRowId = table.getRowModel().rows[0]?.id;
5
5
  const prevFirstRowIdRef = useRef(firstRowId);
6
6
  useEffect(()=>{
7
- if (prevFirstRowIdRef.current !== firstRowId) {
8
- prevFirstRowIdRef.current = firstRowId;
9
- virtualizer.measure();
10
- }
7
+ if (prevFirstRowIdRef.current === firstRowId) return;
8
+ const rows = table.getRowModel().rows;
9
+ const change = detectDataChange(prevFirstRowIdRef.current, rows);
10
+ prevFirstRowIdRef.current = firstRowId;
11
+ if ('replace' === change) virtualizer.measure();
11
12
  }, [
12
13
  firstRowId,
14
+ table,
13
15
  virtualizer
14
16
  ]);
15
17
  };
@@ -8,7 +8,7 @@ import { useTableState } from "../hooks/index.js";
8
8
  import { TABLE_EXPAND_COLUMN_ID, TABLE_MIN_COLUMN_WIDTH, TABLE_SELECT_COLUMN_ID, TABLE_SKELETON_ROWS, TABLE_VIRTUALIZATION_OVERSCAN, createExpandColumn, createSelectionColumn } from "../lib/index.js";
9
9
  import { TableContext } from "./TableContext.js";
10
10
  const TableProvider = (props)=>{
11
- const { data, columns, isLoading = false, skeletonCount = TABLE_SKELETON_ROWS, children, getRowId, sorting: sortingProp, onSortingChange, manualSorting = false, rowSelection: rowSelectionProp, onRowSelectionChange, columnSizing: columnSizingProp, onColumnSizingChange, columnPinning: columnPinningProp, onColumnPinningChange, columnOrder: columnOrderProp, onColumnOrderChange, grouping: groupingProp, onGroupingChange, expanded: expandedProp, onExpandedChange, renderGroupRow, getSubRows, renderExpandedRow, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, defaultColumnVisibility, defaultColumnOrder, virtualized, estimateRowHeight, overscan = TABLE_VIRTUALIZATION_OVERSCAN, onEndReached, onEndReachedThreshold, onMasterCellClick, activeRowId: activeRowIdProp } = props;
11
+ const { data, columns, isLoading = false, isLoadingPrevious = false, skeletonCount = TABLE_SKELETON_ROWS, children, getRowId, sorting: sortingProp, onSortingChange, manualSorting = false, rowSelection: rowSelectionProp, onRowSelectionChange, columnSizing: columnSizingProp, onColumnSizingChange, columnPinning: columnPinningProp, onColumnPinningChange, columnOrder: columnOrderProp, onColumnOrderChange, grouping: groupingProp, onGroupingChange, expanded: expandedProp, onExpandedChange, renderGroupRow, getSubRows, renderExpandedRow, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, defaultColumnVisibility, defaultColumnOrder, virtualized, estimateRowHeight, overscan = TABLE_VIRTUALIZATION_OVERSCAN, onEndReached, onEndReachedThreshold, onStartReached, onStartReachedThreshold, initialScrollToRowId, onMasterCellClick, activeRowId: activeRowIdProp } = props;
12
12
  const masterCellActiveRowId = activeRowIdProp ?? null;
13
13
  const sortingEnabled = !!onSortingChange;
14
14
  const selectionEnabled = !!onRowSelectionChange;
@@ -182,13 +182,13 @@ const TableProvider = (props)=>{
182
182
  });
183
183
  const allLeafColumns = table.getAllLeafColumns();
184
184
  const lastSelectedRowIndexRef = useRef(null);
185
- const theadRef = useRef(null);
186
185
  const containerRef = useRef(null);
187
186
  const tbodyRef = useRef(null);
188
187
  const virtualizerRef = useRef(null);
189
188
  const contextValue = useMemo(()=>({
190
189
  table,
191
190
  isLoading,
191
+ isLoadingPrevious,
192
192
  skeletonCount,
193
193
  sortingEnabled,
194
194
  selectionEnabled,
@@ -209,17 +209,20 @@ const TableProvider = (props)=>{
209
209
  alwaysPinnedLeft,
210
210
  masterColumnId,
211
211
  lastSelectedRowIndexRef,
212
- theadRef,
213
212
  containerRef,
214
213
  tbodyRef,
215
214
  virtualizerRef,
216
215
  onEndReached,
217
216
  onEndReachedThreshold,
217
+ onStartReached,
218
+ onStartReachedThreshold,
219
+ initialScrollToRowId,
218
220
  onMasterCellClick,
219
221
  activeRowId: masterCellActiveRowId
220
222
  }), [
221
223
  table,
222
224
  isLoading,
225
+ isLoadingPrevious,
223
226
  skeletonCount,
224
227
  sortingEnabled,
225
228
  selectionEnabled,
@@ -241,6 +244,9 @@ const TableProvider = (props)=>{
241
244
  masterColumnId,
242
245
  onEndReached,
243
246
  onEndReachedThreshold,
247
+ onStartReached,
248
+ onStartReachedThreshold,
249
+ initialScrollToRowId,
244
250
  masterCellActiveRowId,
245
251
  onMasterCellClick
246
252
  ]);
@@ -11,6 +11,7 @@ export type TableVirtualizerInstance = Virtualizer<Window, Element> | Virtualize
11
11
  export interface TableContextValue<T> {
12
12
  table: TanStackTable<T>;
13
13
  isLoading: boolean;
14
+ isLoadingPrevious: boolean;
14
15
  skeletonCount: number;
15
16
  sortingEnabled: boolean;
16
17
  selectionEnabled: boolean;
@@ -31,12 +32,14 @@ export interface TableContextValue<T> {
31
32
  lastSelectedRowIndexRef: RefObject<number | null>;
32
33
  alwaysPinnedLeft: string[];
33
34
  masterColumnId: string | null;
34
- theadRef: RefObject<HTMLTableSectionElement | null>;
35
35
  containerRef: RefObject<HTMLDivElement | null>;
36
36
  tbodyRef: RefObject<HTMLTableSectionElement | null>;
37
37
  virtualizerRef: RefObject<TableVirtualizerInstance | null>;
38
38
  onEndReached?: () => void;
39
39
  onEndReachedThreshold?: number;
40
+ onStartReached?: () => void;
41
+ onStartReachedThreshold?: number;
42
+ initialScrollToRowId?: string;
40
43
  onMasterCellClick?: (rowId: string) => void;
41
44
  activeRowId: string | null;
42
45
  }
@@ -4,7 +4,7 @@ import { cn } from "../../../utils/cn.js";
4
4
  import { useTestId } from "../../../utils/testId.js";
5
5
  import { ScrollArea, ScrollAreaCorner, ScrollAreaScrollbar, ScrollAreaViewport } from "../../ScrollArea/index.js";
6
6
  import { tableContainerVariants } from "../classes.js";
7
- import { useEndReached } from "../hooks/index.js";
7
+ import { useInfiniteScroll } from "../hooks/index.js";
8
8
  import { useContainerWidth } from "../lib/index.js";
9
9
  import { TableBody } from "../TableBody/index.js";
10
10
  import { TableColGroup } from "../TableColGroup.js";
@@ -12,15 +12,21 @@ import { useTableContext } from "../TableContext/index.js";
12
12
  import { TableHead } from "../TableHead.js";
13
13
  import { TableSettingsMenu } from "../TableSettingsMenu/index.js";
14
14
  const TableInnerContainer = ({ isEmpty, virtualized, showSettings, ariaLabel, children })=>{
15
- const { containerRef, table, onEndReached, onEndReachedThreshold } = useTableContext();
15
+ const { containerRef, table, virtualizerRef, tbodyRef, onEndReached, onEndReachedThreshold, onStartReached, onStartReachedThreshold, initialScrollToRowId } = useTableContext();
16
16
  const testId = useTestId('container');
17
17
  const scrollRootRef = useRef(null);
18
18
  const containerWidth = useContainerWidth(containerRef);
19
- useEndReached({
19
+ useInfiniteScroll({
20
20
  mode: 'container',
21
21
  scrollRef: containerRef,
22
+ table,
23
+ virtualizerRef,
24
+ tbodyRef,
22
25
  onEndReached,
23
- threshold: onEndReachedThreshold
26
+ onEndReachedThreshold,
27
+ onStartReached,
28
+ onStartReachedThreshold,
29
+ initialScrollToRowId
24
30
  });
25
31
  useEffect(()=>{
26
32
  const viewport = containerRef.current;
@@ -2,7 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from "react";
3
3
  import { useTestId } from "../../../utils/testId.js";
4
4
  import { ScrollArea, ScrollAreaScrollbar, ScrollAreaViewport } from "../../ScrollArea/index.js";
5
- import { useEndReached } from "../hooks/index.js";
5
+ import { useInfiniteScroll } from "../hooks/index.js";
6
6
  import { useContainerWidth } from "../lib/index.js";
7
7
  import { TableBody } from "../TableBody/index.js";
8
8
  import { TableColGroup } from "../TableColGroup.js";
@@ -10,15 +10,21 @@ import { useTableContext } from "../TableContext/index.js";
10
10
  import { TableHead } from "../TableHead.js";
11
11
  import { TableSettingsMenu } from "../TableSettingsMenu/index.js";
12
12
  const TableInnerWindow = ({ isEmpty, showSettings, ariaLabel, children })=>{
13
- const { table, onEndReached, onEndReachedThreshold } = useTableContext();
13
+ const { table, virtualizerRef, tbodyRef, onEndReached, onEndReachedThreshold, onStartReached, onStartReachedThreshold, initialScrollToRowId } = useTableContext();
14
14
  const testId = useTestId('window');
15
15
  const rootRef = useRef(null);
16
16
  const scrollRef = useRef(null);
17
17
  const containerWidth = useContainerWidth(rootRef);
18
- useEndReached({
18
+ useInfiniteScroll({
19
19
  mode: 'window',
20
+ table,
21
+ virtualizerRef,
22
+ tbodyRef,
20
23
  onEndReached,
21
- threshold: onEndReachedThreshold
24
+ onEndReachedThreshold,
25
+ onStartReached,
26
+ onStartReachedThreshold,
27
+ initialScrollToRowId
22
28
  });
23
29
  useEffect(()=>{
24
30
  const scrollEl = scrollRef.current;
@@ -1,2 +1,13 @@
1
1
  import type { FC } from 'react';
2
- export declare const TableLoadingState: FC;
2
+ interface TableLoadingStateProps {
3
+ /**
4
+ * Edge the skeletons sit at. 'end' (default) is the bottom loader; 'start'
5
+ * is the prepend loader above the rows — own test id + a data attribute the
6
+ * scroll compensation measures.
7
+ */
8
+ position?: 'start' | 'end';
9
+ /** Number of skeleton rows; defaults to the table-level skeletonCount. */
10
+ count?: number;
11
+ }
12
+ export declare const TableLoadingState: FC<TableLoadingStateProps>;
13
+ export {};
@@ -3,17 +3,18 @@ import { useTestId } from "../../utils/testId.js";
3
3
  import { Skeleton } from "../Skeleton/index.js";
4
4
  import { Td, Tr } from "./primitives/index.js";
5
5
  import { useTableContext } from "./TableContext/index.js";
6
- const TableLoadingState = ()=>{
6
+ const TableLoadingState = ({ position = 'end', count })=>{
7
7
  const { table, skeletonCount } = useTableContext();
8
- const testId = useTestId('loading');
8
+ const testId = useTestId('start' === position ? 'loading-start' : 'loading');
9
9
  const columns = table.getVisibleLeafColumns();
10
10
  return /*#__PURE__*/ jsx(Fragment, {
11
11
  children: Array.from({
12
- length: skeletonCount
12
+ length: count ?? skeletonCount
13
13
  }, (_, rowIdx)=>{
14
14
  const key = `skeleton-${rowIdx}`;
15
15
  return /*#__PURE__*/ jsx(Tr, {
16
16
  "data-testid": 0 === rowIdx ? testId : void 0,
17
+ "data-loading-position": position,
17
18
  children: columns.map((column)=>/*#__PURE__*/ jsx(Td, {
18
19
  className: "px-16 py-8 border-b border-r border-border-primary-light",
19
20
  style: {
@@ -1,4 +1,4 @@
1
- export { useEndReached } from './useEndReached';
1
+ export { useInfiniteScroll } from './infiniteScroll';
2
2
  export { useHorizontalScrollState } from './useHorizontalScrollState';
3
3
  export { useMasterCell } from './useMasterCell';
4
4
  export { useTableState } from './useTableState';
@@ -1,5 +1,5 @@
1
- import { useEndReached } from "./useEndReached.js";
1
+ import { useInfiniteScroll } from "./infiniteScroll/index.js";
2
2
  import { useHorizontalScrollState } from "./useHorizontalScrollState.js";
3
3
  import { useMasterCell } from "./useMasterCell.js";
4
4
  import { useTableState } from "./useTableState.js";
5
- export { useEndReached, useHorizontalScrollState, useMasterCell, useTableState };
5
+ export { useHorizontalScrollState, useInfiniteScroll, useMasterCell, useTableState };
@@ -0,0 +1 @@
1
+ export { useInfiniteScroll } from './useInfiniteScroll';
@@ -0,0 +1,2 @@
1
+ import { useInfiniteScroll } from "./useInfiniteScroll.js";
2
+ export { useInfiniteScroll };
@@ -0,0 +1,19 @@
1
+ import type { RefObject } from 'react';
2
+ import type { Table } from '@tanstack/react-table';
3
+ import type { TableVirtualizerInstance } from '../../TableContext/types';
4
+ interface UseInfiniteScrollOptions<T> {
5
+ mode: 'container' | 'window';
6
+ /** Scroll element ref — required for `container` mode */
7
+ scrollRef?: RefObject<HTMLElement | null>;
8
+ table: Table<T>;
9
+ virtualizerRef: RefObject<TableVirtualizerInstance | null>;
10
+ tbodyRef?: RefObject<HTMLTableSectionElement | null>;
11
+ onStartReached?: () => void;
12
+ onStartReachedThreshold?: number;
13
+ onEndReached?: () => void;
14
+ onEndReachedThreshold?: number;
15
+ initialScrollToRowId?: string;
16
+ }
17
+ /** Single entry point for bidirectional infinite scroll behavior. */
18
+ export declare const useInfiniteScroll: <T>({ mode, scrollRef, table, virtualizerRef, tbodyRef, onStartReached, onStartReachedThreshold, onEndReached, onEndReachedThreshold, initialScrollToRowId, }: UseInfiniteScrollOptions<T>) => void;
19
+ export {};
@@ -0,0 +1,36 @@
1
+ import { TABLE_END_REACHED_THRESHOLD, TABLE_START_REACHED_THRESHOLD } from "../../lib/index.js";
2
+ import { useInitialAnchor } from "./useInitialAnchor.js";
3
+ import { usePrependScrollAnchor } from "./usePrependScrollAnchor.js";
4
+ import { useScrollEdge } from "./useScrollEdge.js";
5
+ const useInfiniteScroll = ({ mode, scrollRef, table, virtualizerRef, tbodyRef, onStartReached, onStartReachedThreshold, onEndReached, onEndReachedThreshold, initialScrollToRowId })=>{
6
+ const rows = table.getRowModel().rows;
7
+ const ready = useInitialAnchor({
8
+ initialScrollToRowId,
9
+ rows,
10
+ virtualizerRef
11
+ });
12
+ usePrependScrollAnchor({
13
+ mode,
14
+ scrollRef,
15
+ rows,
16
+ virtualizerRef,
17
+ tbodyRef
18
+ });
19
+ useScrollEdge({
20
+ edge: 'start',
21
+ mode,
22
+ scrollRef,
23
+ onReached: onStartReached,
24
+ threshold: onStartReachedThreshold ?? TABLE_START_REACHED_THRESHOLD,
25
+ enabled: ready
26
+ });
27
+ useScrollEdge({
28
+ edge: 'end',
29
+ mode,
30
+ scrollRef,
31
+ onReached: onEndReached,
32
+ threshold: onEndReachedThreshold ?? TABLE_END_REACHED_THRESHOLD,
33
+ enabled: ready
34
+ });
35
+ };
36
+ export { useInfiniteScroll };
@@ -0,0 +1,16 @@
1
+ import { type RefObject } from 'react';
2
+ import type { TableVirtualizerInstance } from '../../TableContext/types';
3
+ interface UseInitialAnchorOptions {
4
+ initialScrollToRowId?: string;
5
+ rows: {
6
+ id: string;
7
+ }[];
8
+ virtualizerRef: RefObject<TableVirtualizerInstance | null>;
9
+ }
10
+ /**
11
+ * Scrolls to the anchor row once on mount and returns `ready`, which gates the
12
+ * edge detectors until the initial scroll has settled. Without this, a table
13
+ * mounted at scrollTop 0 would fire `onStartReached` immediately.
14
+ */
15
+ export declare const useInitialAnchor: ({ initialScrollToRowId, rows, virtualizerRef, }: UseInitialAnchorOptions) => boolean;
16
+ export {};
@@ -0,0 +1,28 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ const useInitialAnchor = ({ initialScrollToRowId, rows, virtualizerRef })=>{
3
+ const [ready, setReady] = useState(!initialScrollToRowId);
4
+ const doneRef = useRef(false);
5
+ useEffect(()=>{
6
+ if (doneRef.current || !initialScrollToRowId) return;
7
+ const virtualizer = virtualizerRef.current;
8
+ if (!virtualizer) {
9
+ doneRef.current = true;
10
+ setReady(true);
11
+ return;
12
+ }
13
+ const index = rows.findIndex((r)=>r.id === initialScrollToRowId);
14
+ if (index < 0) return;
15
+ virtualizer.scrollToIndex(index, {
16
+ align: 'center'
17
+ });
18
+ doneRef.current = true;
19
+ const raf = requestAnimationFrame(()=>setReady(true));
20
+ return ()=>cancelAnimationFrame(raf);
21
+ }, [
22
+ initialScrollToRowId,
23
+ rows,
24
+ virtualizerRef
25
+ ]);
26
+ return ready;
27
+ };
28
+ export { useInitialAnchor };
@@ -0,0 +1,27 @@
1
+ import { type RefObject } from 'react';
2
+ import type { TableVirtualizerInstance } from '../../TableContext/types';
3
+ interface UsePrependScrollAnchorOptions {
4
+ mode: 'container' | 'window';
5
+ scrollRef?: RefObject<HTMLElement | null>;
6
+ rows: {
7
+ id: string;
8
+ }[];
9
+ /** Preferred delta source: virtual-list offsets are immune to unrelated layout growth. */
10
+ virtualizerRef?: RefObject<TableVirtualizerInstance | null>;
11
+ /** Scopes the start-loader measurement to this table's body. */
12
+ tbodyRef?: RefObject<HTMLTableSectionElement | null>;
13
+ }
14
+ /**
15
+ * Keeps the viewport stable across a prepend, in a layout effect (pre-paint, no
16
+ * flicker). Delta = how far the previously-first row moved down the virtual list
17
+ * (`start` offset) — immune to unrelated page-height changes, unlike a
18
+ * scrollHeight diff, which matters in `window` mode. Falls back to scrollHeight
19
+ * when there's no virtualizer.
20
+ *
21
+ * Start-edge skeletons (`isLoadingPrevious`) sit above the rows but outside the
22
+ * virtual list, so the offset delta can't see them; their height is tracked per
23
+ * commit (hence no dep array) and its collapse joins the delta when the prepend
24
+ * lands in the same commit.
25
+ */
26
+ export declare const usePrependScrollAnchor: ({ mode, scrollRef, rows, virtualizerRef, tbodyRef, }: UsePrependScrollAnchorOptions) => void;
27
+ export {};
@@ -0,0 +1,70 @@
1
+ import { useLayoutEffect, useRef } from "react";
2
+ import { detectDataChange } from "../../lib/index.js";
3
+ const usePrependScrollAnchor = ({ mode, scrollRef, rows, virtualizerRef, tbodyRef })=>{
4
+ const prevFirstRowIdRef = useRef(void 0);
5
+ const prevFirstRowStartRef = useRef(null);
6
+ const prevScrollHeightRef = useRef(null);
7
+ const prevLoaderRef = useRef({
8
+ count: 0,
9
+ height: 0
10
+ });
11
+ const seededRef = useRef(false);
12
+ useLayoutEffect(()=>{
13
+ const getScrollHeight = ()=>'window' === mode ? document.documentElement.scrollHeight : scrollRef?.current?.scrollHeight ?? 0;
14
+ const getStartLoaderHeight = ()=>{
15
+ const loaderRows = tbodyRef?.current?.querySelectorAll('tr[data-loading-position="start"]');
16
+ const count = loaderRows?.length ?? 0;
17
+ if (count !== prevLoaderRef.current.count) {
18
+ let height = 0;
19
+ loaderRows?.forEach((row)=>{
20
+ height += row.offsetHeight;
21
+ });
22
+ prevLoaderRef.current = {
23
+ count,
24
+ height
25
+ };
26
+ }
27
+ return prevLoaderRef.current.height;
28
+ };
29
+ const getFirstRowStart = ()=>{
30
+ const start = virtualizerRef?.current?.measurementsCache[0]?.start;
31
+ return 'number' == typeof start ? start : null;
32
+ };
33
+ const trackScrollHeight = !virtualizerRef?.current;
34
+ if (!seededRef.current) {
35
+ seededRef.current = true;
36
+ prevScrollHeightRef.current = trackScrollHeight ? getScrollHeight() : null;
37
+ prevFirstRowStartRef.current = getFirstRowStart();
38
+ prevFirstRowIdRef.current = rows[0]?.id;
39
+ getStartLoaderHeight();
40
+ return;
41
+ }
42
+ const prevLoaderHeight = prevLoaderRef.current.height;
43
+ if ('prepend' === detectDataChange(prevFirstRowIdRef.current, rows)) {
44
+ const prevId = prevFirstRowIdRef.current;
45
+ const index = rows.findIndex((row)=>row.id === prevId);
46
+ const newStart = virtualizerRef?.current?.measurementsCache[index]?.start;
47
+ const useVirtualDelta = index >= 0 && 'number' == typeof newStart && null !== prevFirstRowStartRef.current;
48
+ if (useVirtualDelta) {
49
+ const rowDelta = newStart - prevFirstRowStartRef.current;
50
+ const loaderDelta = getStartLoaderHeight() - prevLoaderHeight;
51
+ const delta = rowDelta + loaderDelta;
52
+ if (0 !== delta) {
53
+ if ('window' === mode) window.scrollBy(0, delta);
54
+ else if (scrollRef?.current) scrollRef.current.scrollTop += delta;
55
+ }
56
+ } else if (null !== prevScrollHeightRef.current) {
57
+ const delta = getScrollHeight() - prevScrollHeightRef.current;
58
+ if (delta > 0) {
59
+ if ('window' === mode) window.scrollBy(0, delta);
60
+ else if (scrollRef?.current) scrollRef.current.scrollTop += delta;
61
+ }
62
+ }
63
+ }
64
+ prevFirstRowIdRef.current = rows[0]?.id;
65
+ prevFirstRowStartRef.current = getFirstRowStart();
66
+ prevScrollHeightRef.current = trackScrollHeight ? getScrollHeight() : null;
67
+ getStartLoaderHeight();
68
+ });
69
+ };
70
+ export { usePrependScrollAnchor };
@@ -0,0 +1,20 @@
1
+ import { type RefObject } from 'react';
2
+ type ScrollMode = 'container' | 'window';
3
+ type ScrollEdge = 'start' | 'end';
4
+ interface UseScrollEdgeOptions {
5
+ edge: ScrollEdge;
6
+ mode: ScrollMode;
7
+ /** Scroll element ref — required for `container` mode */
8
+ scrollRef?: RefObject<HTMLElement | null>;
9
+ onReached?: () => void;
10
+ threshold: number;
11
+ /** When false, suppresses firing (e.g. while the initial anchor scroll settles) */
12
+ enabled?: boolean;
13
+ }
14
+ /**
15
+ * Fires `onReached` once when the user scrolls within `threshold` px of the
16
+ * given edge. Re-arms after scrolling back past the threshold. A cooldown
17
+ * guard prevents rapid re-fires when prepended/appended rows grow the content.
18
+ */
19
+ export declare const useScrollEdge: ({ edge, mode, scrollRef, onReached, threshold, enabled, }: UseScrollEdgeOptions) => void;
20
+ export {};