@trackunit/react-components 2.1.64 → 2.2.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 CHANGED
@@ -5733,14 +5733,17 @@ const ListLoadingIndicator = ({ type, hasThumbnail, thumbnailShape, hasDescripti
5733
5733
  */
5734
5734
  const List = ({ children, className, "data-testid": dataTestId, style, ref,
5735
5735
  // UseListResult properties
5736
- containerRef, listRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, separator, topSeparatorOnScroll, isAtTop, contentFillsContainer,
5736
+ containerRef, listRef, listContainerRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, separator, topSeparatorOnScroll, isAtTop, contentFillsContainer,
5737
5737
  // Unused but part of UseListResult interface (can be used from parent)
5738
5738
  scrollOffset: _scrollOffset, getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }) => {
5739
5739
  const mergedContainerRef = useMergeRefs([containerRef, ref]);
5740
+ // The virtualizer owns the `<ul>` height + item positions via directDomUpdates,
5741
+ // so its container callback must be attached to the `<ul>` (alongside listRef).
5742
+ const mergedListRef = useMergeRefs([listRef, listContainerRef]);
5740
5743
  return (jsxRuntime.jsx("div", { className: cvaListContainer({
5741
5744
  withTopSeparator: topSeparatorOnScroll && !isAtTop,
5742
5745
  className,
5743
- }), "data-is-scrolling": isScrolling, "data-testid": dataTestId, ref: mergedContainerRef, style: style, children: jsxRuntime.jsx("ul", { className: cvaList(), ref: listRef, children: rows.map(row => {
5746
+ }), "data-is-scrolling": isScrolling, "data-testid": dataTestId, ref: mergedContainerRef, style: style, children: jsxRuntime.jsx("ul", { className: cvaList(), ref: mergedListRef, children: rows.map(row => {
5744
5747
  // Generate list item props with clean separator styling
5745
5748
  const listItemProps = getListItemProps(row, {
5746
5749
  className: cvaListItem$1({
@@ -5794,7 +5797,7 @@ const DEFAULT_ROW_HEIGHT$1 = 50;
5794
5797
  * @param props.overscan - Optional number of items to render outside the visible area.
5795
5798
  * @param props.onChange - Optional callback when virtualizer changes.
5796
5799
  * @param props.skip - Whether to skip automatic loading of more data (default: false).
5797
- * @returns {Virtualizer} The virtualizer instance with all its properties and methods.
5800
+ * @returns {ReactVirtualizer} The virtualizer instance with all its properties and methods, including `containerRef` for direct DOM positioning.
5798
5801
  * @description
5799
5802
  * This hook is used to implement infinite scrolling in a table. It uses TanStack Virtual's
5800
5803
  * built-in capabilities for virtualization and automatically loads more data when scrolling
@@ -5808,12 +5811,14 @@ const DEFAULT_ROW_HEIGHT$1 = 50;
5808
5811
  * });
5809
5812
  */
5810
5813
  const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip = false, }) => {
5811
- "use no memo"; //! TODO: remove this once Tanstack Virtual is updated to support React Compiler
5812
5814
  const handleChange = react.useCallback((virtualizer) => {
5813
5815
  onChange?.(virtualizer);
5814
5816
  }, [onChange]);
5815
5817
  const handleEstimateSize = react.useCallback((index) => estimateSize?.(index) ?? DEFAULT_ROW_HEIGHT$1, [estimateSize]);
5816
- //! TODO: remove this once Tanstack Virtual is updated to support React Compiler
5818
+ // eslint-plugin-react-hooks hardcodes `useVirtualizer` as a known-incompatible
5819
+ // API (its returned functions can't be memoized), so this warning is emitted
5820
+ // regardless of the installed version. The React Compiler already auto-skips
5821
+ // memoizing this hook, so the behaviour is safe.
5817
5822
  // eslint-disable-next-line react-hooks/incompatible-library
5818
5823
  const rowVirtualizer = reactVirtual.useVirtualizer({
5819
5824
  count,
@@ -5824,6 +5829,12 @@ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize,
5824
5829
  // This option enables wrapping ResizeObserver measurements in requestAnimationFrame for smoother updates and reduced layout thrashing.
5825
5830
  // It helps prevent the "ResizeObserver loop completed with undelivered notifications" error by ensuring that measurements align with the rendering cycle
5826
5831
  useAnimationFrameWithResizeObserver: true,
5832
+ // Let the virtualizer write item positions and container size straight to
5833
+ // the DOM, only re-rendering when the visible index range changes. This
5834
+ // removes the imperative transform/height races that made scrolling jumpy.
5835
+ directDomUpdates: true,
5836
+ directDomUpdatesMode: "transform",
5837
+ useFlushSync: false,
5827
5838
  });
5828
5839
  const virtualItems = rowVirtualizer.getVirtualItems();
5829
5840
  // Auto-load more data based on scroll position and content height
@@ -5883,7 +5894,7 @@ const DEFAULT_LOADING_INDICATOR_CONFIG = {
5883
5894
  *
5884
5895
  * return (
5885
5896
  * <div ref={list.containerRef}>
5886
- * <ul style={{ height: `${list.getTotalSize()}px` }}>
5897
+ * <ul ref={list.listContainerRef} className="relative">
5887
5898
  * {list.rows.map(row => {
5888
5899
  * const props = list.getListItemProps(row, { className: 'list-item' });
5889
5900
  * return <li {...props}>{row.item?.title}</li>;
@@ -5896,7 +5907,6 @@ const DEFAULT_LOADING_INDICATOR_CONFIG = {
5896
5907
  const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAULT_LOADING_INDICATOR_CONFIG, onRowClick, onChange, estimateItemSize, estimateHeaderSize, overscan, separator = "line", topSeparatorOnScroll = false, }) => {
5897
5908
  const containerRef = react.useRef(null);
5898
5909
  const listRef = react.useRef(null);
5899
- const rowRefsMap = react.useRef(new Map());
5900
5910
  const virtualizerRef = react.useRef(null);
5901
5911
  const hasMeasuredOnMount = react.useRef(false);
5902
5912
  // Resolve loading indicator once to avoid unnecessary re-renders
@@ -5988,19 +5998,10 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
5988
5998
  count: totalRowCount,
5989
5999
  estimateSize,
5990
6000
  overscan,
5991
- onChange: instance => {
5992
- if (listRef.current) {
5993
- listRef.current.style.height = `${instance.getTotalSize()}px`;
5994
- }
5995
- // Apply transforms to all virtual items
5996
- instance.getVirtualItems().forEach(virtualRow => {
5997
- const rowRef = rowRefsMap.current.get(virtualRow.index);
5998
- if (rowRef) {
5999
- rowRef.style.transform = `translateY(${virtualRow.start}px)`;
6000
- }
6001
- });
6002
- onChange?.(instance);
6003
- },
6001
+ // Item positions and the `<ul>` size are written directly to the DOM by
6002
+ // the virtualizer (directDomUpdates), so this callback only forwards the
6003
+ // change to consumers.
6004
+ onChange,
6004
6005
  });
6005
6006
  // Keep ref updated with latest virtualizer instance
6006
6007
  react.useEffect(() => {
@@ -6024,9 +6025,9 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
6024
6025
  }
6025
6026
  previousCountRef.current = currentCount;
6026
6027
  }, [virtualizer, pagination?.pageInfo?.count]);
6028
+ const virtualItems = virtualizer.getVirtualItems();
6027
6029
  // Transform virtual items into typed rows
6028
6030
  const rows = react.useMemo(() => {
6029
- const virtualItems = virtualizer.getVirtualItems();
6030
6031
  return virtualItems.map((virtualRow) => {
6031
6032
  const { index } = virtualRow;
6032
6033
  // Determine row type
@@ -6056,8 +6057,7 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
6056
6057
  dataIndex,
6057
6058
  };
6058
6059
  });
6059
- // Call getVirtualItems() fresh each time to get updated measurements after virtualizer state changes
6060
- }, [virtualizer, totalDataRows, hasHeader, dataStartIndex, getItem]);
6060
+ }, [virtualItems, totalDataRows, hasHeader, dataStartIndex, getItem]);
6061
6061
  // Helper to create list item props
6062
6062
  const getListItemProps = react.useCallback((row, options) => {
6063
6063
  const { type, item, dataIndex } = row;
@@ -6072,17 +6072,9 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
6072
6072
  className: options.className,
6073
6073
  "data-index": row.virtualRow.index,
6074
6074
  onClick,
6075
- ref: el => {
6076
- if (el) {
6077
- virtualizer.measureElement(el);
6078
- rowRefsMap.current.set(row.virtualRow.index, el);
6079
- // Apply transform immediately when element is added to map
6080
- const virtualRow = virtualizer.getVirtualItems().find(vr => vr.index === row.virtualRow.index);
6081
- if (virtualRow) {
6082
- el.style.transform = `translateY(${virtualRow.start}px)`;
6083
- }
6084
- }
6085
- },
6075
+ // The virtualizer positions rows itself (directDomUpdates); the ref
6076
+ // only needs to register the element for dynamic height measurement.
6077
+ ref: virtualizer.measureElement,
6086
6078
  tabIndex: -1,
6087
6079
  };
6088
6080
  }, [onRowClick, virtualizer]);
@@ -6096,7 +6088,11 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
6096
6088
  }, [totalSize]);
6097
6089
  return react.useMemo(() => ({
6098
6090
  ...virtualizer,
6091
+ // `containerRef` (scroll element) intentionally overrides the spread
6092
+ // `virtualizer.containerRef`; the latter is re-exposed as
6093
+ // `listContainerRef` for the inner `<ul>` size container.
6099
6094
  containerRef,
6095
+ listContainerRef: virtualizer.containerRef,
6100
6096
  listRef,
6101
6097
  rows,
6102
6098
  getListItemProps,
@@ -12124,8 +12120,7 @@ const DEFAULT_ROW_HEIGHT = 50;
12124
12120
  * scroll position at both ends and triggers the appropriate pagination
12125
12121
  * direction.
12126
12122
  */
12127
- const useBidirectionalScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip = false, onTopItemChange, }) => {
12128
- "use no memo"; //! TODO: remove this once Tanstack Virtual is updated to support React Compiler
12123
+ const useBidirectionalScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip = false, onTopItemChange, getItemKey, }) => {
12129
12124
  const onTopItemChangeRef = react.useRef(onTopItemChange);
12130
12125
  react.useEffect(() => {
12131
12126
  onTopItemChangeRef.current = onTopItemChange;
@@ -12141,7 +12136,10 @@ const useBidirectionalScroll = ({ pagination, scrollElementRef, count, estimateS
12141
12136
  }
12142
12137
  }, [onChange]);
12143
12138
  const handleEstimateSize = react.useCallback((index) => estimateSize?.(index) ?? DEFAULT_ROW_HEIGHT, [estimateSize]);
12144
- //! TODO: remove this once Tanstack Virtual is updated to support React Compiler
12139
+ // eslint-plugin-react-hooks hardcodes `useVirtualizer` as a known-incompatible
12140
+ // API (its returned functions can't be memoized), so this warning is emitted
12141
+ // regardless of the installed version. The React Compiler already auto-skips
12142
+ // memoizing this hook, so the behaviour is safe.
12145
12143
  // eslint-disable-next-line react-hooks/incompatible-library
12146
12144
  const rowVirtualizer = reactVirtual.useVirtualizer({
12147
12145
  count,
@@ -12149,7 +12147,14 @@ const useBidirectionalScroll = ({ pagination, scrollElementRef, count, estimateS
12149
12147
  estimateSize: handleEstimateSize,
12150
12148
  overscan: overscan ?? OVERSCAN,
12151
12149
  onChange: handleChange,
12150
+ getItemKey,
12152
12151
  useAnimationFrameWithResizeObserver: true,
12152
+ // Let the virtualizer write item positions and container size straight to
12153
+ // the DOM, only re-rendering when the visible index range changes. This
12154
+ // removes the imperative transform/height races that made scrolling jumpy.
12155
+ directDomUpdates: true,
12156
+ directDomUpdatesMode: "transform",
12157
+ useFlushSync: false,
12153
12158
  });
12154
12159
  const virtualItems = rowVirtualizer.getVirtualItems();
12155
12160
  // Track count before backward fetch so we can offset scroll position
package/index.esm.js CHANGED
@@ -5731,14 +5731,17 @@ const ListLoadingIndicator = ({ type, hasThumbnail, thumbnailShape, hasDescripti
5731
5731
  */
5732
5732
  const List = ({ children, className, "data-testid": dataTestId, style, ref,
5733
5733
  // UseListResult properties
5734
- containerRef, listRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, separator, topSeparatorOnScroll, isAtTop, contentFillsContainer,
5734
+ containerRef, listRef, listContainerRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, separator, topSeparatorOnScroll, isAtTop, contentFillsContainer,
5735
5735
  // Unused but part of UseListResult interface (can be used from parent)
5736
5736
  scrollOffset: _scrollOffset, getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }) => {
5737
5737
  const mergedContainerRef = useMergeRefs([containerRef, ref]);
5738
+ // The virtualizer owns the `<ul>` height + item positions via directDomUpdates,
5739
+ // so its container callback must be attached to the `<ul>` (alongside listRef).
5740
+ const mergedListRef = useMergeRefs([listRef, listContainerRef]);
5738
5741
  return (jsx("div", { className: cvaListContainer({
5739
5742
  withTopSeparator: topSeparatorOnScroll && !isAtTop,
5740
5743
  className,
5741
- }), "data-is-scrolling": isScrolling, "data-testid": dataTestId, ref: mergedContainerRef, style: style, children: jsx("ul", { className: cvaList(), ref: listRef, children: rows.map(row => {
5744
+ }), "data-is-scrolling": isScrolling, "data-testid": dataTestId, ref: mergedContainerRef, style: style, children: jsx("ul", { className: cvaList(), ref: mergedListRef, children: rows.map(row => {
5742
5745
  // Generate list item props with clean separator styling
5743
5746
  const listItemProps = getListItemProps(row, {
5744
5747
  className: cvaListItem$1({
@@ -5792,7 +5795,7 @@ const DEFAULT_ROW_HEIGHT$1 = 50;
5792
5795
  * @param props.overscan - Optional number of items to render outside the visible area.
5793
5796
  * @param props.onChange - Optional callback when virtualizer changes.
5794
5797
  * @param props.skip - Whether to skip automatic loading of more data (default: false).
5795
- * @returns {Virtualizer} The virtualizer instance with all its properties and methods.
5798
+ * @returns {ReactVirtualizer} The virtualizer instance with all its properties and methods, including `containerRef` for direct DOM positioning.
5796
5799
  * @description
5797
5800
  * This hook is used to implement infinite scrolling in a table. It uses TanStack Virtual's
5798
5801
  * built-in capabilities for virtualization and automatically loads more data when scrolling
@@ -5806,12 +5809,14 @@ const DEFAULT_ROW_HEIGHT$1 = 50;
5806
5809
  * });
5807
5810
  */
5808
5811
  const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip = false, }) => {
5809
- "use no memo"; //! TODO: remove this once Tanstack Virtual is updated to support React Compiler
5810
5812
  const handleChange = useCallback((virtualizer) => {
5811
5813
  onChange?.(virtualizer);
5812
5814
  }, [onChange]);
5813
5815
  const handleEstimateSize = useCallback((index) => estimateSize?.(index) ?? DEFAULT_ROW_HEIGHT$1, [estimateSize]);
5814
- //! TODO: remove this once Tanstack Virtual is updated to support React Compiler
5816
+ // eslint-plugin-react-hooks hardcodes `useVirtualizer` as a known-incompatible
5817
+ // API (its returned functions can't be memoized), so this warning is emitted
5818
+ // regardless of the installed version. The React Compiler already auto-skips
5819
+ // memoizing this hook, so the behaviour is safe.
5815
5820
  // eslint-disable-next-line react-hooks/incompatible-library
5816
5821
  const rowVirtualizer = useVirtualizer({
5817
5822
  count,
@@ -5822,6 +5827,12 @@ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize,
5822
5827
  // This option enables wrapping ResizeObserver measurements in requestAnimationFrame for smoother updates and reduced layout thrashing.
5823
5828
  // It helps prevent the "ResizeObserver loop completed with undelivered notifications" error by ensuring that measurements align with the rendering cycle
5824
5829
  useAnimationFrameWithResizeObserver: true,
5830
+ // Let the virtualizer write item positions and container size straight to
5831
+ // the DOM, only re-rendering when the visible index range changes. This
5832
+ // removes the imperative transform/height races that made scrolling jumpy.
5833
+ directDomUpdates: true,
5834
+ directDomUpdatesMode: "transform",
5835
+ useFlushSync: false,
5825
5836
  });
5826
5837
  const virtualItems = rowVirtualizer.getVirtualItems();
5827
5838
  // Auto-load more data based on scroll position and content height
@@ -5881,7 +5892,7 @@ const DEFAULT_LOADING_INDICATOR_CONFIG = {
5881
5892
  *
5882
5893
  * return (
5883
5894
  * <div ref={list.containerRef}>
5884
- * <ul style={{ height: `${list.getTotalSize()}px` }}>
5895
+ * <ul ref={list.listContainerRef} className="relative">
5885
5896
  * {list.rows.map(row => {
5886
5897
  * const props = list.getListItemProps(row, { className: 'list-item' });
5887
5898
  * return <li {...props}>{row.item?.title}</li>;
@@ -5894,7 +5905,6 @@ const DEFAULT_LOADING_INDICATOR_CONFIG = {
5894
5905
  const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAULT_LOADING_INDICATOR_CONFIG, onRowClick, onChange, estimateItemSize, estimateHeaderSize, overscan, separator = "line", topSeparatorOnScroll = false, }) => {
5895
5906
  const containerRef = useRef(null);
5896
5907
  const listRef = useRef(null);
5897
- const rowRefsMap = useRef(new Map());
5898
5908
  const virtualizerRef = useRef(null);
5899
5909
  const hasMeasuredOnMount = useRef(false);
5900
5910
  // Resolve loading indicator once to avoid unnecessary re-renders
@@ -5986,19 +5996,10 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
5986
5996
  count: totalRowCount,
5987
5997
  estimateSize,
5988
5998
  overscan,
5989
- onChange: instance => {
5990
- if (listRef.current) {
5991
- listRef.current.style.height = `${instance.getTotalSize()}px`;
5992
- }
5993
- // Apply transforms to all virtual items
5994
- instance.getVirtualItems().forEach(virtualRow => {
5995
- const rowRef = rowRefsMap.current.get(virtualRow.index);
5996
- if (rowRef) {
5997
- rowRef.style.transform = `translateY(${virtualRow.start}px)`;
5998
- }
5999
- });
6000
- onChange?.(instance);
6001
- },
5999
+ // Item positions and the `<ul>` size are written directly to the DOM by
6000
+ // the virtualizer (directDomUpdates), so this callback only forwards the
6001
+ // change to consumers.
6002
+ onChange,
6002
6003
  });
6003
6004
  // Keep ref updated with latest virtualizer instance
6004
6005
  useEffect(() => {
@@ -6022,9 +6023,9 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
6022
6023
  }
6023
6024
  previousCountRef.current = currentCount;
6024
6025
  }, [virtualizer, pagination?.pageInfo?.count]);
6026
+ const virtualItems = virtualizer.getVirtualItems();
6025
6027
  // Transform virtual items into typed rows
6026
6028
  const rows = useMemo(() => {
6027
- const virtualItems = virtualizer.getVirtualItems();
6028
6029
  return virtualItems.map((virtualRow) => {
6029
6030
  const { index } = virtualRow;
6030
6031
  // Determine row type
@@ -6054,8 +6055,7 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
6054
6055
  dataIndex,
6055
6056
  };
6056
6057
  });
6057
- // Call getVirtualItems() fresh each time to get updated measurements after virtualizer state changes
6058
- }, [virtualizer, totalDataRows, hasHeader, dataStartIndex, getItem]);
6058
+ }, [virtualItems, totalDataRows, hasHeader, dataStartIndex, getItem]);
6059
6059
  // Helper to create list item props
6060
6060
  const getListItemProps = useCallback((row, options) => {
6061
6061
  const { type, item, dataIndex } = row;
@@ -6070,17 +6070,9 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
6070
6070
  className: options.className,
6071
6071
  "data-index": row.virtualRow.index,
6072
6072
  onClick,
6073
- ref: el => {
6074
- if (el) {
6075
- virtualizer.measureElement(el);
6076
- rowRefsMap.current.set(row.virtualRow.index, el);
6077
- // Apply transform immediately when element is added to map
6078
- const virtualRow = virtualizer.getVirtualItems().find(vr => vr.index === row.virtualRow.index);
6079
- if (virtualRow) {
6080
- el.style.transform = `translateY(${virtualRow.start}px)`;
6081
- }
6082
- }
6083
- },
6073
+ // The virtualizer positions rows itself (directDomUpdates); the ref
6074
+ // only needs to register the element for dynamic height measurement.
6075
+ ref: virtualizer.measureElement,
6084
6076
  tabIndex: -1,
6085
6077
  };
6086
6078
  }, [onRowClick, virtualizer]);
@@ -6094,7 +6086,11 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAUL
6094
6086
  }, [totalSize]);
6095
6087
  return useMemo(() => ({
6096
6088
  ...virtualizer,
6089
+ // `containerRef` (scroll element) intentionally overrides the spread
6090
+ // `virtualizer.containerRef`; the latter is re-exposed as
6091
+ // `listContainerRef` for the inner `<ul>` size container.
6097
6092
  containerRef,
6093
+ listContainerRef: virtualizer.containerRef,
6098
6094
  listRef,
6099
6095
  rows,
6100
6096
  getListItemProps,
@@ -12122,8 +12118,7 @@ const DEFAULT_ROW_HEIGHT = 50;
12122
12118
  * scroll position at both ends and triggers the appropriate pagination
12123
12119
  * direction.
12124
12120
  */
12125
- const useBidirectionalScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip = false, onTopItemChange, }) => {
12126
- "use no memo"; //! TODO: remove this once Tanstack Virtual is updated to support React Compiler
12121
+ const useBidirectionalScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip = false, onTopItemChange, getItemKey, }) => {
12127
12122
  const onTopItemChangeRef = useRef(onTopItemChange);
12128
12123
  useEffect(() => {
12129
12124
  onTopItemChangeRef.current = onTopItemChange;
@@ -12139,7 +12134,10 @@ const useBidirectionalScroll = ({ pagination, scrollElementRef, count, estimateS
12139
12134
  }
12140
12135
  }, [onChange]);
12141
12136
  const handleEstimateSize = useCallback((index) => estimateSize?.(index) ?? DEFAULT_ROW_HEIGHT, [estimateSize]);
12142
- //! TODO: remove this once Tanstack Virtual is updated to support React Compiler
12137
+ // eslint-plugin-react-hooks hardcodes `useVirtualizer` as a known-incompatible
12138
+ // API (its returned functions can't be memoized), so this warning is emitted
12139
+ // regardless of the installed version. The React Compiler already auto-skips
12140
+ // memoizing this hook, so the behaviour is safe.
12143
12141
  // eslint-disable-next-line react-hooks/incompatible-library
12144
12142
  const rowVirtualizer = useVirtualizer({
12145
12143
  count,
@@ -12147,7 +12145,14 @@ const useBidirectionalScroll = ({ pagination, scrollElementRef, count, estimateS
12147
12145
  estimateSize: handleEstimateSize,
12148
12146
  overscan: overscan ?? OVERSCAN,
12149
12147
  onChange: handleChange,
12148
+ getItemKey,
12150
12149
  useAnimationFrameWithResizeObserver: true,
12150
+ // Let the virtualizer write item positions and container size straight to
12151
+ // the DOM, only re-rendering when the visible index range changes. This
12152
+ // removes the imperative transform/height races that made scrolling jumpy.
12153
+ directDomUpdates: true,
12154
+ directDomUpdatesMode: "transform",
12155
+ useFlushSync: false,
12151
12156
  });
12152
12157
  const virtualItems = rowVirtualizer.getVirtualItems();
12153
12158
  // Track count before backward fetch so we can offset scroll position
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "2.1.64",
3
+ "version": "2.2.0",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "migrations": "./migrations.json",
@@ -14,17 +14,17 @@
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.13.57",
18
- "@trackunit/css-class-variance-utilities": "1.13.58",
19
- "@trackunit/shared-utils": "1.15.60",
20
- "@trackunit/ui-icons": "1.13.59",
17
+ "@trackunit/ui-design-tokens": "1.13.58",
18
+ "@trackunit/css-class-variance-utilities": "1.13.59",
19
+ "@trackunit/shared-utils": "1.15.62",
20
+ "@trackunit/ui-icons": "1.13.60",
21
21
  "es-toolkit": "^1.39.10",
22
- "@tanstack/react-virtual": "3.13.12",
22
+ "@tanstack/react-virtual": "^3.14.3",
23
23
  "dequal": "^2.0.3",
24
24
  "fflate": "^0.8.2",
25
25
  "superjson": "^2.2.6",
26
26
  "zod": "^3.25.76",
27
- "@trackunit/i18n-library-translation": "2.0.62"
27
+ "@trackunit/i18n-library-translation": "2.1.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "react": "^19.0.0",
@@ -93,4 +93,4 @@ export interface ListProps<TItem = unknown> extends CommonProps, Styleable, Refa
93
93
  * );
94
94
  * ```
95
95
  */
96
- export declare const List: <TItem = unknown>({ children, className, "data-testid": dataTestId, style, ref, 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;
96
+ export declare const List: <TItem = unknown>({ children, className, "data-testid": dataTestId, style, ref, containerRef, listRef, listContainerRef, 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;
@@ -28,13 +28,13 @@ type CustomLoadingIndicator = {
28
28
  /**
29
29
  * Number of loading indicators to show on initial load
30
30
  *
31
- * @default 3
31
+ * @default 10
32
32
  */
33
33
  initialLoadingCount?: number;
34
34
  /**
35
35
  * Number of loading indicators to show when loading more items during scroll
36
36
  *
37
- * @default 10
37
+ * @default 3
38
38
  */
39
39
  scrollLoadingCount?: number;
40
40
  hasThumbnail?: never;
@@ -54,13 +54,13 @@ type SkeletonLoadingIndicator = {
54
54
  /**
55
55
  * Number of loading indicators to show on initial load
56
56
  *
57
- * @default 3
57
+ * @default 10
58
58
  */
59
59
  initialLoadingCount?: number;
60
60
  /**
61
61
  * Number of loading indicators to show when loading more items during scroll
62
62
  *
63
- * @default 10
63
+ * @default 3
64
64
  */
65
65
  scrollLoadingCount?: number;
66
66
  };
@@ -132,6 +132,13 @@ export interface UseListResult<TItem> extends Pick<Virtualizer<HTMLDivElement, H
132
132
  containerRef: RefObject<HTMLDivElement | null>;
133
133
  /** Reference to attach to the list */
134
134
  listRef: RefObject<HTMLUListElement | null>;
135
+ /**
136
+ * Callback ref for the inner size container (`<ul>`). The virtualizer writes
137
+ * the container's height and item positions directly to the DOM
138
+ * (directDomUpdates), so this MUST be attached to the `<ul>` and the `<ul>`
139
+ * must not set its own height.
140
+ */
141
+ listContainerRef: (node: HTMLElement | null) => void;
135
142
  /** Array of row data to render */
136
143
  readonly rows: ReadonlyArray<ListRow<TItem>>;
137
144
  /** Helper to create list item props for a given row */
@@ -178,7 +185,7 @@ export interface UseListResult<TItem> extends Pick<Virtualizer<HTMLDivElement, H
178
185
  *
179
186
  * return (
180
187
  * <div ref={list.containerRef}>
181
- * <ul style={{ height: `${list.getTotalSize()}px` }}>
188
+ * <ul ref={list.listContainerRef} className="relative">
182
189
  * {list.rows.map(row => {
183
190
  * const props = list.getListItemProps(row, { className: 'list-item' });
184
191
  * return <li {...props}>{row.item?.title}</li>;
@@ -1,4 +1,4 @@
1
- import { type Virtualizer } from "@tanstack/react-virtual";
1
+ import { type ReactVirtualizer, type Virtualizer } from "@tanstack/react-virtual";
2
2
  import { type RefObject } from "react";
3
3
  import type { RelayPagination } from "./useRelayPagination";
4
4
  interface BidirectionalScrollProps<TScrollElement extends Element, TItemElement extends Element> {
@@ -10,6 +10,15 @@ interface BidirectionalScrollProps<TScrollElement extends Element, TItemElement
10
10
  readonly onChange?: (virtualizer: Virtualizer<TScrollElement, TItemElement>) => void;
11
11
  readonly skip?: boolean;
12
12
  readonly onTopItemChange?: (index: number) => void;
13
+ /**
14
+ * Derives a stable per-item key from the index. Must agree with the React
15
+ * `key` used by the consumer's rendered items, otherwise the virtualizer's
16
+ * `elementsCache` (keyed by this value) and React's reconciliation (keyed
17
+ * by the rendered `key`) can disagree when indexes shift without a remount
18
+ * (e.g. sorting, filtering, or prepending pages), causing `directDomUpdates`
19
+ * to position rows incorrectly. Defaults to the item index.
20
+ */
21
+ readonly getItemKey?: (index: number) => string | number;
13
22
  }
14
23
  /**
15
24
  * Bidirectional infinite scroll hook that supports both forward (nextPage)
@@ -19,5 +28,5 @@ interface BidirectionalScrollProps<TScrollElement extends Element, TItemElement
19
28
  * scroll position at both ends and triggers the appropriate pagination
20
29
  * direction.
21
30
  */
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>;
31
+ export declare const useBidirectionalScroll: <TScrollElement extends Element, TItemElement extends Element>({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip, onTopItemChange, getItemKey, }: BidirectionalScrollProps<TScrollElement, TItemElement>) => ReactVirtualizer<TScrollElement, TItemElement>;
23
32
  export {};
@@ -1,4 +1,4 @@
1
- import { type Virtualizer } from "@tanstack/react-virtual";
1
+ import { type ReactVirtualizer, type Virtualizer } from "@tanstack/react-virtual";
2
2
  import { RefObject } from "react";
3
3
  import { RelayPagination } from "./useRelayPagination";
4
4
  interface InfiniteScrollProps<TScrollElement extends Element, TItemElement extends Element> {
@@ -21,7 +21,7 @@ interface InfiniteScrollProps<TScrollElement extends Element, TItemElement exten
21
21
  * @param props.overscan - Optional number of items to render outside the visible area.
22
22
  * @param props.onChange - Optional callback when virtualizer changes.
23
23
  * @param props.skip - Whether to skip automatic loading of more data (default: false).
24
- * @returns {Virtualizer} The virtualizer instance with all its properties and methods.
24
+ * @returns {ReactVirtualizer} The virtualizer instance with all its properties and methods, including `containerRef` for direct DOM positioning.
25
25
  * @description
26
26
  * This hook is used to implement infinite scrolling in a table. It uses TanStack Virtual's
27
27
  * built-in capabilities for virtualization and automatically loads more data when scrolling
@@ -34,5 +34,5 @@ interface InfiniteScrollProps<TScrollElement extends Element, TItemElement exten
34
34
  * estimateSize: () => 35,
35
35
  * });
36
36
  */
37
- export declare const useInfiniteScroll: <TScrollElement extends Element, TItemElement extends Element>({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip, }: InfiniteScrollProps<TScrollElement, TItemElement>) => Virtualizer<TScrollElement, TItemElement>;
37
+ export declare const useInfiniteScroll: <TScrollElement extends Element, TItemElement extends Element>({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, skip, }: InfiniteScrollProps<TScrollElement, TItemElement>) => ReactVirtualizer<TScrollElement, TItemElement>;
38
38
  export {};