@trackunit/react-components 1.9.31 → 1.9.33

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 CHANGED
@@ -1353,31 +1353,46 @@ const useGeometry = (ref, { skip = false } = {}) => {
1353
1353
  };
1354
1354
  });
1355
1355
  const resizeObserver = react.useRef(null);
1356
+ const [element, setElement] = react.useState(ref.current);
1357
+ // Track changes to ref.current on every render
1358
+ if (ref.current !== element) {
1359
+ setElement(ref.current);
1360
+ }
1356
1361
  react.useEffect(() => {
1357
- if (skip || !ref.current) {
1362
+ if (skip || !element) {
1358
1363
  return;
1359
1364
  }
1365
+ // Update geometry immediately when element changes
1366
+ const elementRect = element.getBoundingClientRect();
1367
+ setGeometry({
1368
+ width: elementRect.width,
1369
+ height: elementRect.height,
1370
+ top: elementRect.top,
1371
+ bottom: elementRect.bottom,
1372
+ left: elementRect.left,
1373
+ right: elementRect.right,
1374
+ x: elementRect.x,
1375
+ y: elementRect.y,
1376
+ });
1360
1377
  const observe = () => {
1361
1378
  if (!resizeObserver.current) {
1362
1379
  resizeObserver.current = new ResizeObserver(entries => {
1363
1380
  for (const entry of entries) {
1364
- const rect = entry.target.getBoundingClientRect();
1381
+ const entryRect = entry.target.getBoundingClientRect();
1365
1382
  setGeometry({
1366
- width: rect.width,
1367
- height: rect.height,
1368
- top: rect.top,
1369
- bottom: rect.bottom,
1370
- left: rect.left,
1371
- right: rect.right,
1372
- x: rect.x,
1373
- y: rect.y,
1383
+ width: entryRect.width,
1384
+ height: entryRect.height,
1385
+ top: entryRect.top,
1386
+ bottom: entryRect.bottom,
1387
+ left: entryRect.left,
1388
+ right: entryRect.right,
1389
+ x: entryRect.x,
1390
+ y: entryRect.y,
1374
1391
  });
1375
1392
  }
1376
1393
  });
1377
1394
  }
1378
- if (ref.current) {
1379
- resizeObserver.current.observe(ref.current);
1380
- }
1395
+ resizeObserver.current.observe(element);
1381
1396
  };
1382
1397
  observe();
1383
1398
  return () => {
@@ -1385,7 +1400,7 @@ const useGeometry = (ref, { skip = false } = {}) => {
1385
1400
  resizeObserver.current.disconnect();
1386
1401
  }
1387
1402
  };
1388
- }, [ref, skip]);
1403
+ }, [element, skip]);
1389
1404
  return geometry;
1390
1405
  };
1391
1406
 
@@ -1445,6 +1460,9 @@ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize,
1445
1460
  estimateSize: handleEstimateSize,
1446
1461
  overscan: overscan ?? OVERSCAN,
1447
1462
  onChange: handleChange,
1463
+ // This option enables wrapping ResizeObserver measurements in requestAnimationFrame for smoother updates and reduced layout thrashing.
1464
+ // It helps prevent the "ResizeObserver loop completed with undelivered notifications" error by ensuring that measurements align with the rendering cycle
1465
+ useAnimationFrameWithResizeObserver: true,
1448
1466
  });
1449
1467
  const virtualItems = rowVirtualizer.getVirtualItems();
1450
1468
  // Auto-load more data based on scroll position and content height
@@ -3454,12 +3472,28 @@ const cvaList = cssClassVarianceUtilities.cvaMerge(["relative"]);
3454
3472
  const cvaListItem$1 = cssClassVarianceUtilities.cvaMerge(["absolute", "top-0", "left-0", "w-full"], {
3455
3473
  variants: {
3456
3474
  separator: {
3457
- line: ["border-b", "border-neutral-200"],
3475
+ line: "border-neutral-200",
3458
3476
  none: "",
3459
3477
  },
3478
+ contentFillsContainer: {
3479
+ true: "",
3480
+ false: "",
3481
+ },
3460
3482
  },
3483
+ compoundVariants: [
3484
+ {
3485
+ contentFillsContainer: true,
3486
+ separator: "line",
3487
+ class: ["[&:not(:last-child)]:border-b"],
3488
+ },
3489
+ {
3490
+ contentFillsContainer: false,
3491
+ separator: "line",
3492
+ class: ["border-b"],
3493
+ },
3494
+ ],
3461
3495
  defaultVariants: {
3462
- separator: "none",
3496
+ separator: "line",
3463
3497
  },
3464
3498
  });
3465
3499
 
@@ -3592,16 +3626,19 @@ const ListLoadingIndicator = ({ type, hasThumbnail, thumbnailShape, hasDescripti
3592
3626
  */
3593
3627
  const List = ({ children, className, dataTestId,
3594
3628
  // UseListResult properties
3595
- containerRef, listRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, scrollOffset, separator, topSeparatorOnScroll,
3596
- // Unused but part of UseListResult interface
3597
- getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }) => {
3629
+ containerRef, listRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, separator, topSeparatorOnScroll, isAtTop, contentFillsContainer,
3630
+ // Unused but part of UseListResult interface (can be used from parent)
3631
+ scrollOffset: _scrollOffset, getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }) => {
3598
3632
  return (jsxRuntime.jsx("div", { className: cvaListContainer({
3599
- withTopSeparator: topSeparatorOnScroll && (scrollOffset ?? 0) > 0,
3633
+ withTopSeparator: topSeparatorOnScroll && !isAtTop,
3600
3634
  className,
3601
3635
  }), "data-is-scrolling": isScrolling, "data-testid": dataTestId, ref: containerRef, children: jsxRuntime.jsx("ul", { className: cvaList(), ref: listRef, children: rows.map(row => {
3602
- // Generate list item props with separator styling
3636
+ // Generate list item props with clean separator styling
3603
3637
  const listItemProps = getListItemProps(row, {
3604
- className: cvaListItem$1({ separator }),
3638
+ className: cvaListItem$1({
3639
+ separator,
3640
+ contentFillsContainer,
3641
+ }),
3605
3642
  });
3606
3643
  const key = row.virtualRow.key;
3607
3644
  // Render loading row
@@ -3775,6 +3812,10 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
3775
3812
  // eslint-disable-next-line react-hooks/react-compiler
3776
3813
  // eslint-disable-next-line react-hooks/exhaustive-deps
3777
3814
  }, []);
3815
+ // Scroll to top when item count changes (like after filtering)
3816
+ react.useLayoutEffect(() => {
3817
+ virtualizer.scrollToOffset(0);
3818
+ }, [count, virtualizer]);
3778
3819
  // Transform virtual items into typed rows
3779
3820
  const rows = react.useMemo(() => {
3780
3821
  const virtualItems = virtualizer.getVirtualItems();
@@ -3837,6 +3878,11 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
3837
3878
  tabIndex: -1,
3838
3879
  };
3839
3880
  }, [onRowClick, virtualizer]);
3881
+ // Calculate scroll position states
3882
+ const isAtTop = react.useMemo(() => virtualizer.scrollOffset === 0, [virtualizer.scrollOffset]);
3883
+ // totalSize must be out from from useMemo since otherwise we'll not be able to have the the totalSize as a dependency for the contentFillsContainer
3884
+ const totalSize = virtualizer.getTotalSize();
3885
+ const contentFillsContainer = react.useMemo(() => (containerRef.current ? totalSize >= containerRef.current.clientHeight : false), [totalSize]);
3840
3886
  return {
3841
3887
  ...virtualizer,
3842
3888
  containerRef,
@@ -3849,6 +3895,8 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
3849
3895
  count,
3850
3896
  separator,
3851
3897
  topSeparatorOnScroll,
3898
+ isAtTop,
3899
+ contentFillsContainer,
3852
3900
  };
3853
3901
  };
3854
3902
  const getResolvedLoadingIndicator = (loadingIndicator) => {
package/index.esm.js CHANGED
@@ -1351,31 +1351,46 @@ const useGeometry = (ref, { skip = false } = {}) => {
1351
1351
  };
1352
1352
  });
1353
1353
  const resizeObserver = useRef(null);
1354
+ const [element, setElement] = useState(ref.current);
1355
+ // Track changes to ref.current on every render
1356
+ if (ref.current !== element) {
1357
+ setElement(ref.current);
1358
+ }
1354
1359
  useEffect(() => {
1355
- if (skip || !ref.current) {
1360
+ if (skip || !element) {
1356
1361
  return;
1357
1362
  }
1363
+ // Update geometry immediately when element changes
1364
+ const elementRect = element.getBoundingClientRect();
1365
+ setGeometry({
1366
+ width: elementRect.width,
1367
+ height: elementRect.height,
1368
+ top: elementRect.top,
1369
+ bottom: elementRect.bottom,
1370
+ left: elementRect.left,
1371
+ right: elementRect.right,
1372
+ x: elementRect.x,
1373
+ y: elementRect.y,
1374
+ });
1358
1375
  const observe = () => {
1359
1376
  if (!resizeObserver.current) {
1360
1377
  resizeObserver.current = new ResizeObserver(entries => {
1361
1378
  for (const entry of entries) {
1362
- const rect = entry.target.getBoundingClientRect();
1379
+ const entryRect = entry.target.getBoundingClientRect();
1363
1380
  setGeometry({
1364
- width: rect.width,
1365
- height: rect.height,
1366
- top: rect.top,
1367
- bottom: rect.bottom,
1368
- left: rect.left,
1369
- right: rect.right,
1370
- x: rect.x,
1371
- y: rect.y,
1381
+ width: entryRect.width,
1382
+ height: entryRect.height,
1383
+ top: entryRect.top,
1384
+ bottom: entryRect.bottom,
1385
+ left: entryRect.left,
1386
+ right: entryRect.right,
1387
+ x: entryRect.x,
1388
+ y: entryRect.y,
1372
1389
  });
1373
1390
  }
1374
1391
  });
1375
1392
  }
1376
- if (ref.current) {
1377
- resizeObserver.current.observe(ref.current);
1378
- }
1393
+ resizeObserver.current.observe(element);
1379
1394
  };
1380
1395
  observe();
1381
1396
  return () => {
@@ -1383,7 +1398,7 @@ const useGeometry = (ref, { skip = false } = {}) => {
1383
1398
  resizeObserver.current.disconnect();
1384
1399
  }
1385
1400
  };
1386
- }, [ref, skip]);
1401
+ }, [element, skip]);
1387
1402
  return geometry;
1388
1403
  };
1389
1404
 
@@ -1443,6 +1458,9 @@ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize,
1443
1458
  estimateSize: handleEstimateSize,
1444
1459
  overscan: overscan ?? OVERSCAN,
1445
1460
  onChange: handleChange,
1461
+ // This option enables wrapping ResizeObserver measurements in requestAnimationFrame for smoother updates and reduced layout thrashing.
1462
+ // It helps prevent the "ResizeObserver loop completed with undelivered notifications" error by ensuring that measurements align with the rendering cycle
1463
+ useAnimationFrameWithResizeObserver: true,
1446
1464
  });
1447
1465
  const virtualItems = rowVirtualizer.getVirtualItems();
1448
1466
  // Auto-load more data based on scroll position and content height
@@ -3452,12 +3470,28 @@ const cvaList = cvaMerge(["relative"]);
3452
3470
  const cvaListItem$1 = cvaMerge(["absolute", "top-0", "left-0", "w-full"], {
3453
3471
  variants: {
3454
3472
  separator: {
3455
- line: ["border-b", "border-neutral-200"],
3473
+ line: "border-neutral-200",
3456
3474
  none: "",
3457
3475
  },
3476
+ contentFillsContainer: {
3477
+ true: "",
3478
+ false: "",
3479
+ },
3458
3480
  },
3481
+ compoundVariants: [
3482
+ {
3483
+ contentFillsContainer: true,
3484
+ separator: "line",
3485
+ class: ["[&:not(:last-child)]:border-b"],
3486
+ },
3487
+ {
3488
+ contentFillsContainer: false,
3489
+ separator: "line",
3490
+ class: ["border-b"],
3491
+ },
3492
+ ],
3459
3493
  defaultVariants: {
3460
- separator: "none",
3494
+ separator: "line",
3461
3495
  },
3462
3496
  });
3463
3497
 
@@ -3590,16 +3624,19 @@ const ListLoadingIndicator = ({ type, hasThumbnail, thumbnailShape, hasDescripti
3590
3624
  */
3591
3625
  const List = ({ children, className, dataTestId,
3592
3626
  // UseListResult properties
3593
- containerRef, listRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, scrollOffset, separator, topSeparatorOnScroll,
3594
- // Unused but part of UseListResult interface
3595
- getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }) => {
3627
+ containerRef, listRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, separator, topSeparatorOnScroll, isAtTop, contentFillsContainer,
3628
+ // Unused but part of UseListResult interface (can be used from parent)
3629
+ scrollOffset: _scrollOffset, getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }) => {
3596
3630
  return (jsx("div", { className: cvaListContainer({
3597
- withTopSeparator: topSeparatorOnScroll && (scrollOffset ?? 0) > 0,
3631
+ withTopSeparator: topSeparatorOnScroll && !isAtTop,
3598
3632
  className,
3599
3633
  }), "data-is-scrolling": isScrolling, "data-testid": dataTestId, ref: containerRef, children: jsx("ul", { className: cvaList(), ref: listRef, children: rows.map(row => {
3600
- // Generate list item props with separator styling
3634
+ // Generate list item props with clean separator styling
3601
3635
  const listItemProps = getListItemProps(row, {
3602
- className: cvaListItem$1({ separator }),
3636
+ className: cvaListItem$1({
3637
+ separator,
3638
+ contentFillsContainer,
3639
+ }),
3603
3640
  });
3604
3641
  const key = row.virtualRow.key;
3605
3642
  // Render loading row
@@ -3773,6 +3810,10 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
3773
3810
  // eslint-disable-next-line react-hooks/react-compiler
3774
3811
  // eslint-disable-next-line react-hooks/exhaustive-deps
3775
3812
  }, []);
3813
+ // Scroll to top when item count changes (like after filtering)
3814
+ useLayoutEffect(() => {
3815
+ virtualizer.scrollToOffset(0);
3816
+ }, [count, virtualizer]);
3776
3817
  // Transform virtual items into typed rows
3777
3818
  const rows = useMemo(() => {
3778
3819
  const virtualItems = virtualizer.getVirtualItems();
@@ -3835,6 +3876,11 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
3835
3876
  tabIndex: -1,
3836
3877
  };
3837
3878
  }, [onRowClick, virtualizer]);
3879
+ // Calculate scroll position states
3880
+ const isAtTop = useMemo(() => virtualizer.scrollOffset === 0, [virtualizer.scrollOffset]);
3881
+ // totalSize must be out from from useMemo since otherwise we'll not be able to have the the totalSize as a dependency for the contentFillsContainer
3882
+ const totalSize = virtualizer.getTotalSize();
3883
+ const contentFillsContainer = useMemo(() => (containerRef.current ? totalSize >= containerRef.current.clientHeight : false), [totalSize]);
3838
3884
  return {
3839
3885
  ...virtualizer,
3840
3886
  containerRef,
@@ -3847,6 +3893,8 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
3847
3893
  count,
3848
3894
  separator,
3849
3895
  topSeparatorOnScroll,
3896
+ isAtTop,
3897
+ contentFillsContainer,
3850
3898
  };
3851
3899
  };
3852
3900
  const getResolvedLoadingIndicator = (loadingIndicator) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "1.9.31",
3
+ "version": "1.9.33",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -16,11 +16,11 @@
16
16
  "@floating-ui/react": "^0.26.25",
17
17
  "string-ts": "^2.0.0",
18
18
  "tailwind-merge": "^2.0.0",
19
- "@trackunit/ui-design-tokens": "1.7.22",
20
- "@trackunit/css-class-variance-utilities": "1.7.21",
21
- "@trackunit/shared-utils": "1.9.21",
22
- "@trackunit/ui-icons": "1.7.23",
23
- "@trackunit/react-test-setup": "1.4.21",
19
+ "@trackunit/ui-design-tokens": "1.7.24",
20
+ "@trackunit/css-class-variance-utilities": "1.7.23",
21
+ "@trackunit/shared-utils": "1.9.23",
22
+ "@trackunit/ui-icons": "1.7.25",
23
+ "@trackunit/react-test-setup": "1.4.23",
24
24
  "@tanstack/react-router": "1.114.29",
25
25
  "es-toolkit": "^1.39.10",
26
26
  "@tanstack/react-virtual": "3.13.12"
@@ -89,4 +89,4 @@ export interface ListProps<TItem = unknown> extends CommonProps, UseListResult<T
89
89
  * );
90
90
  * ```
91
91
  */
92
- export declare const List: <TItem = unknown>({ children, className, dataTestId, containerRef, listRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, scrollOffset, separator, topSeparatorOnScroll, getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }: ListProps<TItem>) => ReactElement;
92
+ export declare const List: <TItem = unknown>({ children, className, dataTestId, containerRef, listRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, separator, topSeparatorOnScroll, isAtTop, contentFillsContainer, scrollOffset: _scrollOffset, getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }: ListProps<TItem>) => ReactElement;
@@ -4,4 +4,5 @@ export declare const cvaListContainer: (props?: ({
4
4
  export declare const cvaList: (props?: import("class-variance-authority/dist/types").ClassProp | undefined) => string;
5
5
  export declare const cvaListItem: (props?: ({
6
6
  separator?: "line" | "none" | null | undefined;
7
+ contentFillsContainer?: boolean | null | undefined;
7
8
  } & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
@@ -150,6 +150,10 @@ export interface UseListResult<TItem> extends Pick<Virtualizer<HTMLDivElement, H
150
150
  readonly separator: "none" | "line";
151
151
  /** Show a top separator when the list is scrolled */
152
152
  readonly topSeparatorOnScroll: boolean;
153
+ /** Whether the user is scrolled to the top */
154
+ readonly isAtTop: boolean;
155
+ /** Whether the content fills the container */
156
+ readonly contentFillsContainer: boolean;
153
157
  }
154
158
  /**
155
159
  * A hook for managing virtualized list state and behavior.