@trackunit/react-components 1.9.20 → 1.9.21

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
@@ -3579,7 +3579,7 @@ const ListLoadingIndicator = ({ type, hasThumbnail, thumbnailShape, hasDescripti
3579
3579
  */
3580
3580
  const List = ({ children, className, dataTestId, topSeparatorOnScroll = false, separator = "line",
3581
3581
  // UseListResult properties
3582
- containerRef, listRef, rows, getListItemProps, header, loadingIndicator, isScrolling, scrollOffset,
3582
+ containerRef, listRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, scrollOffset,
3583
3583
  // Unused but part of UseListResult interface
3584
3584
  getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }) => {
3585
3585
  return (jsxRuntime.jsx("div", { className: cvaListContainer({
@@ -3597,10 +3597,12 @@ getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset:
3597
3597
  type: "skeleton",
3598
3598
  ...DEFAULT_SKELETON_LIST_ITEM_PROPS,
3599
3599
  };
3600
- const hasHeader = rows.some(r => r.type === "header");
3601
- const dataRowCount = rows.filter(r => r.type === "data").length;
3602
- const loaderIndex = row.virtualRow.index - dataRowCount - (hasHeader ? 1 : 0);
3603
- const shouldShowLoader = loadingConfig.type !== "none" && loaderIndex < (loadingConfig.initialLoadingCount ?? 10);
3600
+ // Use the total data count from useList, not visible rows (which are virtualized)
3601
+ const hasHeaderRow = !!header;
3602
+ const dataStartIndex = hasHeaderRow ? 1 : 0;
3603
+ const totalDataRows = dataStartIndex + count;
3604
+ const loaderIndex = row.virtualRow.index - totalDataRows;
3605
+ const shouldShowLoader = shouldShowLoaderAtIndex(loaderIndex);
3604
3606
  return (jsxRuntime.jsx("li", { ...listItemProps, children: shouldShowLoader ? jsxRuntime.jsx(ListLoadingIndicator, { ...loadingConfig }) : null }, key));
3605
3607
  }
3606
3608
  // Render header row
@@ -3612,6 +3614,11 @@ getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset:
3612
3614
  }) }) }));
3613
3615
  };
3614
3616
 
3617
+ const DEFAULT_LOADING_INDICATOR_CONFIG = {
3618
+ type: "skeleton",
3619
+ initialLoadingCount: 10,
3620
+ scrollLoadingCount: 3,
3621
+ };
3615
3622
  /**
3616
3623
  * A hook for managing virtualized list state and behavior.
3617
3624
  *
@@ -3644,7 +3651,7 @@ getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset:
3644
3651
  * );
3645
3652
  * ```
3646
3653
  */
3647
- const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowClick, onChange, estimateItemSize, estimateHeaderSize, overscan, }) => {
3654
+ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAULT_LOADING_INDICATOR_CONFIG, onRowClick, onChange, estimateItemSize, estimateHeaderSize, overscan, }) => {
3648
3655
  const containerRef = react.useRef(null);
3649
3656
  const listRef = react.useRef(null);
3650
3657
  const rowRefsMap = react.useRef(new Map());
@@ -3655,8 +3662,6 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowCl
3655
3662
  const totalDataRows = dataStartIndex + dataCount;
3656
3663
  // Calculate how many loading rows we need
3657
3664
  const getLoadingRowsCount = react.useCallback(() => {
3658
- if (!loadingIndicator)
3659
- return 0;
3660
3665
  if (pagination?.isLoading === false)
3661
3666
  return 0;
3662
3667
  const { type: loadingIndicatorType } = loadingIndicator;
@@ -3677,15 +3682,49 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowCl
3677
3682
  }
3678
3683
  }
3679
3684
  }, [loadingIndicator, pagination?.isLoading, pagination?.pageInfo]);
3680
- const totalRowCount = react.useMemo(() => totalDataRows + (pagination?.isLoading === true ? getLoadingRowsCount() : 0), [totalDataRows, pagination?.isLoading, getLoadingRowsCount]);
3685
+ // Helper to determine if a specific loader index should be shown
3686
+ const shouldShowLoaderAtIndex = react.useCallback((loaderIndex) => {
3687
+ if (pagination?.isLoading === false)
3688
+ return false;
3689
+ const { type: loadingIndicatorType } = loadingIndicator;
3690
+ let result;
3691
+ switch (loadingIndicatorType) {
3692
+ case "none":
3693
+ result = false;
3694
+ break;
3695
+ case "spinner":
3696
+ result = loaderIndex === 0;
3697
+ break;
3698
+ case "custom":
3699
+ case "skeleton": {
3700
+ const isInitialLoading = !pagination?.pageInfo;
3701
+ const initialCount = loadingIndicator.initialLoadingCount ?? 10;
3702
+ const scrollCount = loadingIndicator.scrollLoadingCount ?? 3;
3703
+ const maxCount = isInitialLoading ? initialCount : scrollCount;
3704
+ result = loaderIndex < maxCount;
3705
+ break;
3706
+ }
3707
+ default: {
3708
+ throw new Error(`${loadingIndicatorType} is not known`);
3709
+ }
3710
+ }
3711
+ return result;
3712
+ }, [loadingIndicator, pagination?.isLoading, pagination?.pageInfo]);
3713
+ const totalRowCount = react.useMemo(() => {
3714
+ // Only add the exact number of loading rows we want to show
3715
+ const loadingRows = pagination?.isLoading === true ? getLoadingRowsCount() : 0;
3716
+ const total = totalDataRows + loadingRows;
3717
+ return total;
3718
+ }, [totalDataRows, pagination?.isLoading, getLoadingRowsCount]);
3681
3719
  // Estimate size for all rows (header, data, loading)
3682
3720
  const estimateSize = react.useCallback((index) => {
3683
3721
  // Loading rows
3684
3722
  if (index >= totalDataRows) {
3685
3723
  const loaderIndex = index - totalDataRows;
3686
- const shouldShowLoader = pagination?.isLoading === true && loaderIndex < getLoadingRowsCount();
3724
+ const shouldShowLoader = shouldShowLoaderAtIndex(loaderIndex);
3725
+ const estimatedHeight = shouldShowLoader ? estimateItemSize(0) : 0;
3687
3726
  // Empty loader rows should be estimated at 0 height to prevent blank space
3688
- return shouldShowLoader ? estimateItemSize(0) : 0;
3727
+ return estimatedHeight;
3689
3728
  }
3690
3729
  // Header row (if exists, it's always at index 0)
3691
3730
  if (hasHeader && index === 0) {
@@ -3695,15 +3734,7 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowCl
3695
3734
  // Data rows - calculate the data index
3696
3735
  const dataIndex = index - dataStartIndex;
3697
3736
  return estimateItemSize(dataIndex);
3698
- }, [
3699
- estimateItemSize,
3700
- estimateHeaderSize,
3701
- totalDataRows,
3702
- pagination?.isLoading,
3703
- getLoadingRowsCount,
3704
- hasHeader,
3705
- dataStartIndex,
3706
- ]);
3737
+ }, [estimateItemSize, estimateHeaderSize, totalDataRows, shouldShowLoaderAtIndex, hasHeader, dataStartIndex]);
3707
3738
  // Set up virtualization
3708
3739
  const virtualizer = useInfiniteScroll({
3709
3740
  pagination: pagination ?? noPagination,
@@ -3735,7 +3766,8 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowCl
3735
3766
  }, []);
3736
3767
  // Transform virtual items into typed rows
3737
3768
  const rows = react.useMemo(() => {
3738
- return virtualizer.getVirtualItems().map((virtualRow) => {
3769
+ const virtualItems = virtualizer.getVirtualItems();
3770
+ return virtualItems.map((virtualRow) => {
3739
3771
  const { index } = virtualRow;
3740
3772
  // Determine row type
3741
3773
  const isLoaderRow = index >= totalDataRows;
@@ -3802,6 +3834,8 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowCl
3802
3834
  getListItemProps,
3803
3835
  header,
3804
3836
  loadingIndicator,
3837
+ shouldShowLoaderAtIndex,
3838
+ count,
3805
3839
  };
3806
3840
  };
3807
3841
 
package/index.esm.js CHANGED
@@ -3577,7 +3577,7 @@ const ListLoadingIndicator = ({ type, hasThumbnail, thumbnailShape, hasDescripti
3577
3577
  */
3578
3578
  const List = ({ children, className, dataTestId, topSeparatorOnScroll = false, separator = "line",
3579
3579
  // UseListResult properties
3580
- containerRef, listRef, rows, getListItemProps, header, loadingIndicator, isScrolling, scrollOffset,
3580
+ containerRef, listRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, scrollOffset,
3581
3581
  // Unused but part of UseListResult interface
3582
3582
  getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }) => {
3583
3583
  return (jsx("div", { className: cvaListContainer({
@@ -3595,10 +3595,12 @@ getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset:
3595
3595
  type: "skeleton",
3596
3596
  ...DEFAULT_SKELETON_LIST_ITEM_PROPS,
3597
3597
  };
3598
- const hasHeader = rows.some(r => r.type === "header");
3599
- const dataRowCount = rows.filter(r => r.type === "data").length;
3600
- const loaderIndex = row.virtualRow.index - dataRowCount - (hasHeader ? 1 : 0);
3601
- const shouldShowLoader = loadingConfig.type !== "none" && loaderIndex < (loadingConfig.initialLoadingCount ?? 10);
3598
+ // Use the total data count from useList, not visible rows (which are virtualized)
3599
+ const hasHeaderRow = !!header;
3600
+ const dataStartIndex = hasHeaderRow ? 1 : 0;
3601
+ const totalDataRows = dataStartIndex + count;
3602
+ const loaderIndex = row.virtualRow.index - totalDataRows;
3603
+ const shouldShowLoader = shouldShowLoaderAtIndex(loaderIndex);
3602
3604
  return (jsx("li", { ...listItemProps, children: shouldShowLoader ? jsx(ListLoadingIndicator, { ...loadingConfig }) : null }, key));
3603
3605
  }
3604
3606
  // Render header row
@@ -3610,6 +3612,11 @@ getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset:
3610
3612
  }) }) }));
3611
3613
  };
3612
3614
 
3615
+ const DEFAULT_LOADING_INDICATOR_CONFIG = {
3616
+ type: "skeleton",
3617
+ initialLoadingCount: 10,
3618
+ scrollLoadingCount: 3,
3619
+ };
3613
3620
  /**
3614
3621
  * A hook for managing virtualized list state and behavior.
3615
3622
  *
@@ -3642,7 +3649,7 @@ getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset:
3642
3649
  * );
3643
3650
  * ```
3644
3651
  */
3645
- const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowClick, onChange, estimateItemSize, estimateHeaderSize, overscan, }) => {
3652
+ const useList = ({ count, pagination, header, getItem, loadingIndicator = DEFAULT_LOADING_INDICATOR_CONFIG, onRowClick, onChange, estimateItemSize, estimateHeaderSize, overscan, }) => {
3646
3653
  const containerRef = useRef(null);
3647
3654
  const listRef = useRef(null);
3648
3655
  const rowRefsMap = useRef(new Map());
@@ -3653,8 +3660,6 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowCl
3653
3660
  const totalDataRows = dataStartIndex + dataCount;
3654
3661
  // Calculate how many loading rows we need
3655
3662
  const getLoadingRowsCount = useCallback(() => {
3656
- if (!loadingIndicator)
3657
- return 0;
3658
3663
  if (pagination?.isLoading === false)
3659
3664
  return 0;
3660
3665
  const { type: loadingIndicatorType } = loadingIndicator;
@@ -3675,15 +3680,49 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowCl
3675
3680
  }
3676
3681
  }
3677
3682
  }, [loadingIndicator, pagination?.isLoading, pagination?.pageInfo]);
3678
- const totalRowCount = useMemo(() => totalDataRows + (pagination?.isLoading === true ? getLoadingRowsCount() : 0), [totalDataRows, pagination?.isLoading, getLoadingRowsCount]);
3683
+ // Helper to determine if a specific loader index should be shown
3684
+ const shouldShowLoaderAtIndex = useCallback((loaderIndex) => {
3685
+ if (pagination?.isLoading === false)
3686
+ return false;
3687
+ const { type: loadingIndicatorType } = loadingIndicator;
3688
+ let result;
3689
+ switch (loadingIndicatorType) {
3690
+ case "none":
3691
+ result = false;
3692
+ break;
3693
+ case "spinner":
3694
+ result = loaderIndex === 0;
3695
+ break;
3696
+ case "custom":
3697
+ case "skeleton": {
3698
+ const isInitialLoading = !pagination?.pageInfo;
3699
+ const initialCount = loadingIndicator.initialLoadingCount ?? 10;
3700
+ const scrollCount = loadingIndicator.scrollLoadingCount ?? 3;
3701
+ const maxCount = isInitialLoading ? initialCount : scrollCount;
3702
+ result = loaderIndex < maxCount;
3703
+ break;
3704
+ }
3705
+ default: {
3706
+ throw new Error(`${loadingIndicatorType} is not known`);
3707
+ }
3708
+ }
3709
+ return result;
3710
+ }, [loadingIndicator, pagination?.isLoading, pagination?.pageInfo]);
3711
+ const totalRowCount = useMemo(() => {
3712
+ // Only add the exact number of loading rows we want to show
3713
+ const loadingRows = pagination?.isLoading === true ? getLoadingRowsCount() : 0;
3714
+ const total = totalDataRows + loadingRows;
3715
+ return total;
3716
+ }, [totalDataRows, pagination?.isLoading, getLoadingRowsCount]);
3679
3717
  // Estimate size for all rows (header, data, loading)
3680
3718
  const estimateSize = useCallback((index) => {
3681
3719
  // Loading rows
3682
3720
  if (index >= totalDataRows) {
3683
3721
  const loaderIndex = index - totalDataRows;
3684
- const shouldShowLoader = pagination?.isLoading === true && loaderIndex < getLoadingRowsCount();
3722
+ const shouldShowLoader = shouldShowLoaderAtIndex(loaderIndex);
3723
+ const estimatedHeight = shouldShowLoader ? estimateItemSize(0) : 0;
3685
3724
  // Empty loader rows should be estimated at 0 height to prevent blank space
3686
- return shouldShowLoader ? estimateItemSize(0) : 0;
3725
+ return estimatedHeight;
3687
3726
  }
3688
3727
  // Header row (if exists, it's always at index 0)
3689
3728
  if (hasHeader && index === 0) {
@@ -3693,15 +3732,7 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowCl
3693
3732
  // Data rows - calculate the data index
3694
3733
  const dataIndex = index - dataStartIndex;
3695
3734
  return estimateItemSize(dataIndex);
3696
- }, [
3697
- estimateItemSize,
3698
- estimateHeaderSize,
3699
- totalDataRows,
3700
- pagination?.isLoading,
3701
- getLoadingRowsCount,
3702
- hasHeader,
3703
- dataStartIndex,
3704
- ]);
3735
+ }, [estimateItemSize, estimateHeaderSize, totalDataRows, shouldShowLoaderAtIndex, hasHeader, dataStartIndex]);
3705
3736
  // Set up virtualization
3706
3737
  const virtualizer = useInfiniteScroll({
3707
3738
  pagination: pagination ?? noPagination,
@@ -3733,7 +3764,8 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowCl
3733
3764
  }, []);
3734
3765
  // Transform virtual items into typed rows
3735
3766
  const rows = useMemo(() => {
3736
- return virtualizer.getVirtualItems().map((virtualRow) => {
3767
+ const virtualItems = virtualizer.getVirtualItems();
3768
+ return virtualItems.map((virtualRow) => {
3737
3769
  const { index } = virtualRow;
3738
3770
  // Determine row type
3739
3771
  const isLoaderRow = index >= totalDataRows;
@@ -3800,6 +3832,8 @@ const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowCl
3800
3832
  getListItemProps,
3801
3833
  header,
3802
3834
  loadingIndicator,
3835
+ shouldShowLoaderAtIndex,
3836
+ count,
3803
3837
  };
3804
3838
  };
3805
3839
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "1.9.20",
3
+ "version": "1.9.21",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -100,4 +100,4 @@ export interface ListProps<TItem = unknown> extends CommonProps, UseListResult<T
100
100
  * );
101
101
  * ```
102
102
  */
103
- export declare const List: <TItem = unknown>({ children, className, dataTestId, topSeparatorOnScroll, separator, containerRef, listRef, rows, getListItemProps, header, loadingIndicator, isScrolling, scrollOffset, getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }: ListProps<TItem>) => ReactElement;
103
+ export declare const List: <TItem = unknown>({ children, className, dataTestId, topSeparatorOnScroll, separator, containerRef, listRef, rows, getListItemProps, header, loadingIndicator, shouldShowLoaderAtIndex, count, isScrolling, scrollOffset, getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }: ListProps<TItem>) => ReactElement;
@@ -121,16 +121,20 @@ export interface UseListResult<TItem> extends Pick<Virtualizer<HTMLDivElement, H
121
121
  /** Reference to attach to the list */
122
122
  listRef: RefObject<HTMLUListElement | null>;
123
123
  /** Array of row data to render */
124
- rows: Array<ListRow<TItem>>;
124
+ readonly rows: ReadonlyArray<ListRow<TItem>>;
125
125
  /** Helper to create list item props for a given row */
126
- getListItemProps: (row: ListRow<TItem>, options: {
126
+ readonly getListItemProps: (row: ListRow<TItem>, options: {
127
127
  className: string;
128
128
  onClick?: () => void;
129
129
  }) => VirtualizationListItemProps;
130
130
  /** Header element (if provided) */
131
- header?: ReactElement;
131
+ readonly header?: ReactElement;
132
132
  /** Loading indicator configuration */
133
- loadingIndicator?: ListLoadingIndicatorProps;
133
+ readonly loadingIndicator?: ListLoadingIndicatorProps;
134
+ /** Helper to determine if a specific loader index should be shown */
135
+ readonly shouldShowLoaderAtIndex: (loaderIndex: number) => boolean;
136
+ /** Total number of data items (for consistent calculations) */
137
+ readonly count: number;
134
138
  }
135
139
  /**
136
140
  * A hook for managing virtualized list state and behavior.