@trackunit/react-components 1.9.18 → 1.9.20

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
@@ -12,12 +12,12 @@ var IconSpriteSolid = require('@trackunit/ui-icons/icons-sprite-solid.svg');
12
12
  var stringTs = require('string-ts');
13
13
  var cssClassVarianceUtilities = require('@trackunit/css-class-variance-utilities');
14
14
  var reactSlot = require('@radix-ui/react-slot');
15
+ var reactVirtual = require('@tanstack/react-virtual');
15
16
  var usehooksTs = require('usehooks-ts');
16
17
  var reactRouter = require('@tanstack/react-router');
17
18
  var react$1 = require('@floating-ui/react');
18
19
  var esToolkit = require('es-toolkit');
19
20
  var tailwindMerge = require('tailwind-merge');
20
- var reactTablePagination = require('@trackunit/react-table-pagination');
21
21
  var reactHelmetAsync = require('react-helmet-async');
22
22
  var reactTabs = require('@radix-ui/react-tabs');
23
23
 
@@ -993,6 +993,21 @@ const Badge = ({ color = "primary", size = "default", compact = false, className
993
993
  return (jsxRuntime.jsx("span", { className: cvaBadge({ color, size, className, compact, isSingleChar }), "data-testid": dataTestId, children: compact ? null : displayedCount }));
994
994
  };
995
995
 
996
+ const noPagination = {
997
+ isLoading: false,
998
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
999
+ nextPage: () => { },
1000
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
1001
+ previousPage: () => { },
1002
+ pageInfo: {
1003
+ count: null,
1004
+ hasNextPage: false,
1005
+ hasPreviousPage: false,
1006
+ endCursor: null,
1007
+ startCursor: null,
1008
+ },
1009
+ };
1010
+
996
1011
  /**
997
1012
  * Custom hook to handle click outside events.
998
1013
  *
@@ -1381,6 +1396,73 @@ const useHover = ({ debounced = false, delay = 100, direction = "out" } = { debo
1381
1396
  return { onMouseEnter, onMouseLeave, hovering: debounced ? debouncedHovering : hovering };
1382
1397
  };
1383
1398
 
1399
+ const OVERSCAN = 10;
1400
+ const DEFAULT_ROW_HEIGHT = 50;
1401
+ /**
1402
+ * Custom hook for implementing infinite scrolling in a table using TanStack Virtual.
1403
+ *
1404
+ * @param props - The configuration object for the infinite scroll hook.
1405
+ * @param props.pagination - The relay pagination object for managing data loading.
1406
+ * @param props.scrollElementRef - Reference to the scrollable container element.
1407
+ * @param props.count - Total number of items to virtualize.
1408
+ * @param props.estimateSize - Optional function to estimate item height.
1409
+ * @param props.overscan - Optional number of items to render outside the visible area.
1410
+ * @param props.onChange - Optional callback when virtualizer changes.
1411
+ * @returns {Virtualizer} The virtualizer instance with all its properties and methods.
1412
+ * @description
1413
+ * This hook is used to implement infinite scrolling in a table. It uses TanStack Virtual's
1414
+ * built-in capabilities for virtualization and automatically loads more data when scrolling
1415
+ * approaches the end of the available content.
1416
+ * @example
1417
+ * const virtualizer = useInfiniteScroll<HTMLDivElement, HTMLDivElement>({
1418
+ * pagination: relayPaginationObject,
1419
+ * scrollElementRef: tableScrollElementRef,
1420
+ * count: 50,
1421
+ * estimateSize: () => 35,
1422
+ * });
1423
+ */
1424
+ const useInfiniteScroll = ({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, }) => {
1425
+ const handleChange = react.useCallback((virtualizer) => {
1426
+ onChange?.(virtualizer);
1427
+ }, [onChange]);
1428
+ const handleEstimateSize = react.useCallback((index) => estimateSize?.(index) ?? DEFAULT_ROW_HEIGHT, [estimateSize]);
1429
+ const rowVirtualizer = reactVirtual.useVirtualizer({
1430
+ count,
1431
+ getScrollElement: () => scrollElementRef.current,
1432
+ estimateSize: handleEstimateSize,
1433
+ overscan: overscan ?? OVERSCAN,
1434
+ onChange: handleChange,
1435
+ });
1436
+ const virtualItems = rowVirtualizer.getVirtualItems();
1437
+ // Auto-load more data based on scroll position and content height
1438
+ react.useEffect(() => {
1439
+ if (pagination.pageInfo?.hasNextPage !== true || pagination.isLoading) {
1440
+ return;
1441
+ }
1442
+ const container = scrollElementRef.current;
1443
+ if (!container) {
1444
+ return;
1445
+ }
1446
+ // Check if we should load more data based on virtual items (scroll-based loading)
1447
+ const [lastItem] = [...virtualItems].reverse();
1448
+ const shouldLoadFromScroll = lastItem !== undefined && lastItem.index >= count - 1;
1449
+ // Check if content is insufficient to fill container (initial loading)
1450
+ const shouldLoadFromInsufficientContent = container.scrollHeight <= container.clientHeight;
1451
+ if (shouldLoadFromScroll || shouldLoadFromInsufficientContent) {
1452
+ pagination.nextPage();
1453
+ }
1454
+ }, [
1455
+ pagination.pageInfo?.hasNextPage,
1456
+ pagination.nextPage,
1457
+ pagination.isLoading,
1458
+ pagination,
1459
+ scrollElementRef,
1460
+ virtualItems,
1461
+ count,
1462
+ ]);
1463
+ return rowVirtualizer;
1464
+ };
1465
+
1384
1466
  /**
1385
1467
  * Differentiate between the first and subsequent renders.
1386
1468
  *
@@ -1483,6 +1565,60 @@ const useModifierKey = ({ exclude = [] } = {}) => {
1483
1565
  return isModifierPressed;
1484
1566
  };
1485
1567
 
1568
+ const defaultPageSize = 50;
1569
+ /**
1570
+ * Custom hook for handling Relay pagination in tables.
1571
+ *
1572
+ * @param {RelayPaginationProps} props - The props object containing pagination configuration.
1573
+ * @returns {RelayPaginationSupport} An object containing functions and state for managing Relay pagination.
1574
+ */
1575
+ const useRelayPagination = ({ onReset, pageSize } = { pageSize: defaultPageSize }) => {
1576
+ const [variables, setVariables] = react.useState({ first: pageSize });
1577
+ const [pageInfo, setPageInfo] = react.useState();
1578
+ const [isLoading, setIsLoading] = react.useState(true);
1579
+ const nextPage = react.useCallback(() => {
1580
+ if (pageInfo?.hasNextPage === true) {
1581
+ setVariables({
1582
+ after: pageInfo.endCursor === null ? undefined : pageInfo.endCursor,
1583
+ first: pageSize,
1584
+ });
1585
+ }
1586
+ }, [pageInfo?.endCursor, pageInfo?.hasNextPage, pageSize]);
1587
+ const previousPage = react.useCallback(() => {
1588
+ if (pageInfo?.hasPreviousPage === true) {
1589
+ setVariables({
1590
+ before: pageInfo.startCursor === null ? undefined : pageInfo.startCursor,
1591
+ last: pageSize,
1592
+ });
1593
+ }
1594
+ }, [pageInfo?.hasPreviousPage, pageInfo?.startCursor, pageSize]);
1595
+ const reset = react.useCallback(() => {
1596
+ setVariables({
1597
+ last: undefined,
1598
+ before: undefined,
1599
+ after: undefined,
1600
+ first: pageSize,
1601
+ });
1602
+ if (onReset) {
1603
+ onReset();
1604
+ }
1605
+ }, [onReset, pageSize, setVariables]);
1606
+ return react.useMemo(() => {
1607
+ return {
1608
+ variables,
1609
+ table: {
1610
+ nextPage,
1611
+ previousPage,
1612
+ isLoading,
1613
+ setIsLoading,
1614
+ reset,
1615
+ pageInfo: pageInfo === null ? undefined : pageInfo,
1616
+ setPageInfo,
1617
+ },
1618
+ };
1619
+ }, [variables, nextPage, previousPage, isLoading, reset, pageInfo]);
1620
+ };
1621
+
1486
1622
  /**
1487
1623
  * Custom hook to handle window resize events and provide the current window size.
1488
1624
  *
@@ -3333,7 +3469,7 @@ const ListItemSkeleton = ({ hasThumbnail = DEFAULT_SKELETON_LIST_ITEM_PROPS.hasT
3333
3469
  return (jsxRuntime.jsxs("div", { className: cvaListItem$1({ className: "w-full" }), children: [jsxRuntime.jsxs("div", { className: cvaMainInformationClass({ hasThumbnail, className: "w-full" }), children: [hasThumbnail ? (jsxRuntime.jsx("div", { className: cvaThumbnailContainer({ className: "bg-gray-200" }), children: jsxRuntime.jsx("div", { className: tailwindMerge.twMerge("bg-gray-300", thumbnailShape === "circle" ? "rounded-full" : "rounded"), style: { width: 20, height: 20 } }) })) : null, jsxRuntime.jsxs("div", { className: "grid-rows-min-fr grid w-full items-center gap-1 text-sm", children: [jsxRuntime.jsx(SkeletonLines, { height: "0.875em", lines: 1, width: lineWidths.title }), hasDescription ? jsxRuntime.jsx(SkeletonLines, { height: "0.75em", lines: 1, width: lineWidths.description }) : null, hasMeta ? jsxRuntime.jsx(SkeletonLines, { height: "0.75em", lines: 1, width: lineWidths.meta }) : null] })] }), hasDetails ? (jsxRuntime.jsx("div", { className: "pl-2 text-sm", children: jsxRuntime.jsx(SkeletonLines, { height: "0.875em", lines: 1, width: lineWidths.details }) })) : null] }));
3334
3470
  };
3335
3471
 
3336
- const cvaListContainer = cssClassVarianceUtilities.cvaMerge(["overflow-auto", "h-full"], {
3472
+ const cvaListContainer = cssClassVarianceUtilities.cvaMerge(["overflow-y-auto", "overflow-x-hidden", "h-full"], {
3337
3473
  variants: {
3338
3474
  withTopSeparator: {
3339
3475
  true: ["border-t", "border-neutral-200", "transition-colors duration-200 ease-in"],
@@ -3376,12 +3512,14 @@ const ListLoadingIndicator = ({ type, hasThumbnail, thumbnailShape, hasDescripti
3376
3512
  }
3377
3513
  };
3378
3514
 
3379
- const DEFAULT_ROW_HEIGHT = 61; // 61px is the height of the ListItem component as of 2025-09-26
3380
3515
  /**
3381
3516
  * A performant virtualized list component with infinite scrolling support.
3382
3517
  *
3383
3518
  * ⚠️ **Important**: Requires a container with defined height to work properly.
3384
3519
  *
3520
+ * **Usage Pattern**: Always use the `useList` hook in your component and spread the result into this component.
3521
+ * This gives you access to the virtualizer state (scroll position, isScrolling, etc.) in the parent.
3522
+ *
3385
3523
  * Features:
3386
3524
  * - Virtualized rendering using TanStack Virtual for performance with large datasets
3387
3525
  * - Automatic infinite scroll loading when approaching the end of the list
@@ -3390,24 +3528,138 @@ const DEFAULT_ROW_HEIGHT = 61; // 61px is the height of the ListItem component a
3390
3528
  * - Configurable loading indicators (skeleton, spinner, or custom)
3391
3529
  * - Scroll state detection and callbacks
3392
3530
  * - Variable-height item support via `estimateItemSize`
3531
+ * - Dynamic measurement for accurate positioning of variable-height items
3393
3532
  *
3394
3533
  * The component automatically loads more data when:
3395
3534
  * - User scrolls to the last visible item
3396
3535
  * - Content height is insufficient to fill the container
3536
+ *
3537
+ * **Headers with Different Heights**: When using a header that differs in height from list items,
3538
+ * provide `estimateHeaderSize` for optimal initial rendering. The list automatically measures
3539
+ * actual heights on mount to ensure correct positioning.
3540
+ *
3541
+ * @example Basic usage
3542
+ * ```tsx
3543
+ * const list = useList({
3544
+ * count: items.length,
3545
+ * getItem: index => items[index],
3546
+ * estimateItemSize: () => 61,
3547
+ * });
3548
+ *
3549
+ * return (
3550
+ * <List {...list}>
3551
+ * {({ key, listItemProps, item }) => (
3552
+ * <ListItem key={key} {...listItemProps} title={item?.name} />
3553
+ * )}
3554
+ * </List>
3555
+ * );
3556
+ * ```
3557
+ * @example With header
3558
+ * ```tsx
3559
+ * const list = useList({
3560
+ * count: items.length,
3561
+ * getItem: index => items[index],
3562
+ * header: <KPI title="Total" value={42} />,
3563
+ * estimateHeaderSize: () => 72, // Actual KPI height
3564
+ * estimateItemSize: () => 61, // Actual ListItem height
3565
+ * });
3566
+ *
3567
+ * // Access virtualizer state in parent
3568
+ * console.log('Scroll position:', list.scrollOffset);
3569
+ * console.log('Is scrolling:', list.isScrolling);
3570
+ *
3571
+ * return (
3572
+ * <List {...list}>
3573
+ * {({ key, listItemProps, item }) => (
3574
+ * <ListItem key={key} {...listItemProps} title={item?.name} />
3575
+ * )}
3576
+ * </List>
3577
+ * );
3578
+ * ```
3579
+ */
3580
+ const List = ({ children, className, dataTestId, topSeparatorOnScroll = false, separator = "line",
3581
+ // UseListResult properties
3582
+ containerRef, listRef, rows, getListItemProps, header, loadingIndicator, isScrolling, scrollOffset,
3583
+ // Unused but part of UseListResult interface
3584
+ getTotalSize: _getTotalSize, getVirtualItems: _getVirtualItems, scrollToOffset: _scrollToOffset, scrollToIndex: _scrollToIndex, measure: _measure, }) => {
3585
+ return (jsxRuntime.jsx("div", { className: cvaListContainer({
3586
+ withTopSeparator: topSeparatorOnScroll && (scrollOffset ?? 0) > 0,
3587
+ className,
3588
+ }), "data-is-scrolling": isScrolling, "data-testid": dataTestId, ref: containerRef, children: jsxRuntime.jsx("ul", { className: cvaList(), ref: listRef, children: rows.map(row => {
3589
+ // Generate list item props with separator styling
3590
+ const listItemProps = getListItemProps(row, {
3591
+ className: cvaListItem({ separator }),
3592
+ });
3593
+ const key = row.virtualRow.key;
3594
+ // Render loading row
3595
+ if (row.type === "loader") {
3596
+ const loadingConfig = loadingIndicator ?? {
3597
+ type: "skeleton",
3598
+ ...DEFAULT_SKELETON_LIST_ITEM_PROPS,
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);
3604
+ return (jsxRuntime.jsx("li", { ...listItemProps, children: shouldShowLoader ? jsxRuntime.jsx(ListLoadingIndicator, { ...loadingConfig }) : null }, key));
3605
+ }
3606
+ // Render header row
3607
+ if (row.type === "header") {
3608
+ return (jsxRuntime.jsx("li", { ...listItemProps, children: header }, key));
3609
+ }
3610
+ // Render data row
3611
+ return children({ key, listItemProps, item: row.item, index: row.dataIndex });
3612
+ }) }) }));
3613
+ };
3614
+
3615
+ /**
3616
+ * A hook for managing virtualized list state and behavior.
3617
+ *
3618
+ * This hook encapsulates the logic for:
3619
+ * - Virtualizing list items using TanStack Virtual
3620
+ * - Managing infinite scroll pagination
3621
+ * - Handling header, data, and loading rows
3622
+ * - Calculating proper indices and measurements
3623
+ *
3624
+ * @example
3625
+ * ```tsx
3626
+ * const list = useList({
3627
+ * count: items.length,
3628
+ * getItem: index => items[index],
3629
+ * pagination,
3630
+ * header: <KPI title="Total" value={42} />,
3631
+ * estimateHeaderSize: () => 72, // Measure actual KPI height
3632
+ * estimateItemSize: () => 61, // Measure actual ListItem height
3633
+ * });
3634
+ *
3635
+ * return (
3636
+ * <div ref={list.containerRef}>
3637
+ * <ul style={{ height: `${list.getTotalSize()}px` }}>
3638
+ * {list.rows.map(row => {
3639
+ * const props = list.getListItemProps(row, { className: 'list-item' });
3640
+ * return <li {...props}>{row.item?.title}</li>;
3641
+ * })}
3642
+ * </ul>
3643
+ * </div>
3644
+ * );
3645
+ * ```
3397
3646
  */
3398
- const List = ({ count, pagination, children, className, dataTestId, separator = "line", loadingIndicator = { type: "skeleton", ...DEFAULT_SKELETON_LIST_ITEM_PROPS }, onRowClick, onScrollStateChange, topSeparatorOnScroll = false, estimateItemSize, header, getItem, }) => {
3399
- const parentRef = react.useRef(null);
3400
- // Calculate the actual count including header
3401
- const actualCount = count + (header ? 1 : 0);
3402
- // Helper function to get item for a given data index
3403
- const getItemAtIndex = react.useCallback((dataIndex) => {
3404
- return getItem(dataIndex);
3405
- }, [getItem]);
3647
+ const useList = ({ count, pagination, header, getItem, loadingIndicator, onRowClick, onChange, estimateItemSize, estimateHeaderSize, overscan, }) => {
3648
+ const containerRef = react.useRef(null);
3649
+ const listRef = react.useRef(null);
3650
+ const rowRefsMap = react.useRef(new Map());
3651
+ // Calculate total count including header
3652
+ const hasHeader = Boolean(header);
3653
+ const dataStartIndex = hasHeader ? 1 : 0;
3654
+ const dataCount = count;
3655
+ const totalDataRows = dataStartIndex + dataCount;
3406
3656
  // Calculate how many loading rows we need
3407
3657
  const getLoadingRowsCount = react.useCallback(() => {
3408
- const { type: loadingIndicatorType } = loadingIndicator;
3658
+ if (!loadingIndicator)
3659
+ return 0;
3409
3660
  if (pagination?.isLoading === false)
3410
3661
  return 0;
3662
+ const { type: loadingIndicatorType } = loadingIndicator;
3411
3663
  switch (loadingIndicatorType) {
3412
3664
  case "none":
3413
3665
  return 0;
@@ -3424,64 +3676,181 @@ const List = ({ count, pagination, children, className, dataTestId, separator =
3424
3676
  throw new Error(`${loadingIndicatorType} is not known`);
3425
3677
  }
3426
3678
  }
3427
- }, [loadingIndicator, pagination]);
3679
+ }, [loadingIndicator, pagination?.isLoading, pagination?.pageInfo]);
3680
+ const totalRowCount = react.useMemo(() => totalDataRows + (pagination?.isLoading === true ? getLoadingRowsCount() : 0), [totalDataRows, pagination?.isLoading, getLoadingRowsCount]);
3681
+ // Estimate size for all rows (header, data, loading)
3428
3682
  const estimateSize = react.useCallback((index) => {
3429
- // Check if this is a loading row
3430
- if (index >= actualCount) {
3431
- const loaderIndex = index - actualCount;
3683
+ // Loading rows
3684
+ if (index >= totalDataRows) {
3685
+ const loaderIndex = index - totalDataRows;
3432
3686
  const shouldShowLoader = pagination?.isLoading === true && loaderIndex < getLoadingRowsCount();
3433
3687
  // Empty loader rows should be estimated at 0 height to prevent blank space
3434
- return shouldShowLoader ? DEFAULT_ROW_HEIGHT : 0;
3688
+ return shouldShowLoader ? estimateItemSize(0) : 0;
3435
3689
  }
3436
- // For data rows (including header), use custom estimator if provided
3437
- return estimateItemSize ? estimateItemSize(index) : DEFAULT_ROW_HEIGHT;
3438
- }, [estimateItemSize, actualCount, pagination?.isLoading, getLoadingRowsCount]);
3439
- const { getVirtualItems, getTotalSize, measureElement, scrollOffset } = reactTablePagination.useInfiniteScroll({
3440
- pagination: pagination || reactTablePagination.noPagination,
3441
- containerRef: parentRef,
3442
- count: actualCount + (pagination?.isLoading === true ? getLoadingRowsCount() : 0),
3690
+ // Header row (if exists, it's always at index 0)
3691
+ if (hasHeader && index === 0) {
3692
+ // Use dedicated header size estimate if provided, otherwise fall back to first item estimate
3693
+ return estimateHeaderSize ? estimateHeaderSize() : estimateItemSize(0);
3694
+ }
3695
+ // Data rows - calculate the data index
3696
+ const dataIndex = index - dataStartIndex;
3697
+ return estimateItemSize(dataIndex);
3698
+ }, [
3699
+ estimateItemSize,
3700
+ estimateHeaderSize,
3701
+ totalDataRows,
3702
+ pagination?.isLoading,
3703
+ getLoadingRowsCount,
3704
+ hasHeader,
3705
+ dataStartIndex,
3706
+ ]);
3707
+ // Set up virtualization
3708
+ const virtualizer = useInfiniteScroll({
3709
+ pagination: pagination ?? noPagination,
3710
+ scrollElementRef: containerRef,
3711
+ count: totalRowCount,
3443
3712
  estimateSize,
3444
- onChange: virtualizer => onScrollStateChange?.(virtualizer.scrollOffset ?? 0, virtualizer.isScrolling),
3713
+ overscan,
3714
+ onChange: instance => {
3715
+ if (listRef.current) {
3716
+ listRef.current.style.height = `${instance.getTotalSize()}px`;
3717
+ }
3718
+ // Apply transforms to all virtual items
3719
+ instance.getVirtualItems().forEach(virtualRow => {
3720
+ const rowRef = rowRefsMap.current.get(virtualRow.index);
3721
+ if (rowRef) {
3722
+ rowRef.style.transform = `translateY(${virtualRow.start}px)`;
3723
+ }
3724
+ });
3725
+ onChange?.(instance);
3726
+ },
3445
3727
  });
3446
- const isAtTop = scrollOffset <= 0;
3447
- return (jsxRuntime.jsx("div", { className: cvaListContainer({
3448
- withTopSeparator: topSeparatorOnScroll && !isAtTop,
3449
- className,
3450
- }), "data-testid": dataTestId, ref: parentRef, children: jsxRuntime.jsx("ul", { className: cvaList(), style: { height: `${getTotalSize()}px` }, children: getVirtualItems().map(virtualRow => {
3451
- const isLoaderRow = virtualRow.index >= actualCount;
3452
- const isHeaderRow = Boolean(header) && virtualRow.index === 0;
3453
- // Calculate data index: if header exists, subtract 1 from index for data items
3454
- const dataIndex = Boolean(header) && !isHeaderRow ? virtualRow.index - 1 : virtualRow.index;
3455
- // Calculate which loading indicator this is (for multiple loading indicators)
3456
- const loaderIndex = isLoaderRow ? virtualRow.index - actualCount : 0;
3457
- // Props required by the virtualizer for proper positioning and behavior
3458
- const listItemProps = {
3459
- className: cvaListItem({ separator }), // List styling (separators, spacing)
3460
- "data-index": isLoaderRow || isHeaderRow ? virtualRow.index : dataIndex, // For accessibility and debugging
3461
- onClick: onRowClick && !isLoaderRow && !isHeaderRow
3462
- ? () => {
3463
- const clickedItem = getItemAtIndex(dataIndex);
3464
- onRowClick(clickedItem, dataIndex);
3465
- }
3466
- : undefined, // Row-level click handling (skip header)
3467
- ref: measureElement, // Required for virtualizer to measure item dimensions
3468
- style: {
3469
- transform: `translateY(${virtualRow.start}px)`, // Critical: positions item in virtual scroll
3470
- },
3471
- tabIndex: -1, // Keyboard navigation support
3728
+ // Measure the list on mount
3729
+ react.useLayoutEffect(() => {
3730
+ // Automatically measure if header is present, or if explicitly requested
3731
+ virtualizer.measure();
3732
+ // This is straight out of the TanStack Virtual docs, so we'll trust it.
3733
+ // eslint-disable-next-line react-hooks/react-compiler
3734
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3735
+ }, []);
3736
+ // Transform virtual items into typed rows
3737
+ const rows = react.useMemo(() => {
3738
+ return virtualizer.getVirtualItems().map((virtualRow) => {
3739
+ const { index } = virtualRow;
3740
+ // Determine row type
3741
+ const isLoaderRow = index >= totalDataRows;
3742
+ const isHeaderRow = hasHeader && index === 0;
3743
+ if (isLoaderRow) {
3744
+ return {
3745
+ type: "loader",
3746
+ virtualRow,
3747
+ dataIndex: -1,
3472
3748
  };
3473
- // Handle loading rows
3474
- if (isLoaderRow) {
3475
- return (jsxRuntime.jsx("li", { ...listItemProps, children: pagination?.isLoading === true && loaderIndex < getLoadingRowsCount() ? (jsxRuntime.jsx(ListLoadingIndicator, { ...loadingIndicator })) : null }, virtualRow.index));
3476
- }
3477
- // Handle header row
3478
- if (isHeaderRow && header) {
3479
- return (jsxRuntime.jsx("li", { ...listItemProps, children: header }, "header"));
3749
+ }
3750
+ if (isHeaderRow) {
3751
+ return {
3752
+ type: "header",
3753
+ virtualRow,
3754
+ dataIndex: -1,
3755
+ };
3756
+ }
3757
+ // Data row
3758
+ const dataIndex = index - dataStartIndex;
3759
+ const item = getItem(dataIndex);
3760
+ return {
3761
+ type: "data",
3762
+ virtualRow,
3763
+ item,
3764
+ dataIndex,
3765
+ };
3766
+ });
3767
+ // Call getVirtualItems() fresh each time to get updated measurements after virtualizer state changes
3768
+ }, [virtualizer, totalDataRows, hasHeader, dataStartIndex, getItem]);
3769
+ // Helper to create list item props
3770
+ const getListItemProps = react.useCallback((row, options) => {
3771
+ const { type, item, dataIndex } = row;
3772
+ // Handle click for data rows
3773
+ const handleClick = type === "data" && onRowClick && item !== undefined
3774
+ ? () => {
3775
+ onRowClick({ item, index: dataIndex });
3776
+ }
3777
+ : undefined;
3778
+ const onClick = handleClick ?? options.onClick;
3779
+ return {
3780
+ className: options.className,
3781
+ "data-index": row.virtualRow.index,
3782
+ onClick,
3783
+ ref: el => {
3784
+ if (el) {
3785
+ virtualizer.measureElement(el);
3786
+ rowRefsMap.current.set(row.virtualRow.index, el);
3787
+ // Apply transform immediately when element is added to map
3788
+ const virtualRow = virtualizer.getVirtualItems().find(vr => vr.index === row.virtualRow.index);
3789
+ if (virtualRow) {
3790
+ el.style.transform = `translateY(${virtualRow.start}px)`;
3791
+ }
3480
3792
  }
3481
- // For regular children, call the children function with virtualization props and item data
3482
- const item = getItemAtIndex(dataIndex);
3483
- return children(listItemProps, item, dataIndex);
3484
- }) }) }));
3793
+ },
3794
+ tabIndex: -1,
3795
+ };
3796
+ }, [onRowClick, virtualizer]);
3797
+ return {
3798
+ ...virtualizer,
3799
+ containerRef,
3800
+ listRef,
3801
+ rows,
3802
+ getListItemProps,
3803
+ header,
3804
+ loadingIndicator,
3805
+ };
3806
+ };
3807
+
3808
+ // Height constants (in pixels) - based on ListItem.variants.ts styling
3809
+ const MIN_ITEM_HEIGHT = 56; // min-h-14 = 56px (minimum height for ListItem)
3810
+ const VERTICAL_PADDING = 12 + 12; // py-3 = 12px top + 12px bottom = 24px
3811
+ const TITLE_LINE_HEIGHT = 20;
3812
+ const DESCRIPTION_LINE_HEIGHT = 16;
3813
+ const META_LINE_HEIGHT = 16 + 2; // 2px padding above meta line
3814
+ /**
3815
+ * Hook that provides a memoized getListItemHeight function.
3816
+ *
3817
+ * Calculates the estimated height of a ListItem based on its configuration.
3818
+ * This function adds up the heights of each visible line plus gaps between them.
3819
+ * Height values are placeholders and need to be measured in browser DevTools.
3820
+ *
3821
+ * @returns {object} An object containing the memoized getListItemHeight function
3822
+ * @example
3823
+ * ```tsx
3824
+ * const { getListItemHeight } = useListItemHeight();
3825
+ *
3826
+ * const estimateItemSize = () => getListItemHeight({
3827
+ * hasThumbnail: true,
3828
+ * hasDescription: true,
3829
+ * });
3830
+ * ```
3831
+ */
3832
+ const useListItemHeight = () => {
3833
+ const getListItemHeight = react.useCallback(({ hasThumbnail: _hasThumbnail = false, // does not affect height
3834
+ hasDescription = false, hasMeta = false, hasDetails: _hasDetails = false, // does not affect height
3835
+ }) => {
3836
+ // Calculate content height (excluding padding)
3837
+ let contentHeight = TITLE_LINE_HEIGHT;
3838
+ // Add description line if present
3839
+ if (hasDescription) {
3840
+ contentHeight += DESCRIPTION_LINE_HEIGHT;
3841
+ }
3842
+ // Add meta line if present
3843
+ if (hasMeta) {
3844
+ contentHeight += META_LINE_HEIGHT;
3845
+ }
3846
+ // Add vertical padding
3847
+ const totalHeight = contentHeight + VERTICAL_PADDING;
3848
+ // Return the larger of calculated height or minimum height
3849
+ return Math.max(totalHeight, MIN_ITEM_HEIGHT);
3850
+ }, []);
3851
+ return {
3852
+ getListItemHeight,
3853
+ };
3485
3854
  };
3486
3855
 
3487
3856
  /**
@@ -4842,12 +5211,14 @@ exports.cvaToggleItemContent = cvaToggleItemContent;
4842
5211
  exports.cvaToggleItemText = cvaToggleItemText;
4843
5212
  exports.cvaZStackContainer = cvaZStackContainer;
4844
5213
  exports.cvaZStackItem = cvaZStackItem;
5214
+ exports.defaultPageSize = defaultPageSize;
4845
5215
  exports.docs = docs;
4846
5216
  exports.getDevicePixelRatio = getDevicePixelRatio;
4847
5217
  exports.getResponsiveRandomWidthPercentage = getResponsiveRandomWidthPercentage;
4848
5218
  exports.getValueBarColorByValue = getValueBarColorByValue;
4849
5219
  exports.iconColorNames = iconColorNames;
4850
5220
  exports.iconPalette = iconPalette;
5221
+ exports.noPagination = noPagination;
4851
5222
  exports.useClickOutside = useClickOutside;
4852
5223
  exports.useContainerBreakpoints = useContainerBreakpoints;
4853
5224
  exports.useContinuousTimeout = useContinuousTimeout;
@@ -4857,13 +5228,17 @@ exports.useElevatedReducer = useElevatedReducer;
4857
5228
  exports.useElevatedState = useElevatedState;
4858
5229
  exports.useGeometry = useGeometry;
4859
5230
  exports.useHover = useHover;
5231
+ exports.useInfiniteScroll = useInfiniteScroll;
4860
5232
  exports.useIsFirstRender = useIsFirstRender;
4861
5233
  exports.useIsFullscreen = useIsFullscreen;
4862
5234
  exports.useIsTextTruncated = useIsTextTruncated;
5235
+ exports.useList = useList;
5236
+ exports.useListItemHeight = useListItemHeight;
4863
5237
  exports.useModifierKey = useModifierKey;
4864
5238
  exports.useOverflowItems = useOverflowItems;
4865
5239
  exports.usePopoverContext = usePopoverContext;
4866
5240
  exports.usePrompt = usePrompt;
5241
+ exports.useRelayPagination = useRelayPagination;
4867
5242
  exports.useResize = useResize;
4868
5243
  exports.useScrollDetection = useScrollDetection;
4869
5244
  exports.useSelfUpdatingRef = useSelfUpdatingRef;