@wallarm-org/design-system 1.28.2-rc-fix-table-window-scroll-root.2 → 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.
- package/dist/components/Table/TableBody/TableBodyVirtualizedWindow.js +5 -1
- package/dist/components/Table/TableInner/TableInnerWindow.js +2 -2
- package/dist/components/Table/hooks/infiniteScroll/useInfiniteScroll.js +2 -1
- package/dist/components/Table/hooks/infiniteScroll/useInitialAnchor.d.ts +7 -1
- package/dist/components/Table/hooks/infiniteScroll/useInitialAnchor.js +4 -3
- package/dist/components/Table/hooks/infiniteScroll/useScrollEdge.js +1 -0
- package/dist/components/Table/hooks/useWindowScrollRoot.d.ts +17 -5
- package/dist/components/Table/hooks/useWindowScrollRoot.js +41 -14
- package/dist/components/Table/lib/scrollRoot.d.ts +1 -1
- package/dist/components/Table/lib/scrollRoot.js +1 -1
- package/dist/metadata/components.json +21 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useCallback, useContext, useEffect, useLayoutEffect, useRef } from "react";
|
|
3
3
|
import { elementScroll, observeElementOffset, observeElementRect, observeWindowOffset, observeWindowRect, useVirtualizer } from "@tanstack/react-virtual";
|
|
4
4
|
import { WindowScrollRootContext } from "../hooks/useWindowScrollRoot.js";
|
|
5
|
-
import { TABLE_VIRTUALIZATION_OVERSCAN, getOffsetTopInScrollRoot, getRowKey, getScrollMetrics } from "../lib/index.js";
|
|
5
|
+
import { TABLE_VIRTUALIZATION_OVERSCAN, getOffsetTopInScrollRoot, getRowKey, getScrollMetrics, isWindowScrollRoot } from "../lib/index.js";
|
|
6
6
|
import { useTableContext } from "../TableContext/index.js";
|
|
7
7
|
import { measureRowElement } from "./lib/measureRowElement.js";
|
|
8
8
|
import { TableBodyVirtualizedCore } from "./TableBodyVirtualizedCore.js";
|
|
@@ -27,6 +27,10 @@ const TableBodyVirtualizedWindow = ()=>{
|
|
|
27
27
|
observeElementRect: observeRootRect,
|
|
28
28
|
observeElementOffset: observeRootOffset,
|
|
29
29
|
scrollToFn: elementScroll,
|
|
30
|
+
initialRect: scrollRoot && isWindowScrollRoot(scrollRoot) ? {
|
|
31
|
+
width: scrollRoot.innerWidth,
|
|
32
|
+
height: scrollRoot.innerHeight
|
|
33
|
+
} : void 0,
|
|
30
34
|
initialOffset: ()=>{
|
|
31
35
|
const root = getScrollElement() ?? ("u" < typeof window ? null : window);
|
|
32
36
|
return root ? getScrollMetrics(root).scrollTop : 0;
|
|
@@ -15,10 +15,10 @@ const TableInnerWindow = ({ isEmpty, showSettings, hasConsumerSettingsMenu, aria
|
|
|
15
15
|
const testId = useTestId('window');
|
|
16
16
|
const rootRef = useRef(null);
|
|
17
17
|
const containerWidth = useContainerWidth(rootRef);
|
|
18
|
-
const scrollRoot = useWindowScrollRoot(containerRef);
|
|
18
|
+
const { scrollRoot, isSettled } = useWindowScrollRoot(containerRef, !isEmpty && table.getRowModel().rows.length > 0);
|
|
19
19
|
useInfiniteScroll({
|
|
20
20
|
mode: 'window',
|
|
21
|
-
scrollRoot,
|
|
21
|
+
scrollRoot: isSettled ? scrollRoot : null,
|
|
22
22
|
table,
|
|
23
23
|
virtualizerRef,
|
|
24
24
|
tbodyRef,
|
|
@@ -7,7 +7,8 @@ const useInfiniteScroll = ({ mode, scrollRef, scrollRoot, table, virtualizerRef,
|
|
|
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,
|
|
@@ -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
|
};
|
|
@@ -14,6 +14,7 @@ const useScrollEdge = ({ edge, mode, scrollRef, onReached, threshold, enabled =
|
|
|
14
14
|
useEffect(()=>{
|
|
15
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;
|
|
@@ -6,9 +6,21 @@ export declare const WindowScrollRootContext: import("react").Context<ScrollRoot
|
|
|
6
6
|
* Resolves the scroll root of a `virtualized='window'` table once, for the
|
|
7
7
|
* virtualizer, the edge detection and the prepend compensation alike.
|
|
8
8
|
*
|
|
9
|
-
* Starts on the window.
|
|
10
|
-
*
|
|
11
|
-
* window
|
|
12
|
-
*
|
|
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.
|
|
13
22
|
*/
|
|
14
|
-
export declare const useWindowScrollRoot: (containerRef: RefObject<HTMLElement | null
|
|
23
|
+
export declare const useWindowScrollRoot: (containerRef: RefObject<HTMLElement | null>, hasRows: boolean) => {
|
|
24
|
+
scrollRoot: ScrollRoot | null;
|
|
25
|
+
isSettled: boolean;
|
|
26
|
+
};
|
|
@@ -1,35 +1,62 @@
|
|
|
1
1
|
import { createContext, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { getScrollRoot, isWindowScrollRoot } from "../lib/index.js";
|
|
3
3
|
const WindowScrollRootContext = createContext(null);
|
|
4
|
-
const useWindowScrollRoot = (containerRef)=>{
|
|
4
|
+
const useWindowScrollRoot = (containerRef, hasRows)=>{
|
|
5
5
|
const [scrollRoot, setScrollRoot] = useState(()=>"u" < typeof window ? null : window);
|
|
6
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);
|
|
7
11
|
useLayoutEffect(()=>{
|
|
8
12
|
const container = containerRef.current;
|
|
9
|
-
if (!container) return;
|
|
10
|
-
const
|
|
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;
|
|
11
28
|
const next = getScrollRoot(container);
|
|
12
29
|
if (next === scrollRootRef.current) return;
|
|
13
30
|
scrollRootRef.current = next;
|
|
14
31
|
setScrollRoot(next);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
if (!isWindowScrollRoot(next)) {
|
|
33
|
+
detach();
|
|
34
|
+
setIsSettled(true);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function trySettle() {
|
|
21
38
|
resolve();
|
|
22
|
-
|
|
39
|
+
if (hasRowsRef.current && isLaidOut()) setIsSettled(true);
|
|
40
|
+
}
|
|
41
|
+
trySettleRef.current = trySettle;
|
|
23
42
|
document.addEventListener('scroll', onScroll, {
|
|
24
43
|
capture: true,
|
|
25
44
|
passive: true
|
|
26
45
|
});
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
resizeObserver.observe(container);
|
|
47
|
+
trySettle();
|
|
48
|
+
return detach;
|
|
30
49
|
}, [
|
|
31
50
|
containerRef
|
|
32
51
|
]);
|
|
33
|
-
|
|
52
|
+
useLayoutEffect(()=>{
|
|
53
|
+
if (hasRows) trySettleRef.current?.();
|
|
54
|
+
}, [
|
|
55
|
+
hasRows
|
|
56
|
+
]);
|
|
57
|
+
return {
|
|
58
|
+
scrollRoot,
|
|
59
|
+
isSettled
|
|
60
|
+
};
|
|
34
61
|
};
|
|
35
62
|
export { WindowScrollRootContext, useWindowScrollRoot };
|
|
@@ -11,7 +11,7 @@ export declare const isWindowScrollRoot: (root: ScrollRoot) => root is Window;
|
|
|
11
11
|
* `overflow-x-hidden` layout wrapper) computes `overflow-y: visible` to `auto`
|
|
12
12
|
* on a box that grows with its content and never scrolls. Only a box whose
|
|
13
13
|
* content overflows it counts — so a pane that does not overflow yet resolves
|
|
14
|
-
* to the window (`useWindowScrollRoot` re-resolves
|
|
14
|
+
* to the window (`useWindowScrollRoot` re-resolves as the table grows).
|
|
15
15
|
*/
|
|
16
16
|
export declare const getScrollRoot: (el: Element | null) => ScrollRoot;
|
|
17
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.
|
|
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) ? {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "1.28.1",
|
|
3
|
-
"generatedAt": "2026-
|
|
3
|
+
"generatedAt": "2026-10-01T11:03:35.647Z",
|
|
4
4
|
"components": [
|
|
5
5
|
{
|
|
6
6
|
"name": "Accordion",
|
|
@@ -91926,6 +91926,26 @@
|
|
|
91926
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
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
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
|
+
},
|
|
91929
91949
|
{
|
|
91930
91950
|
"name": "HeaderColumnDescription",
|
|
91931
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.
|
|
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",
|