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

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,37 @@
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, useEffect, useRef } from "react";
3
+ import { elementScroll, observeElementOffset, observeElementRect, observeWindowOffset, observeWindowRect, useVirtualizer } from "@tanstack/react-virtual";
4
+ import { TABLE_VIRTUALIZATION_OVERSCAN, getOffsetTopInScrollRoot, getRowKey, getScrollMetrics, getScrollRoot } from "../lib/index.js";
5
5
  import { useTableContext } from "../TableContext/index.js";
6
- import { getDocumentOffsetTop } from "./lib/getDocumentOffsetTop.js";
7
6
  import { measureRowElement } from "./lib/measureRowElement.js";
8
7
  import { TableBodyVirtualizedCore } from "./TableBodyVirtualizedCore.js";
9
8
  import { useResetVirtualizerOnDataChange } from "./useResetVirtualizerOnDataChange.js";
10
9
  import { useSmoothScrollOnSort } from "./useSmoothScrollOnSort.js";
10
+ const isWindowRoot = (instance)=>instance.scrollElement === window;
11
+ const observeRootRect = (instance, cb)=>isWindowRoot(instance) ? observeWindowRect(instance, cb) : observeElementRect(instance, cb);
12
+ const observeRootOffset = (instance, cb)=>isWindowRoot(instance) ? observeWindowOffset(instance, cb) : observeElementOffset(instance, cb);
11
13
  const TableBodyVirtualizedWindow = ()=>{
12
- const { table, estimateRowHeight, overscan, tbodyRef, virtualizerRef } = useTableContext();
13
- const virtualizer = useWindowVirtualizer({
14
+ const { table, estimateRowHeight, overscan, tbodyRef, virtualizerRef, containerRef } = useTableContext();
15
+ const scrollRootRef = useRef(null);
16
+ const getScrollElement = useCallback(()=>{
17
+ const anchor = tbodyRef.current ?? containerRef.current;
18
+ if (!scrollRootRef.current && anchor) scrollRootRef.current = getScrollRoot(anchor);
19
+ return scrollRootRef.current;
20
+ }, [
21
+ tbodyRef,
22
+ containerRef
23
+ ]);
24
+ const scrollRoot = getScrollElement();
25
+ const virtualizer = useVirtualizer({
14
26
  count: table.getRowModel().rows.length,
27
+ getScrollElement: getScrollElement,
28
+ observeElementRect: observeRootRect,
29
+ observeElementOffset: observeRootOffset,
30
+ scrollToFn: elementScroll,
31
+ initialOffset: ()=>getScrollMetrics(getScrollElement() ?? window).scrollTop,
15
32
  estimateSize: estimateRowHeight ?? (()=>40),
16
33
  overscan: overscan ?? TABLE_VIRTUALIZATION_OVERSCAN,
17
- scrollMargin: tbodyRef.current ? getDocumentOffsetTop(tbodyRef.current) : 0,
34
+ scrollMargin: tbodyRef.current && scrollRoot ? getOffsetTopInScrollRoot(tbodyRef.current, scrollRoot) : 0,
18
35
  getItemKey: useCallback((index)=>getRowKey(table.getRowModel().rows, index), [
19
36
  table
20
37
  ]),
@@ -27,8 +44,7 @@ const TableBodyVirtualizedWindow = ()=>{
27
44
  virtualizerRef
28
45
  ]);
29
46
  useResetVirtualizerOnDataChange(table, virtualizer);
30
- const getScrollTarget = useCallback(()=>window, []);
31
- useSmoothScrollOnSort(table, getScrollTarget);
47
+ useSmoothScrollOnSort(table, getScrollElement);
32
48
  return /*#__PURE__*/ jsx(TableBodyVirtualizedCore, {
33
49
  tbodyRef: tbodyRef,
34
50
  virtualizer: virtualizer
@@ -17,6 +17,7 @@ const TableInnerWindow = ({ isEmpty, showSettings, hasConsumerSettingsMenu, aria
17
17
  const containerWidth = useContainerWidth(rootRef);
18
18
  useInfiniteScroll({
19
19
  mode: 'window',
20
+ scrollRef: containerRef,
20
21
  table,
21
22
  virtualizerRef,
22
23
  tbodyRef,
@@ -4,7 +4,7 @@ import { type DSTableFeatures } from '../../lib';
4
4
  import type { TableVirtualizerInstance } from '../../TableContext/types';
5
5
  interface UseInfiniteScrollOptions<T extends RowData> {
6
6
  mode: 'container' | 'window';
7
- /** Scroll element ref — required for `container` mode */
7
+ /** `container`: the scroll element. `window`: an element to resolve the scroll root from. */
8
8
  scrollRef?: RefObject<HTMLElement | null>;
9
9
  table: Table<DSTableFeatures, T>;
10
10
  virtualizerRef: RefObject<TableVirtualizerInstance | null>;
@@ -2,6 +2,7 @@ import { type RefObject } from 'react';
2
2
  import type { TableVirtualizerInstance } from '../../TableContext/types';
3
3
  interface UsePrependScrollAnchorOptions {
4
4
  mode: 'container' | 'window';
5
+ /** `container`: the scroll element. `window`: an element to resolve the scroll root from. */
5
6
  scrollRef?: RefObject<HTMLElement | null>;
6
7
  rows: {
7
8
  id: string;
@@ -1,5 +1,5 @@
1
1
  import { useLayoutEffect, useRef } from "react";
2
- import { detectDataChange } from "../../lib/index.js";
2
+ import { detectDataChange, getScrollMetrics, getScrollRoot, scrollRootBy } from "../../lib/index.js";
3
3
  const usePrependScrollAnchor = ({ mode, scrollRef, rows, virtualizerRef, tbodyRef, isLoadingPrevious })=>{
4
4
  const prevFirstRowIdRef = useRef(void 0);
5
5
  const prevLastRowIdRef = useRef(void 0);
@@ -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 ? getScrollRoot(scrollRef?.current ?? null) : 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;
@@ -4,7 +4,10 @@ type ScrollEdge = 'start' | 'end';
4
4
  interface UseScrollEdgeOptions {
5
5
  edge: ScrollEdge;
6
6
  mode: ScrollMode;
7
- /** Scroll element ref — required for `container` mode */
7
+ /**
8
+ * `container`: the scroll element. `window`: any element inside the table —
9
+ * the scroll root is resolved from it (see `getScrollRoot`).
10
+ */
8
11
  scrollRef?: RefObject<HTMLElement | null>;
9
12
  onReached?: () => void;
10
13
  threshold: number;
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef } from "react";
2
- import { SCROLL_EDGE_COOLDOWN_MS } from "../../lib/index.js";
2
+ import { SCROLL_EDGE_COOLDOWN_MS, getScrollMetrics, getScrollRoot } from "../../lib/index.js";
3
3
  const useScrollEdge = ({ edge, mode, scrollRef, onReached, threshold, enabled = true })=>{
4
4
  const firedRef = useRef(false);
5
5
  const lastFiredAtRef = useRef(0);
@@ -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 ? getScrollRoot(scrollRef?.current ?? null) : 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
  });
@@ -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,18 @@
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
+ * vertically scrollable ancestor, 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
+ export declare const getScrollRoot: (el: Element | null) => ScrollRoot;
11
+ export declare const getScrollMetrics: (root: ScrollRoot) => {
12
+ scrollTop: number;
13
+ clientHeight: number;
14
+ scrollHeight: number;
15
+ };
16
+ export declare const scrollRootBy: (root: ScrollRoot, delta: number) => void;
17
+ /** `el`'s top in the root's scroll-content coordinates (the virtualizer's `scrollMargin`). */
18
+ 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)) 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.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-30T15:40:03.193Z",
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",
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.1",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",