@wallarm-org/design-system 1.28.1 → 1.28.2-rc-fix-table-window-scroll-root.2

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.
@@ -47,7 +47,7 @@ const TableBodyVirtualizedCore = ({ tbodyRef, virtualizer })=>{
47
47
  virtualRows.length > 0 && /*#__PURE__*/ jsx(Tr, {
48
48
  children: /*#__PURE__*/ jsx(Td, {
49
49
  style: {
50
- height: `${totalSize - (virtualRows[virtualRows.length - 1]?.end ?? 0)}px`,
50
+ height: `${totalSize + scrollMargin - (virtualRows[virtualRows.length - 1]?.end ?? 0)}px`,
51
51
  padding: 0,
52
52
  border: 'none'
53
53
  },
@@ -1,20 +1,39 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { useCallback, useEffect } from "react";
3
- import { useWindowVirtualizer } from "@tanstack/react-virtual";
4
- import { TABLE_VIRTUALIZATION_OVERSCAN, getRowKey } from "../lib/index.js";
2
+ import { useCallback, useContext, useEffect, useLayoutEffect, useRef } from "react";
3
+ import { elementScroll, observeElementOffset, observeElementRect, observeWindowOffset, observeWindowRect, useVirtualizer } from "@tanstack/react-virtual";
4
+ import { WindowScrollRootContext } from "../hooks/useWindowScrollRoot.js";
5
+ import { TABLE_VIRTUALIZATION_OVERSCAN, getOffsetTopInScrollRoot, getRowKey, getScrollMetrics } from "../lib/index.js";
5
6
  import { useTableContext } from "../TableContext/index.js";
6
- import { getDocumentOffsetTop } from "./lib/getDocumentOffsetTop.js";
7
7
  import { measureRowElement } from "./lib/measureRowElement.js";
8
8
  import { TableBodyVirtualizedCore } from "./TableBodyVirtualizedCore.js";
9
9
  import { useResetVirtualizerOnDataChange } from "./useResetVirtualizerOnDataChange.js";
10
10
  import { useSmoothScrollOnSort } from "./useSmoothScrollOnSort.js";
11
+ const isWindowRoot = (instance)=>instance.scrollElement === window;
12
+ const observeRootRect = (instance, cb)=>isWindowRoot(instance) ? observeWindowRect(instance, cb) : observeElementRect(instance, cb);
13
+ const observeRootOffset = (instance, cb)=>isWindowRoot(instance) ? observeWindowOffset(instance, cb) : observeElementOffset(instance, cb);
11
14
  const TableBodyVirtualizedWindow = ()=>{
12
15
  const { table, estimateRowHeight, overscan, tbodyRef, virtualizerRef } = useTableContext();
13
- const virtualizer = useWindowVirtualizer({
16
+ const scrollRoot = useContext(WindowScrollRootContext);
17
+ const scrollRootRef = useRef(scrollRoot);
18
+ scrollRootRef.current = scrollRoot;
19
+ const getScrollElement = useCallback(()=>scrollRootRef.current, []);
20
+ useLayoutEffect(()=>{
21
+ const instance = virtualizerRef.current;
22
+ if (instance && instance.scrollElement !== scrollRoot) instance.scrollOffset = null;
23
+ });
24
+ const virtualizer = useVirtualizer({
14
25
  count: table.getRowModel().rows.length,
26
+ getScrollElement: getScrollElement,
27
+ observeElementRect: observeRootRect,
28
+ observeElementOffset: observeRootOffset,
29
+ scrollToFn: elementScroll,
30
+ initialOffset: ()=>{
31
+ const root = getScrollElement() ?? ("u" < typeof window ? null : window);
32
+ return root ? getScrollMetrics(root).scrollTop : 0;
33
+ },
15
34
  estimateSize: estimateRowHeight ?? (()=>40),
16
35
  overscan: overscan ?? TABLE_VIRTUALIZATION_OVERSCAN,
17
- scrollMargin: tbodyRef.current ? getDocumentOffsetTop(tbodyRef.current) : 0,
36
+ scrollMargin: tbodyRef.current && scrollRoot ? getOffsetTopInScrollRoot(tbodyRef.current, scrollRoot) : 0,
18
37
  getItemKey: useCallback((index)=>getRowKey(table.getRowModel().rows, index), [
19
38
  table
20
39
  ]),
@@ -27,8 +46,7 @@ const TableBodyVirtualizedWindow = ()=>{
27
46
  virtualizerRef
28
47
  ]);
29
48
  useResetVirtualizerOnDataChange(table, virtualizer);
30
- const getScrollTarget = useCallback(()=>window, []);
31
- useSmoothScrollOnSort(table, getScrollTarget);
49
+ useSmoothScrollOnSort(table, getScrollElement);
32
50
  return /*#__PURE__*/ jsx(TableBodyVirtualizedCore, {
33
51
  tbodyRef: tbodyRef,
34
52
  virtualizer: virtualizer
@@ -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 { useInfiniteScroll, useWheelHorizontalScroll } from "../hooks/index.js";
5
+ import { WindowScrollRootContext, useInfiniteScroll, useWheelHorizontalScroll, useWindowScrollRoot } from "../hooks/index.js";
6
6
  import { useContainerWidth } from "../lib/index.js";
7
7
  import { StickyGroupParent } from "../StickyGroupParent.js";
8
8
  import { TableBody } from "../TableBody/index.js";
@@ -15,8 +15,10 @@ const TableInnerWindow = ({ isEmpty, showSettings, hasConsumerSettingsMenu, aria
15
15
  const testId = useTestId('window');
16
16
  const rootRef = useRef(null);
17
17
  const containerWidth = useContainerWidth(rootRef);
18
+ const scrollRoot = useWindowScrollRoot(containerRef);
18
19
  useInfiniteScroll({
19
20
  mode: 'window',
21
+ scrollRoot,
20
22
  table,
21
23
  virtualizerRef,
22
24
  tbodyRef,
@@ -102,7 +104,10 @@ const TableInnerWindow = ({ isEmpty, showSettings, hasConsumerSettingsMenu, aria
102
104
  /*#__PURE__*/ jsx(TableColGroup, {
103
105
  tableWidth: tableWidth
104
106
  }),
105
- !isEmpty && /*#__PURE__*/ jsx(TableBody, {})
107
+ !isEmpty && /*#__PURE__*/ jsx(WindowScrollRootContext, {
108
+ value: scrollRoot,
109
+ children: /*#__PURE__*/ jsx(TableBody, {})
110
+ })
106
111
  ]
107
112
  }),
108
113
  children
@@ -4,3 +4,4 @@ export { useMasterCell } from './useMasterCell';
4
4
  export { useStickyGroupParent } from './useStickyGroupParent';
5
5
  export { useTableState } from './useTableState';
6
6
  export { useWheelHorizontalScroll } from './useWheelHorizontalScroll';
7
+ export { useWindowScrollRoot, WindowScrollRootContext } from './useWindowScrollRoot';
@@ -4,3 +4,4 @@ export { useMasterCell } from "./useMasterCell.js";
4
4
  export { useStickyGroupParent } from "./useStickyGroupParent.js";
5
5
  export { useTableState } from "./useTableState.js";
6
6
  export { useWheelHorizontalScroll } from "./useWheelHorizontalScroll.js";
7
+ export { WindowScrollRootContext, useWindowScrollRoot } from "./useWindowScrollRoot.js";
@@ -1,11 +1,13 @@
1
1
  import type { RefObject } from 'react';
2
2
  import type { RowData, Table } from '@tanstack/react-table';
3
- import { type DSTableFeatures } from '../../lib';
3
+ import { type DSTableFeatures, type ScrollRoot } from '../../lib';
4
4
  import type { TableVirtualizerInstance } from '../../TableContext/types';
5
5
  interface UseInfiniteScrollOptions<T extends RowData> {
6
6
  mode: 'container' | 'window';
7
7
  /** Scroll element ref — required for `container` mode */
8
8
  scrollRef?: RefObject<HTMLElement | null>;
9
+ /** `window` mode: the resolved scroll root (see `useWindowScrollRoot`) */
10
+ scrollRoot?: ScrollRoot | null;
9
11
  table: Table<DSTableFeatures, T>;
10
12
  virtualizerRef: RefObject<TableVirtualizerInstance | null>;
11
13
  tbodyRef?: RefObject<HTMLTableSectionElement | null>;
@@ -18,5 +20,5 @@ interface UseInfiniteScrollOptions<T extends RowData> {
18
20
  isLoadingPrevious?: boolean;
19
21
  }
20
22
  /** Single entry point for bidirectional infinite scroll behavior. */
21
- export declare const useInfiniteScroll: <T extends RowData>({ mode, scrollRef, table, virtualizerRef, tbodyRef, onStartReached, onStartReachedThreshold, onEndReached, onEndReachedThreshold, initialScrollToRowId, isLoadingPrevious, }: UseInfiniteScrollOptions<T>) => void;
23
+ export declare const useInfiniteScroll: <T extends RowData>({ mode, scrollRef, scrollRoot, table, virtualizerRef, tbodyRef, onStartReached, onStartReachedThreshold, onEndReached, onEndReachedThreshold, initialScrollToRowId, isLoadingPrevious, }: UseInfiniteScrollOptions<T>) => void;
22
24
  export {};
@@ -2,7 +2,7 @@ import { TABLE_END_REACHED_THRESHOLD, TABLE_START_REACHED_THRESHOLD } from "../.
2
2
  import { useInitialAnchor } from "./useInitialAnchor.js";
3
3
  import { usePrependScrollAnchor } from "./usePrependScrollAnchor.js";
4
4
  import { useScrollEdge } from "./useScrollEdge.js";
5
- const useInfiniteScroll = ({ mode, scrollRef, table, virtualizerRef, tbodyRef, onStartReached, onStartReachedThreshold, onEndReached, onEndReachedThreshold, initialScrollToRowId, isLoadingPrevious })=>{
5
+ const useInfiniteScroll = ({ mode, scrollRef, scrollRoot, table, virtualizerRef, tbodyRef, onStartReached, onStartReachedThreshold, onEndReached, onEndReachedThreshold, initialScrollToRowId, isLoadingPrevious })=>{
6
6
  const rows = table.getRowModel().rows;
7
7
  const ready = useInitialAnchor({
8
8
  initialScrollToRowId,
@@ -12,6 +12,7 @@ const useInfiniteScroll = ({ mode, scrollRef, table, virtualizerRef, tbodyRef, o
12
12
  usePrependScrollAnchor({
13
13
  mode,
14
14
  scrollRef,
15
+ scrollRoot,
15
16
  rows,
16
17
  virtualizerRef,
17
18
  tbodyRef,
@@ -21,6 +22,7 @@ const useInfiniteScroll = ({ mode, scrollRef, table, virtualizerRef, tbodyRef, o
21
22
  edge: 'start',
22
23
  mode,
23
24
  scrollRef,
25
+ scrollRoot,
24
26
  onReached: onStartReached,
25
27
  threshold: onStartReachedThreshold ?? TABLE_START_REACHED_THRESHOLD,
26
28
  enabled: ready
@@ -29,6 +31,7 @@ const useInfiniteScroll = ({ mode, scrollRef, table, virtualizerRef, tbodyRef, o
29
31
  edge: 'end',
30
32
  mode,
31
33
  scrollRef,
34
+ scrollRoot,
32
35
  onReached: onEndReached,
33
36
  threshold: onEndReachedThreshold ?? TABLE_END_REACHED_THRESHOLD,
34
37
  enabled: ready
@@ -1,8 +1,11 @@
1
1
  import { type RefObject } from 'react';
2
+ import { type ScrollRoot } from '../../lib';
2
3
  import type { TableVirtualizerInstance } from '../../TableContext/types';
3
4
  interface UsePrependScrollAnchorOptions {
4
5
  mode: 'container' | 'window';
5
6
  scrollRef?: RefObject<HTMLElement | null>;
7
+ /** `window` mode: the resolved scroll root (see `useWindowScrollRoot`) */
8
+ scrollRoot?: ScrollRoot | null;
6
9
  rows: {
7
10
  id: string;
8
11
  }[];
@@ -31,5 +34,5 @@ interface UsePrependScrollAnchorOptions {
31
34
  * commit (hence no dep array) and its collapse joins the delta when the prepend
32
35
  * lands in the same commit.
33
36
  */
34
- export declare const usePrependScrollAnchor: ({ mode, scrollRef, rows, virtualizerRef, tbodyRef, isLoadingPrevious, }: UsePrependScrollAnchorOptions) => void;
37
+ export declare const usePrependScrollAnchor: ({ mode, scrollRef, scrollRoot, rows, virtualizerRef, tbodyRef, isLoadingPrevious, }: UsePrependScrollAnchorOptions) => void;
35
38
  export {};
@@ -1,6 +1,6 @@
1
1
  import { useLayoutEffect, useRef } from "react";
2
- import { detectDataChange } from "../../lib/index.js";
3
- const usePrependScrollAnchor = ({ mode, scrollRef, rows, virtualizerRef, tbodyRef, isLoadingPrevious })=>{
2
+ import { detectDataChange, getScrollMetrics, scrollRootBy } from "../../lib/index.js";
3
+ const usePrependScrollAnchor = ({ mode, scrollRef, scrollRoot, rows, virtualizerRef, tbodyRef, isLoadingPrevious })=>{
4
4
  const prevFirstRowIdRef = useRef(void 0);
5
5
  const prevLastRowIdRef = useRef(void 0);
6
6
  const prevIsLoadingPreviousRef = useRef(false);
@@ -12,7 +12,15 @@ const usePrependScrollAnchor = ({ mode, scrollRef, rows, virtualizerRef, tbodyRe
12
12
  });
13
13
  const seededRef = useRef(false);
14
14
  useLayoutEffect(()=>{
15
- const getScrollHeight = ()=>'window' === mode ? document.documentElement.scrollHeight : scrollRef?.current?.scrollHeight ?? 0;
15
+ const getRoot = ()=>'window' === mode ? scrollRoot : scrollRef?.current;
16
+ const getScrollHeight = ()=>{
17
+ const root = getRoot();
18
+ return root ? getScrollMetrics(root).scrollHeight : 0;
19
+ };
20
+ const scrollBy = (delta)=>{
21
+ const root = getRoot();
22
+ if (root) scrollRootBy(root, delta);
23
+ };
16
24
  const getStartLoaderHeight = ()=>{
17
25
  const loaderRows = tbodyRef?.current?.querySelectorAll('tr[data-loading-position="start"]');
18
26
  const count = loaderRows?.length ?? 0;
@@ -54,16 +62,10 @@ const usePrependScrollAnchor = ({ mode, scrollRef, rows, virtualizerRef, tbodyRe
54
62
  const rowDelta = newStart - prevFirstRowStartRef.current;
55
63
  const loaderDelta = getStartLoaderHeight() - prevLoaderHeight;
56
64
  const delta = rowDelta + loaderDelta;
57
- if (0 !== delta) {
58
- if ('window' === mode) window.scrollBy(0, delta);
59
- else if (scrollRef?.current) scrollRef.current.scrollTop += delta;
60
- }
65
+ if (0 !== delta) scrollBy(delta);
61
66
  } else if (null !== prevScrollHeightRef.current) {
62
67
  const delta = getScrollHeight() - prevScrollHeightRef.current;
63
- if (delta > 0) {
64
- if ('window' === mode) window.scrollBy(0, delta);
65
- else if (scrollRef?.current) scrollRef.current.scrollTop += delta;
66
- }
68
+ if (delta > 0) scrollBy(delta);
67
69
  }
68
70
  }
69
71
  prevFirstRowIdRef.current = rows[0]?.id;
@@ -1,4 +1,5 @@
1
1
  import { type RefObject } from 'react';
2
+ import { type ScrollRoot } from '../../lib';
2
3
  type ScrollMode = 'container' | 'window';
3
4
  type ScrollEdge = 'start' | 'end';
4
5
  interface UseScrollEdgeOptions {
@@ -10,11 +11,13 @@ interface UseScrollEdgeOptions {
10
11
  threshold: number;
11
12
  /** When false, suppresses firing (e.g. while the initial anchor scroll settles) */
12
13
  enabled?: boolean;
14
+ /** `window` mode: the resolved scroll root (see `useWindowScrollRoot`) */
15
+ scrollRoot?: ScrollRoot | null;
13
16
  }
14
17
  /**
15
18
  * Fires `onReached` once when the user scrolls within `threshold` px of the
16
19
  * given edge. Re-arms after scrolling back past the threshold. A cooldown
17
20
  * guard prevents rapid re-fires when prepended/appended rows grow the content.
18
21
  */
19
- export declare const useScrollEdge: ({ edge, mode, scrollRef, onReached, threshold, enabled, }: UseScrollEdgeOptions) => void;
22
+ export declare const useScrollEdge: ({ edge, mode, scrollRef, onReached, threshold, enabled, scrollRoot, }: UseScrollEdgeOptions) => void;
20
23
  export {};
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef } from "react";
2
- import { SCROLL_EDGE_COOLDOWN_MS } from "../../lib/index.js";
3
- const useScrollEdge = ({ edge, mode, scrollRef, onReached, threshold, enabled = true })=>{
2
+ import { SCROLL_EDGE_COOLDOWN_MS, getScrollMetrics } from "../../lib/index.js";
3
+ const useScrollEdge = ({ edge, mode, scrollRef, onReached, threshold, enabled = true, scrollRoot })=>{
4
4
  const firedRef = useRef(false);
5
5
  const lastFiredAtRef = useRef(0);
6
6
  const onReachedRef = useRef(onReached);
@@ -12,23 +12,12 @@ const useScrollEdge = ({ edge, mode, scrollRef, onReached, threshold, enabled =
12
12
  enabledRef.current = enabled;
13
13
  });
14
14
  useEffect(()=>{
15
+ const target = 'window' === mode ? scrollRoot : scrollRef?.current;
16
+ if (!target) return;
15
17
  const check = ()=>{
16
18
  const callback = onReachedRef.current;
17
19
  if (!callback || !enabledRef.current) return;
18
- let scrollTop;
19
- let clientHeight;
20
- let scrollHeight;
21
- if ('window' === mode) {
22
- scrollTop = window.scrollY;
23
- clientHeight = window.innerHeight;
24
- scrollHeight = document.documentElement.scrollHeight;
25
- } else {
26
- const el = scrollRef?.current;
27
- if (!el) return;
28
- scrollTop = el.scrollTop;
29
- clientHeight = el.clientHeight;
30
- scrollHeight = el.scrollHeight;
31
- }
20
+ const { scrollTop, clientHeight, scrollHeight } = getScrollMetrics(target);
32
21
  const distance = 'start' === edge ? scrollTop : scrollHeight - scrollTop - clientHeight;
33
22
  if (distance <= threshold) {
34
23
  const now = Date.now();
@@ -39,8 +28,6 @@ const useScrollEdge = ({ edge, mode, scrollRef, onReached, threshold, enabled =
39
28
  }
40
29
  } else firedRef.current = false;
41
30
  };
42
- const target = 'window' === mode ? window : scrollRef?.current;
43
- if (!target) return;
44
31
  target.addEventListener('scroll', check, {
45
32
  passive: true
46
33
  });
@@ -52,6 +39,7 @@ const useScrollEdge = ({ edge, mode, scrollRef, onReached, threshold, enabled =
52
39
  edge,
53
40
  mode,
54
41
  scrollRef,
42
+ scrollRoot,
55
43
  threshold
56
44
  ]);
57
45
  };
@@ -0,0 +1,14 @@
1
+ import { type RefObject } from 'react';
2
+ import { type ScrollRoot } from '../lib';
3
+ /** The `virtualized='window'` scroll root, shared from the table shell to its body. */
4
+ export declare const WindowScrollRootContext: import("react").Context<ScrollRoot | null>;
5
+ /**
6
+ * Resolves the scroll root of a `virtualized='window'` table once, for the
7
+ * virtualizer, the edge detection and the prepend compensation alike.
8
+ *
9
+ * Starts on the window. At mount the body has no rows yet, so a pane that will
10
+ * scroll does not overflow and stays on the window. While the root is the
11
+ * window, a scroll of any ancestor of the table re-resolves it: a pane the user
12
+ * scrolls overflows by then. An element root is final.
13
+ */
14
+ export declare const useWindowScrollRoot: (containerRef: RefObject<HTMLElement | null>) => ScrollRoot | null;
@@ -0,0 +1,35 @@
1
+ import { createContext, useLayoutEffect, useRef, useState } from "react";
2
+ import { getScrollRoot, isWindowScrollRoot } from "../lib/index.js";
3
+ const WindowScrollRootContext = createContext(null);
4
+ const useWindowScrollRoot = (containerRef)=>{
5
+ const [scrollRoot, setScrollRoot] = useState(()=>"u" < typeof window ? null : window);
6
+ const scrollRootRef = useRef(scrollRoot);
7
+ useLayoutEffect(()=>{
8
+ const container = containerRef.current;
9
+ if (!container) return;
10
+ const resolve = ()=>{
11
+ const next = getScrollRoot(container);
12
+ if (next === scrollRootRef.current) return;
13
+ scrollRootRef.current = next;
14
+ setScrollRoot(next);
15
+ };
16
+ resolve();
17
+ const onScroll = (event)=>{
18
+ const { target } = event;
19
+ if (scrollRootRef.current && !isWindowScrollRoot(scrollRootRef.current)) return;
20
+ if (!(target instanceof Element) || target === container || !target.contains(container)) return;
21
+ resolve();
22
+ };
23
+ document.addEventListener('scroll', onScroll, {
24
+ capture: true,
25
+ passive: true
26
+ });
27
+ return ()=>document.removeEventListener('scroll', onScroll, {
28
+ capture: true
29
+ });
30
+ }, [
31
+ containerRef
32
+ ]);
33
+ return scrollRoot;
34
+ };
35
+ export { WindowScrollRootContext, useWindowScrollRoot };
@@ -11,6 +11,7 @@ export { getDndStyles } from './getDndStyles';
11
11
  export { getPinningStyles } from './getPinningStyles';
12
12
  export { getRowKey } from './getRowKey';
13
13
  export { isLastPinnedLeft } from './isLastPinnedLeft';
14
+ export { getOffsetTopInScrollRoot, getScrollMetrics, getScrollRoot, isWindowScrollRoot, type ScrollRoot, scrollRootBy, } from './scrollRoot';
14
15
  export { useColumnDnd } from './useColumnDnd';
15
16
  export { useContainerWidth } from './useContainerWidth';
16
17
  export { useRowDnd } from './useRowDnd';
@@ -11,6 +11,7 @@ export { getDndStyles } from "./getDndStyles.js";
11
11
  export { getPinningStyles } from "./getPinningStyles.js";
12
12
  export { getRowKey } from "./getRowKey.js";
13
13
  export { isLastPinnedLeft } from "./isLastPinnedLeft.js";
14
+ export { getOffsetTopInScrollRoot, getScrollMetrics, getScrollRoot, isWindowScrollRoot, scrollRootBy } from "./scrollRoot.js";
14
15
  export { useColumnDnd } from "./useColumnDnd.js";
15
16
  export { useContainerWidth } from "./useContainerWidth.js";
16
17
  export { useRowDnd } from "./useRowDnd.js";
@@ -0,0 +1,24 @@
1
+ export type ScrollRoot = HTMLElement | Window;
2
+ export declare const isWindowScrollRoot: (root: ScrollRoot) => root is Window;
3
+ /**
4
+ * What a `virtualized='window'` table actually scrolls with: the nearest
5
+ * ancestor that scrolls vertically, else the window. An app shell that scrolls
6
+ * its content pane instead of the document (e.g. a micro-frontend host) keeps
7
+ * `window.scrollY` at 0 forever, so tracking the window there never virtualizes
8
+ * past the first screen and never reaches the end.
9
+ *
10
+ * `overflow-y` alone is not enough: a non-`visible` `overflow-x` (a plain
11
+ * `overflow-x-hidden` layout wrapper) computes `overflow-y: visible` to `auto`
12
+ * on a box that grows with its content and never scrolls. Only a box whose
13
+ * content overflows it counts — so a pane that does not overflow yet resolves
14
+ * to the window (`useWindowScrollRoot` re-resolves once an ancestor scrolls).
15
+ */
16
+ export declare const getScrollRoot: (el: Element | null) => ScrollRoot;
17
+ export declare const getScrollMetrics: (root: ScrollRoot) => {
18
+ scrollTop: number;
19
+ clientHeight: number;
20
+ scrollHeight: number;
21
+ };
22
+ export declare const scrollRootBy: (root: ScrollRoot, delta: number) => void;
23
+ /** `el`'s top in the root's scroll-content coordinates (the virtualizer's `scrollMargin`). */
24
+ export declare const getOffsetTopInScrollRoot: (el: HTMLElement, root: ScrollRoot) => number;
@@ -0,0 +1,22 @@
1
+ import { getDocumentOffsetTop } from "../TableBody/lib/getDocumentOffsetTop.js";
2
+ const SCROLLABLE_OVERFLOW = /(auto|scroll|overlay)/;
3
+ const isWindowScrollRoot = (root)=>root === window;
4
+ const getScrollRoot = (el)=>{
5
+ for(let node = el?.parentElement; node && node !== document.body; node = node.parentElement)if (SCROLLABLE_OVERFLOW.test(getComputedStyle(node).overflowY) && node.scrollHeight > node.clientHeight) return node;
6
+ return window;
7
+ };
8
+ const getScrollMetrics = (root)=>isWindowScrollRoot(root) ? {
9
+ scrollTop: root.scrollY,
10
+ clientHeight: root.innerHeight,
11
+ scrollHeight: document.documentElement.scrollHeight
12
+ } : {
13
+ scrollTop: root.scrollTop,
14
+ clientHeight: root.clientHeight,
15
+ scrollHeight: root.scrollHeight
16
+ };
17
+ const scrollRootBy = (root, delta)=>{
18
+ if (isWindowScrollRoot(root)) root.scrollBy(0, delta);
19
+ else root.scrollTop += delta;
20
+ };
21
+ const getOffsetTopInScrollRoot = (el, root)=>isWindowScrollRoot(root) ? getDocumentOffsetTop(el) : el.getBoundingClientRect().top - root.getBoundingClientRect().top - root.clientTop + root.scrollTop;
22
+ export { getOffsetTopInScrollRoot, getScrollMetrics, getScrollRoot, isWindowScrollRoot, scrollRootBy };
@@ -245,7 +245,10 @@ export interface TableProps<T> extends TestableProps {
245
245
  * the close edge here.
246
246
  */
247
247
  onSettingsOpenChange?: (open: boolean) => void;
248
- /** Enable row virtualization. `'container'` virtualizes within the scroll container; `'window'` virtualizes against the browser window. */
248
+ /**
249
+ * Enable row virtualization. `'container'` virtualizes within the table's own scroll container;
250
+ * `'window'` follows the page scroll — the nearest scrollable ancestor, or the browser window.
251
+ */
249
252
  virtualized?: TableVirtualized;
250
253
  /**
251
254
  * Whether columns stretch proportionally to fill the container width.
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.28.0",
3
- "generatedAt": "2026-09-30T12:59:20.784Z",
2
+ "version": "1.28.1",
3
+ "generatedAt": "2026-09-30T16:05:28.988Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
@@ -85754,7 +85754,7 @@
85754
85754
  "name": "virtualized",
85755
85755
  "type": "TableVirtualized | undefined",
85756
85756
  "required": false,
85757
- "description": "Enable row virtualization. `'container'` virtualizes within the scroll container; `'window'` virtualizes against the browser window."
85757
+ "description": "Enable row virtualization. `'container'` virtualizes within the table's own scroll container;\n`'window'` follows the page scroll — the nearest scrollable ancestor, or the browser window."
85758
85758
  },
85759
85759
  {
85760
85760
  "name": "stretch",
@@ -91921,6 +91921,11 @@
91921
91921
  "code": "() => {\n const {\n data,\n anchorId,\n isFetchingPrev,\n isFetchingNext,\n hasPrev,\n hasNext,\n fetchPrevPage,\n fetchNextPage,\n } = useBidirectionalData();\n\n return (\n <VStack gap={8}>\n <Text size='sm' color='secondary'>\n Window of {data.length} rows around the anchor\n {(isFetchingPrev || isFetchingNext) && ' — loading...'}\n {!hasPrev && ' — top reached'}\n {!hasNext && ' — bottom reached'}\n </Text>\n <Table\n data={data}\n columns={securityColumns}\n getRowId={row => row.id}\n virtualized='window'\n isLoading={isFetchingNext}\n isLoadingPrevious={isFetchingPrev}\n initialScrollToRowId={anchorId}\n onStartReached={fetchPrevPage}\n onStartReachedThreshold={200}\n onEndReached={fetchNextPage}\n onEndReachedThreshold={200}\n />\n </VStack>\n );\n}",
91922
91922
  "description": "The prepend case against the document scroll, where unrelated page content shares the height\nbeing measured — the regression this story exists to catch."
91923
91923
  },
91924
+ {
91925
+ "name": "InfiniteScrollWindowInPane",
91926
+ "code": "() => {\n const { data, isFetchingPrev, isFetchingNext, hasPrev, hasNext, fetchPrevPage, fetchNextPage } =\n useBidirectionalData();\n\n return (\n <div data-testid='scroll-pane' style={{ height: 480, overflowY: 'auto' }}>\n <div style={{ overflowX: 'hidden' }}>\n <VStack gap={8}>\n <Text size='sm' color='secondary'>\n Window of {data.length} rows around the anchor\n {(isFetchingPrev || isFetchingNext) && ' — loading...'}\n {!hasPrev && ' — top reached'}\n {!hasNext && ' — bottom reached'}\n </Text>\n <Table\n data={data}\n columns={securityColumns}\n getRowId={row => row.id}\n virtualized='window'\n isLoading={isFetchingNext}\n isLoadingPrevious={isFetchingPrev}\n onStartReached={fetchPrevPage}\n onStartReachedThreshold={200}\n onEndReached={fetchNextPage}\n onEndReachedThreshold={200}\n />\n </VStack>\n </div>\n </div>\n );\n}",
91927
+ "description": "`virtualized='window'` in an app shell that scrolls a content pane instead of the document: the\ntable follows the pane. The `overflow-x: hidden` wrapper in between never scrolls (its\n`overflow-y` only computes to `auto`), so it must not be taken for the scroll root."
91928
+ },
91924
91929
  {
91925
91930
  "name": "HeaderColumnDescription",
91926
91931
  "code": "() => {\n const [sorting, setSorting] = useState<TableSortingState>([]);\n\n const columns = useMemo<TableColumnDef<(typeof securityEvents)[number]>[]>(\n () =>\n securityColumns.map(col => {\n const key = 'accessorKey' in col ? col.accessorKey : undefined;\n if (key === 'objectName') {\n return {\n ...col,\n meta: {\n ...col.meta,\n description: { type: 'text' as const, content: 'Target resource' },\n },\n };\n }\n if (key === 'sourceIp') {\n return {\n ...col,\n meta: {\n ...col.meta,\n description: { type: 'tooltip' as const, content: 'Request origin IP' },\n },\n };\n }\n if (key === 'requests') {\n return {\n ...col,\n meta: { ...col.meta, description: { type: 'tooltip' as const, content: 'Total hits' } },\n };\n }\n if (key === 'parameter') {\n return {\n ...col,\n meta: {\n ...col.meta,\n description: { type: 'text' as const, content: 'Affected param' },\n },\n };\n }\n return col;\n }),\n [],\n );\n\n return (\n <Table\n data={securityEvents}\n columns={columns}\n getRowId={row => row.id}\n sorting={sorting}\n onSortingChange={setSorting}\n />\n );\n}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "1.28.1",
3
+ "version": "1.28.2-rc-fix-table-window-scroll-root.2",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",