@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.
- package/dist/components/RemoteShell/NavPanelContent.d.ts +5 -0
- package/dist/components/RemoteShell/NavPanelContent.js +6 -4
- package/dist/components/RemoteShell/RemoteShell.d.ts +30 -1
- package/dist/components/RemoteShell/RemoteShell.js +35 -18
- package/dist/components/RemoteShell/RemoteShellPanel.js +10 -4
- package/dist/components/RemoteShell/model/index.d.ts +1 -1
- package/dist/components/RemoteShell/model/index.js +1 -1
- package/dist/components/RemoteShell/model/useDrillTransition.d.ts +12 -1
- package/dist/components/RemoteShell/model/useDrillTransition.js +24 -12
- package/dist/components/RemoteShell/model/useLocationPathname.d.ts +2 -0
- package/dist/components/RemoteShell/model/useLocationPathname.js +4 -1
- package/dist/components/Table/TableBody/TableBodyVirtualizedCore.js +1 -1
- package/dist/components/Table/TableBody/TableBodyVirtualizedWindow.js +25 -9
- package/dist/components/Table/TableInner/TableInnerWindow.js +1 -0
- package/dist/components/Table/hooks/infiniteScroll/useInfiniteScroll.d.ts +1 -1
- package/dist/components/Table/hooks/infiniteScroll/usePrependScrollAnchor.d.ts +1 -0
- package/dist/components/Table/hooks/infiniteScroll/usePrependScrollAnchor.js +12 -10
- package/dist/components/Table/hooks/infiniteScroll/useScrollEdge.d.ts +4 -1
- package/dist/components/Table/hooks/infiniteScroll/useScrollEdge.js +4 -17
- package/dist/components/Table/lib/index.d.ts +1 -0
- package/dist/components/Table/lib/index.js +1 -0
- package/dist/components/Table/lib/scrollRoot.d.ts +18 -0
- package/dist/components/Table/lib/scrollRoot.js +22 -0
- package/dist/components/Table/types.d.ts +4 -1
- package/dist/metadata/components.json +26 -8
- package/package.json +2 -1
- package/dist/components/AppShell/story-content/_storyAccountDropdown.d.ts +0 -11
- package/dist/components/AppShell/story-content/_storyAccountDropdown.js +0 -242
- package/dist/components/AppShell/story-content/_storyConfigRenderer.d.ts +0 -12
- package/dist/components/AppShell/story-content/_storyConfigRenderer.js +0 -81
- package/dist/components/AppShell/story-content/_storyHeaderActions.d.ts +0 -1
- package/dist/components/AppShell/story-content/_storyHeaderActions.js +0 -59
- package/dist/components/AppShell/story-content/_storyHomeContent.d.ts +0 -2
- package/dist/components/AppShell/story-content/_storyHomeContent.js +0 -15
- package/dist/components/AppShell/story-content/_storyLib.d.ts +0 -9
- package/dist/components/AppShell/story-content/_storyLib.js +0 -39
- package/dist/components/AppShell/story-content/_storyNavConfigs.d.ts +0 -6
- package/dist/components/AppShell/story-content/_storyNavConfigs.js +0 -772
- package/dist/components/AppShell/story-content/_storyNavRailFooter.d.ts +0 -13
- package/dist/components/AppShell/story-content/_storyNavRailFooter.js +0 -26
- package/dist/components/AppShell/story-content/_storyProductNavItems.d.ts +0 -7
- package/dist/components/AppShell/story-content/_storyProductNavItems.js +0 -49
- package/dist/components/AppShell/story-content/_storyQuickHelpDropdown.d.ts +0 -1
- package/dist/components/AppShell/story-content/_storyQuickHelpDropdown.js +0 -144
- package/dist/components/AppShell/story-content/_storyRecentDropdown.d.ts +0 -1
- package/dist/components/AppShell/story-content/_storyRecentDropdown.js +0 -89
- package/dist/components/AppShell/story-content/_storySearchModal.d.ts +0 -1
- package/dist/components/AppShell/story-content/_storySearchModal.js +0 -252
- package/dist/components/AppShell/story-content/_storyWallarmLogo.d.ts +0 -1
- package/dist/components/AppShell/story-content/_storyWallarmLogo.js +0 -21
- package/dist/components/AppShell/story-content/index.d.ts +0 -11
- 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
|
|
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:
|
|
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:
|
|
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
|
-
/**
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
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)
|
|
12
|
-
|
|
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 [
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
const current = prev
|
|
69
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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 {
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
31
|
-
useSmoothScrollOnSort(table, getScrollTarget);
|
|
47
|
+
useSmoothScrollOnSort(table, getScrollElement);
|
|
32
48
|
return /*#__PURE__*/ jsx(TableBodyVirtualizedCore, {
|
|
33
49
|
tbodyRef: tbodyRef,
|
|
34
50
|
virtualizer: virtualizer
|
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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.
|