@trackunit/react-components 1.17.49 → 1.18.1-alpha-c496ead6241.0
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/index.cjs.js +164 -7
- package/index.esm.js +164 -9
- package/package.json +5 -5
- package/src/hooks/useBidirectionalScroll.d.ts +23 -0
- package/src/hooks/useCursorUrlSync.d.ts +18 -0
- package/src/hooks/useRelayPagination.d.ts +2 -1
- package/src/index.d.ts +2 -0
package/index.cjs.js
CHANGED
|
@@ -5534,8 +5534,8 @@ const noPagination = {
|
|
|
5534
5534
|
},
|
|
5535
5535
|
};
|
|
5536
5536
|
|
|
5537
|
-
const OVERSCAN = 10;
|
|
5538
|
-
const DEFAULT_ROW_HEIGHT = 50;
|
|
5537
|
+
const OVERSCAN$1 = 10;
|
|
5538
|
+
const DEFAULT_ROW_HEIGHT$1 = 50;
|
|
5539
5539
|
/**
|
|
5540
5540
|
* Custom hook for implementing infinite scrolling in a table using TanStack Virtual.
|
|
5541
5541
|
*
|
|
@@ -5565,14 +5565,14 @@ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize,
|
|
|
5565
5565
|
const handleChange = react.useCallback((virtualizer) => {
|
|
5566
5566
|
onChange?.(virtualizer);
|
|
5567
5567
|
}, [onChange]);
|
|
5568
|
-
const handleEstimateSize = react.useCallback((index) => estimateSize?.(index) ?? DEFAULT_ROW_HEIGHT, [estimateSize]);
|
|
5568
|
+
const handleEstimateSize = react.useCallback((index) => estimateSize?.(index) ?? DEFAULT_ROW_HEIGHT$1, [estimateSize]);
|
|
5569
5569
|
//! TODO: remove this once Tanstack Virtual is updated to support React Compiler
|
|
5570
5570
|
// eslint-disable-next-line react-hooks/incompatible-library
|
|
5571
5571
|
const rowVirtualizer = reactVirtual.useVirtualizer({
|
|
5572
5572
|
count,
|
|
5573
5573
|
getScrollElement: () => scrollElementRef.current,
|
|
5574
5574
|
estimateSize: handleEstimateSize,
|
|
5575
|
-
overscan: overscan ?? OVERSCAN,
|
|
5575
|
+
overscan: overscan ?? OVERSCAN$1,
|
|
5576
5576
|
onChange: handleChange,
|
|
5577
5577
|
// This option enables wrapping ResizeObserver measurements in requestAnimationFrame for smoother updates and reduced layout thrashing.
|
|
5578
5578
|
// It helps prevent the "ResizeObserver loop completed with undelivered notifications" error by ensuring that measurements align with the rendering cycle
|
|
@@ -8415,6 +8415,111 @@ const useLocalStorageReducer = ({ key, defaultState, reducer, schema, onValidati
|
|
|
8415
8415
|
return [state, dispatch];
|
|
8416
8416
|
};
|
|
8417
8417
|
|
|
8418
|
+
const OVERSCAN = 10;
|
|
8419
|
+
const DEFAULT_ROW_HEIGHT = 50;
|
|
8420
|
+
/**
|
|
8421
|
+
* Bidirectional infinite scroll hook that supports both forward (nextPage)
|
|
8422
|
+
* and backward (previousPage) pagination with TanStack Virtual.
|
|
8423
|
+
*
|
|
8424
|
+
* Unlike `useInfiniteScroll` which only loads forward, this hook detects
|
|
8425
|
+
* scroll position at both ends and triggers the appropriate pagination
|
|
8426
|
+
* direction.
|
|
8427
|
+
*/
|
|
8428
|
+
const useBidirectionalScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip = false, onTopItemChange, }) => {
|
|
8429
|
+
"use no memo"; //! TODO: remove this once Tanstack Virtual is updated to support React Compiler
|
|
8430
|
+
const onTopItemChangeRef = react.useRef(onTopItemChange);
|
|
8431
|
+
react.useEffect(() => {
|
|
8432
|
+
onTopItemChangeRef.current = onTopItemChange;
|
|
8433
|
+
}, [onTopItemChange]);
|
|
8434
|
+
const lastReportedTopIndex = react.useRef(-1);
|
|
8435
|
+
const handleChange = react.useCallback((virtualizer) => {
|
|
8436
|
+
onChange?.(virtualizer);
|
|
8437
|
+
const scrollOffset = virtualizer.scrollOffset ?? 0;
|
|
8438
|
+
const firstVisibleItem = virtualizer.getVirtualItems().find(item => item.start + item.size > scrollOffset);
|
|
8439
|
+
if (firstVisibleItem !== undefined && firstVisibleItem.index !== lastReportedTopIndex.current) {
|
|
8440
|
+
lastReportedTopIndex.current = firstVisibleItem.index;
|
|
8441
|
+
onTopItemChangeRef.current?.(firstVisibleItem.index);
|
|
8442
|
+
}
|
|
8443
|
+
}, [onChange]);
|
|
8444
|
+
const handleEstimateSize = react.useCallback((index) => estimateSize?.(index) ?? DEFAULT_ROW_HEIGHT, [estimateSize]);
|
|
8445
|
+
//! TODO: remove this once Tanstack Virtual is updated to support React Compiler
|
|
8446
|
+
// eslint-disable-next-line react-hooks/incompatible-library
|
|
8447
|
+
const rowVirtualizer = reactVirtual.useVirtualizer({
|
|
8448
|
+
count,
|
|
8449
|
+
getScrollElement: () => scrollElementRef.current,
|
|
8450
|
+
estimateSize: handleEstimateSize,
|
|
8451
|
+
overscan: overscan ?? OVERSCAN,
|
|
8452
|
+
onChange: handleChange,
|
|
8453
|
+
useAnimationFrameWithResizeObserver: true,
|
|
8454
|
+
});
|
|
8455
|
+
const virtualItems = rowVirtualizer.getVirtualItems();
|
|
8456
|
+
// Track count before backward fetch so we can offset scroll position
|
|
8457
|
+
// after items are prepended, preventing an infinite backward-load loop.
|
|
8458
|
+
const prevCountRef = react.useRef(count);
|
|
8459
|
+
const isLoadingBackwardRef = react.useRef(false);
|
|
8460
|
+
react.useEffect(() => {
|
|
8461
|
+
if (isLoadingBackwardRef.current && !pagination.isLoading && count > prevCountRef.current) {
|
|
8462
|
+
const prependedCount = count - prevCountRef.current;
|
|
8463
|
+
const container = scrollElementRef.current;
|
|
8464
|
+
if (container) {
|
|
8465
|
+
const estimatedRowHeight = estimateSize?.(0) ?? DEFAULT_ROW_HEIGHT;
|
|
8466
|
+
container.scrollTop += prependedCount * estimatedRowHeight;
|
|
8467
|
+
}
|
|
8468
|
+
isLoadingBackwardRef.current = false;
|
|
8469
|
+
}
|
|
8470
|
+
prevCountRef.current = count;
|
|
8471
|
+
}, [count, pagination.isLoading, scrollElementRef, estimateSize]);
|
|
8472
|
+
// Forward loading: scroll reached bottom or insufficient content
|
|
8473
|
+
react.useEffect(() => {
|
|
8474
|
+
if (skip || pagination.pageInfo?.hasNextPage !== true || pagination.isLoading) {
|
|
8475
|
+
return;
|
|
8476
|
+
}
|
|
8477
|
+
const container = scrollElementRef.current;
|
|
8478
|
+
if (!container) {
|
|
8479
|
+
return;
|
|
8480
|
+
}
|
|
8481
|
+
const [lastItem] = [...virtualItems].reverse();
|
|
8482
|
+
const shouldLoadFromScroll = lastItem !== undefined && lastItem.index >= count - 1;
|
|
8483
|
+
const shouldLoadFromInsufficientContent = container.scrollHeight <= container.clientHeight;
|
|
8484
|
+
if (shouldLoadFromScroll || shouldLoadFromInsufficientContent) {
|
|
8485
|
+
pagination.nextPage();
|
|
8486
|
+
}
|
|
8487
|
+
}, [
|
|
8488
|
+
skip,
|
|
8489
|
+
pagination.pageInfo?.hasNextPage,
|
|
8490
|
+
pagination.nextPage,
|
|
8491
|
+
pagination.isLoading,
|
|
8492
|
+
pagination,
|
|
8493
|
+
scrollElementRef,
|
|
8494
|
+
virtualItems,
|
|
8495
|
+
count,
|
|
8496
|
+
]);
|
|
8497
|
+
// Backward loading: fires when the scroll container is at the very top.
|
|
8498
|
+
// Uses scrollTop instead of virtualItems[0].index because after data
|
|
8499
|
+
// prepends the scroll-offset effect adjusts scrollTop away from 0,
|
|
8500
|
+
// which correctly prevents re-triggering. virtualItems[0].index would
|
|
8501
|
+
// still be 0 (overscan always renders from the start) causing a loop.
|
|
8502
|
+
react.useEffect(() => {
|
|
8503
|
+
if (skip || pagination.pageInfo?.hasPreviousPage !== true || pagination.isLoading) {
|
|
8504
|
+
return;
|
|
8505
|
+
}
|
|
8506
|
+
const container = scrollElementRef.current;
|
|
8507
|
+
if (container !== null && container.scrollTop === 0) {
|
|
8508
|
+
isLoadingBackwardRef.current = true;
|
|
8509
|
+
pagination.previousPage();
|
|
8510
|
+
}
|
|
8511
|
+
}, [
|
|
8512
|
+
skip,
|
|
8513
|
+
pagination.pageInfo?.hasPreviousPage,
|
|
8514
|
+
pagination.previousPage,
|
|
8515
|
+
pagination.isLoading,
|
|
8516
|
+
pagination,
|
|
8517
|
+
scrollElementRef,
|
|
8518
|
+
virtualItems,
|
|
8519
|
+
]);
|
|
8520
|
+
return rowVirtualizer;
|
|
8521
|
+
};
|
|
8522
|
+
|
|
8418
8523
|
/**
|
|
8419
8524
|
* Custom hook to handle click outside events.
|
|
8420
8525
|
*
|
|
@@ -8603,6 +8708,53 @@ const useContinuousTimeout = ({ onTimeout, onMaxRetries, duration, maxRetries, }
|
|
|
8603
8708
|
}), [startTimeouts, stopTimeouts, isRunning]);
|
|
8604
8709
|
};
|
|
8605
8710
|
|
|
8711
|
+
const DEFAULT_PARAM_NAME = "cursor";
|
|
8712
|
+
const DEFAULT_DEBOUNCE_MS = 300;
|
|
8713
|
+
/**
|
|
8714
|
+
* Reads a cursor from the URL search params on mount and provides a debounced
|
|
8715
|
+
* updater for writing it back. Uses `replace: true` to avoid polluting browser
|
|
8716
|
+
* history with every scroll position change.
|
|
8717
|
+
*
|
|
8718
|
+
* Wire `initialCursor` into `usePaginationQuery` and `updateCursor` into
|
|
8719
|
+
* `useBidirectionalScroll`'s `onTopItemChange` callback (mapping index to cursor).
|
|
8720
|
+
*/
|
|
8721
|
+
const useCursorUrlSync = ({ paramName = DEFAULT_PARAM_NAME, debounceMs = DEFAULT_DEBOUNCE_MS, } = {}) => {
|
|
8722
|
+
const search = reactRouter.useSearch({ strict: false });
|
|
8723
|
+
const navigate = reactRouter.useNavigate();
|
|
8724
|
+
const [initialCursor] = react.useState(() => {
|
|
8725
|
+
const entry = Object.entries(search).find(([key]) => key === paramName);
|
|
8726
|
+
return typeof entry?.[1] === "string" ? entry[1] : undefined;
|
|
8727
|
+
});
|
|
8728
|
+
const timerRef = react.useRef(undefined);
|
|
8729
|
+
const paramNameRef = react.useRef(paramName);
|
|
8730
|
+
react.useEffect(() => {
|
|
8731
|
+
paramNameRef.current = paramName;
|
|
8732
|
+
}, [paramName]);
|
|
8733
|
+
const updateCursor = react.useCallback((cursor) => {
|
|
8734
|
+
if (timerRef.current !== undefined) {
|
|
8735
|
+
clearTimeout(timerRef.current);
|
|
8736
|
+
}
|
|
8737
|
+
timerRef.current = setTimeout(() => {
|
|
8738
|
+
void navigate({
|
|
8739
|
+
to: ".",
|
|
8740
|
+
search: (prev) => ({
|
|
8741
|
+
...prev,
|
|
8742
|
+
[paramNameRef.current]: cursor,
|
|
8743
|
+
}),
|
|
8744
|
+
replace: true,
|
|
8745
|
+
});
|
|
8746
|
+
}, debounceMs);
|
|
8747
|
+
}, [debounceMs, navigate]);
|
|
8748
|
+
react.useEffect(() => {
|
|
8749
|
+
return () => {
|
|
8750
|
+
if (timerRef.current !== undefined) {
|
|
8751
|
+
clearTimeout(timerRef.current);
|
|
8752
|
+
}
|
|
8753
|
+
};
|
|
8754
|
+
}, []);
|
|
8755
|
+
return react.useMemo(() => ({ initialCursor, updateCursor }), [initialCursor, updateCursor]);
|
|
8756
|
+
};
|
|
8757
|
+
|
|
8606
8758
|
// Copied from https://github.com/rexxars/use-device-pixel-ratio
|
|
8607
8759
|
/**
|
|
8608
8760
|
* Get the device pixel ratio, potentially rounded and capped.
|
|
@@ -9263,9 +9415,12 @@ const defaultPageSize = 50;
|
|
|
9263
9415
|
* @param {RelayPaginationProps} props - The props object containing pagination configuration.
|
|
9264
9416
|
* @returns {RelayPaginationSupport} An object containing functions and state for managing Relay pagination.
|
|
9265
9417
|
*/
|
|
9266
|
-
const useRelayPagination = ({ onReset, pageSize } = { pageSize: defaultPageSize }) => {
|
|
9267
|
-
const [variables, setVariables] = react.useState(
|
|
9268
|
-
|
|
9418
|
+
const useRelayPagination = ({ onReset, pageSize, initialCursor } = { pageSize: defaultPageSize }) => {
|
|
9419
|
+
const [variables, setVariables] = react.useState(() => ({
|
|
9420
|
+
first: pageSize,
|
|
9421
|
+
after: initialCursor,
|
|
9422
|
+
}));
|
|
9423
|
+
const [pageInfo, setPageInfo] = react.useState(() => initialCursor !== undefined ? { hasPreviousPage: true } : null);
|
|
9269
9424
|
const [isLoading, setIsLoading] = react.useState(true);
|
|
9270
9425
|
// Destructure pageInfo properties early to avoid depending on the entire object
|
|
9271
9426
|
const { hasNextPage, endCursor, hasPreviousPage, startCursor } = pageInfo ?? {};
|
|
@@ -9725,10 +9880,12 @@ exports.iconColorNames = iconColorNames;
|
|
|
9725
9880
|
exports.iconPalette = iconPalette;
|
|
9726
9881
|
exports.noPagination = noPagination;
|
|
9727
9882
|
exports.preferenceCardGrid = preferenceCardGrid;
|
|
9883
|
+
exports.useBidirectionalScroll = useBidirectionalScroll;
|
|
9728
9884
|
exports.useClickOutside = useClickOutside;
|
|
9729
9885
|
exports.useContainerBreakpoints = useContainerBreakpoints;
|
|
9730
9886
|
exports.useContinuousTimeout = useContinuousTimeout;
|
|
9731
9887
|
exports.useCopyToClipboard = useCopyToClipboard;
|
|
9888
|
+
exports.useCursorUrlSync = useCursorUrlSync;
|
|
9732
9889
|
exports.useCustomEncoding = useCustomEncoding;
|
|
9733
9890
|
exports.useDebounce = useDebounce;
|
|
9734
9891
|
exports.useDevicePixelRatio = useDevicePixelRatio;
|
package/index.esm.js
CHANGED
|
@@ -10,7 +10,7 @@ import IconSpriteSolid from '@trackunit/ui-icons/icons-sprite-solid.svg';
|
|
|
10
10
|
import { snakeCase, titleCase } from 'string-ts';
|
|
11
11
|
import { cvaMerge } from '@trackunit/css-class-variance-utilities';
|
|
12
12
|
import { Slot, Slottable } from '@radix-ui/react-slot';
|
|
13
|
-
import { Link, useBlocker } from '@tanstack/react-router';
|
|
13
|
+
import { Link, useBlocker, useSearch, useNavigate } from '@tanstack/react-router';
|
|
14
14
|
import { isEqual, omit } from 'es-toolkit';
|
|
15
15
|
import { twMerge } from 'tailwind-merge';
|
|
16
16
|
import { useFloating, offset, flip, shift, size, autoUpdate, useClick, useDismiss, useHover as useHover$1, safePolygon, useRole, useInteractions, FloatingPortal, useMergeRefs as useMergeRefs$1, FloatingFocusManager, arrow, useTransitionStatus, FloatingArrow } from '@floating-ui/react';
|
|
@@ -5532,8 +5532,8 @@ const noPagination = {
|
|
|
5532
5532
|
},
|
|
5533
5533
|
};
|
|
5534
5534
|
|
|
5535
|
-
const OVERSCAN = 10;
|
|
5536
|
-
const DEFAULT_ROW_HEIGHT = 50;
|
|
5535
|
+
const OVERSCAN$1 = 10;
|
|
5536
|
+
const DEFAULT_ROW_HEIGHT$1 = 50;
|
|
5537
5537
|
/**
|
|
5538
5538
|
* Custom hook for implementing infinite scrolling in a table using TanStack Virtual.
|
|
5539
5539
|
*
|
|
@@ -5563,14 +5563,14 @@ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize,
|
|
|
5563
5563
|
const handleChange = useCallback((virtualizer) => {
|
|
5564
5564
|
onChange?.(virtualizer);
|
|
5565
5565
|
}, [onChange]);
|
|
5566
|
-
const handleEstimateSize = useCallback((index) => estimateSize?.(index) ?? DEFAULT_ROW_HEIGHT, [estimateSize]);
|
|
5566
|
+
const handleEstimateSize = useCallback((index) => estimateSize?.(index) ?? DEFAULT_ROW_HEIGHT$1, [estimateSize]);
|
|
5567
5567
|
//! TODO: remove this once Tanstack Virtual is updated to support React Compiler
|
|
5568
5568
|
// eslint-disable-next-line react-hooks/incompatible-library
|
|
5569
5569
|
const rowVirtualizer = useVirtualizer({
|
|
5570
5570
|
count,
|
|
5571
5571
|
getScrollElement: () => scrollElementRef.current,
|
|
5572
5572
|
estimateSize: handleEstimateSize,
|
|
5573
|
-
overscan: overscan ?? OVERSCAN,
|
|
5573
|
+
overscan: overscan ?? OVERSCAN$1,
|
|
5574
5574
|
onChange: handleChange,
|
|
5575
5575
|
// This option enables wrapping ResizeObserver measurements in requestAnimationFrame for smoother updates and reduced layout thrashing.
|
|
5576
5576
|
// It helps prevent the "ResizeObserver loop completed with undelivered notifications" error by ensuring that measurements align with the rendering cycle
|
|
@@ -8413,6 +8413,111 @@ const useLocalStorageReducer = ({ key, defaultState, reducer, schema, onValidati
|
|
|
8413
8413
|
return [state, dispatch];
|
|
8414
8414
|
};
|
|
8415
8415
|
|
|
8416
|
+
const OVERSCAN = 10;
|
|
8417
|
+
const DEFAULT_ROW_HEIGHT = 50;
|
|
8418
|
+
/**
|
|
8419
|
+
* Bidirectional infinite scroll hook that supports both forward (nextPage)
|
|
8420
|
+
* and backward (previousPage) pagination with TanStack Virtual.
|
|
8421
|
+
*
|
|
8422
|
+
* Unlike `useInfiniteScroll` which only loads forward, this hook detects
|
|
8423
|
+
* scroll position at both ends and triggers the appropriate pagination
|
|
8424
|
+
* direction.
|
|
8425
|
+
*/
|
|
8426
|
+
const useBidirectionalScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip = false, onTopItemChange, }) => {
|
|
8427
|
+
"use no memo"; //! TODO: remove this once Tanstack Virtual is updated to support React Compiler
|
|
8428
|
+
const onTopItemChangeRef = useRef(onTopItemChange);
|
|
8429
|
+
useEffect(() => {
|
|
8430
|
+
onTopItemChangeRef.current = onTopItemChange;
|
|
8431
|
+
}, [onTopItemChange]);
|
|
8432
|
+
const lastReportedTopIndex = useRef(-1);
|
|
8433
|
+
const handleChange = useCallback((virtualizer) => {
|
|
8434
|
+
onChange?.(virtualizer);
|
|
8435
|
+
const scrollOffset = virtualizer.scrollOffset ?? 0;
|
|
8436
|
+
const firstVisibleItem = virtualizer.getVirtualItems().find(item => item.start + item.size > scrollOffset);
|
|
8437
|
+
if (firstVisibleItem !== undefined && firstVisibleItem.index !== lastReportedTopIndex.current) {
|
|
8438
|
+
lastReportedTopIndex.current = firstVisibleItem.index;
|
|
8439
|
+
onTopItemChangeRef.current?.(firstVisibleItem.index);
|
|
8440
|
+
}
|
|
8441
|
+
}, [onChange]);
|
|
8442
|
+
const handleEstimateSize = useCallback((index) => estimateSize?.(index) ?? DEFAULT_ROW_HEIGHT, [estimateSize]);
|
|
8443
|
+
//! TODO: remove this once Tanstack Virtual is updated to support React Compiler
|
|
8444
|
+
// eslint-disable-next-line react-hooks/incompatible-library
|
|
8445
|
+
const rowVirtualizer = useVirtualizer({
|
|
8446
|
+
count,
|
|
8447
|
+
getScrollElement: () => scrollElementRef.current,
|
|
8448
|
+
estimateSize: handleEstimateSize,
|
|
8449
|
+
overscan: overscan ?? OVERSCAN,
|
|
8450
|
+
onChange: handleChange,
|
|
8451
|
+
useAnimationFrameWithResizeObserver: true,
|
|
8452
|
+
});
|
|
8453
|
+
const virtualItems = rowVirtualizer.getVirtualItems();
|
|
8454
|
+
// Track count before backward fetch so we can offset scroll position
|
|
8455
|
+
// after items are prepended, preventing an infinite backward-load loop.
|
|
8456
|
+
const prevCountRef = useRef(count);
|
|
8457
|
+
const isLoadingBackwardRef = useRef(false);
|
|
8458
|
+
useEffect(() => {
|
|
8459
|
+
if (isLoadingBackwardRef.current && !pagination.isLoading && count > prevCountRef.current) {
|
|
8460
|
+
const prependedCount = count - prevCountRef.current;
|
|
8461
|
+
const container = scrollElementRef.current;
|
|
8462
|
+
if (container) {
|
|
8463
|
+
const estimatedRowHeight = estimateSize?.(0) ?? DEFAULT_ROW_HEIGHT;
|
|
8464
|
+
container.scrollTop += prependedCount * estimatedRowHeight;
|
|
8465
|
+
}
|
|
8466
|
+
isLoadingBackwardRef.current = false;
|
|
8467
|
+
}
|
|
8468
|
+
prevCountRef.current = count;
|
|
8469
|
+
}, [count, pagination.isLoading, scrollElementRef, estimateSize]);
|
|
8470
|
+
// Forward loading: scroll reached bottom or insufficient content
|
|
8471
|
+
useEffect(() => {
|
|
8472
|
+
if (skip || pagination.pageInfo?.hasNextPage !== true || pagination.isLoading) {
|
|
8473
|
+
return;
|
|
8474
|
+
}
|
|
8475
|
+
const container = scrollElementRef.current;
|
|
8476
|
+
if (!container) {
|
|
8477
|
+
return;
|
|
8478
|
+
}
|
|
8479
|
+
const [lastItem] = [...virtualItems].reverse();
|
|
8480
|
+
const shouldLoadFromScroll = lastItem !== undefined && lastItem.index >= count - 1;
|
|
8481
|
+
const shouldLoadFromInsufficientContent = container.scrollHeight <= container.clientHeight;
|
|
8482
|
+
if (shouldLoadFromScroll || shouldLoadFromInsufficientContent) {
|
|
8483
|
+
pagination.nextPage();
|
|
8484
|
+
}
|
|
8485
|
+
}, [
|
|
8486
|
+
skip,
|
|
8487
|
+
pagination.pageInfo?.hasNextPage,
|
|
8488
|
+
pagination.nextPage,
|
|
8489
|
+
pagination.isLoading,
|
|
8490
|
+
pagination,
|
|
8491
|
+
scrollElementRef,
|
|
8492
|
+
virtualItems,
|
|
8493
|
+
count,
|
|
8494
|
+
]);
|
|
8495
|
+
// Backward loading: fires when the scroll container is at the very top.
|
|
8496
|
+
// Uses scrollTop instead of virtualItems[0].index because after data
|
|
8497
|
+
// prepends the scroll-offset effect adjusts scrollTop away from 0,
|
|
8498
|
+
// which correctly prevents re-triggering. virtualItems[0].index would
|
|
8499
|
+
// still be 0 (overscan always renders from the start) causing a loop.
|
|
8500
|
+
useEffect(() => {
|
|
8501
|
+
if (skip || pagination.pageInfo?.hasPreviousPage !== true || pagination.isLoading) {
|
|
8502
|
+
return;
|
|
8503
|
+
}
|
|
8504
|
+
const container = scrollElementRef.current;
|
|
8505
|
+
if (container !== null && container.scrollTop === 0) {
|
|
8506
|
+
isLoadingBackwardRef.current = true;
|
|
8507
|
+
pagination.previousPage();
|
|
8508
|
+
}
|
|
8509
|
+
}, [
|
|
8510
|
+
skip,
|
|
8511
|
+
pagination.pageInfo?.hasPreviousPage,
|
|
8512
|
+
pagination.previousPage,
|
|
8513
|
+
pagination.isLoading,
|
|
8514
|
+
pagination,
|
|
8515
|
+
scrollElementRef,
|
|
8516
|
+
virtualItems,
|
|
8517
|
+
]);
|
|
8518
|
+
return rowVirtualizer;
|
|
8519
|
+
};
|
|
8520
|
+
|
|
8416
8521
|
/**
|
|
8417
8522
|
* Custom hook to handle click outside events.
|
|
8418
8523
|
*
|
|
@@ -8601,6 +8706,53 @@ const useContinuousTimeout = ({ onTimeout, onMaxRetries, duration, maxRetries, }
|
|
|
8601
8706
|
}), [startTimeouts, stopTimeouts, isRunning]);
|
|
8602
8707
|
};
|
|
8603
8708
|
|
|
8709
|
+
const DEFAULT_PARAM_NAME = "cursor";
|
|
8710
|
+
const DEFAULT_DEBOUNCE_MS = 300;
|
|
8711
|
+
/**
|
|
8712
|
+
* Reads a cursor from the URL search params on mount and provides a debounced
|
|
8713
|
+
* updater for writing it back. Uses `replace: true` to avoid polluting browser
|
|
8714
|
+
* history with every scroll position change.
|
|
8715
|
+
*
|
|
8716
|
+
* Wire `initialCursor` into `usePaginationQuery` and `updateCursor` into
|
|
8717
|
+
* `useBidirectionalScroll`'s `onTopItemChange` callback (mapping index to cursor).
|
|
8718
|
+
*/
|
|
8719
|
+
const useCursorUrlSync = ({ paramName = DEFAULT_PARAM_NAME, debounceMs = DEFAULT_DEBOUNCE_MS, } = {}) => {
|
|
8720
|
+
const search = useSearch({ strict: false });
|
|
8721
|
+
const navigate = useNavigate();
|
|
8722
|
+
const [initialCursor] = useState(() => {
|
|
8723
|
+
const entry = Object.entries(search).find(([key]) => key === paramName);
|
|
8724
|
+
return typeof entry?.[1] === "string" ? entry[1] : undefined;
|
|
8725
|
+
});
|
|
8726
|
+
const timerRef = useRef(undefined);
|
|
8727
|
+
const paramNameRef = useRef(paramName);
|
|
8728
|
+
useEffect(() => {
|
|
8729
|
+
paramNameRef.current = paramName;
|
|
8730
|
+
}, [paramName]);
|
|
8731
|
+
const updateCursor = useCallback((cursor) => {
|
|
8732
|
+
if (timerRef.current !== undefined) {
|
|
8733
|
+
clearTimeout(timerRef.current);
|
|
8734
|
+
}
|
|
8735
|
+
timerRef.current = setTimeout(() => {
|
|
8736
|
+
void navigate({
|
|
8737
|
+
to: ".",
|
|
8738
|
+
search: (prev) => ({
|
|
8739
|
+
...prev,
|
|
8740
|
+
[paramNameRef.current]: cursor,
|
|
8741
|
+
}),
|
|
8742
|
+
replace: true,
|
|
8743
|
+
});
|
|
8744
|
+
}, debounceMs);
|
|
8745
|
+
}, [debounceMs, navigate]);
|
|
8746
|
+
useEffect(() => {
|
|
8747
|
+
return () => {
|
|
8748
|
+
if (timerRef.current !== undefined) {
|
|
8749
|
+
clearTimeout(timerRef.current);
|
|
8750
|
+
}
|
|
8751
|
+
};
|
|
8752
|
+
}, []);
|
|
8753
|
+
return useMemo(() => ({ initialCursor, updateCursor }), [initialCursor, updateCursor]);
|
|
8754
|
+
};
|
|
8755
|
+
|
|
8604
8756
|
// Copied from https://github.com/rexxars/use-device-pixel-ratio
|
|
8605
8757
|
/**
|
|
8606
8758
|
* Get the device pixel ratio, potentially rounded and capped.
|
|
@@ -9261,9 +9413,12 @@ const defaultPageSize = 50;
|
|
|
9261
9413
|
* @param {RelayPaginationProps} props - The props object containing pagination configuration.
|
|
9262
9414
|
* @returns {RelayPaginationSupport} An object containing functions and state for managing Relay pagination.
|
|
9263
9415
|
*/
|
|
9264
|
-
const useRelayPagination = ({ onReset, pageSize } = { pageSize: defaultPageSize }) => {
|
|
9265
|
-
const [variables, setVariables] = useState(
|
|
9266
|
-
|
|
9416
|
+
const useRelayPagination = ({ onReset, pageSize, initialCursor } = { pageSize: defaultPageSize }) => {
|
|
9417
|
+
const [variables, setVariables] = useState(() => ({
|
|
9418
|
+
first: pageSize,
|
|
9419
|
+
after: initialCursor,
|
|
9420
|
+
}));
|
|
9421
|
+
const [pageInfo, setPageInfo] = useState(() => initialCursor !== undefined ? { hasPreviousPage: true } : null);
|
|
9267
9422
|
const [isLoading, setIsLoading] = useState(true);
|
|
9268
9423
|
// Destructure pageInfo properties early to avoid depending on the entire object
|
|
9269
9424
|
const { hasNextPage, endCursor, hasPreviousPage, startCursor } = pageInfo ?? {};
|
|
@@ -9595,4 +9750,4 @@ const useWindowActivity = ({ onFocus, onBlur, skip = false } = { onBlur: undefin
|
|
|
9595
9750
|
return useMemo(() => ({ focused }), [focused]);
|
|
9596
9751
|
};
|
|
9597
9752
|
|
|
9598
|
-
export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, KPICardSkeleton, KPISkeleton, List, ListItem, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SectionHeader, SegmentedValueBar, Sidebar, SkeletonBlock, SkeletonLabel, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGeolocation, useGridAreas, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useKeyboardShortcut, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMergeRefs, useModifierKey, useOverflowItems, usePopoverContext, usePrevious, usePrompt, useRandomCSSLengths, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSelfUpdatingRef, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
|
|
9753
|
+
export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, KPICardSkeleton, KPISkeleton, List, ListItem, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SectionHeader, SegmentedValueBar, Sidebar, SkeletonBlock, SkeletonLabel, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, useBidirectionalScroll, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCursorUrlSync, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGeolocation, useGridAreas, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useKeyboardShortcut, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMergeRefs, useModifierKey, useOverflowItems, usePopoverContext, usePrevious, usePrompt, useRandomCSSLengths, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSelfUpdatingRef, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.1-alpha-c496ead6241.0",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
"@floating-ui/react": "^0.26.25",
|
|
15
15
|
"string-ts": "^2.0.0",
|
|
16
16
|
"tailwind-merge": "^2.0.0",
|
|
17
|
-
"@trackunit/ui-design-tokens": "1.11.
|
|
18
|
-
"@trackunit/css-class-variance-utilities": "1.11.
|
|
19
|
-
"@trackunit/shared-utils": "1.13.
|
|
20
|
-
"@trackunit/ui-icons": "1.11.
|
|
17
|
+
"@trackunit/ui-design-tokens": "1.11.61-alpha-c496ead6241.0",
|
|
18
|
+
"@trackunit/css-class-variance-utilities": "1.11.62-alpha-c496ead6241.0",
|
|
19
|
+
"@trackunit/shared-utils": "1.13.62-alpha-c496ead6241.0",
|
|
20
|
+
"@trackunit/ui-icons": "1.11.60-alpha-c496ead6241.0",
|
|
21
21
|
"@tanstack/react-router": "1.114.29",
|
|
22
22
|
"es-toolkit": "^1.39.10",
|
|
23
23
|
"@tanstack/react-virtual": "3.13.12",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Virtualizer } from "@tanstack/react-virtual";
|
|
2
|
+
import { type RefObject } from "react";
|
|
3
|
+
import type { RelayPagination } from "./useRelayPagination";
|
|
4
|
+
interface BidirectionalScrollProps<TScrollElement extends Element, TItemElement extends Element> {
|
|
5
|
+
readonly pagination: RelayPagination;
|
|
6
|
+
readonly scrollElementRef: RefObject<TScrollElement | null>;
|
|
7
|
+
readonly count: number;
|
|
8
|
+
readonly estimateSize?: (index: number) => number;
|
|
9
|
+
readonly overscan?: number;
|
|
10
|
+
readonly onChange?: (virtualizer: Virtualizer<TScrollElement, TItemElement>) => void;
|
|
11
|
+
readonly skip?: boolean;
|
|
12
|
+
readonly onTopItemChange?: (index: number) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Bidirectional infinite scroll hook that supports both forward (nextPage)
|
|
16
|
+
* and backward (previousPage) pagination with TanStack Virtual.
|
|
17
|
+
*
|
|
18
|
+
* Unlike `useInfiniteScroll` which only loads forward, this hook detects
|
|
19
|
+
* scroll position at both ends and triggers the appropriate pagination
|
|
20
|
+
* direction.
|
|
21
|
+
*/
|
|
22
|
+
export declare const useBidirectionalScroll: <TScrollElement extends Element, TItemElement extends Element>({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip, onTopItemChange, }: BidirectionalScrollProps<TScrollElement, TItemElement>) => Virtualizer<TScrollElement, TItemElement>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type UseCursorUrlSyncOptions = {
|
|
2
|
+
readonly paramName?: string;
|
|
3
|
+
readonly debounceMs?: number;
|
|
4
|
+
};
|
|
5
|
+
type UseCursorUrlSyncResult = {
|
|
6
|
+
readonly initialCursor: string | undefined;
|
|
7
|
+
readonly updateCursor: (cursor: string | undefined) => void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Reads a cursor from the URL search params on mount and provides a debounced
|
|
11
|
+
* updater for writing it back. Uses `replace: true` to avoid polluting browser
|
|
12
|
+
* history with every scroll position change.
|
|
13
|
+
*
|
|
14
|
+
* Wire `initialCursor` into `usePaginationQuery` and `updateCursor` into
|
|
15
|
+
* `useBidirectionalScroll`'s `onTopItemChange` callback (mapping index to cursor).
|
|
16
|
+
*/
|
|
17
|
+
export declare const useCursorUrlSync: ({ paramName, debounceMs, }?: UseCursorUrlSyncOptions) => UseCursorUrlSyncResult;
|
|
18
|
+
export {};
|
|
@@ -2,6 +2,7 @@ import { Dispatch, SetStateAction } from "react";
|
|
|
2
2
|
export interface RelayPaginationProps {
|
|
3
3
|
pageSize?: number;
|
|
4
4
|
onReset?: () => void;
|
|
5
|
+
initialCursor?: string;
|
|
5
6
|
}
|
|
6
7
|
export interface RelayPaginationQueryVariables {
|
|
7
8
|
first?: number | null;
|
|
@@ -40,4 +41,4 @@ export declare const defaultPageSize = 50;
|
|
|
40
41
|
* @param {RelayPaginationProps} props - The props object containing pagination configuration.
|
|
41
42
|
* @returns {RelayPaginationSupport} An object containing functions and state for managing Relay pagination.
|
|
42
43
|
*/
|
|
43
|
-
export declare const useRelayPagination: ({ onReset, pageSize }?: RelayPaginationProps) => RelayPaginationSupport;
|
|
44
|
+
export declare const useRelayPagination: ({ onReset, pageSize, initialCursor }?: RelayPaginationProps) => RelayPaginationSupport;
|
package/src/index.d.ts
CHANGED
|
@@ -108,10 +108,12 @@ export * from "./hooks/encoding/useCustomEncoding";
|
|
|
108
108
|
export * from "./hooks/localStorage/useLocalStorage";
|
|
109
109
|
export * from "./hooks/localStorage/useLocalStorageReducer";
|
|
110
110
|
export * from "./hooks/noPagination";
|
|
111
|
+
export * from "./hooks/useBidirectionalScroll";
|
|
111
112
|
export * from "./hooks/useClickOutside";
|
|
112
113
|
export * from "./hooks/useContainerBreakpoints";
|
|
113
114
|
export * from "./hooks/useContinuousTimeout";
|
|
114
115
|
export * from "./hooks/useCopyToClipboard";
|
|
116
|
+
export * from "./hooks/useCursorUrlSync";
|
|
115
117
|
export * from "./hooks/useDebounce";
|
|
116
118
|
export * from "./hooks/useDevicePixelRatio";
|
|
117
119
|
export * from "./hooks/useElevatedReducer";
|