@trackunit/react-components 1.9.19 → 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.
@@ -1,112 +1,48 @@
1
1
  import { VariantProps } from "@trackunit/css-class-variance-utilities";
2
- import { RelayPagination } from "@trackunit/react-table-pagination";
3
- import { CSSProperties, ReactElement, Ref } from "react";
2
+ import { ReactElement } from "react";
4
3
  import { CommonProps } from "../../common/CommonProps";
5
4
  import { cvaListItem } from "./List.variants";
6
- import { ListLoadingIndicatorProps } from "./ListLoadingIndicator";
5
+ import { UseListResult, VirtualizationListItemProps } from "./useList";
7
6
  type Separator = NonNullable<VariantProps<typeof cvaListItem>["separator"]>;
8
- /**
9
- * Props that must be spread onto list items for proper virtualization.
10
- * These handle positioning, measurement, accessibility, and interaction.
11
- *
12
- * **Important**: Consumers must add a `key` prop to their list items using the index parameter.
13
- */ export interface VirtualizationListItemProps {
14
- /** CSS classes for list styling and separators */
15
- className: string;
16
- /** Critical positioning styles for virtual scrolling */
17
- style: CSSProperties;
18
- /** Element ref for virtualization measurement */
19
- ref?: Ref<any>;
20
- /** Click handler for row-level interactions */
21
- onClick?: () => void;
22
- /** Data attribute for accessibility and debugging */
23
- "data-index": number;
24
- /** Tab index for keyboard navigation */
25
- tabIndex: number;
26
- }
27
- export interface ListProps<TItem = unknown> extends CommonProps {
28
- /**
29
- * Number of items in the list (excluding header if provided).
30
- */
31
- count: number;
32
- /**
33
- * Pagination configuration for infinite scrolling.
34
- */
35
- pagination?: RelayPagination;
36
- /**
37
- * Optional header element displayed at the top of the list.
38
- *
39
- * @example
40
- * ```tsx
41
- * <List header={<KPI title="Total" value={42} />} ... />
42
- * ```
43
- */
44
- header?: ReactElement;
7
+ export type { VirtualizationListItemProps } from "./useList";
8
+ export interface ListProps<TItem = unknown> extends CommonProps, UseListResult<TItem> {
45
9
  /**
46
- * Function that returns the item data for a given index.
10
+ * Function that renders each list item. Must spread `listItemProps` onto your element
11
+ * and use the provided `key` prop for React's reconciliation.
47
12
  *
48
- * @example
49
- * ```tsx
50
- * getItem={index => items[index]}
51
- * ```
52
- */
53
- getItem: (index: number) => TItem;
54
- /**
55
- * Function that renders each list item. Must spread `listItemProps` onto your element and apply a `key` prop.
13
+ * **Note**: `item` may be undefined if `getItem` returns undefined (legacy pattern).
14
+ * Consumers are responsible for handling this case.
56
15
  *
57
16
  * @example
58
17
  * ```tsx
59
- * {(listItemProps, item, index) => (
60
- * <ListItem key={index} {...listItemProps} title={item.name} />
18
+ * {({ key, listItemProps, item, index }) => (
19
+ * <ListItem key={key} {...listItemProps} title={item?.name} />
61
20
  * )}
62
21
  * ```
63
22
  */
64
- children: (listItemProps: VirtualizationListItemProps, item: TItem, index: number) => ReactElement | null;
23
+ children: (params: {
24
+ key: string | number | bigint;
25
+ listItemProps: VirtualizationListItemProps;
26
+ item: TItem | undefined;
27
+ index: number;
28
+ }) => ReactElement | null;
65
29
  /**
66
30
  * Separator style between list items.
67
31
  */
68
32
  separator?: Separator;
69
- /**
70
- * Loading indicator configuration for pagination loading.
71
- */
72
- loadingIndicator?: ListLoadingIndicatorProps;
73
- /**
74
- * Callback fired when a row is clicked.
75
- */
76
- onRowClick?: (item: TItem, index: number) => void;
77
- /**
78
- * Callback for scroll state changes.
79
- */
80
- onScrollStateChange?: (scrollOffset: number, isScrolling: boolean) => void;
81
33
  /**
82
34
  * Show a top separator when the list is scrolled.
83
35
  */
84
36
  topSeparatorOnScroll?: boolean;
85
- /**
86
- * Function to estimate item height for scroll performance.
87
- *
88
- * **Best Practice:** Always provide this function, even for fixed-height items.
89
- * Measure the actual height in browser DevTools and return that value for optimal scrolling.
90
- *
91
- * The virtualizer uses these estimates for scroll calculations and viewport positioning.
92
- * More accurate estimates result in smoother scrolling.
93
- *
94
- * @example Fixed height
95
- * ```tsx
96
- * estimateItemSize={() => 61} // Measured ListItem height
97
- * ```
98
- * @example Variable height
99
- * ```tsx
100
- * estimateItemSize={(index) => items[index]?.hasDescription ? 80 : 50}
101
- * ```
102
- */
103
- estimateItemSize?: (index: number) => number;
104
37
  }
105
38
  /**
106
39
  * A performant virtualized list component with infinite scrolling support.
107
40
  *
108
41
  * ⚠️ **Important**: Requires a container with defined height to work properly.
109
42
  *
43
+ * **Usage Pattern**: Always use the `useList` hook in your component and spread the result into this component.
44
+ * This gives you access to the virtualizer state (scroll position, isScrolling, etc.) in the parent.
45
+ *
110
46
  * Features:
111
47
  * - Virtualized rendering using TanStack Virtual for performance with large datasets
112
48
  * - Automatic infinite scroll loading when approaching the end of the list
@@ -115,10 +51,53 @@ export interface ListProps<TItem = unknown> extends CommonProps {
115
51
  * - Configurable loading indicators (skeleton, spinner, or custom)
116
52
  * - Scroll state detection and callbacks
117
53
  * - Variable-height item support via `estimateItemSize`
54
+ * - Dynamic measurement for accurate positioning of variable-height items
118
55
  *
119
56
  * The component automatically loads more data when:
120
57
  * - User scrolls to the last visible item
121
58
  * - Content height is insufficient to fill the container
59
+ *
60
+ * **Headers with Different Heights**: When using a header that differs in height from list items,
61
+ * provide `estimateHeaderSize` for optimal initial rendering. The list automatically measures
62
+ * actual heights on mount to ensure correct positioning.
63
+ *
64
+ * @example Basic usage
65
+ * ```tsx
66
+ * const list = useList({
67
+ * count: items.length,
68
+ * getItem: index => items[index],
69
+ * estimateItemSize: () => 61,
70
+ * });
71
+ *
72
+ * return (
73
+ * <List {...list}>
74
+ * {({ key, listItemProps, item }) => (
75
+ * <ListItem key={key} {...listItemProps} title={item?.name} />
76
+ * )}
77
+ * </List>
78
+ * );
79
+ * ```
80
+ * @example With header
81
+ * ```tsx
82
+ * const list = useList({
83
+ * count: items.length,
84
+ * getItem: index => items[index],
85
+ * header: <KPI title="Total" value={42} />,
86
+ * estimateHeaderSize: () => 72, // Actual KPI height
87
+ * estimateItemSize: () => 61, // Actual ListItem height
88
+ * });
89
+ *
90
+ * // Access virtualizer state in parent
91
+ * console.log('Scroll position:', list.scrollOffset);
92
+ * console.log('Is scrolling:', list.isScrolling);
93
+ *
94
+ * return (
95
+ * <List {...list}>
96
+ * {({ key, listItemProps, item }) => (
97
+ * <ListItem key={key} {...listItemProps} title={item?.name} />
98
+ * )}
99
+ * </List>
100
+ * );
101
+ * ```
122
102
  */
123
- export declare const List: <TItem = unknown>({ count, pagination, children, className, dataTestId, separator, loadingIndicator, onRowClick, onScrollStateChange, topSeparatorOnScroll, estimateItemSize, header, getItem, }: ListProps<TItem>) => ReactElement;
124
- export {};
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;
@@ -0,0 +1,167 @@
1
+ import type { VirtualItem, Virtualizer } from "@tanstack/react-virtual";
2
+ import { ReactElement, RefObject } from "react";
3
+ import { RelayPagination } from "../../hooks";
4
+ import { ListLoadingIndicatorProps } from "./ListLoadingIndicator";
5
+ /**
6
+ * Props that must be spread onto list items for proper virtualization.
7
+ * These handle positioning, measurement, accessibility, and interaction.
8
+ *
9
+ * **Important**: Consumers must add a `key` prop to their list items using the index parameter.
10
+ */
11
+ export interface VirtualizationListItemProps {
12
+ /** CSS classes for list styling and separators */
13
+ className: string;
14
+ /** Element ref for virtualization measurement and setting positioning transform styles */
15
+ ref?: (node: HTMLLIElement | null) => void;
16
+ /** Click handler for row-level interactions */
17
+ onClick?: () => void;
18
+ /** Data attribute for accessibility and debugging */
19
+ "data-index": number;
20
+ /** Tab index for keyboard navigation */
21
+ tabIndex: number;
22
+ }
23
+ export interface UseListOptions<TItem = unknown> {
24
+ /**
25
+ * Number of data items in the list (excluding header if provided).
26
+ */
27
+ count: number;
28
+ /**
29
+ * Pagination configuration for infinite scrolling.
30
+ */
31
+ pagination?: RelayPagination;
32
+ /**
33
+ * Optional header element displayed at the top of the list.
34
+ */
35
+ header?: ReactElement;
36
+ /**
37
+ * Function that returns the item data for a given data index.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * getItem={index => items[index]}
42
+ * ```
43
+ */
44
+ getItem: (index: number) => TItem;
45
+ /**
46
+ * Loading indicator configuration for pagination loading.
47
+ */
48
+ loadingIndicator?: ListLoadingIndicatorProps;
49
+ /**
50
+ * Callback fired when a row is clicked.
51
+ */
52
+ onRowClick?: (params: {
53
+ item: TItem;
54
+ index: number;
55
+ }) => void;
56
+ /**
57
+ * Callback for scroll state changes.
58
+ */
59
+ onChange?: (virtualizer: Virtualizer<HTMLDivElement, HTMLLIElement>) => void;
60
+ /**
61
+ * Function to estimate item height for scroll performance.
62
+ *
63
+ * Measure the actual height in browser DevTools and return that value for optimal scrolling.
64
+ *
65
+ * The virtualizer uses these estimates for scroll calculations and viewport positioning.
66
+ * More accurate estimates result in smoother scrolling.
67
+ *
68
+ * **Important**: If providing a header, also provide `estimateHeaderSize` to avoid overlap issues.
69
+ *
70
+ * @example Fixed height
71
+ * ```tsx
72
+ * estimateItemSize={() => 61} // Measured ListItem height
73
+ * ```
74
+ * @example Variable height
75
+ * ```tsx
76
+ * estimateItemSize={(index) => items[index]?.hasDescription ? 80 : 50}
77
+ * ```
78
+ */
79
+ estimateItemSize: (index: number) => number;
80
+ /**
81
+ * Function to estimate the header height for scroll performance.
82
+ *
83
+ * Only used when `header` is provided. If not specified, falls back to `estimateItemSize(0)`.
84
+ *
85
+ * Measure the actual header height in browser DevTools for optimal positioning.
86
+ *
87
+ * @example
88
+ * ```tsx
89
+ * header: <KPI title="Total" value={42} />,
90
+ * estimateHeaderSize: () => 72, // Measured KPI height
91
+ * estimateItemSize: () => 61, // Measured ListItem height
92
+ * ```
93
+ */
94
+ estimateHeaderSize?: () => number;
95
+ /**
96
+ * Additional overscan for virtualization (items to render outside viewport).
97
+ * Higher values reduce blank areas during fast scrolling but increase memory usage.
98
+ */
99
+ overscan?: number;
100
+ }
101
+ export type ListRowType = "header" | "data" | "loader";
102
+ export type ListRow<TItem> = {
103
+ type: "header";
104
+ virtualRow: VirtualItem;
105
+ item?: never;
106
+ dataIndex: -1;
107
+ } | {
108
+ type: "loader";
109
+ virtualRow: VirtualItem;
110
+ item?: never;
111
+ dataIndex: -1;
112
+ } | {
113
+ type: "data";
114
+ virtualRow: VirtualItem;
115
+ item: TItem | undefined;
116
+ dataIndex: number;
117
+ };
118
+ export interface UseListResult<TItem> extends Pick<Virtualizer<HTMLDivElement, HTMLLIElement>, "scrollOffset" | "isScrolling" | "getTotalSize" | "getVirtualItems" | "scrollToOffset" | "scrollToIndex" | "measure"> {
119
+ /** Reference to attach to the scrollable container */
120
+ containerRef: RefObject<HTMLDivElement | null>;
121
+ /** Reference to attach to the list */
122
+ listRef: RefObject<HTMLUListElement | null>;
123
+ /** Array of row data to render */
124
+ rows: Array<ListRow<TItem>>;
125
+ /** Helper to create list item props for a given row */
126
+ getListItemProps: (row: ListRow<TItem>, options: {
127
+ className: string;
128
+ onClick?: () => void;
129
+ }) => VirtualizationListItemProps;
130
+ /** Header element (if provided) */
131
+ header?: ReactElement;
132
+ /** Loading indicator configuration */
133
+ loadingIndicator?: ListLoadingIndicatorProps;
134
+ }
135
+ /**
136
+ * A hook for managing virtualized list state and behavior.
137
+ *
138
+ * This hook encapsulates the logic for:
139
+ * - Virtualizing list items using TanStack Virtual
140
+ * - Managing infinite scroll pagination
141
+ * - Handling header, data, and loading rows
142
+ * - Calculating proper indices and measurements
143
+ *
144
+ * @example
145
+ * ```tsx
146
+ * const list = useList({
147
+ * count: items.length,
148
+ * getItem: index => items[index],
149
+ * pagination,
150
+ * header: <KPI title="Total" value={42} />,
151
+ * estimateHeaderSize: () => 72, // Measure actual KPI height
152
+ * estimateItemSize: () => 61, // Measure actual ListItem height
153
+ * });
154
+ *
155
+ * return (
156
+ * <div ref={list.containerRef}>
157
+ * <ul style={{ height: `${list.getTotalSize()}px` }}>
158
+ * {list.rows.map(row => {
159
+ * const props = list.getListItemProps(row, { className: 'list-item' });
160
+ * return <li {...props}>{row.item?.title}</li>;
161
+ * })}
162
+ * </ul>
163
+ * </div>
164
+ * );
165
+ * ```
166
+ */
167
+ export declare const useList: <TItem = unknown>({ count, pagination, header, getItem, loadingIndicator, onRowClick, onChange, estimateItemSize, estimateHeaderSize, overscan, }: UseListOptions<TItem>) => UseListResult<TItem>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Configuration for calculating ListItem height based on visible content.
3
+ * These props match the optional content sections of ListItem.
4
+ */
5
+ export interface ListItemHeightConfig {
6
+ /** Whether the item has a thumbnail (icon or image) */
7
+ hasThumbnail?: boolean;
8
+ /** Whether the item has a description line */
9
+ hasDescription?: boolean;
10
+ /** Whether the item has a meta line */
11
+ hasMeta?: boolean;
12
+ /** Whether the item has details on the right side (doesn't affect height) */
13
+ hasDetails?: boolean;
14
+ }
15
+ /**
16
+ * Hook that provides a memoized getListItemHeight function.
17
+ *
18
+ * Calculates the estimated height of a ListItem based on its configuration.
19
+ * This function adds up the heights of each visible line plus gaps between them.
20
+ * Height values are placeholders and need to be measured in browser DevTools.
21
+ *
22
+ * @returns {object} An object containing the memoized getListItemHeight function
23
+ * @example
24
+ * ```tsx
25
+ * const { getListItemHeight } = useListItemHeight();
26
+ *
27
+ * const estimateItemSize = () => getListItemHeight({
28
+ * hasThumbnail: true,
29
+ * hasDescription: true,
30
+ * });
31
+ * ```
32
+ */
33
+ export declare const useListItemHeight: () => {
34
+ getListItemHeight: (config: ListItemHeightConfig) => number;
35
+ };
@@ -20,6 +20,8 @@ export * from "./KPI/KPI";
20
20
  export * from "./KPICard/KPICard";
21
21
  export * from "./List/List";
22
22
  export * from "./List/List.variants";
23
+ export * from "./List/useList";
24
+ export * from "./ListItem/useListItemHeight";
23
25
  export * from "./ListItem/ListItem";
24
26
  export * from "./Menu";
25
27
  export * from "./Notice";
@@ -1,3 +1,4 @@
1
+ export * from "./noPagination";
1
2
  export * from "./useClickOutside";
2
3
  export * from "./useContainerBreakpoints";
3
4
  export * from "./useContinuousTimeout";
@@ -7,10 +8,12 @@ export * from "./useElevatedReducer";
7
8
  export * from "./useElevatedState";
8
9
  export * from "./useGeometry";
9
10
  export * from "./useHover";
11
+ export * from "./useInfiniteScroll";
10
12
  export * from "./useIsFirstRender";
11
13
  export * from "./useIsFullScreen";
12
14
  export * from "./useIsTextTruncated";
13
15
  export * from "./useModifierKey";
16
+ export * from "./useRelayPagination";
14
17
  export * from "./useResize";
15
18
  export * from "./useScrollDetection";
16
19
  export * from "./useSelfUpdatingRef";
@@ -0,0 +1,2 @@
1
+ import { RelayPagination } from "./useRelayPagination";
2
+ export declare const noPagination: RelayPagination;
@@ -0,0 +1,36 @@
1
+ import { type Virtualizer } from "@tanstack/react-virtual";
2
+ import { RefObject } from "react";
3
+ import { RelayPagination } from "./useRelayPagination";
4
+ interface InfiniteScrollProps<TScrollElement extends Element, TItemElement extends Element> {
5
+ pagination: RelayPagination;
6
+ scrollElementRef: RefObject<TScrollElement | null>;
7
+ count: number;
8
+ estimateSize?: (index: number) => number;
9
+ overscan?: number;
10
+ onChange?: (virtualizer: Virtualizer<TScrollElement, TItemElement>) => void;
11
+ }
12
+ /**
13
+ * Custom hook for implementing infinite scrolling in a table using TanStack Virtual.
14
+ *
15
+ * @param props - The configuration object for the infinite scroll hook.
16
+ * @param props.pagination - The relay pagination object for managing data loading.
17
+ * @param props.scrollElementRef - Reference to the scrollable container element.
18
+ * @param props.count - Total number of items to virtualize.
19
+ * @param props.estimateSize - Optional function to estimate item height.
20
+ * @param props.overscan - Optional number of items to render outside the visible area.
21
+ * @param props.onChange - Optional callback when virtualizer changes.
22
+ * @returns {Virtualizer} The virtualizer instance with all its properties and methods.
23
+ * @description
24
+ * This hook is used to implement infinite scrolling in a table. It uses TanStack Virtual's
25
+ * built-in capabilities for virtualization and automatically loads more data when scrolling
26
+ * approaches the end of the available content.
27
+ * @example
28
+ * const virtualizer = useInfiniteScroll<HTMLDivElement, HTMLDivElement>({
29
+ * pagination: relayPaginationObject,
30
+ * scrollElementRef: tableScrollElementRef,
31
+ * count: 50,
32
+ * estimateSize: () => 35,
33
+ * });
34
+ */
35
+ export declare const useInfiniteScroll: <TScrollElement extends Element, TItemElement extends Element>({ pagination, scrollElementRef, count, estimateSize, overscan, onChange, }: InfiniteScrollProps<TScrollElement, TItemElement>) => Virtualizer<TScrollElement, TItemElement>;
36
+ export {};
@@ -0,0 +1,43 @@
1
+ import { Dispatch, SetStateAction } from "react";
2
+ export interface RelayPaginationProps {
3
+ pageSize?: number;
4
+ onReset?: () => void;
5
+ }
6
+ export interface RelayPaginationQueryVariables {
7
+ first?: number | null;
8
+ last?: number | null;
9
+ before?: string | null;
10
+ after?: string | null;
11
+ }
12
+ export interface RelayPageInfo {
13
+ count?: number | null;
14
+ endCursor?: string | null;
15
+ hasNextPage?: boolean;
16
+ hasPreviousPage?: boolean;
17
+ startCursor?: string | null;
18
+ isCountCapped?: boolean;
19
+ }
20
+ export interface RelayPagination {
21
+ nextPage: () => void;
22
+ previousPage: () => void;
23
+ pageInfo?: RelayPageInfo;
24
+ isLoading: boolean;
25
+ }
26
+ export interface RelayTableSupport extends RelayPagination {
27
+ isLoading: boolean;
28
+ setIsLoading: Dispatch<SetStateAction<boolean>>;
29
+ reset: () => void;
30
+ setPageInfo: Dispatch<SetStateAction<RelayPageInfo | null | undefined>>;
31
+ }
32
+ export interface RelayPaginationSupport {
33
+ variables: RelayPaginationQueryVariables;
34
+ table: RelayTableSupport;
35
+ }
36
+ export declare const defaultPageSize = 50;
37
+ /**
38
+ * Custom hook for handling Relay pagination in tables.
39
+ *
40
+ * @param {RelayPaginationProps} props - The props object containing pagination configuration.
41
+ * @returns {RelayPaginationSupport} An object containing functions and state for managing Relay pagination.
42
+ */
43
+ export declare const useRelayPagination: ({ onReset, pageSize }?: RelayPaginationProps) => RelayPaginationSupport;