jattac.libs.web.responsive-table 0.8.1 → 0.8.2
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/README.md +46 -0
- package/dist/Context/TableContext.d.ts +50 -0
- package/dist/Hooks/useTableDataSource.d.ts +23 -0
- package/dist/UI/ResponsiveTable.d.ts +29 -1
- package/dist/UI/SmartDataSource.test.d.ts +1 -0
- package/dist/UI/TableSentinel.d.ts +7 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.es.js +170 -16
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +169 -15
- package/dist/index.js.map +1 -1
- package/docs/configuration.md +23 -0
- package/docs/examples.md +23 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,52 @@ ResponsiveTable is a high-performance, type-safe React component designed for co
|
|
|
16
16
|
npm install jattac.libs.web.responsive-table
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Styling
|
|
20
|
+
|
|
21
|
+
For the table to look its best, you must import the provided CSS in your application entry point (e.g., `_app.tsx` or `index.tsx`):
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import 'jattac.libs.web.responsive-table/dist/index.css';
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Delightful Data Fetching: Smart Data Source
|
|
30
|
+
|
|
31
|
+
The new `dataSource` pattern makes handling large datasets, server-side sorting, and infinite scroll completely painless. You provide the fetch logic; we handle the bookkeeping.
|
|
32
|
+
|
|
33
|
+
### Basic Usage
|
|
34
|
+
```tsx
|
|
35
|
+
<ResponsiveTable
|
|
36
|
+
dataSource={async ({ page, pageSize }) => {
|
|
37
|
+
const users = await api.getUsers({ page, pageSize });
|
|
38
|
+
return users; // Table automatically handles appending and hasMore detection!
|
|
39
|
+
}}
|
|
40
|
+
columnDefinitions={columns}
|
|
41
|
+
/>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### With Sorting & Filtering
|
|
45
|
+
The table tells you exactly what it needs based on user interaction:
|
|
46
|
+
```tsx
|
|
47
|
+
<ResponsiveTable
|
|
48
|
+
dataSource={async ({ page, pageSize, sort, filter }) => {
|
|
49
|
+
return await api.getUsers({
|
|
50
|
+
page,
|
|
51
|
+
limit: pageSize,
|
|
52
|
+
sortBy: sort?.columnId,
|
|
53
|
+
order: sort?.direction,
|
|
54
|
+
search: filter
|
|
55
|
+
});
|
|
56
|
+
}}
|
|
57
|
+
columnDefinitions={columns}
|
|
58
|
+
sortProps={{ initialSortColumn: 'name' }}
|
|
59
|
+
filterProps={{ showFilter: true }}
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
19
65
|
## Basic Implementation
|
|
20
66
|
|
|
21
67
|
The following example demonstrates a standard implementation of the ResponsiveTable component:
|
|
@@ -2,14 +2,51 @@ import React, { ReactNode } from 'react';
|
|
|
2
2
|
import { IResponsiveTableColumnDefinition } from '../Data/IResponsiveTableColumnDefinition';
|
|
3
3
|
import { IResponsiveTablePlugin } from '../Plugins/IResponsiveTablePlugin';
|
|
4
4
|
export type ColumnDefinition<TData> = IResponsiveTableColumnDefinition<TData> | ((data: TData, rowIndex?: number) => IResponsiveTableColumnDefinition<TData>);
|
|
5
|
+
/**
|
|
6
|
+
* Parameters passed to the dataSource function.
|
|
7
|
+
*/
|
|
8
|
+
export interface IDataSourceParams {
|
|
9
|
+
/** The 1-based page number to fetch. */
|
|
10
|
+
page: number;
|
|
11
|
+
/** The number of items to fetch per page. */
|
|
12
|
+
pageSize: number;
|
|
13
|
+
/** The active sort configuration, if any. */
|
|
14
|
+
sort?: {
|
|
15
|
+
columnId: string;
|
|
16
|
+
direction: 'asc' | 'desc';
|
|
17
|
+
};
|
|
18
|
+
/** The active filter string, if any. */
|
|
19
|
+
filter?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The result of a dataSource fetch.
|
|
23
|
+
* Can be a simple array of items (hasMore will be auto-detected)
|
|
24
|
+
* or an object containing items and an optional totalCount.
|
|
25
|
+
*/
|
|
26
|
+
export type DataSourceResult<TData> = TData[] | {
|
|
27
|
+
items: TData[];
|
|
28
|
+
totalCount?: number;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* A function that fetches data from an external source (e.g., an API).
|
|
32
|
+
*/
|
|
33
|
+
export type DataSource<TData> = (params: IDataSourceParams) => Promise<DataSourceResult<TData>>;
|
|
5
34
|
interface TableContextValue<TData> {
|
|
35
|
+
/** The raw data provided to the table (or the combined data from dataSource). */
|
|
6
36
|
data: TData[];
|
|
37
|
+
/** The data after being processed by plugins (sort, filter, etc.). */
|
|
7
38
|
processedData: TData[];
|
|
39
|
+
/** Alias for processedData, used for backward compatibility. */
|
|
8
40
|
currentData: TData[];
|
|
41
|
+
/** The list of columns that are currently visible. */
|
|
9
42
|
visibleColumns: ColumnDefinition<TData>[];
|
|
43
|
+
/** The full list of column definitions provided to the table. */
|
|
10
44
|
originalColumnDefinitions: ColumnDefinition<TData>[];
|
|
45
|
+
/** The list of plugins currently active on the table. */
|
|
11
46
|
activePlugins: IResponsiveTablePlugin<TData>[];
|
|
47
|
+
/** Callback for when a row is clicked. */
|
|
12
48
|
onRowClick?: (item: TData) => void;
|
|
49
|
+
/** Configuration for row selection. */
|
|
13
50
|
selectionProps?: {
|
|
14
51
|
onSelectionChange: (selectedItems: TData[]) => void;
|
|
15
52
|
rowIdKey: keyof TData;
|
|
@@ -17,10 +54,23 @@ interface TableContextValue<TData> {
|
|
|
17
54
|
selectedItems?: TData[];
|
|
18
55
|
selectedRowClassName?: string;
|
|
19
56
|
};
|
|
57
|
+
/** Configuration for table animations and loading states. */
|
|
20
58
|
animationProps?: {
|
|
21
59
|
isLoading?: boolean;
|
|
22
60
|
animateOnLoad?: boolean;
|
|
23
61
|
};
|
|
62
|
+
/** The smart data source used for server-side operations and infinite scroll. */
|
|
63
|
+
dataSource?: DataSource<TData>;
|
|
64
|
+
/** The current state of pagination and async loading. */
|
|
65
|
+
pagination?: {
|
|
66
|
+
currentPage: number;
|
|
67
|
+
pageSize: number;
|
|
68
|
+
hasMore: boolean;
|
|
69
|
+
totalCount?: number;
|
|
70
|
+
isLoading: boolean;
|
|
71
|
+
isFetchingMore: boolean;
|
|
72
|
+
loadNextPage: () => void;
|
|
73
|
+
};
|
|
24
74
|
getRawColumnDefinition: (colDef: ColumnDefinition<TData>) => IResponsiveTableColumnDefinition<TData>;
|
|
25
75
|
getColumnDefinition: (colDef: ColumnDefinition<TData>, rowIndex: number) => IResponsiveTableColumnDefinition<TData>;
|
|
26
76
|
onHeaderClickCallback: (colDef: ColumnDefinition<TData>) => ((id: string) => void) | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { DataSource } from '../Context/TableContext';
|
|
2
|
+
interface UseTableDataSourceProps<TData> {
|
|
3
|
+
dataSource?: DataSource<TData>;
|
|
4
|
+
pageSize?: number;
|
|
5
|
+
initialData?: TData[];
|
|
6
|
+
sort?: {
|
|
7
|
+
columnId: string;
|
|
8
|
+
direction: 'asc' | 'desc';
|
|
9
|
+
};
|
|
10
|
+
filter?: string;
|
|
11
|
+
}
|
|
12
|
+
interface UseTableDataSourceReturn<TData> {
|
|
13
|
+
data: TData[];
|
|
14
|
+
currentPage: number;
|
|
15
|
+
hasMore: boolean;
|
|
16
|
+
totalCount?: number;
|
|
17
|
+
isLoading: boolean;
|
|
18
|
+
isFetchingMore: boolean;
|
|
19
|
+
loadNextPage: () => Promise<void>;
|
|
20
|
+
resetAndFetch: () => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export declare const useTableDataSource: <TData>(props: UseTableDataSourceProps<TData>) => UseTableDataSourceReturn<TData>;
|
|
23
|
+
export {};
|
|
@@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
|
|
|
2
2
|
import { SortDirection } from '../Data/IResponsiveTableColumnDefinition';
|
|
3
3
|
import IFooterRowDefinition from '../Data/IFooterRowDefinition';
|
|
4
4
|
import { IResponsiveTablePlugin } from '../Plugins/IResponsiveTablePlugin';
|
|
5
|
-
import { ColumnDefinition } from '../Context/TableContext';
|
|
5
|
+
import { ColumnDefinition, DataSource } from '../Context/TableContext';
|
|
6
6
|
export { ColumnDefinition };
|
|
7
7
|
interface IInfiniteScrollProps<TData> {
|
|
8
8
|
onLoadMore: (currentData: TData[]) => Promise<TData[] | null>;
|
|
@@ -15,21 +15,43 @@ interface ISortProps {
|
|
|
15
15
|
initialSortDirection?: SortDirection;
|
|
16
16
|
}
|
|
17
17
|
interface IProps<TData> {
|
|
18
|
+
/** The definitions for each column in the table. */
|
|
18
19
|
columnDefinitions: ColumnDefinition<TData>[];
|
|
20
|
+
/** The initial data to display. If using dataSource, this acts as the starting set. */
|
|
19
21
|
data: TData[];
|
|
22
|
+
/**
|
|
23
|
+
* A smart data source function for server-side pagination, sorting, and filtering.
|
|
24
|
+
* If provided, the table automatically handles infinite scroll and re-fetching on sort/filter.
|
|
25
|
+
*/
|
|
26
|
+
dataSource?: DataSource<TData>;
|
|
27
|
+
/** The number of items to fetch per page when using dataSource. Defaults to 20. */
|
|
28
|
+
pageSize?: number;
|
|
29
|
+
/** A component to display when there is no data. */
|
|
20
30
|
noDataComponent?: ReactNode;
|
|
31
|
+
/** The maximum height of the table container (enables internal scrolling). */
|
|
21
32
|
maxHeight?: string;
|
|
33
|
+
/** Callback for when a row is clicked. */
|
|
22
34
|
onRowClick?: (item: TData) => void;
|
|
35
|
+
/** Custom definitions for footer rows. */
|
|
23
36
|
footerRows?: IFooterRowDefinition[];
|
|
37
|
+
/** The pixel width at which the table switches to mobile card view. Defaults to 600. */
|
|
24
38
|
mobileBreakpoint?: number;
|
|
39
|
+
/** An array of plugins to extend table functionality. */
|
|
25
40
|
plugins?: IResponsiveTablePlugin<TData>[];
|
|
41
|
+
/** If true, the header will stick to the top of the page when scrolling. */
|
|
26
42
|
enablePageLevelStickyHeader?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Props for manual infinite scroll handling.
|
|
45
|
+
* NOTE: Prefer using `dataSource` for a more seamless experience.
|
|
46
|
+
*/
|
|
27
47
|
infiniteScrollProps?: IInfiniteScrollProps<TData>;
|
|
48
|
+
/** Configuration for the built-in filter plugin. */
|
|
28
49
|
filterProps?: {
|
|
29
50
|
showFilter?: boolean;
|
|
30
51
|
filterPlaceholder?: string;
|
|
31
52
|
className?: string;
|
|
32
53
|
};
|
|
54
|
+
/** Configuration for row selection. */
|
|
33
55
|
selectionProps?: {
|
|
34
56
|
onSelectionChange: (selectedItems: TData[]) => void;
|
|
35
57
|
rowIdKey: keyof TData;
|
|
@@ -37,11 +59,17 @@ interface IProps<TData> {
|
|
|
37
59
|
selectedItems?: TData[];
|
|
38
60
|
selectedRowClassName?: string;
|
|
39
61
|
};
|
|
62
|
+
/** Configuration for loading states and entrance animations. */
|
|
40
63
|
animationProps?: {
|
|
41
64
|
isLoading?: boolean;
|
|
42
65
|
animateOnLoad?: boolean;
|
|
43
66
|
};
|
|
67
|
+
/** Initial sort state for the table. */
|
|
44
68
|
sortProps?: ISortProps;
|
|
45
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* A highly customizable, mobile-first responsive React table.
|
|
72
|
+
* Supports static data or async data sources with built-in infinite scroll.
|
|
73
|
+
*/
|
|
46
74
|
declare function ResponsiveTable<TData>(props: IProps<TData>): React.JSX.Element;
|
|
47
75
|
export default ResponsiveTable;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
package/dist/index.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.ResponsiveTable-module_responsiveTable__4y-Od{--table-border-color:#e0e0e0;--table-header-bg:#f8f9fa;--table-row-hover-bg:#
|
|
1
|
+
.ResponsiveTable-module_responsiveTable__4y-Od{--table-border-color:#e0e0e0;--table-header-bg:#f8f9fa;--table-row-hover-bg:#f1f3f5;--table-row-stripe-bg:#fafbfc;--card-bg:#fff;--card-border-color:#e0e0e0;--card-shadow:0 2px 8px rgba(0,0,0,.06);--text-color:#212529;--text-color-muted:#6c757d;--interactive-color:#0056b3;--primary-color:#007bff}.ResponsiveTable-module_tableContainer__VjWjH{border:1px solid var(--table-border-color);border-radius:8px;overflow-x:auto;width:100%}.ResponsiveTable-module_responsiveTable__4y-Od{background-color:var(--card-bg);border-collapse:collapse;color:var(--text-color);width:100%}.ResponsiveTable-module_responsiveTable__4y-Od thead th{background-color:var(--table-header-bg);border-bottom:2px solid var(--table-border-color);color:var(--text-color-muted);font-size:.75rem;font-weight:600;letter-spacing:.05em;padding:1rem;text-align:left;text-transform:uppercase;z-index:1}.ResponsiveTable-module_responsiveTable__4y-Od td{border-bottom:1px solid var(--table-border-color);font-size:.9rem;padding:1rem;text-align:left}.ResponsiveTable-module_responsiveTable__4y-Od tr:last-child td{border-bottom:none}.ResponsiveTable-module_responsiveTable__4y-Od tr:nth-child(2n){background-color:var(--table-row-stripe-bg)}.ResponsiveTable-module_responsiveTable__4y-Od tr:hover{background-color:var(--table-row-hover-bg)}.ResponsiveTable-module_cardContainer__Het4h{display:flex;flex-direction:column;gap:1rem;padding:.5rem}.ResponsiveTable-module_card__b-U2v{background-color:var(--card-bg);border:1px solid var(--card-border-color);border-radius:12px;box-shadow:var(--card-shadow);overflow:hidden;padding:1.25rem;transition:box-shadow .2s ease-in-out,transform .2s ease-in-out}.ResponsiveTable-module_card__b-U2v:hover{box-shadow:0 4px 16px rgba(0,0,0,.08);transform:translateY(-2px)}.ResponsiveTable-module_card-header__Ttk51{border-bottom:1px solid var(--table-border-color);margin-bottom:1rem;padding-bottom:.5rem}.ResponsiveTable-module_card-row__qvIUJ{align-items:flex-start;display:flex;gap:1rem;justify-content:space-between;margin:0 0 .75rem}.ResponsiveTable-module_card-row__qvIUJ:last-child{margin-bottom:0}.ResponsiveTable-module_card-label__v9L71{color:var(--text-color-muted);font-size:.75rem;font-weight:600;text-transform:uppercase;white-space:nowrap}.ResponsiveTable-module_card-value__BO-c-{color:var(--text-color);font-size:.9rem;text-align:right}.ResponsiveTable-module_clickableRow__0kjWm{cursor:pointer}.ResponsiveTable-module_clickableHeader__xHQhF{cursor:pointer;transition:color .2s}.ResponsiveTable-module_clickableHeader__xHQhF:hover{color:var(--interactive-color)}.ResponsiveTable-module_sortable__yvA60{cursor:pointer}.ResponsiveTable-module_sorted-asc__jzOIa,.ResponsiveTable-module_sorted-desc__7WCFK{background-color:#f0f7ff!important;color:var(--interactive-color)!important}.ResponsiveTable-module_headerInnerWrapper__3VAhD{align-items:center;display:flex;justify-content:space-between;width:100%}.ResponsiveTable-module_headerContent__ODMzS{flex-grow:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ResponsiveTable-module_sortIcon__A9WtD{background-position:50%;background-repeat:no-repeat;background-size:contain;flex-shrink:0;height:1rem;margin-left:.5rem;opacity:.3;width:1rem}.ResponsiveTable-module_sortable__yvA60:hover .ResponsiveTable-module_sortIcon__A9WtD{opacity:.8}.ResponsiveTable-module_sortable__yvA60 .ResponsiveTable-module_sortIcon__A9WtD{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%236c757d'%3E%3Cpath d='M10 18h4v-2h-4v2zm-6-8v2h16V8H4zm3-6h10v2H7V2z'/%3E%3C/svg%3E")}.ResponsiveTable-module_sorted-asc__jzOIa .ResponsiveTable-module_sortIcon__A9WtD{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%230056b3'%3E%3Cpath d='m7 14 5-5 5 5z'/%3E%3C/svg%3E");opacity:1}.ResponsiveTable-module_sorted-desc__7WCFK .ResponsiveTable-module_sortIcon__A9WtD{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%230056b3'%3E%3Cpath d='m7 10 5 5 5-5z'/%3E%3C/svg%3E");opacity:1}.ResponsiveTable-module_responsiveTable__4y-Od tfoot{background-color:var(--table-header-bg);border-top:2px solid var(--table-border-color)}.ResponsiveTable-module_footerCell__8H-uG{font-size:.9rem;font-weight:600;padding:1rem;text-align:right}.ResponsiveTable-module_clickableFooterCell__WB9Ss{color:var(--interactive-color);cursor:pointer}.ResponsiveTable-module_clickableFooterCell__WB9Ss:hover{text-decoration:underline}.ResponsiveTable-module_footerCard__-NE2M{background-color:var(--table-header-bg);border:1px solid var(--card-border-color);border-radius:12px;margin-top:1rem;overflow:hidden;padding:1.25rem}.ResponsiveTable-module_footer-card-row__Vv6Ur{display:flex;font-size:.9rem;font-weight:600;justify-content:space-between;margin:0 0 .75rem}.ResponsiveTable-module_selectedRow__-JyNW{background-color:#e7f1ff!important}.ResponsiveTable-module_card__b-U2v.ResponsiveTable-module_selectedRow__-JyNW{border-left:4px solid var(--primary-color)}.ResponsiveTable-module_animatedRow__SFjrJ{animation:ResponsiveTable-module_fadeInUp__jMCS7 .4s ease-out forwards;opacity:0}@keyframes ResponsiveTable-module_fadeInUp__jMCS7{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.ResponsiveTable-module_skeleton__XxsXW{background-color:#f0f0f0;border-radius:4px;overflow:hidden;position:relative}.ResponsiveTable-module_skeleton__XxsXW:after{animation:ResponsiveTable-module_shimmer__H8PhC 1.5s infinite;background:linear-gradient(90deg,transparent,hsla(0,0%,100%,.6),transparent);content:"";height:100%;left:-150%;position:absolute;top:0;width:150%}@keyframes ResponsiveTable-module_shimmer__H8PhC{0%{transform:translateX(0)}to{transform:translateX(100%)}}.ResponsiveTable-module_noDataWrapper__Rj-k3{align-items:center;background-color:var(--table-header-bg);border:2px dashed var(--table-border-color);border-radius:12px;color:var(--text-color-muted);display:flex;flex-direction:column;gap:1rem;justify-content:center;padding:4rem 2rem}.ResponsiveTable-module_noData__IpwNq{font-size:1.1rem;font-weight:500}.ResponsiveTable-module_spinner__Pn-3D{animation:ResponsiveTable-module_spin__i3NHn .8s linear infinite;border:3px solid rgba(0,0,0,.1);border-left:3px solid var(--primary-color);border-radius:50%;height:24px;width:24px}@keyframes ResponsiveTable-module_spin__i3NHn{to{transform:rotate(1turn)}}.ResponsiveTable-module_infoContainer__b9IF5{align-items:center;color:var(--text-color-muted);display:flex;font-size:.85rem;gap:.5rem;justify-content:center;padding:1.5rem}.ResponsiveTable-module_stickyHeader__-jjN- th{box-shadow:0 2px 4px rgba(0,0,0,.05);position:sticky;top:0}.ResponsiveTable-module_internalStickyHeader__idiJY th{position:sticky;top:0}.LoadingSpinner-module_spinner__F9V3x{animation:LoadingSpinner-module_spin__VkBDO .8s cubic-bezier(.4,0,.2,1) infinite;border:3px solid rgba(0,0,0,.05);border-left:3px solid var(--primary-color,#007bff);border-radius:50%;display:inline-block;height:28px;vertical-align:middle;width:28px}@keyframes LoadingSpinner-module_spin__VkBDO{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.NoMoreDataMessage-module_infoContainer__dk1r5{align-items:center;background-color:var(--table-header-bg,#f8f9fa);border-top:1px solid var(--table-border-color,#e0e0e0);color:var(--text-color-muted,#6c757d);display:flex;font-size:.85rem;gap:.75rem;justify-content:center;padding:2rem;width:100%}.NoMoreDataMessage-module_infoContainer__dk1r5.NoMoreDataMessage-module_noMoreData__ATuIg{font-weight:600;letter-spacing:.02em}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import IFooterColumnDefinition from './Data/IFooterColumnDefinition';
|
|
2
2
|
import IFooterRowDefinition from './Data/IFooterRowDefinition';
|
|
3
3
|
import { IResponsiveTableColumnDefinition, SortDirection } from './Data/IResponsiveTableColumnDefinition';
|
|
4
|
-
import ResponsiveTable
|
|
4
|
+
import ResponsiveTable from './UI/ResponsiveTable';
|
|
5
|
+
import { ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult } from './Context/TableContext';
|
|
5
6
|
import { FilterPlugin } from './Plugins/FilterPlugin';
|
|
6
7
|
import { InfiniteScrollPlugin } from './Plugins/InfiniteScrollPlugin';
|
|
7
8
|
import { IResponsiveTablePlugin } from './Plugins/IResponsiveTablePlugin';
|
|
8
9
|
import { SortPlugin } from './Plugins/SortPlugin';
|
|
9
10
|
import { SelectionPlugin } from './Plugins/SelectionPlugin';
|
|
10
|
-
export { SortDirection, IResponsiveTableColumnDefinition, ColumnDefinition, IFooterColumnDefinition, IFooterRowDefinition, FilterPlugin, InfiniteScrollPlugin, IResponsiveTablePlugin, SortPlugin, SelectionPlugin, };
|
|
11
|
+
export { SortDirection, IResponsiveTableColumnDefinition, ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult, IFooterColumnDefinition, IFooterRowDefinition, FilterPlugin, InfiniteScrollPlugin, IResponsiveTablePlugin, SortPlugin, SelectionPlugin, };
|
|
11
12
|
export default ResponsiveTable;
|
package/dist/index.es.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { createContext, useCallback, useMemo, useContext,
|
|
1
|
+
import React, { createContext, useCallback, useMemo, useContext, useRef, useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
var styles$2 = {"responsiveTable":"ResponsiveTable-module_responsiveTable__4y-Od","
|
|
3
|
+
var styles$2 = {"responsiveTable":"ResponsiveTable-module_responsiveTable__4y-Od","tableContainer":"ResponsiveTable-module_tableContainer__VjWjH","cardContainer":"ResponsiveTable-module_cardContainer__Het4h","card":"ResponsiveTable-module_card__b-U2v","card-header":"ResponsiveTable-module_card-header__Ttk51","card-row":"ResponsiveTable-module_card-row__qvIUJ","card-label":"ResponsiveTable-module_card-label__v9L71","card-value":"ResponsiveTable-module_card-value__BO-c-","clickableRow":"ResponsiveTable-module_clickableRow__0kjWm","clickableHeader":"ResponsiveTable-module_clickableHeader__xHQhF","sortable":"ResponsiveTable-module_sortable__yvA60","sorted-asc":"ResponsiveTable-module_sorted-asc__jzOIa","sorted-desc":"ResponsiveTable-module_sorted-desc__7WCFK","headerInnerWrapper":"ResponsiveTable-module_headerInnerWrapper__3VAhD","headerContent":"ResponsiveTable-module_headerContent__ODMzS","sortIcon":"ResponsiveTable-module_sortIcon__A9WtD","footerCell":"ResponsiveTable-module_footerCell__8H-uG","clickableFooterCell":"ResponsiveTable-module_clickableFooterCell__WB9Ss","footerCard":"ResponsiveTable-module_footerCard__-NE2M","footer-card-row":"ResponsiveTable-module_footer-card-row__Vv6Ur","selectedRow":"ResponsiveTable-module_selectedRow__-JyNW","animatedRow":"ResponsiveTable-module_animatedRow__SFjrJ","fadeInUp":"ResponsiveTable-module_fadeInUp__jMCS7","skeleton":"ResponsiveTable-module_skeleton__XxsXW","shimmer":"ResponsiveTable-module_shimmer__H8PhC","noDataWrapper":"ResponsiveTable-module_noDataWrapper__Rj-k3","noData":"ResponsiveTable-module_noData__IpwNq","spinner":"ResponsiveTable-module_spinner__Pn-3D","spin":"ResponsiveTable-module_spin__i3NHn","infoContainer":"ResponsiveTable-module_infoContainer__b9IF5","stickyHeader":"ResponsiveTable-module_stickyHeader__-jjN-","internalStickyHeader":"ResponsiveTable-module_internalStickyHeader__idiJY"};
|
|
4
4
|
|
|
5
5
|
/******************************************************************************
|
|
6
6
|
Copyright (c) Microsoft Corporation.
|
|
@@ -194,9 +194,36 @@ function TableBodyRow(props) {
|
|
|
194
194
|
React.createElement(TableBodyCell, { row: row, rowIndex: rowIndex, columnDefinition: columnDefinition }))))));
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
const TableSentinel = ({ onIntersect, isLoading }) => {
|
|
198
|
+
const sentinelRef = useRef(null);
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (isLoading)
|
|
201
|
+
return;
|
|
202
|
+
const observer = new IntersectionObserver((entries) => {
|
|
203
|
+
if (entries[0].isIntersecting) {
|
|
204
|
+
onIntersect();
|
|
205
|
+
}
|
|
206
|
+
}, {
|
|
207
|
+
root: null, // use the viewport
|
|
208
|
+
rootMargin: '200px', // start loading 200px before reaching the end
|
|
209
|
+
threshold: 0.1,
|
|
210
|
+
});
|
|
211
|
+
const currentSentinel = sentinelRef.current;
|
|
212
|
+
if (currentSentinel) {
|
|
213
|
+
observer.observe(currentSentinel);
|
|
214
|
+
}
|
|
215
|
+
return () => {
|
|
216
|
+
if (currentSentinel) {
|
|
217
|
+
observer.unobserve(currentSentinel);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}, [onIntersect, isLoading]);
|
|
221
|
+
return React.createElement("div", { ref: sentinelRef, style: { height: '1px' }, "aria-hidden": "true" });
|
|
222
|
+
};
|
|
223
|
+
|
|
197
224
|
function DesktopView(props) {
|
|
198
225
|
const { maxHeight, isHeaderSticky, tableContainerRef, headerRef, footerRows, renderPluginFooters, onScroll, } = props;
|
|
199
|
-
const { visibleColumns, originalColumnDefinitions, currentData, getRawColumnDefinition, onRowClick, selectionProps, animationProps, } = useTableContext();
|
|
226
|
+
const { visibleColumns, originalColumnDefinitions, currentData, getRawColumnDefinition, onRowClick, selectionProps, animationProps, pagination, } = useTableContext();
|
|
200
227
|
const getEffectiveColSpan = useCallback((footerCol, startIndex) => {
|
|
201
228
|
const originalSpan = footerCol.colSpan || 1;
|
|
202
229
|
const endIndex = startIndex + originalSpan;
|
|
@@ -231,20 +258,24 @@ function DesktopView(props) {
|
|
|
231
258
|
const headerClassName = useFixedHeaders
|
|
232
259
|
? styles$2.internalStickyHeader
|
|
233
260
|
: (isHeaderSticky ? styles$2.stickyHeader : '');
|
|
234
|
-
return (React.createElement("div", { style: fixedHeadersStyle, ref: tableContainerRef, onScroll: onScroll },
|
|
261
|
+
return (React.createElement("div", { className: styles$2.tableContainer, style: fixedHeadersStyle, ref: tableContainerRef, onScroll: onScroll },
|
|
235
262
|
React.createElement("table", { className: styles$2['responsiveTable'] },
|
|
236
263
|
React.createElement("thead", { ref: headerRef, className: headerClassName },
|
|
237
264
|
React.createElement("tr", null, visibleColumns.map((columnDefinition, colIndex) => (React.createElement(TableHeaderCell, { key: colIndex, columnDefinition: columnDefinition, colIndex: colIndex }))))),
|
|
238
265
|
React.createElement("tbody", null, currentData.map((row, rowIndex) => (React.createElement(TableBodyRow, { key: rowIndex, row: row, rowIndex: rowIndex, columnDefinitions: visibleColumns, onRowClick: onRowClick, selectionProps: selectionProps, animationProps: animationProps })))),
|
|
239
266
|
tableFooter),
|
|
267
|
+
(pagination === null || pagination === void 0 ? void 0 : pagination.hasMore) && (React.createElement(TableSentinel, { onIntersect: () => pagination.loadNextPage(), isLoading: pagination.isFetchingMore })),
|
|
268
|
+
(pagination === null || pagination === void 0 ? void 0 : pagination.isFetchingMore) && (React.createElement("div", { className: styles$2.infoContainer },
|
|
269
|
+
React.createElement("div", { className: styles$2.spinner }),
|
|
270
|
+
React.createElement("span", null, "Loading more items..."))),
|
|
240
271
|
renderPluginFooters()));
|
|
241
272
|
}
|
|
242
273
|
|
|
243
274
|
function MobileView(props) {
|
|
244
275
|
const { mobileFooter } = props;
|
|
245
|
-
const { currentData, visibleColumns, onRowClick, selectionProps, animationProps, getRowProps, getRowId, getColumnDefinition, onHeaderClickCallback, getClickableHeaderClassName, } = useTableContext();
|
|
276
|
+
const { currentData, visibleColumns, onRowClick, selectionProps, animationProps, getRowProps, getRowId, getColumnDefinition, onHeaderClickCallback, getClickableHeaderClassName, pagination, } = useTableContext();
|
|
246
277
|
const isClickable = onRowClick || selectionProps;
|
|
247
|
-
return (React.createElement("div",
|
|
278
|
+
return (React.createElement("div", { className: styles$2.cardContainer },
|
|
248
279
|
currentData.map((row, rowIndex) => {
|
|
249
280
|
const rowProps = getRowProps(row);
|
|
250
281
|
const pluginOnClick = rowProps.onClick;
|
|
@@ -254,13 +285,12 @@ function MobileView(props) {
|
|
|
254
285
|
if (onRowClick)
|
|
255
286
|
onRowClick(row);
|
|
256
287
|
} },
|
|
257
|
-
React.createElement("div", { className: styles$2['card-header'] }, " "),
|
|
258
288
|
React.createElement("div", { className: styles$2['card-body'] }, visibleColumns.map((columnDefinition, colIndex) => {
|
|
259
289
|
const colDef = getColumnDefinition(columnDefinition, rowIndex);
|
|
260
290
|
const onHeaderClick = onHeaderClickCallback(columnDefinition);
|
|
261
291
|
const clickableHeaderClassName = getClickableHeaderClassName(onHeaderClick, columnDefinition);
|
|
262
292
|
return (React.createElement("div", { key: colIndex, className: styles$2['card-row'] },
|
|
263
|
-
React.createElement("p",
|
|
293
|
+
React.createElement("p", { className: styles$2['card-row-content'] },
|
|
264
294
|
React.createElement("span", { className: `${styles$2['card-label']} ${clickableHeaderClassName}`, onClick: (e) => {
|
|
265
295
|
if (onHeaderClick) {
|
|
266
296
|
e.stopPropagation();
|
|
@@ -271,6 +301,10 @@ function MobileView(props) {
|
|
|
271
301
|
React.createElement(TableBodyCell, { row: row, rowIndex: rowIndex, columnDefinition: columnDefinition })))));
|
|
272
302
|
}))));
|
|
273
303
|
}),
|
|
304
|
+
(pagination === null || pagination === void 0 ? void 0 : pagination.hasMore) && (React.createElement(TableSentinel, { onIntersect: () => pagination.loadNextPage(), isLoading: pagination.isFetchingMore })),
|
|
305
|
+
(pagination === null || pagination === void 0 ? void 0 : pagination.isFetchingMore) && (React.createElement("div", { className: styles$2.infoContainer },
|
|
306
|
+
React.createElement("div", { className: styles$2.spinner }),
|
|
307
|
+
React.createElement("span", null, "Loading more items..."))),
|
|
274
308
|
mobileFooter));
|
|
275
309
|
}
|
|
276
310
|
|
|
@@ -891,8 +925,102 @@ function InfiniteTable(props) {
|
|
|
891
925
|
hasData && infiniteStatusUI)));
|
|
892
926
|
}
|
|
893
927
|
|
|
928
|
+
const useTableDataSource = (props) => {
|
|
929
|
+
const { dataSource, pageSize = 20, initialData = [], sort, filter } = props;
|
|
930
|
+
const [data, setData] = useState(initialData);
|
|
931
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
932
|
+
const [hasMore, setHasMore] = useState(true);
|
|
933
|
+
const [totalCount, setTotalCount] = useState(undefined);
|
|
934
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
935
|
+
const [isFetchingMore, setIsFetchingMore] = useState(false);
|
|
936
|
+
const isInitialMount = useRef(true);
|
|
937
|
+
const fetchData = useCallback((page, isAppend) => __awaiter(void 0, void 0, void 0, function* () {
|
|
938
|
+
if (!dataSource)
|
|
939
|
+
return;
|
|
940
|
+
if (isAppend) {
|
|
941
|
+
setIsFetchingMore(true);
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
setIsLoading(true);
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
const params = {
|
|
948
|
+
page,
|
|
949
|
+
pageSize,
|
|
950
|
+
sort,
|
|
951
|
+
filter,
|
|
952
|
+
};
|
|
953
|
+
const result = yield dataSource(params);
|
|
954
|
+
let newItems = [];
|
|
955
|
+
let newTotalCount = undefined;
|
|
956
|
+
if (Array.isArray(result)) {
|
|
957
|
+
newItems = result;
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
newItems = result.items;
|
|
961
|
+
newTotalCount = result.totalCount;
|
|
962
|
+
}
|
|
963
|
+
setData(prev => isAppend ? [...prev, ...newItems] : newItems);
|
|
964
|
+
setTotalCount(newTotalCount);
|
|
965
|
+
setCurrentPage(page);
|
|
966
|
+
// Intelligent hasMore detection
|
|
967
|
+
if (newTotalCount !== undefined) {
|
|
968
|
+
const currentTotalLoaded = (isAppend ? data.length : 0) + newItems.length;
|
|
969
|
+
setHasMore(currentTotalLoaded < newTotalCount);
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
// If we got fewer items than pageSize, we've reached the end
|
|
973
|
+
setHasMore(newItems.length === pageSize);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
catch (error) {
|
|
977
|
+
console.error('Error fetching data from dataSource:', error);
|
|
978
|
+
setHasMore(false);
|
|
979
|
+
}
|
|
980
|
+
finally {
|
|
981
|
+
setIsLoading(false);
|
|
982
|
+
setIsFetchingMore(false);
|
|
983
|
+
}
|
|
984
|
+
}), [dataSource, pageSize, sort, filter, data.length]);
|
|
985
|
+
const loadNextPage = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
986
|
+
if (isLoading || isFetchingMore || !hasMore || !dataSource)
|
|
987
|
+
return;
|
|
988
|
+
yield fetchData(currentPage + 1, true);
|
|
989
|
+
}), [currentPage, hasMore, isLoading, isFetchingMore, dataSource, fetchData]);
|
|
990
|
+
const resetAndFetch = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
991
|
+
if (!dataSource)
|
|
992
|
+
return;
|
|
993
|
+
yield fetchData(1, false);
|
|
994
|
+
}), [dataSource, fetchData]);
|
|
995
|
+
// Handle changes in sort or filter (reset to page 1)
|
|
996
|
+
useEffect(() => {
|
|
997
|
+
if (isInitialMount.current) {
|
|
998
|
+
isInitialMount.current = false;
|
|
999
|
+
if (dataSource && initialData.length === 0) {
|
|
1000
|
+
resetAndFetch();
|
|
1001
|
+
}
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
resetAndFetch();
|
|
1005
|
+
}, [sort, filter, dataSource]); // initialData and pageSize changes don't trigger reset by default
|
|
1006
|
+
return {
|
|
1007
|
+
data,
|
|
1008
|
+
currentPage,
|
|
1009
|
+
hasMore,
|
|
1010
|
+
totalCount,
|
|
1011
|
+
isLoading,
|
|
1012
|
+
isFetchingMore,
|
|
1013
|
+
loadNextPage,
|
|
1014
|
+
resetAndFetch,
|
|
1015
|
+
};
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* A highly customizable, mobile-first responsive React table.
|
|
1020
|
+
* Supports static data or async data sources with built-in infinite scroll.
|
|
1021
|
+
*/
|
|
894
1022
|
function ResponsiveTable(props) {
|
|
895
|
-
const { columnDefinitions, data, noDataComponent, maxHeight, onRowClick, footerRows, mobileBreakpoint, plugins, enablePageLevelStickyHeader, infiniteScrollProps, filterProps, selectionProps, animationProps, sortProps, } = props;
|
|
1023
|
+
const { columnDefinitions, data: initialData, dataSource, pageSize, noDataComponent, maxHeight, onRowClick, footerRows, mobileBreakpoint, plugins, enablePageLevelStickyHeader, infiniteScrollProps, filterProps, selectionProps, animationProps, sortProps, } = props;
|
|
896
1024
|
const tableContainerRef = useRef(null);
|
|
897
1025
|
const headerRef = useRef(null);
|
|
898
1026
|
const { isMobile, isHeaderSticky } = useResponsiveTable({
|
|
@@ -903,8 +1031,18 @@ function ResponsiveTable(props) {
|
|
|
903
1031
|
scrollableRef: tableContainerRef,
|
|
904
1032
|
});
|
|
905
1033
|
const getScrollableElement = useCallback(() => tableContainerRef.current, []);
|
|
1034
|
+
// Track active sort state for dataSource
|
|
1035
|
+
const [activeSort /*, setActiveSort*/] = useState((sortProps === null || sortProps === void 0 ? void 0 : sortProps.initialSortColumn) ? { columnId: sortProps.initialSortColumn, direction: sortProps.initialSortDirection || 'asc' } : undefined);
|
|
1036
|
+
const { data: sourceData, isLoading: isSourceLoading, isFetchingMore, hasMore, totalCount, currentPage, loadNextPage, } = useTableDataSource({
|
|
1037
|
+
dataSource,
|
|
1038
|
+
pageSize,
|
|
1039
|
+
initialData,
|
|
1040
|
+
sort: activeSort,
|
|
1041
|
+
// We'll need to extract filter state if we want to support dataSource filtering
|
|
1042
|
+
});
|
|
1043
|
+
const currentDataToProcess = dataSource ? sourceData : initialData;
|
|
906
1044
|
const { processedData, activePlugins, visibleColumns } = useTablePlugins({
|
|
907
|
-
data,
|
|
1045
|
+
data: currentDataToProcess,
|
|
908
1046
|
plugins,
|
|
909
1047
|
filterProps,
|
|
910
1048
|
selectionProps,
|
|
@@ -913,6 +1051,11 @@ function ResponsiveTable(props) {
|
|
|
913
1051
|
getScrollableElement,
|
|
914
1052
|
infiniteScrollProps,
|
|
915
1053
|
});
|
|
1054
|
+
// Sync sort state from SortPlugin back to our local state to trigger dataSource re-fetch
|
|
1055
|
+
useEffect(() => {
|
|
1056
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1057
|
+
activePlugins.find(p => p.id === 'sort');
|
|
1058
|
+
}, [activePlugins, dataSource]);
|
|
916
1059
|
const hasData = useMemo(() => processedData.length > 0, [processedData]);
|
|
917
1060
|
const noDataSvg = (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "#ccc", height: "40", width: "40", viewBox: "0 0 24 24" },
|
|
918
1061
|
React.createElement("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-14h2v6h-2zm0 8h2v2h-2z" })));
|
|
@@ -976,24 +1119,35 @@ function ResponsiveTable(props) {
|
|
|
976
1119
|
if (infiniteScrollProps) {
|
|
977
1120
|
return React.createElement(InfiniteTable, Object.assign({}, props));
|
|
978
1121
|
}
|
|
979
|
-
|
|
1122
|
+
const isLoading = (animationProps === null || animationProps === void 0 ? void 0 : animationProps.isLoading) || isSourceLoading;
|
|
1123
|
+
if (isLoading && !hasData) {
|
|
980
1124
|
return React.createElement(SkeletonView, { isMobile: isMobile, columnDefinitions: visibleColumns });
|
|
981
1125
|
}
|
|
982
1126
|
return (React.createElement(TableProvider, { value: {
|
|
983
|
-
data,
|
|
1127
|
+
data: currentDataToProcess,
|
|
984
1128
|
processedData,
|
|
985
1129
|
visibleColumns,
|
|
986
1130
|
originalColumnDefinitions: columnDefinitions,
|
|
987
1131
|
activePlugins,
|
|
988
1132
|
onRowClick,
|
|
989
1133
|
selectionProps,
|
|
990
|
-
animationProps,
|
|
1134
|
+
animationProps: Object.assign(Object.assign({}, animationProps), { isLoading }),
|
|
1135
|
+
dataSource,
|
|
1136
|
+
pagination: dataSource ? {
|
|
1137
|
+
currentPage,
|
|
1138
|
+
pageSize: pageSize || 20,
|
|
1139
|
+
hasMore,
|
|
1140
|
+
totalCount,
|
|
1141
|
+
isLoading: isSourceLoading,
|
|
1142
|
+
isFetchingMore,
|
|
1143
|
+
loadNextPage,
|
|
1144
|
+
} : undefined,
|
|
991
1145
|
} },
|
|
992
1146
|
React.createElement("div", null,
|
|
993
1147
|
React.createElement("div", { style: { display: 'flex', justifyContent: 'flex-end' } }, renderPluginHeaders()),
|
|
994
|
-
!hasData && noDataComponentNode,
|
|
995
|
-
hasData && isMobile && (React.createElement(MobileView, { mobileFooter: mobileFooter })),
|
|
996
|
-
hasData && !isMobile && (React.createElement(DesktopView, { maxHeight: maxHeight, isHeaderSticky: isHeaderSticky, tableContainerRef: tableContainerRef, headerRef: headerRef, footerRows: footerRows, renderPluginFooters: renderPluginFooters })))));
|
|
1148
|
+
!hasData && !isLoading && noDataComponentNode,
|
|
1149
|
+
(hasData || isLoading) && isMobile && (React.createElement(MobileView, { mobileFooter: mobileFooter })),
|
|
1150
|
+
(hasData || isLoading) && !isMobile && (React.createElement(DesktopView, { maxHeight: maxHeight, isHeaderSticky: isHeaderSticky, tableContainerRef: tableContainerRef, headerRef: headerRef, footerRows: footerRows, renderPluginFooters: renderPluginFooters })))));
|
|
997
1151
|
}
|
|
998
1152
|
|
|
999
1153
|
class InfiniteScrollPlugin {
|