@wallarm-org/design-system 1.28.0 → 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.
Files changed (52) hide show
  1. package/dist/components/RemoteShell/NavPanelContent.d.ts +5 -0
  2. package/dist/components/RemoteShell/NavPanelContent.js +6 -4
  3. package/dist/components/RemoteShell/RemoteShell.d.ts +30 -1
  4. package/dist/components/RemoteShell/RemoteShell.js +35 -18
  5. package/dist/components/RemoteShell/RemoteShellPanel.js +10 -4
  6. package/dist/components/RemoteShell/model/index.d.ts +1 -1
  7. package/dist/components/RemoteShell/model/index.js +1 -1
  8. package/dist/components/RemoteShell/model/useDrillTransition.d.ts +12 -1
  9. package/dist/components/RemoteShell/model/useDrillTransition.js +24 -12
  10. package/dist/components/RemoteShell/model/useLocationPathname.d.ts +2 -0
  11. package/dist/components/RemoteShell/model/useLocationPathname.js +4 -1
  12. package/dist/components/Table/TableBody/TableBodyVirtualizedCore.js +1 -1
  13. package/dist/components/Table/TableBody/TableBodyVirtualizedWindow.js +25 -9
  14. package/dist/components/Table/TableInner/TableInnerWindow.js +1 -0
  15. package/dist/components/Table/hooks/infiniteScroll/useInfiniteScroll.d.ts +1 -1
  16. package/dist/components/Table/hooks/infiniteScroll/usePrependScrollAnchor.d.ts +1 -0
  17. package/dist/components/Table/hooks/infiniteScroll/usePrependScrollAnchor.js +12 -10
  18. package/dist/components/Table/hooks/infiniteScroll/useScrollEdge.d.ts +4 -1
  19. package/dist/components/Table/hooks/infiniteScroll/useScrollEdge.js +4 -17
  20. package/dist/components/Table/lib/index.d.ts +1 -0
  21. package/dist/components/Table/lib/index.js +1 -0
  22. package/dist/components/Table/lib/scrollRoot.d.ts +18 -0
  23. package/dist/components/Table/lib/scrollRoot.js +22 -0
  24. package/dist/components/Table/types.d.ts +4 -1
  25. package/dist/metadata/components.json +26 -8
  26. package/package.json +2 -1
  27. package/dist/components/AppShell/story-content/_storyAccountDropdown.d.ts +0 -11
  28. package/dist/components/AppShell/story-content/_storyAccountDropdown.js +0 -242
  29. package/dist/components/AppShell/story-content/_storyConfigRenderer.d.ts +0 -12
  30. package/dist/components/AppShell/story-content/_storyConfigRenderer.js +0 -81
  31. package/dist/components/AppShell/story-content/_storyHeaderActions.d.ts +0 -1
  32. package/dist/components/AppShell/story-content/_storyHeaderActions.js +0 -59
  33. package/dist/components/AppShell/story-content/_storyHomeContent.d.ts +0 -2
  34. package/dist/components/AppShell/story-content/_storyHomeContent.js +0 -15
  35. package/dist/components/AppShell/story-content/_storyLib.d.ts +0 -9
  36. package/dist/components/AppShell/story-content/_storyLib.js +0 -39
  37. package/dist/components/AppShell/story-content/_storyNavConfigs.d.ts +0 -6
  38. package/dist/components/AppShell/story-content/_storyNavConfigs.js +0 -772
  39. package/dist/components/AppShell/story-content/_storyNavRailFooter.d.ts +0 -13
  40. package/dist/components/AppShell/story-content/_storyNavRailFooter.js +0 -26
  41. package/dist/components/AppShell/story-content/_storyProductNavItems.d.ts +0 -7
  42. package/dist/components/AppShell/story-content/_storyProductNavItems.js +0 -49
  43. package/dist/components/AppShell/story-content/_storyQuickHelpDropdown.d.ts +0 -1
  44. package/dist/components/AppShell/story-content/_storyQuickHelpDropdown.js +0 -144
  45. package/dist/components/AppShell/story-content/_storyRecentDropdown.d.ts +0 -1
  46. package/dist/components/AppShell/story-content/_storyRecentDropdown.js +0 -89
  47. package/dist/components/AppShell/story-content/_storySearchModal.d.ts +0 -1
  48. package/dist/components/AppShell/story-content/_storySearchModal.js +0 -252
  49. package/dist/components/AppShell/story-content/_storyWallarmLogo.d.ts +0 -1
  50. package/dist/components/AppShell/story-content/_storyWallarmLogo.js +0 -21
  51. package/dist/components/AppShell/story-content/index.d.ts +0 -11
  52. package/dist/components/AppShell/story-content/index.js +0 -11
@@ -1,6 +1,11 @@
1
1
  import type { FC } from 'react';
2
+ import type { NavStackEntry } from './model';
3
+ interface NavigationSnapshot {
4
+ navStack: NavStackEntry[];
5
+ }
2
6
  interface NavPanelContentProps {
3
7
  level: number;
8
+ navigationSnapshot?: NavigationSnapshot;
4
9
  }
5
10
  export declare const NavPanelContent: FC<NavPanelContentProps>;
6
11
  export {};
@@ -4,10 +4,12 @@ import { Text } from "../Text/index.js";
4
4
  import { HeaderActions } from "./HeaderActions.js";
5
5
  import { findDrillNode, useRemoteShellContext } from "./model/index.js";
6
6
  import { NavItemsList } from "./NavItemsList.js";
7
- const NavPanelContent = ({ level: rawLevel })=>{
8
- const { config, navStack, effectiveActiveItemId, navigate, drillInto, goBack } = useRemoteShellContext();
7
+ const NavPanelContent = ({ level: rawLevel, navigationSnapshot })=>{
8
+ const { config, navStack: currentNavStack, navigate, drillInto, goBack } = useRemoteShellContext();
9
+ const navStack = navigationSnapshot?.navStack ?? currentNavStack;
9
10
  const level = Math.min(rawLevel, navStack.length - 1);
10
11
  const entry = navStack[level];
12
+ const activeItemId = entry.activeItemId;
11
13
  const hasHeaderActions = !!config.headerActions?.length;
12
14
  if (0 === level) return /*#__PURE__*/ jsxs(Fragment, {
13
15
  children: [
@@ -28,7 +30,7 @@ const NavPanelContent = ({ level: rawLevel })=>{
28
30
  }),
29
31
  /*#__PURE__*/ jsx(NavItemsList, {
30
32
  items: config.items,
31
- activeItemId: effectiveActiveItemId,
33
+ activeItemId: activeItemId,
32
34
  onNavigate: navigate,
33
35
  onDrillClick: drillInto
34
36
  })
@@ -54,7 +56,7 @@ const NavPanelContent = ({ level: rawLevel })=>{
54
56
  }),
55
57
  /*#__PURE__*/ jsx(NavItemsList, {
56
58
  items: entry.items,
57
- activeItemId: effectiveActiveItemId,
59
+ activeItemId: activeItemId,
58
60
  onNavigate: navigate,
59
61
  onDrillClick: drillInto
60
62
  })
@@ -8,7 +8,36 @@ export interface RemoteShellProps extends HTMLAttributes<HTMLDivElement>, Testab
8
8
  config: NavConfig;
9
9
  /** URL prefix stripped before matching and prepended when navigating (e.g. `"/edge"`). */
10
10
  basePath?: string;
11
- /** Custom navigation handler for router integration (React Router, Next.js, etc.). */
11
+ /**
12
+ * Current pathname. When set, it is the single source of truth for the active nav
13
+ * item, drill level and breadcrumbs, and `window.location` is ignored.
14
+ *
15
+ * Pass it — **together with `onNavigate`** — whenever a router owns the URL: router
16
+ * navigations (links in page content, `router.navigate`, memory history, the host's
17
+ * rail) do not fire `popstate`, so without it the shell can go stale. Without
18
+ * `onNavigate` the shell's own navigations would only reach `window.history`, which
19
+ * a controlled shell ignores (a dev warning flags this).
20
+ *
21
+ * Either form is accepted: with the `basePath` prefix (what `onNavigate` receives) or
22
+ * without it — the prefix is stripped only when the pathname starts with a whole
23
+ * `basePath` segment. With TanStack Router, `useRouterState({ select: s =>
24
+ * s.location.pathname })` is router-relative: it excludes the router's own
25
+ * `basepath`. So when the router has `basepath`:
26
+ * - omit `basePath` here — `pathname` and `onNavigate` are then both router-relative
27
+ * and `onNavigate={to => navigate({ to })}` is correct; or
28
+ * - keep `basePath` and strip it in `onNavigate`
29
+ * (`to => navigate({ to: to.slice(basePath.length) || '/' })`), otherwise the
30
+ * router prefixes it a second time.
31
+ *
32
+ * Omit it to read `window.location.pathname` (updated on `popstate` and on the
33
+ * shell's own navigations).
34
+ */
35
+ pathname?: string;
36
+ /**
37
+ * Navigation handler for router integration (TanStack Router, React Router,
38
+ * Next.js, …). Receives the target pathname, prefixed with `basePath` when one is
39
+ * set. Without it the shell pushes to `window.history` itself.
40
+ */
12
41
  onNavigate?: (pathname: string) => void;
13
42
  }
14
43
  export declare const RemoteShell: FC<RemoteShellProps>;
@@ -2,14 +2,30 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useState } from "react";
3
3
  import { cn } from "../../utils/cn.js";
4
4
  import { TestIdProvider } from "../../utils/testId.js";
5
- import { RemoteShellContextProvider, findFirstLinkPath, matchNav, pushPathname, useLocationPathname } from "./model/index.js";
6
- const RemoteShell = ({ ref, className, children, config, basePath, onNavigate, 'data-testid': testId, ...props })=>{
7
- const fullPathname = useLocationPathname();
8
- const pathname = basePath && fullPathname.startsWith(basePath) ? fullPathname.slice(basePath.length) || '/' : fullPathname;
5
+ import { RemoteShellContextProvider, findFirstLinkPath, matchNav, notifyPathnameChanged, pushPathname, useLocationPathname } from "./model/index.js";
6
+ const stripBasePath = (fullPathname, basePath)=>{
7
+ if (!basePath) return fullPathname;
8
+ if (fullPathname === basePath) return '/';
9
+ return fullPathname.startsWith(`${basePath}/`) ? fullPathname.slice(basePath.length) : fullPathname;
10
+ };
11
+ const RemoteShell = ({ ref, className, children, config, basePath, onNavigate, pathname: controlledPathname, 'data-testid': testId, ...props })=>{
12
+ const locationPathname = useLocationPathname();
13
+ const fullPathname = controlledPathname ?? locationPathname;
14
+ const pathname = stripBasePath(fullPathname, basePath);
15
+ const isControlled = void 0 !== controlledPathname;
16
+ const hasOnNavigate = void 0 !== onNavigate;
17
+ useEffect(()=>{
18
+ if (isControlled && !hasOnNavigate && 'production' !== process.env.NODE_ENV) console.warn('[RemoteShell] `pathname` is controlled but `onNavigate` is missing: shell navigations will update `window.history` only, and the shell will keep showing the old route. Pass `onNavigate` to route them through your router.');
19
+ }, [
20
+ isControlled,
21
+ hasOnNavigate
22
+ ]);
9
23
  const setPathname = useCallback((next)=>{
10
24
  const fullPath = basePath ? `${basePath}${next}` : next;
11
- if (onNavigate) onNavigate(fullPath);
12
- else pushPathname(fullPath);
25
+ if (onNavigate) {
26
+ onNavigate(fullPath);
27
+ notifyPathnameChanged();
28
+ } else pushPathname(fullPath);
13
29
  }, [
14
30
  basePath,
15
31
  onNavigate
@@ -19,17 +35,14 @@ const RemoteShell = ({ ref, className, children, config, basePath, onNavigate, '
19
35
  config
20
36
  ]);
21
37
  const urlDrillLevel = navStack.length - 1;
22
- const [visualDrillLevel, setVisualDrillLevel] = useState(null);
23
- useEffect(()=>{
24
- setVisualDrillLevel(null);
25
- }, [
26
- pathname
27
- ]);
38
+ const [drillOverride, setDrillOverride] = useState(null);
39
+ const visualDrillLevel = drillOverride?.pathname === pathname ? drillOverride.level : null;
40
+ if (drillOverride && drillOverride.pathname !== pathname) setDrillOverride(null);
28
41
  const effectiveDrillLevel = visualDrillLevel ?? urlDrillLevel;
29
42
  const effectiveActiveItemId = effectiveDrillLevel < urlDrillLevel ? navStack[effectiveDrillLevel]?.activeItemId ?? activeItemId : activeItemId;
30
43
  const prefixSegmentCount = navStack[effectiveDrillLevel]?.segmentCount ?? 2 * effectiveDrillLevel;
31
44
  const navigate = useCallback((path)=>{
32
- setVisualDrillLevel(null);
45
+ setDrillOverride(null);
33
46
  const segments = pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
34
47
  const prefixSegments = segments.slice(0, prefixSegmentCount);
35
48
  setPathname(`/${[
@@ -42,7 +55,7 @@ const RemoteShell = ({ ref, className, children, config, basePath, onNavigate, '
42
55
  setPathname
43
56
  ]);
44
57
  const drillInto = useCallback((drill)=>{
45
- setVisualDrillLevel(null);
58
+ setDrillOverride(null);
46
59
  const segments = pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
47
60
  const prefixSegments = segments.slice(0, prefixSegmentCount);
48
61
  const defaultEntity = drill.entities?.[0]?.id ?? 'default';
@@ -64,15 +77,19 @@ const RemoteShell = ({ ref, className, children, config, basePath, onNavigate, '
64
77
  setPathname
65
78
  ]);
66
79
  const goBack = useCallback(()=>{
67
- setVisualDrillLevel((prev)=>{
68
- const current = prev ?? urlDrillLevel;
69
- return Math.max(current - 1, 0);
80
+ setDrillOverride((prev)=>{
81
+ const current = prev?.pathname === pathname ? prev.level : urlDrillLevel;
82
+ return {
83
+ level: Math.max(current - 1, 0),
84
+ pathname
85
+ };
70
86
  });
71
87
  }, [
88
+ pathname,
72
89
  urlDrillLevel
73
90
  ]);
74
91
  const navigateTo = useCallback((href)=>{
75
- setVisualDrillLevel(null);
92
+ setDrillOverride(null);
76
93
  href === config.productPath ? setPathname('/') : setPathname(href);
77
94
  }, [
78
95
  config.productPath,
@@ -7,8 +7,8 @@ import { DRILL_ANIMATION_DURATION, DRILL_ANIMATION_EASING, useDrillTransition }
7
7
  import { NavPanelContent } from "./NavPanelContent.js";
8
8
  const RemoteShellPanel = ({ ref, className, resizable, isLoading, loaderCount = 6, ...props })=>{
9
9
  const testId = useTestId('panel');
10
- const { config, drillLevel } = useRemoteShellContext();
11
- const { transition, clearTransition } = useDrillTransition(drillLevel);
10
+ const { config, drillLevel, navStack } = useRemoteShellContext();
11
+ const { transition, clearTransition } = useDrillTransition(drillLevel, navStack, config.productPath);
12
12
  if (isLoading) return /*#__PURE__*/ jsx("div", {
13
13
  ...props,
14
14
  ref: ref,
@@ -53,7 +53,10 @@ const RemoteShellPanel = ({ ref, className, resizable, isLoading, loaderCount =
53
53
  animation: `${isForward ? 'ds-nav-blur-out' : 'ds-nav-blur-in'} ${timing}`
54
54
  },
55
55
  children: /*#__PURE__*/ jsx(NavPanelContent, {
56
- level: isForward ? transition.fromLevel : drillLevel
56
+ level: isForward ? transition.fromLevel : drillLevel,
57
+ navigationSnapshot: isForward ? {
58
+ navStack: transition.fromNavStack
59
+ } : void 0
57
60
  })
58
61
  }),
59
62
  /*#__PURE__*/ jsx("div", {
@@ -62,7 +65,10 @@ const RemoteShellPanel = ({ ref, className, resizable, isLoading, loaderCount =
62
65
  animation: `${isForward ? 'ds-nav-blur-in' : 'ds-nav-blur-out'} ${timing}`
63
66
  },
64
67
  children: /*#__PURE__*/ jsx(NavPanelContent, {
65
- level: isForward ? drillLevel : transition.fromLevel
68
+ level: isForward ? drillLevel : transition.fromLevel,
69
+ navigationSnapshot: isForward ? void 0 : {
70
+ navStack: transition.fromNavStack
71
+ }
66
72
  })
67
73
  })
68
74
  ]
@@ -2,4 +2,4 @@ export { type MatchNavResult, matchNav } from './matchNav';
2
2
  export { findDrillNode, findFirstLinkPath } from './navUtils';
3
3
  export { RemoteShellContextProvider, type RemoteShellContextValue, useRemoteShellContext, } from './RemoteShellContext';
4
4
  export type { BreadcrumbSegment, NavConfig, NavConfigDrill, NavConfigGroup, NavConfigHeaderAction, NavConfigLink, NavConfigNode, NavConfigSectionHeader, NavStackEntry, } from './types';
5
- export { pushPathname, useLocationPathname } from './useLocationPathname';
5
+ export { notifyPathnameChanged, pushPathname, useLocationPathname } from './useLocationPathname';
@@ -1,4 +1,4 @@
1
1
  export { matchNav } from "./matchNav.js";
2
2
  export { findDrillNode, findFirstLinkPath } from "./navUtils.js";
3
3
  export { RemoteShellContextProvider, useRemoteShellContext } from "./RemoteShellContext.js";
4
- export { pushPathname, useLocationPathname } from "./useLocationPathname.js";
4
+ export { notifyPathnameChanged, pushPathname, useLocationPathname } from "./useLocationPathname.js";
@@ -1,10 +1,21 @@
1
+ import type { NavStackEntry } from './types';
1
2
  export declare const DRILL_ANIMATION_DURATION = "220ms";
2
3
  export declare const DRILL_ANIMATION_EASING = "cubic-bezier(0.4, 0, 0.2, 1)";
3
4
  export interface DrillTransition {
4
5
  fromLevel: number;
5
6
  direction: 'forward' | 'backward';
7
+ fromNavStack: NavStackEntry[];
6
8
  }
7
- export declare const useDrillTransition: (drillLevel: number) => {
9
+ /**
10
+ * Tracks drill-level changes and exposes the transition to animate.
11
+ *
12
+ * `scope` identifies the nav tree (the config's `productPath`). A level change that
13
+ * coincides with a scope change — the same `RemoteShell` instance re-rendered for a
14
+ * different product — is not animated: sliding from the previous product's menu would
15
+ * show the wrong items. Keyed on `productPath` rather than config identity so a config
16
+ * rebuilt for the same product (e.g. when scope-switcher entities load) still animates.
17
+ */
18
+ export declare const useDrillTransition: (drillLevel: number, navStack: NavStackEntry[], scope: string) => {
8
19
  transition: DrillTransition | null;
9
20
  clearTransition: () => void;
10
21
  };
@@ -1,24 +1,36 @@
1
1
  import { useLayoutEffect, useRef, useState } from "react";
2
2
  const DRILL_ANIMATION_DURATION = '220ms';
3
3
  const DRILL_ANIMATION_EASING = 'cubic-bezier(0.4, 0, 0.2, 1)';
4
- const useDrillTransition = (drillLevel)=>{
5
- const prevLevelRef = useRef(drillLevel);
4
+ const useDrillTransition = (drillLevel, navStack, scope)=>{
5
+ const previousRef = useRef({
6
+ drillLevel,
7
+ navStack,
8
+ scope
9
+ });
6
10
  const [transition, setTransition] = useState(null);
7
11
  useLayoutEffect(()=>{
8
- const prev = prevLevelRef.current;
9
- if (prev !== drillLevel) {
10
- setTransition({
11
- fromLevel: prev,
12
- direction: drillLevel > prev ? 'forward' : 'backward'
13
- });
14
- prevLevelRef.current = drillLevel;
15
- }
12
+ const previous = previousRef.current;
13
+ if (previous.scope !== scope) setTransition(null);
14
+ else if (previous.drillLevel !== drillLevel) setTransition({
15
+ fromLevel: previous.drillLevel,
16
+ direction: drillLevel > previous.drillLevel ? 'forward' : 'backward',
17
+ fromNavStack: previous.navStack,
18
+ scope
19
+ });
20
+ previousRef.current = {
21
+ drillLevel,
22
+ navStack,
23
+ scope
24
+ };
16
25
  }, [
17
- drillLevel
26
+ drillLevel,
27
+ navStack,
28
+ scope
18
29
  ]);
19
30
  const clearTransition = ()=>setTransition(null);
31
+ const activeTransition = transition && transition.scope === scope ? transition : null;
20
32
  return {
21
- transition,
33
+ transition: activeTransition,
22
34
  clearTransition
23
35
  };
24
36
  };
@@ -1,4 +1,6 @@
1
1
  /** Push a new pathname to the browser history and notify subscribers. */
2
2
  export declare function pushPathname(path: string): void;
3
+ /** Notify pathname subscribers after navigation handled by an external router. */
4
+ export declare function notifyPathnameChanged(): void;
3
5
  /** Subscribe to `window.location.pathname` via `useSyncExternalStore`. */
4
6
  export declare function useLocationPathname(): string;
@@ -16,9 +16,12 @@ function getSnapshot() {
16
16
  }
17
17
  function pushPathname(path) {
18
18
  window.history.pushState(null, '', path);
19
+ notifyPathnameChanged();
20
+ }
21
+ function notifyPathnameChanged() {
19
22
  for (const listener of listeners)listener();
20
23
  }
21
24
  function useLocationPathname() {
22
25
  return useSyncExternalStore(subscribe, getSnapshot);
23
26
  }
24
- export { pushPathname, useLocationPathname };
27
+ export { notifyPathnameChanged, pushPathname, useLocationPathname };
@@ -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.