jattac.libs.web.responsive-table 0.9.2 → 0.11.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.
package/README.md CHANGED
@@ -13,45 +13,81 @@ ResponsiveTable is a high-performance, type-safe React component designed for co
13
13
  ## Installation
14
14
 
15
15
  ```bash
16
- npm install jattac.libs.web.responsive-table
16
+ npm install jattac.libs.web.responsive-table jattac.libs.web.zest-textbox react-icons
17
17
  ```
18
18
 
19
19
  ---
20
20
 
21
+ ## Built-in Filter
22
+
23
+ Enable the search box with one prop. A clear (×) button appears automatically when the field has text.
24
+
25
+ **Client-side** — filters the in-memory `data` array and highlights matches:
26
+ ```tsx
27
+ <ResponsiveTable
28
+ data={rows}
29
+ columnDefinitions={columns}
30
+ filterProps={{ showFilter: true }}
31
+ />
32
+ ```
33
+
34
+ **Server-side** — when `dataSource` is present, server mode is automatic. The table resets to page 1 and calls your fetch function with the current `filter` string on every change:
35
+ ```tsx
36
+ <ResponsiveTable
37
+ dataSource={async ({ page, pageSize, filter }) =>
38
+ api.getUsers({ page, pageSize, search: filter })
39
+ }
40
+ columnDefinitions={columns}
41
+ filterProps={{ showFilter: true }}
42
+ />
43
+ ```
44
+
45
+ > To force client-side filtering even with a `dataSource`, pass `mode: 'client'`.
46
+
47
+ ---
48
+
21
49
  ## Delightful Data Fetching: Smart Data Source
22
50
 
23
- 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.
51
+ The `dataSource` pattern makes handling large datasets, server-side sorting, filtering, and infinite scroll completely painless. You provide the fetch logic; the table handles bookkeeping.
24
52
 
25
- ### Basic Usage
53
+ ### Pagination only
26
54
  ```tsx
27
55
  <ResponsiveTable
28
56
  dataSource={async ({ page, pageSize }) => {
29
57
  const users = await api.getUsers({ page, pageSize });
30
- return users; // Table automatically handles appending and hasMore detection!
58
+ return users; // hasMore is auto-detected from page size
31
59
  }}
32
60
  columnDefinitions={columns}
33
61
  />
34
62
  ```
35
63
 
36
- ### With Sorting & Filtering
37
- The table tells you exactly what it needs based on user interaction:
64
+ ### Pagination + sorting + filtering
38
65
  ```tsx
39
66
  <ResponsiveTable
40
- dataSource={async ({ page, pageSize, sort, filter }) => {
41
- return await api.getUsers({
67
+ dataSource={async ({ page, pageSize, sort, filter }) =>
68
+ api.getUsers({
42
69
  page,
43
70
  limit: pageSize,
44
71
  sortBy: sort?.columnId,
45
72
  order: sort?.direction,
46
- search: filter
47
- });
48
- }}
73
+ search: filter,
74
+ })
75
+ }
49
76
  columnDefinitions={columns}
50
77
  sortProps={{ initialSortColumn: 'name' }}
51
78
  filterProps={{ showFilter: true }}
52
79
  />
53
80
  ```
54
81
 
82
+ ### With total count (accurate hasMore)
83
+ Return `{ items, totalCount }` instead of a plain array and the table derives `hasMore` precisely:
84
+ ```tsx
85
+ dataSource={async ({ page, pageSize }) => {
86
+ const { data, total } = await api.getUsers({ page, pageSize });
87
+ return { items: data, totalCount: total };
88
+ }}
89
+ ```
90
+
55
91
  ---
56
92
 
57
93
  ## Basic Implementation
@@ -70,6 +70,7 @@ interface TableContextValue<TData> {
70
70
  isLoading: boolean;
71
71
  isFetchingMore: boolean;
72
72
  loadNextPage: () => void;
73
+ error?: Error;
73
74
  };
74
75
  /** Custom CSS class to apply to each card in mobile view. */
75
76
  mobileCardClassName?: string;
@@ -9,6 +9,15 @@ interface UseTableDataSourceProps<TData> {
9
9
  };
10
10
  filter?: string;
11
11
  }
12
+ export interface DataSourceState<TData> {
13
+ data: TData[];
14
+ currentPage: number;
15
+ hasMore: boolean;
16
+ totalCount?: number;
17
+ isLoading: boolean;
18
+ isFetchingMore: boolean;
19
+ error?: Error;
20
+ }
12
21
  interface UseTableDataSourceReturn<TData> {
13
22
  data: TData[];
14
23
  currentPage: number;
@@ -18,6 +27,7 @@ interface UseTableDataSourceReturn<TData> {
18
27
  isFetchingMore: boolean;
19
28
  loadNextPage: () => Promise<void>;
20
29
  resetAndFetch: () => Promise<void>;
30
+ error?: Error;
21
31
  }
22
32
  export declare const useTableDataSource: <TData>(props: UseTableDataSourceProps<TData>) => UseTableDataSourceReturn<TData>;
23
33
  export {};
@@ -14,6 +14,7 @@ interface UseTablePluginsProps<TData> {
14
14
  showFilter?: boolean;
15
15
  filterPlaceholder?: string;
16
16
  className?: string;
17
+ mode?: 'client' | 'server';
17
18
  };
18
19
  selectionProps?: {
19
20
  onSelectionChange: (selectedItems: TData[]) => void;
@@ -28,6 +29,7 @@ interface UseTablePluginsProps<TData> {
28
29
  columnDefinitions: (IResponsiveTableColumnDefinition<TData> | ((data: TData, rowIndex?: number) => IResponsiveTableColumnDefinition<TData>))[];
29
30
  getScrollableElement: () => HTMLElement | null;
30
31
  infiniteScrollProps?: IInfiniteScrollProps<TData>;
32
+ onFilterChange?: (filterText: string) => void;
31
33
  }
32
34
  interface UseTablePluginsReturn<TData> {
33
35
  processedData: TData[];
@@ -12,4 +12,5 @@ export declare class FilterPlugin<TData> implements IResponsiveTablePlugin<TData
12
12
  processData: (data: TData[]) => TData[];
13
13
  renderCell: (content: React.ReactNode, _row: TData, _column: IResponsiveTableColumnDefinition<TData>) => React.ReactNode;
14
14
  private handleFilterChange;
15
+ private handleClear;
15
16
  }
@@ -28,6 +28,7 @@ export interface IPluginAPI<TData> {
28
28
  showFilter?: boolean;
29
29
  filterPlaceholder?: string;
30
30
  className?: string;
31
+ mode?: 'client' | 'server';
31
32
  };
32
33
  selectionProps?: {
33
34
  onSelectionChange: (selectedItems: TData[]) => void;
@@ -36,4 +37,5 @@ export interface IPluginAPI<TData> {
36
37
  selectedItems?: TData[];
37
38
  selectedRowClassName?: string;
38
39
  };
40
+ onFilterChange?: (filterText: string) => void;
39
41
  }
@@ -3,7 +3,13 @@ import { SortDirection } from '../Data/IResponsiveTableColumnDefinition';
3
3
  import IFooterRowDefinition from '../Data/IFooterRowDefinition';
4
4
  import { IResponsiveTablePlugin } from '../Plugins/IResponsiveTablePlugin';
5
5
  import { ColumnDefinition, DataSource } from '../Context/TableContext';
6
+ import { DataSourceState } from '../Hooks/useTableDataSource';
6
7
  export { ColumnDefinition };
8
+ export interface ResponsiveTableHandle<TData> {
9
+ loadNextPage: () => Promise<void>;
10
+ resetAndFetch: () => Promise<void>;
11
+ getState: () => DataSourceState<TData>;
12
+ }
7
13
  interface IInfiniteScrollProps<TData> {
8
14
  onLoadMore: (currentData: TData[]) => Promise<TData[] | null>;
9
15
  hasMore?: boolean;
@@ -66,6 +72,8 @@ interface IProps<TData> {
66
72
  showFilter?: boolean;
67
73
  filterPlaceholder?: string;
68
74
  className?: string;
75
+ /** Default: 'server' when dataSource is present, 'client' otherwise. Pass 'client' to force in-memory filtering even with a dataSource. */
76
+ mode?: 'client' | 'server';
69
77
  };
70
78
  /** Configuration for row selection. */
71
79
  selectionProps?: {
@@ -84,10 +92,14 @@ interface IProps<TData> {
84
92
  sortProps?: ISortProps;
85
93
  /** Custom CSS class to apply to each card in mobile view. */
86
94
  mobileCardClassName?: string;
95
+ /** Callback fired whenever the dataSource state changes (data, page, loading, error). */
96
+ onDataSourceStateChange?: (state: DataSourceState<TData>) => void;
97
+ /** Callback fired when the current page changes. */
98
+ onPageChange?: (page: number) => void;
99
+ /** Callback fired when a dataSource fetch fails. */
100
+ onDataSourceError?: (error: Error) => void;
87
101
  }
88
- /**
89
- * A highly customizable, mobile-first responsive React table.
90
- * Supports static data or async data sources with built-in infinite scroll.
91
- */
92
- declare function ResponsiveTable<TData>(props: IProps<TData>): React.JSX.Element;
102
+ declare const ResponsiveTable: <TData>(props: IProps<TData> & {
103
+ ref?: React.Ref<ResponsiveTableHandle<TData>>;
104
+ }) => React.ReactElement;
93
105
  export default ResponsiveTable;
package/dist/index.d.ts CHANGED
@@ -1,12 +1,13 @@
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 from './UI/ResponsiveTable';
5
- import { ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult } from './Context/TableContext';
4
+ import ResponsiveTable, { ResponsiveTableHandle } from './UI/ResponsiveTable';
5
+ import { ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult, useTableContext } from './Context/TableContext';
6
+ import { DataSourceState } from './Hooks/useTableDataSource';
6
7
  import { FilterPlugin } from './Plugins/FilterPlugin';
7
8
  import { InfiniteScrollPlugin } from './Plugins/InfiniteScrollPlugin';
8
9
  import { IResponsiveTablePlugin } from './Plugins/IResponsiveTablePlugin';
9
10
  import { SortPlugin } from './Plugins/SortPlugin';
10
11
  import { SelectionPlugin } from './Plugins/SelectionPlugin';
11
- export { SortDirection, IResponsiveTableColumnDefinition, ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult, IFooterColumnDefinition, IFooterRowDefinition, FilterPlugin, InfiniteScrollPlugin, IResponsiveTablePlugin, SortPlugin, SelectionPlugin, };
12
+ export { SortDirection, IResponsiveTableColumnDefinition, ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult, IFooterColumnDefinition, IFooterRowDefinition, FilterPlugin, InfiniteScrollPlugin, IResponsiveTablePlugin, SortPlugin, SelectionPlugin, ResponsiveTableHandle, DataSourceState, useTableContext, };
12
13
  export default ResponsiveTable;
package/dist/index.es.js CHANGED
@@ -1,4 +1,6 @@
1
- import React, { createContext, useCallback, useMemo, useContext, useRef, useEffect, useState } from 'react';
1
+ import React, { createContext, useCallback, useMemo, useContext, useRef, useEffect, useState, forwardRef, useImperativeHandle } from 'react';
2
+ import ZestTextbox from 'jattac.libs.web.zest-textbox';
3
+ import { MdClose } from 'react-icons/md';
2
4
 
3
5
  function styleInject(css, ref) {
4
6
  if ( ref === void 0 ) ref = {};
@@ -484,18 +486,36 @@ class FilterPlugin {
484
486
  this.api = api;
485
487
  };
486
488
  this.renderHeader = () => {
487
- var _a;
489
+ var _a, _b;
488
490
  if (!((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.showFilter)) {
489
491
  return null;
490
492
  }
491
- return (React.createElement("div", { style: { marginBottom: '1rem' } },
492
- React.createElement("input", { type: "text", placeholder: this.api.filterProps.filterPlaceholder || "Search...", onChange: this.handleFilterChange, className: this.api.filterProps.className, style: {
493
- padding: '0.5rem',
494
- border: '1px solid #ccc',
495
- borderRadius: '4px',
496
- } })));
493
+ return (React.createElement("div", { style: { marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' } },
494
+ React.createElement(ZestTextbox, { value: this.filterText, placeholder: (_b = this.api.filterProps.filterPlaceholder) !== null && _b !== void 0 ? _b : 'Search...', onChange: this.handleFilterChange, className: this.api.filterProps.className, zest: { stretch: true } }),
495
+ React.createElement("button", { onClick: this.handleClear, "aria-label": "Clear filter", style: {
496
+ display: 'flex',
497
+ alignItems: 'center',
498
+ justifyContent: 'center',
499
+ minWidth: '2.75rem',
500
+ minHeight: '2.75rem',
501
+ padding: 0,
502
+ border: 'none',
503
+ borderRadius: '50%',
504
+ background: 'transparent',
505
+ cursor: 'pointer',
506
+ color: '#666',
507
+ opacity: this.filterText ? 1 : 0,
508
+ pointerEvents: this.filterText ? 'auto' : 'none',
509
+ transition: 'opacity 0.15s ease',
510
+ flexShrink: 0,
511
+ } },
512
+ React.createElement(MdClose, { size: 20 }))));
497
513
  };
498
514
  this.processData = (data) => {
515
+ var _a;
516
+ if (((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.mode) === 'server') {
517
+ return data;
518
+ }
499
519
  if (!this.filterText || !this.api.columnDefinitions) {
500
520
  return data;
501
521
  }
@@ -522,6 +542,10 @@ class FilterPlugin {
522
542
  _row,
523
543
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
524
544
  _column) => {
545
+ var _a;
546
+ if (((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.mode) === 'server') {
547
+ return content;
548
+ }
525
549
  if (!this.filterText || typeof content !== 'string') {
526
550
  return content;
527
551
  }
@@ -534,10 +558,21 @@ class FilterPlugin {
534
558
  clearTimeout(this.debounceTimeout);
535
559
  }
536
560
  this.debounceTimeout = setTimeout(() => {
561
+ var _a, _b;
537
562
  this.filterText = currentFilterText;
538
563
  this.api.forceUpdate();
564
+ (_b = (_a = this.api).onFilterChange) === null || _b === void 0 ? void 0 : _b.call(_a, currentFilterText);
539
565
  }, 300);
540
566
  };
567
+ this.handleClear = () => {
568
+ var _a, _b;
569
+ if (this.debounceTimeout) {
570
+ clearTimeout(this.debounceTimeout);
571
+ }
572
+ this.filterText = '';
573
+ this.api.forceUpdate();
574
+ (_b = (_a = this.api).onFilterChange) === null || _b === void 0 ? void 0 : _b.call(_a, '');
575
+ };
541
576
  }
542
577
  }
543
578
 
@@ -757,7 +792,7 @@ class SortPlugin {
757
792
  }
758
793
 
759
794
  const useTablePlugins = (props) => {
760
- const { data, plugins, filterProps, selectionProps, sortProps, columnDefinitions, getScrollableElement, infiniteScrollProps, } = props;
795
+ const { data, plugins, filterProps, selectionProps, sortProps, columnDefinitions, getScrollableElement, infiniteScrollProps, onFilterChange, } = props;
761
796
  const [processedData, setProcessedData] = useState(data);
762
797
  const [activePlugins, setActivePlugins] = useState([]);
763
798
  // Persist internal plugins using refs to prevent state loss
@@ -831,6 +866,7 @@ const useTablePlugins = (props) => {
831
866
  filterProps: filterProps,
832
867
  selectionProps: selectionProps,
833
868
  columnDefinitions: columnDefinitions,
869
+ onFilterChange: onFilterChange,
834
870
  };
835
871
  // Initialize/Refresh all active plugins with the current API
836
872
  newActivePlugins.forEach((plugin) => {
@@ -856,6 +892,7 @@ const useTablePlugins = (props) => {
856
892
  getScrollableElement,
857
893
  infiniteScrollProps,
858
894
  getRawColumnDefinition,
895
+ onFilterChange,
859
896
  ]);
860
897
  const forceUpdatePlugins = useCallback(() => {
861
898
  setProcessedData(initializePlugins());
@@ -1026,7 +1063,10 @@ const useTableDataSource = (props) => {
1026
1063
  const [totalCount, setTotalCount] = useState(undefined);
1027
1064
  const [isLoading, setIsLoading] = useState(false);
1028
1065
  const [isFetchingMore, setIsFetchingMore] = useState(false);
1066
+ const [error, setError] = useState(undefined);
1029
1067
  const isInitialMount = useRef(true);
1068
+ const dataLengthRef = useRef(0);
1069
+ dataLengthRef.current = data.length;
1030
1070
  const fetchData = useCallback((page, isAppend) => __awaiter(void 0, void 0, void 0, function* () {
1031
1071
  if (!dataSource)
1032
1072
  return;
@@ -1037,6 +1077,7 @@ const useTableDataSource = (props) => {
1037
1077
  setIsLoading(true);
1038
1078
  }
1039
1079
  try {
1080
+ setError(undefined);
1040
1081
  const params = {
1041
1082
  page,
1042
1083
  pageSize,
@@ -1058,7 +1099,7 @@ const useTableDataSource = (props) => {
1058
1099
  setCurrentPage(page);
1059
1100
  // Intelligent hasMore detection
1060
1101
  if (newTotalCount !== undefined) {
1061
- const currentTotalLoaded = (isAppend ? data.length : 0) + newItems.length;
1102
+ const currentTotalLoaded = (isAppend ? dataLengthRef.current : 0) + newItems.length;
1062
1103
  setHasMore(currentTotalLoaded < newTotalCount);
1063
1104
  }
1064
1105
  else {
@@ -1066,15 +1107,16 @@ const useTableDataSource = (props) => {
1066
1107
  setHasMore(newItems.length === pageSize);
1067
1108
  }
1068
1109
  }
1069
- catch (error) {
1070
- console.error('Error fetching data from dataSource:', error);
1110
+ catch (err) {
1111
+ setError(err);
1071
1112
  setHasMore(false);
1113
+ console.error('Error fetching data from dataSource:', err);
1072
1114
  }
1073
1115
  finally {
1074
1116
  setIsLoading(false);
1075
1117
  setIsFetchingMore(false);
1076
1118
  }
1077
- }), [dataSource, pageSize, sort, filter, data.length]);
1119
+ }), [dataSource, pageSize, sort, filter]);
1078
1120
  const loadNextPage = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
1079
1121
  if (isLoading || isFetchingMore || !hasMore || !dataSource)
1080
1122
  return;
@@ -1105,6 +1147,7 @@ const useTableDataSource = (props) => {
1105
1147
  isFetchingMore,
1106
1148
  loadNextPage,
1107
1149
  resetAndFetch,
1150
+ error,
1108
1151
  };
1109
1152
  };
1110
1153
 
@@ -1112,8 +1155,9 @@ const useTableDataSource = (props) => {
1112
1155
  * A highly customizable, mobile-first responsive React table.
1113
1156
  * Supports static data or async data sources with built-in infinite scroll.
1114
1157
  */
1115
- function ResponsiveTable(props) {
1116
- const { columnDefinitions, data: initialData, dataSource, pageSize, noDataComponent, maxHeight, onRowClick, footerRows, mobileBreakpoint, plugins, enablePageLevelStickyHeader, infiniteScrollProps, filterProps, selectionProps, animationProps, sortProps, mobileCardClassName, } = props;
1158
+ function ResponsiveTableInner(props, ref) {
1159
+ var _a;
1160
+ const { columnDefinitions, data: initialData, dataSource, pageSize, noDataComponent, maxHeight, onRowClick, footerRows, mobileBreakpoint, plugins, enablePageLevelStickyHeader, infiniteScrollProps, filterProps, selectionProps, animationProps, sortProps, mobileCardClassName, onDataSourceStateChange, onPageChange, onDataSourceError, } = props;
1117
1161
  const tableContainerRef = useRef(null);
1118
1162
  const headerRef = useRef(null);
1119
1163
  const { isMobile, isHeaderSticky } = useResponsiveTable({
@@ -1126,29 +1170,72 @@ function ResponsiveTable(props) {
1126
1170
  const getScrollableElement = useCallback(() => tableContainerRef.current, []);
1127
1171
  // Track active sort state for dataSource
1128
1172
  const [activeSort /*, setActiveSort*/] = useState((sortProps === null || sortProps === void 0 ? void 0 : sortProps.initialSortColumn) ? { columnId: sortProps.initialSortColumn, direction: sortProps.initialSortDirection || 'asc' } : undefined);
1129
- const { data: sourceData, isLoading: isSourceLoading, isFetchingMore, hasMore, totalCount, currentPage, loadNextPage, } = useTableDataSource({
1173
+ // Track active filter state for dataSource
1174
+ const [activeFilter, setActiveFilter] = useState('');
1175
+ const handleFilterChange = useCallback((text) => {
1176
+ setActiveFilter(text);
1177
+ }, []);
1178
+ const isServerFilter = !!dataSource && !!(filterProps === null || filterProps === void 0 ? void 0 : filterProps.showFilter) && (filterProps === null || filterProps === void 0 ? void 0 : filterProps.mode) !== 'client';
1179
+ const resolvedFilterProps = filterProps
1180
+ ? Object.assign(Object.assign({}, filterProps), { mode: isServerFilter ? 'server' : ((_a = filterProps.mode) !== null && _a !== void 0 ? _a : 'client') }) : undefined;
1181
+ const { data: sourceData, isLoading: isSourceLoading, isFetchingMore, hasMore, totalCount, currentPage, loadNextPage, error, resetAndFetch, } = useTableDataSource({
1130
1182
  dataSource,
1131
1183
  pageSize,
1132
1184
  initialData,
1133
1185
  sort: activeSort,
1134
- // We'll need to extract filter state if we want to support dataSource filtering
1186
+ filter: isServerFilter ? activeFilter : undefined,
1135
1187
  });
1188
+ useImperativeHandle(ref, () => ({
1189
+ loadNextPage: () => loadNextPage(),
1190
+ resetAndFetch: () => resetAndFetch(),
1191
+ getState: () => ({
1192
+ data: sourceData,
1193
+ currentPage,
1194
+ hasMore,
1195
+ totalCount,
1196
+ isLoading: isSourceLoading,
1197
+ isFetchingMore,
1198
+ error,
1199
+ }),
1200
+ }), [loadNextPage, resetAndFetch, sourceData, currentPage, hasMore, totalCount, isSourceLoading, isFetchingMore, error]);
1136
1201
  const currentDataToProcess = dataSource ? sourceData : initialData;
1137
1202
  const { processedData, activePlugins, visibleColumns } = useTablePlugins({
1138
1203
  data: currentDataToProcess,
1139
1204
  plugins,
1140
- filterProps,
1205
+ onFilterChange: isServerFilter ? handleFilterChange : undefined,
1206
+ filterProps: resolvedFilterProps,
1141
1207
  selectionProps,
1142
1208
  sortProps,
1143
1209
  columnDefinitions,
1144
1210
  getScrollableElement,
1145
1211
  infiniteScrollProps,
1146
1212
  });
1147
- // Sync sort state from SortPlugin back to our local state to trigger dataSource re-fetch
1213
+ // Fire onDataSourceStateChange when dataSource state changes
1214
+ useEffect(() => {
1215
+ if (dataSource && onDataSourceStateChange) {
1216
+ onDataSourceStateChange({
1217
+ data: sourceData,
1218
+ currentPage,
1219
+ hasMore,
1220
+ totalCount,
1221
+ isLoading: isSourceLoading,
1222
+ isFetchingMore,
1223
+ error,
1224
+ });
1225
+ }
1226
+ }, [dataSource, sourceData, currentPage, hasMore, totalCount, isSourceLoading, isFetchingMore, error, onDataSourceStateChange]);
1227
+ // Fire onPageChange when page changes
1148
1228
  useEffect(() => {
1149
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1150
- activePlugins.find(p => p.id === 'sort');
1151
- }, [activePlugins, dataSource]);
1229
+ if (dataSource && onPageChange) {
1230
+ onPageChange(currentPage);
1231
+ }
1232
+ }, [dataSource, currentPage, onPageChange]);
1233
+ // Fire onDataSourceError when error occurs
1234
+ useEffect(() => {
1235
+ if (dataSource && error && onDataSourceError) {
1236
+ onDataSourceError(error);
1237
+ }
1238
+ }, [dataSource, error, onDataSourceError]);
1152
1239
  const hasData = useMemo(() => processedData.length > 0, [processedData]);
1153
1240
  const noDataSvg = (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "#ccc", height: "40", width: "40", viewBox: "0 0 24 24" },
1154
1241
  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" })));
@@ -1216,6 +1303,31 @@ function ResponsiveTable(props) {
1216
1303
  if (isLoading && !hasData) {
1217
1304
  return React.createElement(SkeletonView, { isMobile: isMobile, columnDefinitions: visibleColumns });
1218
1305
  }
1306
+ if (error && !isLoading && !hasData) {
1307
+ return (React.createElement("div", { style: {
1308
+ display: 'flex',
1309
+ flexDirection: 'column',
1310
+ alignItems: 'center',
1311
+ justifyContent: 'center',
1312
+ padding: '4rem 2rem',
1313
+ gap: '1rem',
1314
+ color: '#6c757d',
1315
+ border: '2px dashed #e0e0e0',
1316
+ borderRadius: '12px',
1317
+ backgroundColor: '#f8f9fa',
1318
+ } },
1319
+ React.createElement("div", { style: { fontWeight: 500, fontSize: '1.1rem' } }, "Failed to load data"),
1320
+ React.createElement("div", { style: { fontSize: '0.85rem', textAlign: 'center' } }, error.message),
1321
+ React.createElement("button", { onClick: resetAndFetch, style: {
1322
+ padding: '0.5rem 1.5rem',
1323
+ backgroundColor: '#007bff',
1324
+ color: '#fff',
1325
+ border: 'none',
1326
+ borderRadius: '6px',
1327
+ cursor: 'pointer',
1328
+ fontWeight: 500,
1329
+ } }, "Retry")));
1330
+ }
1219
1331
  return (React.createElement(TableProvider, { value: {
1220
1332
  data: currentDataToProcess,
1221
1333
  processedData,
@@ -1234,6 +1346,7 @@ function ResponsiveTable(props) {
1234
1346
  isLoading: isSourceLoading,
1235
1347
  isFetchingMore,
1236
1348
  loadNextPage,
1349
+ error,
1237
1350
  } : undefined,
1238
1351
  mobileCardClassName,
1239
1352
  } },
@@ -1243,6 +1356,7 @@ function ResponsiveTable(props) {
1243
1356
  (hasData || isLoading) && isMobile && (React.createElement(MobileView, { mobileFooter: mobileFooter })),
1244
1357
  (hasData || isLoading) && !isMobile && (React.createElement(DesktopView, { maxHeight: maxHeight, isHeaderSticky: isHeaderSticky, tableContainerRef: tableContainerRef, headerRef: headerRef, footerRows: footerRows, renderPluginFooters: renderPluginFooters })))));
1245
1358
  }
1359
+ const ResponsiveTable = forwardRef(ResponsiveTableInner);
1246
1360
 
1247
1361
  class InfiniteScrollPlugin {
1248
1362
  constructor() {
@@ -1297,5 +1411,5 @@ class InfiniteScrollPlugin {
1297
1411
  }
1298
1412
  }
1299
1413
 
1300
- export { FilterPlugin, InfiniteScrollPlugin, SelectionPlugin, SortPlugin, ResponsiveTable as default };
1414
+ export { FilterPlugin, InfiniteScrollPlugin, SelectionPlugin, SortPlugin, ResponsiveTable as default, useTableContext };
1301
1415
  //# sourceMappingURL=index.es.js.map