@eml-payments/ui-kit 1.7.8 → 1.8.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.
@@ -0,0 +1,3 @@
1
+ import type { PageSizeSelectorProps, PaginationControlsProps } from './PaginationControls.types';
2
+ export declare const PageSizeSelector: ({ id, value, onChange, options, label, className, size, }: PageSizeSelectorProps) => import("react/jsx-runtime").JSX.Element;
3
+ export declare const PaginationFooter: <T>({ table, tableId, paginationMode, onPageSizeChange, totalServerRows, onNextPage, onPrevPage, }: PaginationControlsProps<T>) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SelectWrapper } from '../../SelectWrapper';
3
+ import { cn } from '../../../lib/utils';
4
+ import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
5
+ import { Button } from '../../Button';
6
+ export const PageSizeSelector = ({ id, value, onChange, options = ['5', '10', '20', '50', '100'], label = 'Rows per page:', className, size = 'default', }) => {
7
+ return (_jsxs("div", { className: cn('flex items-center gap-2 text-xs', className), id: `page-size-select-${id}`, children: [_jsx("label", { htmlFor: `page-size-select-${id}`, children: label }), _jsx("div", { className: "w-fit", children: _jsx(SelectWrapper, { size: size, value: value, onChange: onChange, options: options.map((option) => ({
8
+ label: option,
9
+ value: option,
10
+ })), className: "bg-transparent shadow-none border-none gap-1" }) })] }));
11
+ };
12
+ export const PaginationFooter = ({ table, tableId, paginationMode = 'client', onPageSizeChange, totalServerRows, onNextPage, onPrevPage, }) => {
13
+ const { pageIndex, pageSize } = table.getState().pagination;
14
+ const totalRows = paginationMode === 'server' ? (totalServerRows !== null && totalServerRows !== void 0 ? totalServerRows : 0) : table.getFilteredRowModel().rows.length;
15
+ const startRow = pageIndex * pageSize + 1;
16
+ const endRow = Math.min((pageIndex + 1) * pageSize, totalRows);
17
+ return (_jsxs("div", { className: "flex justify-end items-center gap-6 px-4 py-2 text-xs text-(--uikit-textSecondary) w-full", children: [_jsx(PageSizeSelector, { id: tableId, value: String(pageSize), onChange: (val) => {
18
+ const newSize = Number(val);
19
+ table.setPageSize(newSize);
20
+ onPageSizeChange === null || onPageSizeChange === void 0 ? void 0 : onPageSizeChange(newSize);
21
+ }, size: "small", label: "Rows per page" }), totalRows > 0 && _jsx("div", { id: `page-indicator-${tableId}`, children: `${startRow} - ${endRow} of ${totalRows}` }), totalRows > 0 && (_jsxs("div", { className: "flex gap-2", id: `pagination-controls-${tableId}`, children: [_jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Previous page", className: cn({ '!bg-transparent': !table.getCanPreviousPage() }), onClick: onPrevPage, disabled: !table.getCanPreviousPage(), children: _jsx(FaChevronLeft, { className: "!w-3 !h-3" }) }), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Next page", onClick: onNextPage, className: cn({ '!bg-transparent': !table.getCanNextPage() }), disabled: !table.getCanNextPage(), children: _jsx(FaChevronRight, { className: "w-3! h-3!" }) })] }))] }));
22
+ };
@@ -0,0 +1,24 @@
1
+ import type { Table } from '@tanstack/react-table';
2
+ import type { SelectWrapperProps } from '../../SelectWrapper';
3
+ import { PaginationMode } from '../Table.types';
4
+ export interface PaginationControlsProps<T> {
5
+ tableId: string;
6
+ table: Table<T>;
7
+ paginationMode?: PaginationMode;
8
+ totalServerRows?: number;
9
+ pageSizeOptions?: string[];
10
+ onPageSizeChange?: (size: number) => void;
11
+ onNextPage: () => void;
12
+ onPrevPage: () => void;
13
+ canNextPage: boolean;
14
+ canPrevPage: boolean;
15
+ }
16
+ export type PageSizeSelectorProps = {
17
+ id: string;
18
+ value: string;
19
+ onChange: (val: string) => void;
20
+ options?: string[];
21
+ label?: string;
22
+ className?: SelectWrapperProps['className'];
23
+ size?: SelectWrapperProps['size'];
24
+ };
@@ -0,0 +1,4 @@
1
+ import type { TableProps } from './Table.types';
2
+ export declare function Table<T extends {
3
+ id: string;
4
+ }>(props: Readonly<TableProps<T>>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { flexRender } from '@tanstack/react-table';
3
+ import { classNames } from '../../utils/classNames';
4
+ import { PaginationFooter } from './Pagination/PaginationControls';
5
+ import { useTableController } from './hooks/useTableController';
6
+ import { FaChevronDown, FaChevronUp } from 'react-icons/fa';
7
+ import { DropdownWrapper } from '../DropdownWrapper';
8
+ import { FiMoreHorizontal } from 'react-icons/fi';
9
+ import { Button } from '../Button';
10
+ export function Table(props) {
11
+ var _a, _b, _c, _d, _e, _f, _g;
12
+ const { table, setPageSize, showHeader, height, virtualizationEnabled, rowVirtualizer, hasNextPage, parentScrollRef, loaderRef, infiniteScroll, ...pagination } = useTableController(props);
13
+ let tableContent;
14
+ const noResultsMessage = props.isSearchActive
15
+ ? ((_a = props.noSearchResultsMessage) !== null && _a !== void 0 ? _a : 'No results meet your search criteria')
16
+ : ((_b = props.noRowsMessage) !== null && _b !== void 0 ? _b : 'No rows to display');
17
+ if (props.isLoading && props.showSkeletonRows) {
18
+ tableContent = Array.from({ length: 3 }).map(() => (_jsx("tr", { className: "border-t animate-pulse", children: table.getVisibleFlatColumns().map((__c) => (_jsx("td", { className: "px-4 py-2", children: _jsx("div", { className: "h-4 w-3/4 bg-(--uikit-tertiary) rounded" }) }, `skeleton-cell-${__c.id}`))) }, "skeleton-row")));
19
+ }
20
+ else if (!virtualizationEnabled &&
21
+ table.getRowModel().rows.length === 0 &&
22
+ !props.isLoading &&
23
+ (!(infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.enabled) || !hasNextPage)) {
24
+ tableContent = (_jsx("tr", { children: _jsx("td", { colSpan: table.getVisibleFlatColumns().length + (props.tableActionsDropdown ? 1 : 0), className: "px-4 py-8 text-center text-muted-foreground", children: noResultsMessage }) }));
25
+ }
26
+ else if (!virtualizationEnabled) {
27
+ const renderRow = (row) => {
28
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
29
+ const rows = [];
30
+ const isRowClickable = (_a = props.isRowClickable) === null || _a === void 0 ? void 0 : _a.call(props, row.original);
31
+ if (row.getIsGrouped() && row.groupingColumnId) {
32
+ const column = table.getColumn(row.groupingColumnId);
33
+ const bgColor = (_c = (_b = column === null || column === void 0 ? void 0 : column.columnDef.meta) === null || _b === void 0 ? void 0 : _b.bgColor) !== null && _c !== void 0 ? _c : 'transparent';
34
+ rows.push(_jsx("tr", { className: "border-none", children: _jsx("td", { colSpan: table.getVisibleLeafColumns().length, className: "p-0", children: _jsx("div", { className: "px-4 py-4 text-sm text-muted-foreground", style: { backgroundColor: bgColor }, children: row.getGroupingValue(row.groupingColumnId) }) }) }, `group-${row.id}`));
35
+ row.subRows.forEach((subRow) => {
36
+ rows.push(...renderRow(subRow));
37
+ });
38
+ }
39
+ else {
40
+ rows.push(_jsxs("tr", { className: classNames('border-t', isRowClickable && 'cursor-pointer', row.getIsSelected() && 'bg-(--uikit-primary-10)'), onClick: () => { var _a; return isRowClickable && ((_a = props.onRowClick) === null || _a === void 0 ? void 0 : _a.call(props, row.original)); }, children: [row.getVisibleCells().map((cell) => {
41
+ const width = cell.column.getSize();
42
+ return (_jsx("td", { style: { width }, className: cell.column.id === 'select' ? 'p-1' : 'p-4', children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
43
+ }), props.tableActionsDropdown && (_jsx("td", { className: "w-[40px] px-2 py-2 text-right", children: _jsx(DropdownWrapper, { structure: (_d = props.tableActionsDropdown.structure) !== null && _d !== void 0 ? _d : 'default', options: props.tableActionsDropdown.getOptions(row.original), menuAlignment: props.tableActionsDropdown.menuAlignment, isTriggerElementDisabled: typeof props.tableActionsDropdown.isDisabled === 'function'
44
+ ? props.tableActionsDropdown.isDisabled(row.original)
45
+ : props.tableActionsDropdown.isDisabled, triggerElement: (_g = (_f = (_e = props.tableActionsDropdown).renderCustomTrigger) === null || _f === void 0 ? void 0 : _f.call(_e, row.original)) !== null && _g !== void 0 ? _g : (_jsx(Button, { "aria-label": "Open actions menu", title: "Open actions menu", size: "icon", variant: "ghost", disabled: typeof props.tableActionsDropdown.isDisabled === 'function'
46
+ ? props.tableActionsDropdown.isDisabled(row.original)
47
+ : ((_h = props.tableActionsDropdown.isDisabled) !== null && _h !== void 0 ? _h : false), className: "p-1 focus-visible:outline-hidden focus-visible:ring-0", children: _jsx(FiMoreHorizontal, { size: 16 }) })) }) }))] }, row.original[((_j = props.rowIdKey) !== null && _j !== void 0 ? _j : 'id')]));
48
+ }
49
+ return rows;
50
+ };
51
+ tableContent = table.getRowModel().rows.flatMap(renderRow);
52
+ }
53
+ const virtualItems = virtualizationEnabled && rowVirtualizer ? rowVirtualizer.getVirtualItems() : [];
54
+ const colSpanFull = table.getVisibleFlatColumns().length + (props.tableActionsDropdown ? 1 : 0);
55
+ const paddingTop = virtualItems.length ? virtualItems[0].start : 0;
56
+ const lastVirtual = virtualItems[virtualItems.length - 1];
57
+ const totalSize = (_d = (_c = rowVirtualizer === null || rowVirtualizer === void 0 ? void 0 : rowVirtualizer.getTotalSize) === null || _c === void 0 ? void 0 : _c.call(rowVirtualizer)) !== null && _d !== void 0 ? _d : 0;
58
+ const end = lastVirtual ? lastVirtual.end : 0;
59
+ const paddingBottom = Math.max(totalSize - end, 0);
60
+ const tableRows = table.getRowModel().rows;
61
+ const isTrulyEmpty = tableRows.length === 0 && !props.isLoading && !hasNextPage;
62
+ return (_jsxs("div", { ref: parentScrollRef, style: { height }, className: "relative w-full overflow-auto", children: [props.isLoading && (_jsx("div", { className: classNames('mui-loader', showHeader ? 'mt-4' : 'mt-0') })), _jsx("div", { className: "rounded-t-(--uikit-radius) overflow-y-hidden overflow-x-auto", children: _jsxs("table", { className: classNames('min-w-full text-sm table-fixed bg-[#ffffff]', props.className), role: "table", id: props.id, children: [showHeader && (_jsx("thead", { className: "bg-(--uikit-tertiary)", children: table.getHeaderGroups().map((headerGroup) => (_jsxs("tr", { className: "p-4", children: [headerGroup.headers.map((header) => {
63
+ return (_jsx("th", { scope: "col", style: { width: header.getSize() }, className: classNames('select-none text-[14px] font-bold ', header.id === 'select' ? 'p-0' : ' p-4 text-left cursor-pointer'), onClick: !(infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.enabled) && header.column.getCanSort()
64
+ ? header.column.getToggleSortingHandler()
65
+ : undefined, children: _jsxs("div", { className: classNames('w-full h-full', header.id === 'select'
66
+ ? 'flex justify-center items-center'
67
+ : 'flex items-center gap-2'), children: [flexRender(header.column.columnDef.header, header.getContext()), !(infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.enabled) && header.column.getCanSort() && (_jsx("span", { className: "text-xs", children: {
68
+ asc: _jsx(FaChevronUp, {}),
69
+ desc: _jsx(FaChevronDown, {}),
70
+ }[header.column.getIsSorted()] || null }))] }) }, header.id));
71
+ }), props.tableActionsDropdown && _jsx("th", { className: "w-[40px] px-2 py-2 text-right" })] }, headerGroup.id))) })), _jsx("tbody", { children: virtualizationEnabled && rowVirtualizer ? (_jsxs(_Fragment, { children: [_jsx("tr", { style: { height: `${paddingTop}px` }, children: _jsx("td", { colSpan: colSpanFull }) }), isTrulyEmpty && (_jsx("tr", { children: _jsx("td", { colSpan: colSpanFull, className: "px-4 py-8 text-center text-muted-foreground", children: noResultsMessage }) })), virtualItems.map((vi) => {
72
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
73
+ const isLoaderRow = vi.index >= tableRows.length;
74
+ if (isLoaderRow) {
75
+ return (_jsx("tr", { className: "border-t", children: _jsx("td", { colSpan: colSpanFull, className: "px-4 py-2", children: hasNextPage
76
+ ? ((_a = infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.loadingMoreMessage) !== null && _a !== void 0 ? _a : 'Loading more...')
77
+ : ((_b = infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.noMoreDataMessage) !== null && _b !== void 0 ? _b : 'Nothing more to load') }) }, `loader-${vi.key}`));
78
+ }
79
+ const row = tableRows[vi.index];
80
+ const isRowClickable = (_c = props.isRowClickable) === null || _c === void 0 ? void 0 : _c.call(props, row.original);
81
+ return (_jsxs("tr", { className: classNames('border-t', isRowClickable && 'cursor-pointer', (row === null || row === void 0 ? void 0 : row.getIsSelected()) && 'bg-(--uikit-primary-10)'), onClick: () => { var _a; return isRowClickable && ((_a = props.onRowClick) === null || _a === void 0 ? void 0 : _a.call(props, row.original)); }, children: [row === null || row === void 0 ? void 0 : row.getVisibleCells().map((cell) => {
82
+ const width = cell.column.getSize();
83
+ return (_jsx("td", { style: { width }, className: cell.column.id === 'select' ? 'p-1' : 'p-4', children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
84
+ }), props.tableActionsDropdown && (_jsx("td", { className: "w-[40px] px-2 py-2 text-right", children: _jsx(DropdownWrapper, { structure: (_d = props.tableActionsDropdown.structure) !== null && _d !== void 0 ? _d : 'default', options: props.tableActionsDropdown.getOptions(row.original), menuAlignment: props.tableActionsDropdown.menuAlignment, isTriggerElementDisabled: typeof props.tableActionsDropdown.isDisabled === 'function'
85
+ ? props.tableActionsDropdown.isDisabled(row.original)
86
+ : props.tableActionsDropdown.isDisabled, triggerElement: (_g = (_f = (_e = props.tableActionsDropdown).renderCustomTrigger) === null || _f === void 0 ? void 0 : _f.call(_e, row.original)) !== null && _g !== void 0 ? _g : (_jsx(Button, { "aria-label": "Open actions menu", title: "Open actions menu", size: "icon", variant: "ghost", disabled: typeof props.tableActionsDropdown.isDisabled ===
87
+ 'function'
88
+ ? props.tableActionsDropdown.isDisabled(row.original)
89
+ : ((_h = props.tableActionsDropdown.isDisabled) !== null && _h !== void 0 ? _h : false), className: "p-1 focus-visible:outline-hidden focus-visible:ring-0", children: _jsx(FiMoreHorizontal, { size: 16 }) })) }) }))] }, row.original[((_j = props.rowIdKey) !== null && _j !== void 0 ? _j : 'id')]));
90
+ }), _jsx("tr", { style: { height: `${paddingBottom}px` }, children: _jsx("td", { colSpan: colSpanFull }) })] })) : (_jsxs(_Fragment, { children: [tableContent, (infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.enabled) && !isTrulyEmpty && (_jsx("tr", { ref: loaderRef, className: "border-t", children: _jsx("td", { colSpan: colSpanFull, className: "px-4 py-2", children: props.isLoading || hasNextPage
91
+ ? ((_e = infiniteScroll.loadingMoreMessage) !== null && _e !== void 0 ? _e : 'Loading more...')
92
+ : ((_f = infiniteScroll.noMoreDataMessage) !== null && _f !== void 0 ? _f : 'Nothing more to load') }) }))] })) })] }) }), !((_g = props.infiniteScroll) === null || _g === void 0 ? void 0 : _g.enabled) && (_jsx(PaginationFooter, { tableId: props.id, table: table, paginationMode: props.paginationMode, totalServerRows: props.totalServerRows, onPageSizeChange: setPageSize, ...pagination }))] }));
93
+ }
@@ -0,0 +1,31 @@
1
+ import { Table } from './Table';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ declare const meta: Meta<typeof Table>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof Table>;
6
+ export declare const Basic: Story;
7
+ export declare const NoRows: Story;
8
+ export declare const LoadingWithSkeleton: Story;
9
+ export declare const ClientPagination: Story;
10
+ export declare const ServerSidePagination: Story;
11
+ export declare const WithCheckboxSelection: Story;
12
+ export declare const WithDisabledSelection: Story;
13
+ export declare const WithSingleRowSelection: Story;
14
+ export declare const WithDefaultRowsSelected: Story;
15
+ export declare const FlexColumnWidths: Story;
16
+ export declare const WithControlledSorting: Story;
17
+ export declare const WithRowActionsDropdown: Story;
18
+ export declare const WithPartiallyDisabledRowActionsDropdown: Story;
19
+ export declare const WithDisabledRowActionsDropdownItem: Story;
20
+ export declare const WithRowClickHandler: Story;
21
+ export declare const WithHiddenHeader: Story;
22
+ export declare const GroupedByDate: Story;
23
+ export declare const WithSearch: Story;
24
+ export declare const ServerSideSearch: Story;
25
+ export declare const ClientPaginationWithQueryParams: StoryObj;
26
+ export declare const ServerPaginationWithQueryParams: StoryObj;
27
+ export declare const TwoTablesWithQueryParams: StoryObj;
28
+ export declare const InfiniteScrollVirtualized: StoryObj;
29
+ export declare const InfiniteScrollNonVirtual: StoryObj;
30
+ export declare const NoRowsInfiniteScrollNonVirtual: StoryObj;
31
+ export declare const NoRowsInfiniteScrollVirtualized: StoryObj;
@@ -0,0 +1,479 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
+ import { Table } from './Table';
4
+ import { SearchInput } from '../SearchInput';
5
+ import { useLocation } from 'react-router-dom';
6
+ const CurrentUrl = () => {
7
+ const location = useLocation();
8
+ return _jsxs("div", { children: ["Current URL: ", location.search] });
9
+ };
10
+ const data = Array.from({ length: 10 }).map((_, i) => ({
11
+ id: `user-${i + 1}`,
12
+ name: `User ${i + 1}`,
13
+ email: `user${i + 1}@example.com`,
14
+ role: i % 2 === 0 ? 'Admin' : 'User',
15
+ }));
16
+ const disabledIds = new Set(['user-1', 'user-2', 'user-3']);
17
+ const columns = [
18
+ {
19
+ accessorKey: 'id',
20
+ header: 'ID',
21
+ flex: 1,
22
+ },
23
+ {
24
+ accessorKey: 'name',
25
+ header: 'Name',
26
+ flex: 1,
27
+ },
28
+ {
29
+ accessorKey: 'email',
30
+ header: 'Email',
31
+ cell: ({ row }) => (_jsx("a", { href: `mailto:${row.original.email}`, className: "text-blue-600 underline", children: row.original.email })),
32
+ flex: 1,
33
+ },
34
+ {
35
+ accessorKey: 'role',
36
+ header: 'Role',
37
+ cell: ({ row }) => {
38
+ const role = row.original.role;
39
+ return _jsx("span", { className: role === 'Admin' ? 'text-red-500 font-bold' : '', children: role });
40
+ },
41
+ flex: 1,
42
+ },
43
+ ];
44
+ const meta = {
45
+ title: 'UIKit/Table',
46
+ component: Table,
47
+ tags: ['autodocs'],
48
+ render: () => _jsx(Table, { id: "example-table", data: data, columns: columns }),
49
+ };
50
+ export default meta;
51
+ export const Basic = {
52
+ render: () => _jsx(Table, { id: "example-table", data: data, columns: columns }),
53
+ };
54
+ export const NoRows = {
55
+ render: () => _jsx(Table, { id: "example-table", data: [], columns: columns }),
56
+ };
57
+ export const LoadingWithSkeleton = {
58
+ render: () => _jsx(Table, { id: "example-table", data: [], columns: columns, isLoading: true, showSkeletonRows: true, rowsPerPage: 5 }),
59
+ };
60
+ export const ClientPagination = {
61
+ render: () => _jsx(Table, { id: "example-table", data: data, columns: columns, paginationMode: "client", rowsPerPage: 5 }),
62
+ };
63
+ const fetchServerData = (pageIndex, pageSize, query) => {
64
+ return new Promise((resolve) => {
65
+ setTimeout(() => {
66
+ const start = pageIndex * pageSize;
67
+ const data = Array.from({ length: pageSize }).map((_, i) => {
68
+ const id = start + i + 1;
69
+ return {
70
+ id: `user-${id}`,
71
+ name: `User ${id}`,
72
+ email: `user${id}@example.com`,
73
+ role: id % 2 === 0 ? 'Admin' : 'User',
74
+ };
75
+ });
76
+ const filteredData = query
77
+ ? data.filter((row) => row.name.toLowerCase().includes(query.toLowerCase()))
78
+ : data;
79
+ resolve(filteredData);
80
+ }, 500);
81
+ });
82
+ };
83
+ export const ServerSidePagination = {
84
+ render: () => {
85
+ const [data, setData] = useState([]);
86
+ const [isLoading, setIsLoading] = useState(false);
87
+ const fetchData = useCallback(async (pageIndex, pageSize) => {
88
+ setIsLoading(true);
89
+ const results = await fetchServerData(pageIndex, pageSize);
90
+ setData(results);
91
+ setIsLoading(false);
92
+ }, []);
93
+ return (_jsx(Table, { id: "server-table", data: data, columns: columns, paginationMode: "server", totalServerRows: 100, onRefetch: fetchData, isLoading: isLoading }));
94
+ },
95
+ };
96
+ const handleSelectionChange = (selectedRowIds) => {
97
+ console.log('Selected Row IDs:', selectedRowIds);
98
+ // Update your state or perform actions based on selected rows
99
+ };
100
+ export const WithCheckboxSelection = {
101
+ render: () => (_jsx(Table, { id: "example-table", data: data, columns: columns, checkboxSelection: true, checkboxPosition: "start", paginationMode: "client", onSelectionChange: handleSelectionChange })),
102
+ };
103
+ export const WithDisabledSelection = {
104
+ render: () => (_jsx(Table, { id: "example-table", data: data, columns: columns, checkboxSelection: true, checkboxPosition: "start", enableRowSelection: (row) => row.original.role !== 'Admin' })),
105
+ };
106
+ export const WithSingleRowSelection = {
107
+ render: () => (_jsx(Table, { id: "example-table", data: data, columns: columns, checkboxSelection: true, isMultiRowSelection: false })),
108
+ };
109
+ export const WithDefaultRowsSelected = {
110
+ render: () => (_jsx(Table, { id: "example-table", data: data, columns: columns, checkboxSelection: true, selectedRowIds: ['user-1', 'user-3'] })),
111
+ };
112
+ export const FlexColumnWidths = {
113
+ render: () => {
114
+ const testColumns = [
115
+ { accessorKey: 'id', header: 'ID', flex: 1 },
116
+ { accessorKey: 'name', header: 'Name', flex: 2 },
117
+ { accessorKey: 'email', header: 'Email', flex: 3 },
118
+ { accessorKey: 'role', header: 'Role', flex: 1 },
119
+ ];
120
+ return (_jsx(Table, { id: "example-table", data: data, columns: testColumns, paginationMode: "client", checkboxSelection: false, rowsPerPage: 5 }));
121
+ },
122
+ };
123
+ export const WithControlledSorting = {
124
+ render: () => {
125
+ const [sorting, setSorting] = useState([{ id: 'name', desc: true }]);
126
+ return (_jsx(Table, { id: "example-table", data: data, columns: columns, checkboxSelection: true, paginationMode: "client", sorting: sorting, onSortingChange: setSorting, rowsPerPage: 5 }));
127
+ },
128
+ };
129
+ export const WithRowActionsDropdown = {
130
+ render: () => {
131
+ const getOptions = (user) => [
132
+ {
133
+ label: 'Edit',
134
+ onClick: () => alert(`Edit ${user.name}`),
135
+ },
136
+ {
137
+ label: 'Delete',
138
+ onClick: () => alert(`Delete ${user.name}`),
139
+ },
140
+ ];
141
+ return (_jsx(Table, { id: "example-table", data: data, columns: columns, tableActionsDropdown: {
142
+ getOptions,
143
+ structure: 'default',
144
+ menuAlignment: 'end',
145
+ } }));
146
+ },
147
+ };
148
+ export const WithPartiallyDisabledRowActionsDropdown = {
149
+ render: () => {
150
+ const getOptions = (user) => [
151
+ {
152
+ label: 'Edit',
153
+ onClick: () => alert(`Edit ${user.name}`),
154
+ },
155
+ {
156
+ label: 'Delete',
157
+ onClick: () => alert(`Delete ${user.name}`),
158
+ },
159
+ ];
160
+ return (_jsx(Table, { id: "example-table", data: data, columns: columns, tableActionsDropdown: {
161
+ getOptions,
162
+ structure: 'default',
163
+ menuAlignment: 'end',
164
+ isDisabled: (row) => disabledIds.has(row.id),
165
+ } }));
166
+ },
167
+ };
168
+ export const WithDisabledRowActionsDropdownItem = {
169
+ render: () => {
170
+ const getOptions = (user) => [
171
+ {
172
+ label: 'Edit',
173
+ onClick: () => alert(`Edit ${user.name}`),
174
+ },
175
+ {
176
+ label: 'Delete',
177
+ onClick: () => alert(`Delete ${user.name}`),
178
+ disabled: user.role === 'Admin',
179
+ },
180
+ ];
181
+ return (_jsx(Table, { id: "example-table", data: data, columns: columns, tableActionsDropdown: {
182
+ getOptions,
183
+ structure: 'default',
184
+ menuAlignment: 'end',
185
+ } }));
186
+ },
187
+ };
188
+ export const WithRowClickHandler = {
189
+ render: () => {
190
+ return (_jsx(Table, { id: "clickable-rows-table", data: data, columns: columns, isRowClickable: (row) => row.role !== 'Admin', onRowClick: (row) => {
191
+ console.log('Row clicked:', row);
192
+ alert(`Clicked on ${row.name}`);
193
+ } }));
194
+ },
195
+ };
196
+ export const WithHiddenHeader = {
197
+ render: () => _jsx(Table, { id: "header-hidden-table", data: data, columns: columns, showHeader: false }),
198
+ };
199
+ const dates = [
200
+ new Date().toISOString(),
201
+ new Date(Date.now() + 86400000).toISOString(),
202
+ new Date(Date.now() + 2 * 86400000).toISOString(),
203
+ ];
204
+ const groupingData = Array.from({ length: 5 }).map((_, i) => ({
205
+ id: `user-${i + 1}`,
206
+ date: dates[i % dates.length],
207
+ name: `User ${i + 1}`,
208
+ amount: Math.floor(Math.random() * 1000) + 100,
209
+ }));
210
+ const groupingColumns = [
211
+ {
212
+ accessorKey: 'name',
213
+ header: 'Name',
214
+ flex: 1,
215
+ },
216
+ {
217
+ accessorKey: 'amount',
218
+ header: 'Amount',
219
+ cell: ({ row }) => (_jsxs("span", { style: { textAlign: 'right', display: 'block' }, children: ["$", row.original.amount.toFixed(2)] })),
220
+ flex: 1,
221
+ },
222
+ {
223
+ accessorKey: 'date',
224
+ header: 'Last Updated',
225
+ flex: 1,
226
+ enableGrouping: true,
227
+ getGroupingValue: (row) => new Date(row.date).toLocaleDateString('en-GB'),
228
+ meta: {
229
+ bgColor: '#f5f5f5',
230
+ },
231
+ },
232
+ ];
233
+ export const GroupedByDate = {
234
+ render: () => (_jsx(Table, { id: "header-hidden-table", data: groupingData, columns: groupingColumns, showHeader: false, grouping: ['date'] })),
235
+ };
236
+ function useSearch(data, selector, minChars = 3) {
237
+ const [query, setQuery] = useState('');
238
+ const isSearchActive = query.trim().length >= minChars;
239
+ const filteredData = useMemo(() => {
240
+ if (!isSearchActive)
241
+ return data;
242
+ const lowerQuery = query.toLowerCase();
243
+ return data.filter((item) => selector(item).toLowerCase().includes(lowerQuery));
244
+ }, [query, isSearchActive, data, selector]);
245
+ return { query, setQuery, filteredData, isSearchActive };
246
+ }
247
+ export const WithSearch = {
248
+ render: () => {
249
+ const { setQuery, filteredData, isSearchActive } = useSearch(data, (user) => user.name);
250
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { style: { width: '300px' }, children: _jsx(SearchInput, { onSearch: (q) => setQuery(q), onClear: () => setQuery(''), placeholder: "Search by name..." }) }), _jsx(Table, { id: "search-table", data: filteredData, columns: columns, isSearchActive: isSearchActive })] }));
251
+ },
252
+ };
253
+ export const ServerSideSearch = {
254
+ render: () => {
255
+ const [data, setData] = useState([]);
256
+ const [isLoading, setIsLoading] = useState(false);
257
+ const [query, setQuery] = useState('');
258
+ const fetchData = useCallback(async (pageIndex, pageSize) => {
259
+ setIsLoading(true);
260
+ const results = await fetchServerData(pageIndex, pageSize, query);
261
+ setData(results);
262
+ setIsLoading(false);
263
+ }, [query]);
264
+ useEffect(() => {
265
+ fetchData(0, 10);
266
+ }, [fetchData]);
267
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { style: { width: '300px' }, children: _jsx(SearchInput, { onSearch: (q) => setQuery(q), onClear: () => setQuery(''), placeholder: "Search users..." }) }), _jsx(Table, { id: "server-search-table", data: data, columns: columns, paginationMode: "server", totalServerRows: 100, onRefetch: fetchData, isLoading: isLoading, isSearchActive: query.trim().length >= 3 })] }));
268
+ },
269
+ };
270
+ // Note: Does not update state search params
271
+ export const ClientPaginationWithQueryParams = {
272
+ args: {
273
+ initialPageNo: 2,
274
+ initialPageSize: 5,
275
+ },
276
+ decorators: [
277
+ (Story, context) => {
278
+ const { initialPageNo, initialPageSize } = context.args;
279
+ const query = `?clients_pageNo=${initialPageNo}&clients_pageSize=${initialPageSize}`;
280
+ window.history.replaceState({}, '', query);
281
+ return _jsx(Story, {});
282
+ },
283
+ ],
284
+ render: () => (_jsxs(_Fragment, { children: [_jsx(CurrentUrl, {}), _jsx(Table, { id: "clients", data: data, columns: columns, paginationMode: "client", showHeader: true })] })),
285
+ };
286
+ // Note: Does not update state search params
287
+ export const ServerPaginationWithQueryParams = {
288
+ args: {
289
+ initialPageNo: 2,
290
+ initialPageSize: 5,
291
+ },
292
+ decorators: [
293
+ (Story, context) => {
294
+ const { initialPageNo, initialPageSize } = context.args;
295
+ const query = `?clients_pageNo=${initialPageNo}&clients_pageSize=${initialPageSize}`;
296
+ window.history.replaceState({}, '', query);
297
+ return _jsx(Story, {});
298
+ },
299
+ ],
300
+ render: () => {
301
+ const [data, setData] = useState([]);
302
+ const [isLoading, setIsLoading] = useState(false);
303
+ const fetchData = useCallback(async (pageIndex, pageSize) => {
304
+ setIsLoading(true);
305
+ const results = await fetchServerData(pageIndex, pageSize);
306
+ setData(results);
307
+ setIsLoading(false);
308
+ }, []);
309
+ return (_jsxs(_Fragment, { children: [_jsx(CurrentUrl, {}), _jsx(Table, { id: "clients", data: data, columns: columns, paginationMode: "server", totalServerRows: 100, onRefetch: fetchData, isLoading: isLoading, showHeader: true })] }));
310
+ },
311
+ };
312
+ // Note: Does not update state search params
313
+ export const TwoTablesWithQueryParams = {
314
+ args: {
315
+ clientsInitialPageNo: 2,
316
+ clientsInitialPageSize: 5,
317
+ usersInitialPageNo: 3,
318
+ usersInitialPageSize: 10,
319
+ },
320
+ decorators: [
321
+ (Story, context) => {
322
+ const { clientsInitialPageNo, clientsInitialPageSize, usersInitialPageNo, usersInitialPageSize } = context.args;
323
+ const query = `?clients_pageNo=${clientsInitialPageNo}&clients_pageSize=${clientsInitialPageSize}&users_pageNo=${usersInitialPageNo}&users_pageSize=${usersInitialPageSize}`;
324
+ window.history.replaceState({}, '', query);
325
+ return _jsx(Story, {});
326
+ },
327
+ ],
328
+ render: () => {
329
+ const [usersData, setUsersData] = useState([]);
330
+ const [isLoadingUsers, setIsLoadingUsers] = useState(false);
331
+ const fetchUsersData = useCallback(async (pageIndex, pageSize) => {
332
+ setIsLoadingUsers(true);
333
+ const results = await fetchServerData(pageIndex, pageSize);
334
+ setUsersData(results);
335
+ setIsLoadingUsers(false);
336
+ }, []);
337
+ return (_jsxs(_Fragment, { children: [_jsx(CurrentUrl, {}), _jsxs("div", { style: { display: 'flex', gap: '2rem' }, children: [_jsx(Table, { id: "clients", data: data, columns: columns, paginationMode: "client", showHeader: true }), _jsx(Table, { id: "users", data: usersData, columns: columns, paginationMode: "server", totalServerRows: 100, onRefetch: fetchUsersData, isLoading: isLoadingUsers, showHeader: true })] })] }));
338
+ },
339
+ };
340
+ function fetchInfiniteUsers(limit, offset = 0) {
341
+ const start = offset * limit;
342
+ const rows = Array.from({ length: limit }).map((_, i) => {
343
+ const idx = start + i + 1;
344
+ return {
345
+ id: `user-${idx}`,
346
+ name: `User ${idx}`,
347
+ email: `user${idx}@example.com`,
348
+ role: idx % 2 === 0 ? 'Admin' : 'User',
349
+ };
350
+ });
351
+ return new Promise((resolve) => {
352
+ setTimeout(() => {
353
+ const nextOffset = offset + 1;
354
+ const maxPages = Math.ceil(100 / limit);
355
+ const hasMore = nextOffset < maxPages;
356
+ resolve({ rows, nextOffset, hasMore });
357
+ }, 300);
358
+ });
359
+ }
360
+ export const InfiniteScrollVirtualized = {
361
+ name: 'Infinite Scroll — Virtualized',
362
+ render: () => {
363
+ const [rows, setRows] = useState([]);
364
+ const [nextOffset, setNextOffset] = useState(0);
365
+ const [hasNextPage, setHasNextPage] = useState(true);
366
+ const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
367
+ useEffect(() => {
368
+ (async () => {
369
+ setIsFetchingNextPage(true);
370
+ const res = await fetchInfiniteUsers(20, 0);
371
+ setRows(res.rows);
372
+ setNextOffset(res.nextOffset);
373
+ setHasNextPage(res.hasMore);
374
+ setIsFetchingNextPage(false);
375
+ })();
376
+ }, []);
377
+ const fetchNextPage = useCallback(async () => {
378
+ if (isFetchingNextPage || !hasNextPage)
379
+ return;
380
+ setIsFetchingNextPage(true);
381
+ const res = await fetchInfiniteUsers(20, nextOffset);
382
+ setRows((prev) => [...prev, ...res.rows]);
383
+ setNextOffset(res.nextOffset);
384
+ setHasNextPage(res.hasMore);
385
+ setIsFetchingNextPage(false);
386
+ }, [isFetchingNextPage, hasNextPage, nextOffset]);
387
+ return (_jsx("div", { children: _jsx(Table, { id: "infinite-virtualized", height: 500, data: rows, columns: columns, showHeader: true, infiniteScroll: {
388
+ enabled: true,
389
+ hasNextPage,
390
+ isFetchingNextPage,
391
+ fetchNextPage,
392
+ }, virtualization: {
393
+ enabled: true,
394
+ rowEstimate: 48,
395
+ overscan: 6,
396
+ } }) }));
397
+ },
398
+ };
399
+ export const InfiniteScrollNonVirtual = {
400
+ name: 'Infinite Scroll — Non-virtual',
401
+ render: () => {
402
+ const [rows, setRows] = useState([]);
403
+ const [nextOffset, setNextOffset] = useState(0);
404
+ const [hasNextPage, setHasNextPage] = useState(true);
405
+ const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
406
+ useEffect(() => {
407
+ (async () => {
408
+ setIsFetchingNextPage(true);
409
+ const res = await fetchInfiniteUsers(20, 0);
410
+ setRows(res.rows);
411
+ setNextOffset(res.nextOffset);
412
+ setHasNextPage(res.hasMore);
413
+ setIsFetchingNextPage(false);
414
+ })();
415
+ }, []);
416
+ const fetchNextPage = useCallback(async () => {
417
+ if (isFetchingNextPage || !hasNextPage)
418
+ return;
419
+ setIsFetchingNextPage(true);
420
+ const res = await fetchInfiniteUsers(20, nextOffset);
421
+ setRows((prev) => [...prev, ...res.rows]);
422
+ setNextOffset(res.nextOffset);
423
+ setHasNextPage(res.hasMore);
424
+ setIsFetchingNextPage(false);
425
+ }, [isFetchingNextPage, hasNextPage, nextOffset]);
426
+ return (_jsx("div", { children: _jsx(Table, { id: "infinite-nonvirtual", height: "80vh", data: rows, columns: columns, showHeader: true, infiniteScroll: {
427
+ enabled: true,
428
+ hasNextPage,
429
+ isFetchingNextPage,
430
+ fetchNextPage,
431
+ rootMarginPx: 300,
432
+ }, virtualization: {
433
+ enabled: false,
434
+ } }) }));
435
+ },
436
+ };
437
+ export const NoRowsInfiniteScrollNonVirtual = {
438
+ name: 'Empty State — Infinite Scroll (Non-virtual)',
439
+ render: () => {
440
+ const [rows] = useState([]);
441
+ const [hasNextPage] = useState(false);
442
+ const [isFetchingNextPage] = useState(false);
443
+ const fetchNextPage = useCallback(async () => {
444
+ // no-op: truly empty scenario
445
+ return;
446
+ }, []);
447
+ return (_jsx("div", { children: _jsx(Table, { id: "no-rows-infinite-nonvirtual", height: 500, data: rows, columns: columns, showHeader: true, isSearchActive: false, isLoading: false, noRowsMessage: "No rows to display", infiniteScroll: {
448
+ enabled: true,
449
+ hasNextPage,
450
+ isFetchingNextPage,
451
+ fetchNextPage,
452
+ rootMarginPx: 300,
453
+ }, virtualization: {
454
+ enabled: false,
455
+ } }) }));
456
+ },
457
+ };
458
+ export const NoRowsInfiniteScrollVirtualized = {
459
+ name: 'Empty State — Infinite Scroll (Virtualized)',
460
+ render: () => {
461
+ const [rows] = useState([]);
462
+ const [hasNextPage] = useState(false);
463
+ const [isFetchingNextPage] = useState(false);
464
+ const fetchNextPage = useCallback(async () => {
465
+ // no-op: truly empty scenario
466
+ return;
467
+ }, []);
468
+ return (_jsx("div", { children: _jsx(Table, { id: "no-rows-infinite-virtualized", height: 500, data: rows, columns: columns, showHeader: true, isSearchActive: false, isLoading: false, noRowsMessage: "No rows to display", infiniteScroll: {
469
+ enabled: true,
470
+ hasNextPage,
471
+ isFetchingNextPage,
472
+ fetchNextPage,
473
+ }, virtualization: {
474
+ enabled: true,
475
+ rowEstimate: 48,
476
+ overscan: 6,
477
+ } }) }));
478
+ },
479
+ };
@@ -0,0 +1,29 @@
1
+ import type { RefObject } from 'react';
2
+ import type { Virtualizer } from '@tanstack/react-virtual';
3
+ import type { InfiniteScrollOptions, VirtualizationOptions } from '../Table.types';
4
+ type PageEnvelope<T> = {
5
+ rows: T[];
6
+ };
7
+ export interface UseInfiniteScrollingParams<T extends {
8
+ id: string;
9
+ }> {
10
+ dataPages?: PageEnvelope<T>[] | undefined;
11
+ flatData?: T[] | undefined;
12
+ infiniteScroll?: InfiniteScrollOptions;
13
+ virtualization?: VirtualizationOptions;
14
+ parentScrollRef: RefObject<HTMLDivElement | null>;
15
+ loaderRef?: RefObject<HTMLElement | null>;
16
+ }
17
+ export interface UseInfiniteScrollingReturn<T extends {
18
+ id: string;
19
+ }> {
20
+ allRows: T[];
21
+ virtualizationEnabled: boolean;
22
+ rowVirtualizer?: Virtualizer<HTMLDivElement, Element>;
23
+ hasNextPage: boolean;
24
+ isFetchingNextPage: boolean;
25
+ }
26
+ export declare function useInfiniteScrolling<T extends {
27
+ id: string;
28
+ }>(params: UseInfiniteScrollingParams<T>): UseInfiniteScrollingReturn<T>;
29
+ export {};
@@ -0,0 +1,96 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { useVirtualizer } from '@tanstack/react-virtual';
3
+ export function useInfiniteScrolling(params) {
4
+ var _a;
5
+ const { dataPages, flatData, infiniteScroll, virtualization, parentScrollRef, loaderRef } = params;
6
+ const isInfinite = !!(infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.enabled);
7
+ const hasNextPage = !!(infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.hasNextPage);
8
+ const isFetchingNextPage = !!(infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.isFetchingNextPage);
9
+ const fetchNextPage = infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.fetchNextPage;
10
+ const allRows = useMemo(() => {
11
+ if (isInfinite && Array.isArray(dataPages)) {
12
+ const byId = new Map();
13
+ for (const p of dataPages) {
14
+ for (const r of p.rows) {
15
+ byId.set(String(r.id), r);
16
+ }
17
+ }
18
+ return Array.from(byId.values());
19
+ }
20
+ return flatData !== null && flatData !== void 0 ? flatData : [];
21
+ }, [isInfinite, dataPages, flatData]);
22
+ const virtualizationEnabled = !!(virtualization === null || virtualization === void 0 ? void 0 : virtualization.enabled);
23
+ const rowVirtualizer = useVirtualizer({
24
+ count: allRows.length + (isInfinite && hasNextPage ? 1 : 0),
25
+ getScrollElement: () => parentScrollRef.current,
26
+ estimateSize: () => { var _a; return (_a = virtualization === null || virtualization === void 0 ? void 0 : virtualization.rowEstimate) !== null && _a !== void 0 ? _a : 48; },
27
+ overscan: (_a = virtualization === null || virtualization === void 0 ? void 0 : virtualization.overscan) !== null && _a !== void 0 ? _a : 6,
28
+ });
29
+ const loadLockRef = useRef(false);
30
+ const virtualItems = rowVirtualizer.getVirtualItems();
31
+ const lastVirtualIndex = virtualItems.length ? virtualItems[virtualItems.length - 1].index : -1;
32
+ const loaderIndex = allRows.length;
33
+ useEffect(() => {
34
+ if (!virtualizationEnabled || !isInfinite)
35
+ return;
36
+ if (!fetchNextPage || !hasNextPage || isFetchingNextPage)
37
+ return;
38
+ if (allRows.length === 0)
39
+ return;
40
+ if (lastVirtualIndex < loaderIndex || loadLockRef.current)
41
+ return;
42
+ loadLockRef.current = true;
43
+ Promise.resolve(fetchNextPage()).finally(() => {
44
+ loadLockRef.current = false;
45
+ });
46
+ }, [
47
+ virtualizationEnabled,
48
+ isInfinite,
49
+ hasNextPage,
50
+ isFetchingNextPage,
51
+ fetchNextPage,
52
+ lastVirtualIndex,
53
+ loaderIndex,
54
+ allRows.length,
55
+ ]);
56
+ useEffect(() => {
57
+ var _a, _b;
58
+ if (!isInfinite || virtualizationEnabled)
59
+ return;
60
+ if (!(loaderRef === null || loaderRef === void 0 ? void 0 : loaderRef.current) || !fetchNextPage)
61
+ return;
62
+ const rootMargin = `${(_a = infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.rootMarginPx) !== null && _a !== void 0 ? _a : 300}px`;
63
+ const observer = new IntersectionObserver((entries) => {
64
+ const entry = entries[0];
65
+ if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) {
66
+ fetchNextPage();
67
+ }
68
+ }, {
69
+ root: (_b = parentScrollRef.current) !== null && _b !== void 0 ? _b : null,
70
+ rootMargin: `0px 0px ${rootMargin} 0px`,
71
+ threshold: 0.01,
72
+ });
73
+ const el = loaderRef.current;
74
+ observer.observe(el);
75
+ return () => {
76
+ observer.unobserve(el);
77
+ observer.disconnect();
78
+ };
79
+ }, [
80
+ isInfinite,
81
+ virtualizationEnabled,
82
+ hasNextPage,
83
+ isFetchingNextPage,
84
+ fetchNextPage,
85
+ infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.rootMarginPx,
86
+ parentScrollRef,
87
+ loaderRef,
88
+ ]);
89
+ return {
90
+ allRows,
91
+ virtualizationEnabled,
92
+ rowVirtualizer,
93
+ hasNextPage,
94
+ isFetchingNextPage,
95
+ };
96
+ }
@@ -0,0 +1,16 @@
1
+ import type { Table } from '@tanstack/react-table';
2
+ import { PaginationMode } from '../Table.types';
3
+ interface UsePaginationControllerOptions<T> {
4
+ table: Table<T>;
5
+ paginationMode: PaginationMode;
6
+ onRefetch?: (pageIndex: number, pageSize: number) => void;
7
+ }
8
+ export declare function usePaginationController<T>({ table, paginationMode, onRefetch }: UsePaginationControllerOptions<T>): {
9
+ pageIndex: number;
10
+ pageSize: number;
11
+ canNextPage: boolean;
12
+ canPrevPage: boolean;
13
+ onNextPage: () => void;
14
+ onPrevPage: () => void;
15
+ };
16
+ export {};
@@ -0,0 +1,30 @@
1
+ import { useCallback, useEffect, useMemo } from 'react';
2
+ export function usePaginationController({ table, paginationMode, onRefetch }) {
3
+ const { pageIndex, pageSize } = table.getState().pagination;
4
+ const canNextPage = table.getCanNextPage();
5
+ const canPrevPage = table.getCanPreviousPage();
6
+ const onNextPage = useCallback(() => {
7
+ if (canNextPage) {
8
+ table.nextPage();
9
+ }
10
+ }, [canNextPage, table]);
11
+ const onPrevPage = useCallback(() => {
12
+ if (canPrevPage) {
13
+ table.previousPage();
14
+ }
15
+ }, [canPrevPage, table]);
16
+ // Only refetch in server mode
17
+ useEffect(() => {
18
+ if (paginationMode === 'server' && onRefetch) {
19
+ onRefetch(pageIndex, pageSize);
20
+ }
21
+ }, [paginationMode, pageIndex, pageSize, onRefetch]);
22
+ return useMemo(() => ({
23
+ pageIndex,
24
+ pageSize,
25
+ canNextPage,
26
+ canPrevPage,
27
+ onNextPage,
28
+ onPrevPage,
29
+ }), [pageIndex, pageSize, canNextPage, canPrevPage, onNextPage, onPrevPage]);
30
+ }
@@ -0,0 +1,26 @@
1
+ import { type SortingState } from '@tanstack/react-table';
2
+ import type { TableProps } from '../Table.types';
3
+ export declare function useTableController<T extends {
4
+ id: string;
5
+ }>({ id, height, data, columns, checkboxSelection, checkboxPosition, paginationMode, sorting, onSortingChange, onSelectionChange, enableRowSelection, rowsPerPage, isMultiRowSelection, selectedRowIds, rowIdKey, totalServerRows, onRefetch, showHeader, grouping, infiniteScroll, virtualization, }: TableProps<T>): {
6
+ pageIndex: number;
7
+ pageSize: number;
8
+ canNextPage: boolean;
9
+ canPrevPage: boolean;
10
+ onNextPage: () => void;
11
+ onPrevPage: () => void;
12
+ table: import("@tanstack/table-core").Table<T>;
13
+ height: string | number | undefined;
14
+ showHeader: boolean;
15
+ grouping: string[] | undefined;
16
+ setPageSize: import("react").Dispatch<import("react").SetStateAction<number>>;
17
+ setPageIndex: import("react").Dispatch<import("react").SetStateAction<number>>;
18
+ sorting: SortingState;
19
+ virtualizationEnabled: boolean;
20
+ rowVirtualizer: import("@tanstack/virtual-core").Virtualizer<HTMLDivElement, Element> | undefined;
21
+ hasNextPage: boolean;
22
+ parentScrollRef: import("react").MutableRefObject<HTMLDivElement | null>;
23
+ loaderRef: import("react").MutableRefObject<HTMLTableRowElement | null>;
24
+ infiniteScroll: import("..").InfiniteScrollOptions | undefined;
25
+ virtualization: import("..").VirtualizationOptions | undefined;
26
+ };
@@ -0,0 +1,146 @@
1
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
+ import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, getGroupedRowModel, getExpandedRowModel, } from '@tanstack/react-table';
3
+ import { applyFlexSizes, getCheckboxSelectionColumn } from '../table.helpers';
4
+ import { usePaginationController } from './usePaginationController';
5
+ import { useUrlPaginationSync } from './useUrlPaginationSync';
6
+ import { useInfiniteScrolling } from './useInfiniteScrolling';
7
+ export function useTableController({ id, height, data, columns, checkboxSelection, checkboxPosition = 'start', paginationMode = 'client', sorting, onSortingChange, onSelectionChange, enableRowSelection, rowsPerPage = 10, isMultiRowSelection = true, selectedRowIds, rowIdKey = 'id', totalServerRows, onRefetch, showHeader = true, grouping, infiniteScroll, virtualization, }) {
8
+ const safeData = Array.isArray(data) ? data : [];
9
+ const stableGrouping = useMemo(() => grouping !== null && grouping !== void 0 ? grouping : [], [grouping]);
10
+ const parentScrollRef = useRef(null);
11
+ const loaderRef = useRef(null);
12
+ const columnVisibility = useMemo(() => {
13
+ return Object.fromEntries(stableGrouping.map((columnId) => [columnId, false]));
14
+ }, [stableGrouping]);
15
+ const [containerWidth, setContainerWidth] = useState(0);
16
+ useLayoutEffect(() => {
17
+ if (!parentScrollRef.current)
18
+ return;
19
+ const ro = new ResizeObserver(([entry]) => {
20
+ setContainerWidth(entry.contentRect.width);
21
+ });
22
+ ro.observe(parentScrollRef.current);
23
+ return () => ro.disconnect();
24
+ }, []);
25
+ const [tableColumns, setTableColumns] = useState(() => applyFlexSizes(columns !== null && columns !== void 0 ? columns : [], containerWidth, !!(virtualization === null || virtualization === void 0 ? void 0 : virtualization.enabled)));
26
+ const [internalSorting, setInternalSorting] = useState(sorting !== null && sorting !== void 0 ? sorting : []);
27
+ const [internalRowSelection, setInternalRowSelection] = useState(() => {
28
+ if (selectedRowIds) {
29
+ return selectedRowIds.reduce((acc, id) => {
30
+ acc[id] = true;
31
+ return acc;
32
+ }, {});
33
+ }
34
+ return {};
35
+ });
36
+ const { pageIndex, setPageIndex, pageSize, setPageSize } = useUrlPaginationSync(rowsPerPage, id);
37
+ const isInfinite = !!(infiniteScroll === null || infiniteScroll === void 0 ? void 0 : infiniteScroll.enabled);
38
+ const dataPages = useMemo(() => {
39
+ if (!isInfinite) {
40
+ return undefined;
41
+ }
42
+ return [{ rows: safeData }];
43
+ }, [isInfinite, safeData.length]);
44
+ const infiniteOpts = useMemo(() => infiniteScroll, [infiniteScroll]);
45
+ const virtualizationOpts = useMemo(() => virtualization, [virtualization]);
46
+ const { allRows, virtualizationEnabled, rowVirtualizer, hasNextPage } = useInfiniteScrolling({
47
+ dataPages,
48
+ flatData: data,
49
+ infiniteScroll: infiniteOpts,
50
+ virtualization: virtualizationOpts,
51
+ parentScrollRef,
52
+ loaderRef,
53
+ });
54
+ const dataForTable = useMemo(() => (virtualizationEnabled || isInfinite ? allRows : safeData), [virtualizationEnabled, isInfinite, allRows, safeData]);
55
+ const table = useReactTable({
56
+ data: dataForTable,
57
+ columns: tableColumns,
58
+ getRowId: (row) => String(row[rowIdKey]),
59
+ state: {
60
+ ...(isInfinite ? {} : { pagination: { pageIndex, pageSize } }),
61
+ sorting: sorting !== null && sorting !== void 0 ? sorting : internalSorting,
62
+ rowSelection: internalRowSelection,
63
+ grouping: stableGrouping,
64
+ columnVisibility,
65
+ },
66
+ getSortedRowModel: getSortedRowModel(),
67
+ getCoreRowModel: getCoreRowModel(),
68
+ getPaginationRowModel: !isInfinite && paginationMode === 'client' ? getPaginationRowModel() : undefined,
69
+ manualPagination: !isInfinite && paginationMode === 'server',
70
+ autoResetPageIndex: false,
71
+ pageCount: !isInfinite && paginationMode === 'server' ? Math.ceil((totalServerRows !== null && totalServerRows !== void 0 ? totalServerRows : 0) / pageSize) : undefined,
72
+ onPaginationChange: (updater) => {
73
+ if (isInfinite)
74
+ return;
75
+ const next = typeof updater === 'function' ? updater({ pageIndex, pageSize }) : updater;
76
+ setPageIndex(next.pageIndex);
77
+ setPageSize(next.pageSize);
78
+ },
79
+ onSortingChange: (updater) => {
80
+ const nextSorting = typeof updater === 'function' ? updater(sorting !== null && sorting !== void 0 ? sorting : internalSorting) : updater;
81
+ setInternalSorting(nextSorting);
82
+ onSortingChange === null || onSortingChange === void 0 ? void 0 : onSortingChange(nextSorting);
83
+ },
84
+ enableRowSelection: enableRowSelection !== null && enableRowSelection !== void 0 ? enableRowSelection : true,
85
+ onRowSelectionChange: (updater) => {
86
+ const newSelection = typeof updater === 'function' ? updater(internalRowSelection) : updater;
87
+ setInternalRowSelection(newSelection);
88
+ if (onSelectionChange) {
89
+ const selectedIdsArray = Object.keys(newSelection).filter((id) => newSelection[id]);
90
+ onSelectionChange(selectedIdsArray);
91
+ }
92
+ },
93
+ enableMultiRowSelection: isMultiRowSelection,
94
+ getGroupedRowModel: getGroupedRowModel(),
95
+ getExpandedRowModel: getExpandedRowModel(),
96
+ });
97
+ useEffect(() => {
98
+ if (paginationMode === 'none') {
99
+ setPageIndex(0);
100
+ }
101
+ }, [paginationMode, setPageIndex]);
102
+ const pagination = usePaginationController({
103
+ table,
104
+ paginationMode,
105
+ onRefetch,
106
+ });
107
+ const selectionColumn = useMemo(() => {
108
+ return checkboxSelection ? getCheckboxSelectionColumn(table) : null;
109
+ }, [checkboxSelection, table]);
110
+ useEffect(() => {
111
+ const normalized = applyFlexSizes(columns !== null && columns !== void 0 ? columns : [], containerWidth, !!(virtualization === null || virtualization === void 0 ? void 0 : virtualization.enabled));
112
+ let finalColumns;
113
+ if (checkboxSelection && selectionColumn) {
114
+ if (checkboxPosition === 'start') {
115
+ finalColumns = [selectionColumn, ...normalized];
116
+ }
117
+ else if (checkboxPosition === 'end') {
118
+ finalColumns = [...normalized, selectionColumn];
119
+ }
120
+ else {
121
+ finalColumns = normalized;
122
+ }
123
+ }
124
+ else {
125
+ finalColumns = normalized;
126
+ }
127
+ setTableColumns(finalColumns);
128
+ }, [columns, checkboxSelection, checkboxPosition, containerWidth, virtualization === null || virtualization === void 0 ? void 0 : virtualization.enabled]);
129
+ return {
130
+ table,
131
+ height,
132
+ showHeader,
133
+ grouping,
134
+ setPageSize,
135
+ setPageIndex,
136
+ sorting: sorting !== null && sorting !== void 0 ? sorting : internalSorting,
137
+ virtualizationEnabled,
138
+ rowVirtualizer,
139
+ hasNextPage,
140
+ parentScrollRef,
141
+ loaderRef,
142
+ infiniteScroll,
143
+ virtualization,
144
+ ...pagination,
145
+ };
146
+ }
@@ -1 +1,2 @@
1
1
  export * from '../config/uikitConfig';
2
+ export type { DateRange } from 'react-day-picker';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eml-payments/ui-kit",
3
- "version": "1.7.8",
3
+ "version": "1.8.0",
4
4
  "private": false,
5
5
  "description": "ARLO UIKit",
6
6
  "homepage": "https://github.com/EML-Payments/arlo.npm.uikit#readme",
@@ -74,7 +74,7 @@
74
74
  "clsx": "^2.1.1",
75
75
  "dayjs": "^1.11.19",
76
76
  "intl-tel-input": "^26.9.1",
77
- "lucide-react": "^0.577.0",
77
+ "lucide-react": "^1.8.0",
78
78
  "prettier": "^3.6.2",
79
79
  "react": ">=18.0.0",
80
80
  "react-day-picker": "^9.13.0",