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

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.
@@ -1,7 +1,8 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef } from "react";
2
+ import { useCallback, useContext, useEffect, useLayoutEffect, useRef } from "react";
3
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";
4
+ import { WindowScrollRootContext } from "../hooks/useWindowScrollRoot.js";
5
+ import { TABLE_VIRTUALIZATION_OVERSCAN, getOffsetTopInScrollRoot, getRowKey, getScrollMetrics, isWindowScrollRoot } from "../lib/index.js";
5
6
  import { useTableContext } from "../TableContext/index.js";
6
7
  import { measureRowElement } from "./lib/measureRowElement.js";
7
8
  import { TableBodyVirtualizedCore } from "./TableBodyVirtualizedCore.js";
@@ -11,24 +12,29 @@ const isWindowRoot = (instance)=>instance.scrollElement === window;
11
12
  const observeRootRect = (instance, cb)=>isWindowRoot(instance) ? observeWindowRect(instance, cb) : observeElementRect(instance, cb);
12
13
  const observeRootOffset = (instance, cb)=>isWindowRoot(instance) ? observeWindowOffset(instance, cb) : observeElementOffset(instance, cb);
13
14
  const TableBodyVirtualizedWindow = ()=>{
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();
15
+ const { table, estimateRowHeight, overscan, tbodyRef, virtualizerRef } = useTableContext();
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
+ });
25
24
  const virtualizer = useVirtualizer({
26
25
  count: table.getRowModel().rows.length,
27
26
  getScrollElement: getScrollElement,
28
27
  observeElementRect: observeRootRect,
29
28
  observeElementOffset: observeRootOffset,
30
29
  scrollToFn: elementScroll,
31
- initialOffset: ()=>getScrollMetrics(getScrollElement() ?? window).scrollTop,
30
+ initialRect: scrollRoot && isWindowScrollRoot(scrollRoot) ? {
31
+ width: scrollRoot.innerWidth,
32
+ height: scrollRoot.innerHeight
33
+ } : void 0,
34
+ initialOffset: ()=>{
35
+ const root = getScrollElement() ?? ("u" < typeof window ? null : window);
36
+ return root ? getScrollMetrics(root).scrollTop : 0;
37
+ },
32
38
  estimateSize: estimateRowHeight ?? (()=>40),
33
39
  overscan: overscan ?? TABLE_VIRTUALIZATION_OVERSCAN,
34
40
  scrollMargin: tbodyRef.current && scrollRoot ? getOffsetTopInScrollRoot(tbodyRef.current, scrollRoot) : 0,
@@ -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,9 +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, isSettled } = useWindowScrollRoot(containerRef, !isEmpty && table.getRowModel().rows.length > 0);
18
19
  useInfiniteScroll({
19
20
  mode: 'window',
20
- scrollRef: containerRef,
21
+ scrollRoot: isSettled ? scrollRoot : null,
21
22
  table,
22
23
  virtualizerRef,
23
24
  tbodyRef,
@@ -103,7 +104,10 @@ const TableInnerWindow = ({ isEmpty, showSettings, hasConsumerSettingsMenu, aria
103
104
  /*#__PURE__*/ jsx(TableColGroup, {
104
105
  tableWidth: tableWidth
105
106
  }),
106
- !isEmpty && /*#__PURE__*/ jsx(TableBody, {})
107
+ !isEmpty && /*#__PURE__*/ jsx(WindowScrollRootContext, {
108
+ value: scrollRoot,
109
+ children: /*#__PURE__*/ jsx(TableBody, {})
110
+ })
107
111
  ]
108
112
  }),
109
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
- /** `container`: the scroll element. `window`: an element to resolve the scroll root from. */
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,16 +2,18 @@ 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,
9
9
  rows,
10
- virtualizerRef
10
+ virtualizerRef,
11
+ enabled: 'window' !== mode || null != scrollRoot
11
12
  });
12
13
  usePrependScrollAnchor({
13
14
  mode,
14
15
  scrollRef,
16
+ scrollRoot,
15
17
  rows,
16
18
  virtualizerRef,
17
19
  tbodyRef,
@@ -21,6 +23,7 @@ const useInfiniteScroll = ({ mode, scrollRef, table, virtualizerRef, tbodyRef, o
21
23
  edge: 'start',
22
24
  mode,
23
25
  scrollRef,
26
+ scrollRoot,
24
27
  onReached: onStartReached,
25
28
  threshold: onStartReachedThreshold ?? TABLE_START_REACHED_THRESHOLD,
26
29
  enabled: ready
@@ -29,6 +32,7 @@ const useInfiniteScroll = ({ mode, scrollRef, table, virtualizerRef, tbodyRef, o
29
32
  edge: 'end',
30
33
  mode,
31
34
  scrollRef,
35
+ scrollRoot,
32
36
  onReached: onEndReached,
33
37
  threshold: onEndReachedThreshold ?? TABLE_END_REACHED_THRESHOLD,
34
38
  enabled: ready
@@ -6,11 +6,17 @@ interface UseInitialAnchorOptions {
6
6
  id: string;
7
7
  }[];
8
8
  virtualizerRef: RefObject<TableVirtualizerInstance | null>;
9
+ /**
10
+ * When false, holds the scroll. `window` mode waits for its scroll root to
11
+ * settle: in a pane host the root is still the window at mount, and an
12
+ * anchor scrolled there never reaches the pane.
13
+ */
14
+ enabled?: boolean;
9
15
  }
10
16
  /**
11
17
  * Scrolls to the anchor row once on mount and returns `ready`, which gates the
12
18
  * edge detectors until the initial scroll has settled. Without this, a table
13
19
  * mounted at scrollTop 0 would fire `onStartReached` immediately.
14
20
  */
15
- export declare const useInitialAnchor: ({ initialScrollToRowId, rows, virtualizerRef, }: UseInitialAnchorOptions) => boolean;
21
+ export declare const useInitialAnchor: ({ initialScrollToRowId, rows, virtualizerRef, enabled, }: UseInitialAnchorOptions) => boolean;
16
22
  export {};
@@ -1,9 +1,9 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
- const useInitialAnchor = ({ initialScrollToRowId, rows, virtualizerRef })=>{
2
+ const useInitialAnchor = ({ initialScrollToRowId, rows, virtualizerRef, enabled = true })=>{
3
3
  const [ready, setReady] = useState(!initialScrollToRowId);
4
4
  const doneRef = useRef(false);
5
5
  useEffect(()=>{
6
- if (doneRef.current || !initialScrollToRowId) return;
6
+ if (doneRef.current || !initialScrollToRowId || !enabled) return;
7
7
  const virtualizer = virtualizerRef.current;
8
8
  if (!virtualizer) {
9
9
  doneRef.current = true;
@@ -21,7 +21,8 @@ const useInitialAnchor = ({ initialScrollToRowId, rows, virtualizerRef })=>{
21
21
  }, [
22
22
  initialScrollToRowId,
23
23
  rows,
24
- virtualizerRef
24
+ virtualizerRef,
25
+ enabled
25
26
  ]);
26
27
  return ready;
27
28
  };
@@ -1,9 +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
- /** `container`: the scroll element. `window`: an element to resolve the scroll root from. */
6
6
  scrollRef?: RefObject<HTMLElement | null>;
7
+ /** `window` mode: the resolved scroll root (see `useWindowScrollRoot`) */
8
+ scrollRoot?: ScrollRoot | null;
7
9
  rows: {
8
10
  id: string;
9
11
  }[];
@@ -32,5 +34,5 @@ interface UsePrependScrollAnchorOptions {
32
34
  * commit (hence no dep array) and its collapse joins the delta when the prepend
33
35
  * lands in the same commit.
34
36
  */
35
- 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;
36
38
  export {};
@@ -1,6 +1,6 @@
1
1
  import { useLayoutEffect, useRef } from "react";
2
- import { detectDataChange, getScrollMetrics, getScrollRoot, scrollRootBy } 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,7 @@ const usePrependScrollAnchor = ({ mode, scrollRef, rows, virtualizerRef, tbodyRe
12
12
  });
13
13
  const seededRef = useRef(false);
14
14
  useLayoutEffect(()=>{
15
- const getRoot = ()=>'window' === mode ? getScrollRoot(scrollRef?.current ?? null) : scrollRef?.current;
15
+ const getRoot = ()=>'window' === mode ? scrollRoot : scrollRef?.current;
16
16
  const getScrollHeight = ()=>{
17
17
  const root = getRoot();
18
18
  return root ? getScrollMetrics(root).scrollHeight : 0;
@@ -1,23 +1,23 @@
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 {
5
6
  edge: ScrollEdge;
6
7
  mode: ScrollMode;
7
- /**
8
- * `container`: the scroll element. `window`: any element inside the table —
9
- * the scroll root is resolved from it (see `getScrollRoot`).
10
- */
8
+ /** Scroll element ref — required for `container` mode */
11
9
  scrollRef?: RefObject<HTMLElement | null>;
12
10
  onReached?: () => void;
13
11
  threshold: number;
14
12
  /** When false, suppresses firing (e.g. while the initial anchor scroll settles) */
15
13
  enabled?: boolean;
14
+ /** `window` mode: the resolved scroll root (see `useWindowScrollRoot`) */
15
+ scrollRoot?: ScrollRoot | null;
16
16
  }
17
17
  /**
18
18
  * Fires `onReached` once when the user scrolls within `threshold` px of the
19
19
  * given edge. Re-arms after scrolling back past the threshold. A cooldown
20
20
  * guard prevents rapid re-fires when prepended/appended rows grow the content.
21
21
  */
22
- 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;
23
23
  export {};
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef } from "react";
2
- import { SCROLL_EDGE_COOLDOWN_MS, getScrollMetrics, getScrollRoot } 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,8 +12,9 @@ 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;
15
+ const target = 'window' === mode ? scrollRoot : scrollRef?.current;
16
16
  if (!target) return;
17
+ firedRef.current = false;
17
18
  const check = ()=>{
18
19
  const callback = onReachedRef.current;
19
20
  if (!callback || !enabledRef.current) return;
@@ -39,6 +40,7 @@ const useScrollEdge = ({ edge, mode, scrollRef, onReached, threshold, enabled =
39
40
  edge,
40
41
  mode,
41
42
  scrollRef,
43
+ scrollRoot,
42
44
  threshold
43
45
  ]);
44
46
  };
@@ -0,0 +1,26 @@
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. Before the body is laid out with rows (loading, a
10
+ * hidden tab or drawer) a pane that will scroll does not overflow and reads as
11
+ * the window. While the root is the window, it re-resolves whenever the table
12
+ * resizes (rows, spacers, being shown) or an ancestor scrolls vertically. An
13
+ * element root is final.
14
+ *
15
+ * `scrollRoot` is for the virtualizer, from the first render. Edge detection,
16
+ * prepend compensation and the initial anchor wait for `isSettled`: an element
17
+ * root, or the window once the table is laid out with rows and still nothing
18
+ * above it overflows. Earlier, a pane host still reads as the window, whose
19
+ * edges both look reached — firing them would load a page at each end in one
20
+ * commit (which the prepend compensation cannot tell from a data swap) and
21
+ * spend the anchor on the window.
22
+ */
23
+ export declare const useWindowScrollRoot: (containerRef: RefObject<HTMLElement | null>, hasRows: boolean) => {
24
+ scrollRoot: ScrollRoot | null;
25
+ isSettled: boolean;
26
+ };
@@ -0,0 +1,62 @@
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, hasRows)=>{
5
+ const [scrollRoot, setScrollRoot] = useState(()=>"u" < typeof window ? null : window);
6
+ const scrollRootRef = useRef(scrollRoot);
7
+ const [isSettled, setIsSettled] = useState(false);
8
+ const hasRowsRef = useRef(hasRows);
9
+ hasRowsRef.current = hasRows;
10
+ const trySettleRef = useRef(null);
11
+ useLayoutEffect(()=>{
12
+ const container = containerRef.current;
13
+ if (!container || scrollRootRef.current && !isWindowScrollRoot(scrollRootRef.current)) return;
14
+ const isLaidOut = ()=>container.clientHeight > 0;
15
+ const onScroll = (event)=>{
16
+ const { target } = event;
17
+ if (target instanceof Element && target !== container && target.scrollTop > 0 && target.contains(container)) trySettle();
18
+ };
19
+ const resizeObserver = new ResizeObserver(()=>trySettle());
20
+ const detach = ()=>{
21
+ document.removeEventListener('scroll', onScroll, {
22
+ capture: true
23
+ });
24
+ resizeObserver.disconnect();
25
+ };
26
+ function resolve() {
27
+ if (scrollRootRef.current && !isWindowScrollRoot(scrollRootRef.current)) return;
28
+ const next = getScrollRoot(container);
29
+ if (next === scrollRootRef.current) return;
30
+ scrollRootRef.current = next;
31
+ setScrollRoot(next);
32
+ if (!isWindowScrollRoot(next)) {
33
+ detach();
34
+ setIsSettled(true);
35
+ }
36
+ }
37
+ function trySettle() {
38
+ resolve();
39
+ if (hasRowsRef.current && isLaidOut()) setIsSettled(true);
40
+ }
41
+ trySettleRef.current = trySettle;
42
+ document.addEventListener('scroll', onScroll, {
43
+ capture: true,
44
+ passive: true
45
+ });
46
+ resizeObserver.observe(container);
47
+ trySettle();
48
+ return detach;
49
+ }, [
50
+ containerRef
51
+ ]);
52
+ useLayoutEffect(()=>{
53
+ if (hasRows) trySettleRef.current?.();
54
+ }, [
55
+ hasRows
56
+ ]);
57
+ return {
58
+ scrollRoot,
59
+ isSettled
60
+ };
61
+ };
62
+ export { WindowScrollRootContext, useWindowScrollRoot };
@@ -2,10 +2,16 @@ export type ScrollRoot = HTMLElement | Window;
2
2
  export declare const isWindowScrollRoot: (root: ScrollRoot) => root is Window;
3
3
  /**
4
4
  * What a `virtualized='window'` table actually scrolls with: the nearest
5
- * vertically scrollable ancestor, else the window. An app shell that scrolls
5
+ * ancestor that scrolls vertically, else the window. An app shell that scrolls
6
6
  * its content pane instead of the document (e.g. a micro-frontend host) keeps
7
7
  * `window.scrollY` at 0 forever, so tracking the window there never virtualizes
8
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 as the table grows).
9
15
  */
10
16
  export declare const getScrollRoot: (el: Element | null) => ScrollRoot;
11
17
  export declare const getScrollMetrics: (root: ScrollRoot) => {
@@ -2,7 +2,7 @@ import { getDocumentOffsetTop } from "../TableBody/lib/getDocumentOffsetTop.js";
2
2
  const SCROLLABLE_OVERFLOW = /(auto|scroll|overlay)/;
3
3
  const isWindowScrollRoot = (root)=>root === window;
4
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;
5
+ for(let node = el?.parentElement; node && node !== document.documentElement; node = node.parentElement)if (SCROLLABLE_OVERFLOW.test(getComputedStyle(node).overflowY) && node.scrollHeight - node.clientHeight > 1) return node;
6
6
  return window;
7
7
  };
8
8
  const getScrollMetrics = (root)=>isWindowScrollRoot(root) ? {
@@ -18,5 +18,5 @@ const scrollRootBy = (root, delta)=>{
18
18
  if (isWindowScrollRoot(root)) root.scrollBy(0, delta);
19
19
  else root.scrollTop += delta;
20
20
  };
21
- const getOffsetTopInScrollRoot = (el, root)=>isWindowScrollRoot(root) ? getDocumentOffsetTop(el) : el.getBoundingClientRect().top - root.getBoundingClientRect().top + root.scrollTop;
21
+ const getOffsetTopInScrollRoot = (el, root)=>isWindowScrollRoot(root) ? getDocumentOffsetTop(el) : el.getBoundingClientRect().top - root.getBoundingClientRect().top - root.clientTop + root.scrollTop;
22
22
  export { getOffsetTopInScrollRoot, getScrollMetrics, getScrollRoot, isWindowScrollRoot, scrollRootBy };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "1.28.1",
3
- "generatedAt": "2026-09-30T15:40:03.193Z",
3
+ "generatedAt": "2026-10-01T11:03:35.647Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
@@ -91921,6 +91921,31 @@
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
+ },
91929
+ {
91930
+ "name": "InfiniteScrollWindowInPaneAnchored",
91931
+ "code": "() => {\n const { data, anchorId, isFetchingPrev, isFetchingNext, fetchPrevPage, fetchNextPage } =\n useBidirectionalData();\n\n return (\n <div data-testid='scroll-pane' style={{ height: 480, overflowY: 'auto' }}>\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 </div>\n );\n}",
91932
+ "description": "A share-link landing in a pane host: the window-mode table opens scrolled to its anchor row,\ninside the pane — the document stays put."
91933
+ },
91934
+ {
91935
+ "name": "InfiniteScrollWindowInPaneLateRows",
91936
+ "code": "() => {\n const { data, isFetchingPrev, isFetchingNext, fetchPrevPage, fetchNextPage } =\n useBidirectionalData();\n const [hasLoaded, setHasLoaded] = useState(false);\n const [calls, setCalls] = useState({ start: 0, end: 0 });\n useEffect(() => {\n const timer = setTimeout(() => setHasLoaded(true), 500);\n return () => clearTimeout(timer);\n }, []);\n\n return (\n <div data-testid='scroll-pane' style={{ height: 480, overflowY: 'auto' }}>\n <Text size='sm' color='secondary'>\n Edge calls: start {calls.start}, end {calls.end}\n </Text>\n <Table\n data={hasLoaded ? data : []}\n columns={securityColumns}\n getRowId={row => row.id}\n virtualized='window'\n isLoading={!hasLoaded || isFetchingNext}\n isLoadingPrevious={isFetchingPrev}\n // Nothing to page from before the first page — as with a real cursor.\n onStartReached={\n hasLoaded\n ? () => {\n setCalls(prev => ({ ...prev, start: prev.start + 1 }));\n fetchPrevPage();\n }\n : undefined\n }\n onStartReachedThreshold={200}\n onEndReached={\n hasLoaded\n ? () => {\n setCalls(prev => ({ ...prev, end: prev.end + 1 }));\n fetchNextPage();\n }\n : undefined\n }\n onEndReachedThreshold={200}\n />\n </div>\n );\n}",
91937
+ "description": "A pane host that mounts the table before its data: rows arrive after a delay. Neither edge may\nfire against the window meanwhile — only the edge the user scrolls to."
91938
+ },
91939
+ {
91940
+ "name": "InfiniteScrollWindowInHiddenPaneAnchored",
91941
+ "code": "() => {\n const [isShown, setIsShown] = useState(false);\n const { data, anchorId, isFetchingPrev, isFetchingNext, fetchPrevPage, fetchNextPage } =\n useBidirectionalData();\n\n return (\n <VStack gap={8}>\n <Button data-testid='show-table' onClick={() => setIsShown(true)}>\n Show table\n </Button>\n <div\n data-testid='scroll-pane'\n style={{ height: 480, overflowY: 'auto', display: isShown ? 'block' : 'none' }}\n >\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 </div>\n </VStack>\n );\n}",
91942
+ "description": "A share-link landing in a pane that starts hidden (a closed tab or drawer): the anchor waits\nuntil the table is shown and laid out, then lands inside the pane."
91943
+ },
91944
+ {
91945
+ "name": "InfiniteScrollWindowInScrolledPane",
91946
+ "code": "() => {\n const [isShown, setIsShown] = useState(false);\n const { data, isFetchingNext, fetchNextPage } = useBidirectionalData();\n\n return (\n <VStack gap={8}>\n <Button data-testid='show-table' onClick={() => setIsShown(true)}>\n Show table\n </Button>\n <div data-testid='scroll-pane' style={{ height: 480, overflowY: 'auto' }}>\n <div style={{ height: 800 }}>\n <Text size='sm' color='secondary'>\n Content above the table\n </Text>\n </div>\n {isShown && (\n <Table\n data={data}\n columns={securityColumns}\n getRowId={row => row.id}\n virtualized='window'\n isLoading={isFetchingNext}\n onEndReached={fetchNextPage}\n onEndReachedThreshold={200}\n />\n )}\n </div>\n </VStack>\n );\n}",
91947
+ "description": "A window-mode table mounting into a pane the user already scrolled (content above it): the pane\nkeeps its position — adopting the root must not reset it."
91948
+ },
91924
91949
  {
91925
91950
  "name": "HeaderColumnDescription",
91926
91951
  "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.2-rc-fix-table-window-scroll-root.1",
3
+ "version": "1.28.2-rc-fix-table-window-scroll-root.3",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",